From ccab070776707b35a55af4c2a9c62afce1713f72 Mon Sep 17 00:00:00 2001 From: Griatch Date: Tue, 7 Apr 2020 23:13:24 +0200 Subject: [PATCH] Add first version of converted wiki --- docs/Makefile | 10 +- docs/pylib/copy_from_wiki.py | 142 +- .../A-voice-operated-elevator-using-events.md | 327 +++ docs/source/API-refactoring.md | 33 + docs/source/Accounts.md | 105 + docs/source/Add-a-simple-new-web-page.md | 100 + docs/source/Add-a-wiki-on-your-website.md | 211 ++ docs/source/Adding-Command-Tutorial.md | 163 ++ .../Adding-Object-Typeclass-Tutorial.md | 109 + docs/source/Administrative-Docs.md | 43 + docs/source/Apache-Config.md | 175 ++ docs/source/Arxcode-installing-help.md | 260 ++ docs/source/Async-Process.md | 229 ++ docs/source/Attributes.md | 376 +++ docs/source/Banning.md | 112 + docs/source/Batch-Code-Processor.md | 158 ++ docs/source/Batch-Command-Processor.md | 121 + docs/source/Batch-Processors.md | 39 + docs/source/Bootstrap-&-Evennia.md | 75 + .../Bootstrap-Components-and-Utilities.md | 63 + docs/source/Builder-Docs.md | 26 + docs/source/Building-Permissions.md | 38 + docs/source/Building-Quickstart.md | 193 ++ docs/source/Building-a-mech-tutorial.md | 183 ++ docs/source/Building-menus.md | 1065 ++++++++ docs/source/Choosing-An-SQL-Server.md | 180 ++ docs/source/Client-Support-Grid.md | 79 + docs/source/Coding-FAQ.md | 312 +++ docs/source/Coding-Introduction.md | 62 + docs/source/Coding-Utils.md | 236 ++ docs/source/Command-Cooldown.md | 98 + docs/source/Command-Duration.md | 351 +++ docs/source/Command-Prompt.md | 113 + docs/source/Command-Sets.md | 216 ++ docs/source/Command-System.md | 9 + docs/source/Commands.md | 452 +++ docs/source/Communications.md | 70 + docs/source/Connection-Screen.md | 35 + docs/source/Continuous-Integration.md | 212 ++ docs/source/Contributing.md | 87 + docs/source/Coordinates.md | 334 +++ docs/source/Custom-Protocols.md | 222 ++ docs/source/Customize-channels.md | 470 ++++ docs/source/Debugging.md | 290 ++ docs/source/Default-Command-Help.md | 2432 +++++++++++++++++ docs/source/Default-Exit-Errors.md | 96 + docs/source/Developer-Central.md | 103 + docs/source/Dialogues-in-events.md | 179 ++ docs/source/Directory-Overview.md | 48 + docs/source/Docs-refactoring.md | 95 + docs/source/Dynamic-In-Game-Map.md | 422 +++ docs/source/EvEditor.md | 158 ++ docs/source/EvMenu.md | 981 +++++++ docs/source/EvMore.md | 38 + docs/source/Evennia-API.md | 58 + docs/source/Evennia-Game-Index.md | 69 + docs/source/Evennia-Introduction.md | 94 + docs/source/Evennia-for-Diku-Users.md | 159 ++ .../Evennia-for-roleplaying-sessions.md | 615 +++++ docs/source/Execute-Python-Code.md | 88 + docs/source/First-Steps-Coding.md | 217 ++ docs/source/Game-Planning.md | 113 + docs/source/Gametime-Tutorial.md | 230 ++ docs/source/Getting-Started.md | 474 ++++ docs/source/Glossary.md | 209 ++ docs/source/Grapevine.md | 71 + docs/source/Guest-Logins.md | 18 + docs/source/HAProxy-Config-(Optional).md | 59 + docs/source/Help-System-Tutorial.md | 383 +++ docs/source/Help-System.md | 86 + docs/source/How-To-Get-And-Give-Help.md | 41 + .../How-to-connect-Evennia-to-Twitter.md | 92 + docs/source/IRC.md | 59 + .../source/Implementing-a-game-rule-system.md | 221 ++ docs/source/Inputfuncs.md | 141 + docs/source/Internationalization.md | 57 + .../Learn-Python-for-Evennia-The-Hard-Way.md | 27 + docs/source/Licensing.md | 32 + docs/source/Links.md | 109 + docs/source/Locks.md | 359 +++ docs/source/Manually-Configuring-Color.md | 135 + docs/source/Mass-and-weight-for-objects.md | 95 + docs/source/Messagepath.md | 155 ++ docs/source/MonitorHandler.md | 63 + docs/source/NPC-shop-Tutorial.md | 273 ++ docs/source/New-Models.md | 187 ++ docs/source/OOB.md | 114 + docs/source/Objects.md | 117 + docs/source/Online-Setup.md | 276 ++ ...nd-arguments,-theory-and-best-practices.md | 614 +++++ docs/source/Portal-And-Server.md | 14 + docs/source/Profiling.md | 84 + docs/source/Python-3.md | 82 + docs/source/Python-basic-introduction.md | 184 ++ docs/source/Python-basic-tutorial-part-two.md | 336 +++ docs/source/Quirks.md | 72 + docs/source/RSS.md | 34 + docs/source/Roadmap.md | 4 + docs/source/Running-Evennia-in-Docker.md | 191 ++ docs/source/Screenshot.md | 13 + docs/source/Scripts.md | 308 +++ docs/source/Security.md | 95 + docs/source/Server-Conf.md | 58 + docs/source/Sessions.md | 101 + docs/source/Setting-up-PyCharm.md | 80 + docs/source/Signals.md | 113 + docs/source/Soft-Code.md | 94 + docs/source/Start-Stop-Reload.md | 190 ++ docs/source/Static-In-Game-Map.md | 337 +++ docs/source/Tags.md | 113 + docs/source/Text-Encodings.md | 66 + docs/source/TextTags.md | 234 ++ docs/source/TickerHandler.md | 94 + docs/source/Turn-based-Combat-System.md | 432 +++ docs/source/Tutorial-Aggressive-NPCs.md | 101 + docs/source/Tutorial-NPCs-listening.md | 89 + docs/source/Tutorial-Searching-For-Objects.md | 295 ++ docs/source/Tutorial-Tweeting-Game-Stats.md | 84 + docs/source/Tutorial-Vehicles.md | 357 +++ docs/source/Tutorial-World-Introduction.md | 79 + .../Tutorial-for-basic-MUSH-like-game.md | 517 ++++ docs/source/Tutorials.md | 110 + docs/source/Typeclasses.md | 247 ++ docs/source/Understanding-Color-Tags.md | 132 + docs/source/Unit-Testing.md | 296 ++ docs/source/Updating-Your-Game.md | 86 + docs/source/Using-MUX-as-a-Standard.md | 70 + docs/source/Using-Travis.md | 25 + docs/source/Version-Control.md | 333 +++ docs/source/Weather-Tutorial.md | 40 + docs/source/Web-Character-Generation.md | 542 ++++ docs/source/Web-Character-View-Tutorial.md | 165 ++ docs/source/Web-Features.md | 78 + docs/source/Web-Tutorial.md | 64 + docs/source/Webclient-brainstorm.md | 251 ++ docs/source/Webclient.md | 134 + docs/source/Wiki-Index.md | 151 + docs/source/Zones.md | 35 + .../api/{modules.rst => evennia-api.rst} | 0 .../api/evennia.accounts.migrations.rst | 13 +- docs/source/api/evennia.accounts.rst | 13 +- docs/source/api/evennia.commands.default.rst | 13 +- docs/source/api/evennia.commands.rst | 13 +- docs/source/api/evennia.comms.migrations.rst | 13 +- docs/source/api/evennia.comms.rst | 13 +- .../api/evennia.contrib.ingame_python.rst | 13 +- docs/source/api/evennia.contrib.rst | 13 +- .../api/evennia.contrib.security.auditing.rst | 13 +- docs/source/api/evennia.contrib.security.rst | 13 +- .../source/api/evennia.contrib.turnbattle.rst | 13 +- .../api/evennia.contrib.tutorial_examples.rst | 13 +- .../api/evennia.contrib.tutorial_world.rst | 13 +- .../api/evennia.game_template.commands.rst | 13 +- docs/source/api/evennia.game_template.rst | 13 +- .../api/evennia.game_template.server.conf.rst | 13 +- .../api/evennia.game_template.server.rst | 13 +- .../api/evennia.game_template.typeclasses.rst | 13 +- docs/source/api/evennia.game_template.web.rst | 13 +- .../api/evennia.game_template.world.rst | 13 +- docs/source/api/evennia.help.migrations.rst | 13 +- docs/source/api/evennia.help.rst | 13 +- docs/source/api/evennia.locks.rst | 13 +- .../source/api/evennia.objects.migrations.rst | 13 +- docs/source/api/evennia.objects.rst | 13 +- docs/source/api/evennia.prototypes.rst | 13 +- docs/source/api/evennia.rst | 13 +- .../source/api/evennia.scripts.migrations.rst | 13 +- docs/source/api/evennia.scripts.rst | 13 +- .../api/evennia.server.game_index_client.rst | 13 +- docs/source/api/evennia.server.migrations.rst | 13 +- docs/source/api/evennia.server.portal.rst | 13 +- docs/source/api/evennia.server.profiling.rst | 13 +- docs/source/api/evennia.server.rst | 13 +- docs/source/api/evennia.server.tests.rst | 13 +- .../api/evennia.typeclasses.migrations.rst | 13 +- docs/source/api/evennia.typeclasses.rst | 13 +- docs/source/api/evennia.utils.idmapper.rst | 13 +- docs/source/api/evennia.utils.rst | 13 +- docs/source/api/evennia.utils.tests.data.rst | 13 +- docs/source/api/evennia.utils.tests.rst | 13 +- docs/source/api/evennia.web.rst | 13 +- docs/source/api/evennia.web.utils.rst | 13 +- docs/source/api/evennia.web.webclient.rst | 13 +- docs/source/api/evennia.web.website.rst | 13 +- .../api/evennia.web.website.templatetags.rst | 13 +- docs/source/conf.py | 81 +- docs/source/foo.md | 11 - docs/source/index.md | 33 +- docs/source/toc.md | 139 + 189 files changed, 26862 insertions(+), 456 deletions(-) create mode 100644 docs/source/A-voice-operated-elevator-using-events.md create mode 100644 docs/source/API-refactoring.md create mode 100644 docs/source/Accounts.md create mode 100644 docs/source/Add-a-simple-new-web-page.md create mode 100644 docs/source/Add-a-wiki-on-your-website.md create mode 100644 docs/source/Adding-Command-Tutorial.md create mode 100644 docs/source/Adding-Object-Typeclass-Tutorial.md create mode 100644 docs/source/Administrative-Docs.md create mode 100644 docs/source/Apache-Config.md create mode 100644 docs/source/Arxcode-installing-help.md create mode 100644 docs/source/Async-Process.md create mode 100644 docs/source/Attributes.md create mode 100644 docs/source/Banning.md create mode 100644 docs/source/Batch-Code-Processor.md create mode 100644 docs/source/Batch-Command-Processor.md create mode 100644 docs/source/Batch-Processors.md create mode 100644 docs/source/Bootstrap-&-Evennia.md create mode 100644 docs/source/Bootstrap-Components-and-Utilities.md create mode 100644 docs/source/Builder-Docs.md create mode 100644 docs/source/Building-Permissions.md create mode 100644 docs/source/Building-Quickstart.md create mode 100644 docs/source/Building-a-mech-tutorial.md create mode 100644 docs/source/Building-menus.md create mode 100644 docs/source/Choosing-An-SQL-Server.md create mode 100644 docs/source/Client-Support-Grid.md create mode 100644 docs/source/Coding-FAQ.md create mode 100644 docs/source/Coding-Introduction.md create mode 100644 docs/source/Coding-Utils.md create mode 100644 docs/source/Command-Cooldown.md create mode 100644 docs/source/Command-Duration.md create mode 100644 docs/source/Command-Prompt.md create mode 100644 docs/source/Command-Sets.md create mode 100644 docs/source/Command-System.md create mode 100644 docs/source/Commands.md create mode 100644 docs/source/Communications.md create mode 100644 docs/source/Connection-Screen.md create mode 100644 docs/source/Continuous-Integration.md create mode 100644 docs/source/Contributing.md create mode 100644 docs/source/Coordinates.md create mode 100644 docs/source/Custom-Protocols.md create mode 100644 docs/source/Customize-channels.md create mode 100644 docs/source/Debugging.md create mode 100644 docs/source/Default-Command-Help.md create mode 100644 docs/source/Default-Exit-Errors.md create mode 100644 docs/source/Developer-Central.md create mode 100644 docs/source/Dialogues-in-events.md create mode 100644 docs/source/Directory-Overview.md create mode 100644 docs/source/Docs-refactoring.md create mode 100644 docs/source/Dynamic-In-Game-Map.md create mode 100644 docs/source/EvEditor.md create mode 100644 docs/source/EvMenu.md create mode 100644 docs/source/EvMore.md create mode 100644 docs/source/Evennia-API.md create mode 100644 docs/source/Evennia-Game-Index.md create mode 100644 docs/source/Evennia-Introduction.md create mode 100644 docs/source/Evennia-for-Diku-Users.md create mode 100644 docs/source/Evennia-for-roleplaying-sessions.md create mode 100644 docs/source/Execute-Python-Code.md create mode 100644 docs/source/First-Steps-Coding.md create mode 100644 docs/source/Game-Planning.md create mode 100644 docs/source/Gametime-Tutorial.md create mode 100644 docs/source/Getting-Started.md create mode 100644 docs/source/Glossary.md create mode 100644 docs/source/Grapevine.md create mode 100644 docs/source/Guest-Logins.md create mode 100644 docs/source/HAProxy-Config-(Optional).md create mode 100644 docs/source/Help-System-Tutorial.md create mode 100644 docs/source/Help-System.md create mode 100644 docs/source/How-To-Get-And-Give-Help.md create mode 100644 docs/source/How-to-connect-Evennia-to-Twitter.md create mode 100644 docs/source/IRC.md create mode 100644 docs/source/Implementing-a-game-rule-system.md create mode 100644 docs/source/Inputfuncs.md create mode 100644 docs/source/Internationalization.md create mode 100644 docs/source/Learn-Python-for-Evennia-The-Hard-Way.md create mode 100644 docs/source/Licensing.md create mode 100644 docs/source/Links.md create mode 100644 docs/source/Locks.md create mode 100644 docs/source/Manually-Configuring-Color.md create mode 100644 docs/source/Mass-and-weight-for-objects.md create mode 100644 docs/source/Messagepath.md create mode 100644 docs/source/MonitorHandler.md create mode 100644 docs/source/NPC-shop-Tutorial.md create mode 100644 docs/source/New-Models.md create mode 100644 docs/source/OOB.md create mode 100644 docs/source/Objects.md create mode 100644 docs/source/Online-Setup.md create mode 100644 docs/source/Parsing-command-arguments,-theory-and-best-practices.md create mode 100644 docs/source/Portal-And-Server.md create mode 100644 docs/source/Profiling.md create mode 100644 docs/source/Python-3.md create mode 100644 docs/source/Python-basic-introduction.md create mode 100644 docs/source/Python-basic-tutorial-part-two.md create mode 100644 docs/source/Quirks.md create mode 100644 docs/source/RSS.md create mode 100644 docs/source/Roadmap.md create mode 100644 docs/source/Running-Evennia-in-Docker.md create mode 100644 docs/source/Screenshot.md create mode 100644 docs/source/Scripts.md create mode 100644 docs/source/Security.md create mode 100644 docs/source/Server-Conf.md create mode 100644 docs/source/Sessions.md create mode 100644 docs/source/Setting-up-PyCharm.md create mode 100644 docs/source/Signals.md create mode 100644 docs/source/Soft-Code.md create mode 100644 docs/source/Start-Stop-Reload.md create mode 100644 docs/source/Static-In-Game-Map.md create mode 100644 docs/source/Tags.md create mode 100644 docs/source/Text-Encodings.md create mode 100644 docs/source/TextTags.md create mode 100644 docs/source/TickerHandler.md create mode 100644 docs/source/Turn-based-Combat-System.md create mode 100644 docs/source/Tutorial-Aggressive-NPCs.md create mode 100644 docs/source/Tutorial-NPCs-listening.md create mode 100644 docs/source/Tutorial-Searching-For-Objects.md create mode 100644 docs/source/Tutorial-Tweeting-Game-Stats.md create mode 100644 docs/source/Tutorial-Vehicles.md create mode 100644 docs/source/Tutorial-World-Introduction.md create mode 100644 docs/source/Tutorial-for-basic-MUSH-like-game.md create mode 100644 docs/source/Tutorials.md create mode 100644 docs/source/Typeclasses.md create mode 100644 docs/source/Understanding-Color-Tags.md create mode 100644 docs/source/Unit-Testing.md create mode 100644 docs/source/Updating-Your-Game.md create mode 100644 docs/source/Using-MUX-as-a-Standard.md create mode 100644 docs/source/Using-Travis.md create mode 100644 docs/source/Version-Control.md create mode 100644 docs/source/Weather-Tutorial.md create mode 100644 docs/source/Web-Character-Generation.md create mode 100644 docs/source/Web-Character-View-Tutorial.md create mode 100644 docs/source/Web-Features.md create mode 100644 docs/source/Web-Tutorial.md create mode 100644 docs/source/Webclient-brainstorm.md create mode 100644 docs/source/Webclient.md create mode 100644 docs/source/Wiki-Index.md create mode 100644 docs/source/Zones.md rename docs/source/api/{modules.rst => evennia-api.rst} (100%) delete mode 100644 docs/source/foo.md create mode 100644 docs/source/toc.md diff --git a/docs/Makefile b/docs/Makefile index cb74ed14a9..fc7ab62ad2 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -7,6 +7,9 @@ SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SPHINXMULTIVERSION ?= sphinx-multiversion SPHINXAPIDOC ?= sphinx-apidoc +SPHINXAPIDOCOPTS = --tocfile evennia-api --module-first --force +SPHINXAPIDOCENV = members,undoc-members,show-inheritance +SPHINXAPIDOCEXCLUDE = */migrations/* SOURCEDIR = source BUILDDIR = build AUTODOCDIR = $(SOURCEDIR)/api @@ -48,11 +51,14 @@ _check-env: _multiversion-check-env: @EVDIR=$(EVDIR) EVGAMEDIR=$(EVGAMEDIR) bash -e checkenv.sh multiversion +_clean_api_index: + rm source/api/* + _autodoc-index: - @EVDIR=$(EVDIR) EVGAMEDIR=$(EVGAMEDIR) $(SPHINXAPIDOC) -f -o $(SOURCEDIR)/api/ $(EVDIR) + @EVDIR=$(EVDIR) EVGAMEDIR=$(EVGAMEDIR) SPHINX_APIDOC_OPTIONS=$(SPHINXAPIDOCENV) $(SPHINXAPIDOC) $(SPHINXAPIDOCOPTS) -o $(SOURCEDIR)/api/ $(EVDIR) $(SPHINXAPIDOCEXCLUDE) _multiversion-autodoc-index: - @EVDIR=$(EVDIR) EVGAMEDIR=$(EVGAMEDIR) $(SPHINXAPIDOC) -f -o $(SOURCEDIR)/api/ $(EVDIR) + @EVDIR=$(EVDIR) EVGAMEDIR=$(EVGAMEDIR) SPHINX_APIDOC_OPTIONS=$(SPHINXAPIDOCENV) $(SPHINXAPIDOC) $(SPHINXAPIDOCOPTS) -o $(SOURCEDIR)/api/ $(EVDIR) $(SPHINXAPIDOCEXCLUDE) git diff-index --quiet HEAD || git commit -a -m "Updated API autodoc index." _build: diff --git a/docs/pylib/copy_from_wiki.py b/docs/pylib/copy_from_wiki.py index 8a0cc98a59..43c3578b7c 100644 --- a/docs/pylib/copy_from_wiki.py +++ b/docs/pylib/copy_from_wiki.py @@ -2,11 +2,15 @@ # -*- coding: utf-8 -*- """ +Copy data from old Evennia github Wiki to static files. + Prepare files for mkdoc. This assumes evennia.wiki is cloned -to a folder at the same level as the evennia-docs repo. +to a folder at the same level as the evennia repo. Just run this to update everything. +We also need to build the toc-tree and should do so automatically for now. + """ import glob @@ -14,45 +18,68 @@ import re _RE_MD_LINK = re.compile(r"\[(?P[\w -]+)\]\((?P\w+?)\)", re.I + re.S + re.U) -_WIKI_DIR = "../../evennia.wiki/" -_INFILES = sorted(glob.glob(_WIKI_DIR + "/*.md")) +_IGNORE_FILES = ( + "_Sidebar.md", + "Evennia-for-MUSH-Users.md", + "Installing-on-Android.md", + "Nicks.md", + "Spawner-and-Prototypes.md" +) + +_WIKI_DIR = "../../../evennia.wiki/" +_INFILES = [path for path in sorted(glob.glob(_WIKI_DIR + "/*.md")) + if path.rsplit('/', 1)[-1] not in _IGNORE_FILES] _FILENAMES = [path.rsplit("/", 1)[-1] for path in _INFILES] +_FILENAMES = [path.split(".", 1)[0] for path in _FILENAMES] _FILENAMESLOW = [path.lower() for path in _FILENAMES] -_OUTDIR = "../sources/" +_OUTDIR = "../source/" _CUSTOM_LINK_REMAP = { - "CmdSets.md": "Command-Sets.md", - "CmdSet.md": "Command-Sets.md", - "Cmdsets.md": "Command-Sets.md", - "CommandSet.md": "Command-Sets.md", - "batch-code-processor.md": "Batch-Code-Processor.md", - "Batch-code-processor.md": "Batch-Code-Processor.md", - "batch-command-processor.md": "Batch-Command-Processor.md", - "Batch-command-processor.md": "Batch-Command-Processor.md", - "evennia-API.md": "Evennia-API.md", - "Win.md": "Win", - "Mac.md": "Mac", - "IOS.md": "IOS", - "Andr.md": "Andr", - "Unix.md": "Unix", - "Chrome.md": "Chrome", - "EvTable.md": "EvTable.md", - "Channels.md": "Communications.md#Channels", - "Comms.md": "Communications.md", - "typeclass.md": "Typeclasses.md", - "Home.md": "index.md" + "CmdSets": "Command-Sets", + "CmdSet": "Command-Sets", + "Cmdsets": "Command-Sets", + "CommandSet": "Command-Sets", + "batch-code-processor": "Batch-Code-Processor", + "Batch-code-processor": "Batch-Code-Processor", + "batch-command-processor": "Batch-Command-Processor", + "Batch-command-processor": "Batch-Command-Processor", + "evennia-API": "Evennia-API", + "Channels": "Communications.md#Channels", + "Comms": "Communications", + "typeclass": "Typeclasses", + "Home": "index", + } +_LINK_SKIP = ( + "[5](Win)", "[6](Win)", "[7](Win)", "[10](Win)", "[11](Mac)", "[13](Win)", + "[14](IOS)", "[15](IOS)", "[16](Andr)", "[17](Andr)", "[18](Unix)", + "[21](Chrome)", + # these should be checked + "[EvTable](EvTable)", + "[styled](OptionStyles)", + "[Inputfunc](Inputfunc)", + "[API](evennia)", + "[online documentation wiki](index)", + "[online documentation](index)", + "[Home](index)", + "[Accounts](Account)", + "[Session](Session)", + "[Inputfuncs](Inputfunc)", + + "[Nicks](Nicks)", + "[Nick](Nicks)", +) + def _sub_link(match): mdict = match.groupdict() txt, url = mdict['txt'], mdict['url'] - if not txt: - # the 'comment' is not supported by Mkdocs - return "" - url = url if url.endswith(".md") or url.startswith("http") else url + ".md" + # if not txt: + # # the 'comment' is not supported by Mkdocs + # return "" + # url = url if url.endswith(".md") or url.startswith("http") else url + ".md" - print("url:", url) url = _CUSTOM_LINK_REMAP.get(url, url) if url not in _FILENAMES and not url.startswith("http") and "#" not in url: @@ -60,7 +87,10 @@ def _sub_link(match): url_plur = url[:-3] + 's' + ".md" url_cap_plur = url_plur.capitalize() - print(f"Link [{txt}]({url}) has no matching target") + link = f"[{txt}]({url})" + if link in _LINK_SKIP: + return link + if url_cap in _FILENAMES: print(f" Replacing (capitalized): {url.capitalize()}") return url_cap @@ -74,27 +104,51 @@ def _sub_link(match): ind = _FILENAMESLOW.index(url.lower()) alt = _FILENAMES[ind] print(f" Possible match (different cap): {alt}") + print(f"\nlink {link} found no file match") inp = input("Enter alternate url (return to keep old): ") if inp.strip(): url = inp.strip() return f"[{txt}]({url})" +def create_toctree(files): -for inpath in _INFILES: - print(f"Converting links in {inpath} ->", end=" ") - with open(inpath) as fil: - text = fil.read() - text = _RE_MD_LINK.sub(_sub_link, text) - text = text.split('\n')[1:] if text.split('\n')[0].strip().startswith('[]') else text.split('\n') - text = '\n'.join(text) + with open("../source/toc.md", "w") as fil: + fil.write("# Toc\n") - outfile = inpath.rsplit('/', 1)[-1] - if outfile == "Home.md": - outfile = "index.md" - outfile = _OUTDIR + outfile + for path in files: + filename = path.rsplit("/", 1)[-1] + ref = filename.rsplit(".", 1)[0] + linkname = ref.replace("-", " ") - with open(outfile, 'w') as fil: - fil.write(text) + fil.write(f"\n* [{linkname}]({ref}.md)") + +def convert_links(files, outdir): + + for inpath in files: + + title = inpath.rsplit("/", 1)[-1].split(".", 1)[0].replace("-", " ") + + print(f"Converting links in {inpath} ->", end=" ") + with open(inpath) as fil: + text = fil.read() + text = _RE_MD_LINK.sub(_sub_link, text) + text = text.split('\n')[1:] if text.split('\n')[0].strip().startswith('[]') else text.split('\n') + text = f"# {title}\n\n" + '\n'.join(text) + + outfile = inpath.rsplit('/', 1)[-1] + if outfile == "Home.md": + outfile = "index.md" + outfile = _OUTDIR + outfile + + with open(outfile, 'w') as fil: + fil.write(text) + + print(f"{outfile}.") + + +if __name__ == "__main__": + + create_toctree(_INFILES) + convert_links(_INFILES, _OUTDIR) - print(f"{outfile}.") diff --git a/docs/source/A-voice-operated-elevator-using-events.md b/docs/source/A-voice-operated-elevator-using-events.md new file mode 100644 index 0000000000..8a604fb28c --- /dev/null +++ b/docs/source/A-voice-operated-elevator-using-events.md @@ -0,0 +1,327 @@ +# A voice operated elevator using events + + +- Previous tutorial: [Adding dialogues in events](Dialogues-in-events) + +This tutorial will walk you through the steps to create a voice-operated elevator, using the [in-game Python system](https://github.com/evennia/evennia/blob/master/evennia/contrib/ingame_python/README.md). This tutorial assumes the in-game Python system is installed in your game. If it isn't, you can follow the installation steps given in [the documentation on in-game Python](https://github.com/evennia/evennia/blob/master/evennia/contrib/ingame_python/README.md), and come back on this tutorial once the system is installed. **You do not need to read** the entire documentation, it's a good reference, but not the easiest way to learn about it. Hence these tutorials. + +The in-game Python system allows to run code on individual objects in some situations. You don't have to modify the source code to add these features, past the installation. The entire system makes it easy to add specific features to some objects, but not all. + +> What will we try to do? + +In this tutorial, we are going to create a simple voice-operated elevator. In terms of features, we will: + +- Explore events with parameters. +- Work on more interesting callbacks. +- Learn about chained events. +- Play with variable modification in callbacks. + +## Our study case + +Let's summarize what we want to achieve first. We would like to create a room that will represent the inside of our elevator. In this room, a character could just say "1", "2" or "3", and the elevator will start moving. The doors will close and open on the new floor (the exits leading in and out of the elevator will be modified). + +We will work on basic features first, and then will adjust some, showing you how easy and powerfully independent actions can be configured through the in-game Python system. + +## Creating the rooms and exits we need + +We'll create an elevator right in our room (generally called "Limbo", of ID 2). You could easily adapt the following instructions if you already have some rooms and exits, of course, just remember to check the IDs. + +> Note: the in-game Python system uses IDs for a lot of things. While it is not mandatory, it is good practice to know the IDs you have for your callbacks, because it will make manipulation much quicker. There are other ways to identify objects, but as they depend on many factors, IDs are usually the safest path in our callbacks. + +Let's go into limbo (`#2`) to add our elevator. We'll add it to the north. To create this room, in-game you could type: + + tunnel n = Inside of an elevator + +The game should respond by telling you: + + Created room Inside of an elevator(#3) of type typeclasses.rooms.Room. + Created Exit from Limbo to Inside of an elevator: north(#4) (n). + Created Exit back from Inside of an elevator to Limbo: south(#5) (s). + +Note the given IDs: + +- `#2` is limbo, the first room the system created. +- `#3` is our room inside of an elevator. +- `#4` is the north exit from Limbo to our elevator. +- `#5` is the south exit from an elevator to Limbo. + +Keep these IDs somewhere for the demonstration. You will shortly see why they are important. + +> Why have we created exits to our elevator and back to Limbo? Isn't the elevator supposed to move? + +It is. But we need to have exits that will represent the way inside the elevator and out. What we will do, at every floor, will be to change these exits so they become connected to the right room. You'll see this process a bit later. + +We have two more rooms to create: our floor 2 and 3. This time, we'll use `dig`, because we don't need exits leading there, not yet anyway. + + dig The second floor + dig The third floor + +Evennia should answer with: + + Created room The second floor(#6) of type typeclasses.rooms.Room. + Created room The third floor(#7) of type typeclasses.rooms.Room. + +Add these IDs to your list, we will use them too. + +## Our first callback in the elevator + +Let's go to the elevator (you could use `tel #3` if you have the same IDs I have). + +This is our elevator room. It looks a bit empty, feel free to add a prettier description or other things to decorate it a bit. + +But what we want now is to be able to say "1", "2" or "3" and have the elevator move in that direction. + +If you have read [the previous tutorial about adding dialogues in events](Dialogues-in-events), you may remember what we need to do. If not, here's a summary: we need to run some code when somebody speaks in the room. So we need to create a callback (the callback will contain our lines of code). We just need to know on which event this should be set. You can enter `call here` to see the possible events in this room. + +In the table, you should see the "say" event, which is called when somebody says something in the room. So we'll need to add a callback to this event. Don't worry if you're a bit lost, just follow the following steps, the way they connect together will become more obvious. + + call/add here = say 1, 2, 3 + +1. We need to add a callback. A callback contains the code that will be executed at a given time. So we use the `call/add` command and switch. +2. `here` is our object, the room in which we are. +3. An equal sign. +4. The name of the event to which the callback should be connected. Here, the event is "say". Meaning this callback will be executed every time somebody says something in the room. +5. But we add an event parameter to indicate the keywords said in the room that should execute our callback. Otherwise, our callback would be called every time somebody speaks, no matter what. Here we limit, indicating our callback should be executed only if the spoken message contains "1", "2" or "3". + +An editor should open, inviting you to enter the Python code that should be executed. The first thing to remember is to read the text provided (it can contain important information) and, most of all, the list of variables that are available in this callback: + +``` +Variables you can use in this event: + + character: the character having spoken in this room. + room: the room connected to this event. + message: the text having been spoken by the character. + +----------Line Editor [Callback say of Inside of an elevator]--------------------- +01| +----------[l:01 w:000 c:0000]------------(:h for help)---------------------------- +``` + +This is important, in order to know what variables we can use in our callback out-of-the-box. Let's write a single line to be sure our callback is called when we expect it to: + +```python +character.msg("You just said {}.".format(message)) +``` + +You can paste this line in-game, then type the `:wq` command to exit the editor and save your modifications. + +Let's check. Try to say "hello" in the room. You should see the standard message, but nothing more. Now try to say "1". Below the standard message, you should see: + + You just said 1. + +You can try it. Our callback is only called when we say "1", "2" or "3". Which is just what we want. + +Let's go back in our code editor and add something more useful. + + call/edit here = say + +> Notice that we used the "edit" switch this time, since the callback exists, we just want to edit it. + +The editor opens again. Let's empty it first: + + :DD + +And turn off automatic indentation, which will help us: + + := + +> Auto-indentation is an interesting feature of the code editor, but we'd better not use it at this point, it will make copy/pasting more complicated. + +## Our entire callback in the elevator + +So here's the time to truly code our callback in-game. Here's a little reminder: + +1. We have all the IDs of our three rooms and two exits. +2. When we say "1", "2" or "3", the elevator should move to the right room, that is change the exits. Remember, we already have the exits, we just need to change their location and destination. + +It's a good idea to try to write this callback yourself, but don't feel bad about checking the solution right now. Here's a possible code that you could paste in the code editor: + +```python +# First let's have some constants +ELEVATOR = get(id=3) +FLOORS = { + "1": get(id=2), + "2": get(id=6), + "3": get(id=7), +} +TO_EXIT = get(id=4) +BACK_EXIT = get(id=5) + +# Now we check that the elevator isn't already at this floor +floor = FLOORS.get(message) +if floor is None: + character.msg("Which floor do you want?") +elif TO_EXIT.location is floor: + character.msg("The elevator already is at this floor.") +else: + # 'floor' contains the new room where the elevator should be + room.msg_contents("The doors of the elevator close with a clank.") + TO_EXIT.location = floor + BACK_EXIT.destination = floor + room.msg_contents("The doors of the elevator open to {floor}.", + mapping=dict(floor=floor)) +``` + +Let's review this longer callback: + +1. We first obtain the objects of both exits and our three floors. We use the `get()` eventfunc, which is a shortcut to obtaining objects. We usually use it to retrieve specific objects with an ID. We put the floors in a dictionary. The keys of the dictionary are the floor number (as str), the values are room objects. +2. Remember, the `message` variable contains the message spoken in the room. So either "1", "2", or "3". We still need to check it, however, because if the character says something like "1 2" in the room, our callback will be executed. Let's be sure what she says is a floor number. +3. We then check if the elevator is already at this floor. Notice that we use `TO_EXIT.location`. `TO_EXIT` contains our "north" exit, leading inside of our elevator. Therefore, its `location` will be the room where the elevator currently is. +4. If the floor is a different one, have the elevator "move", changing just the location and destination of both exits. + - The `BACK_EXIT` (that is "north") should change its location. The elevator shouldn't be accessible through our old floor. + - The `TO_EXIT` (that is "south", the exit leading out of the elevator) should have a different destination. When we go out of the elevator, we should find ourselves in the new floor, not the old one. + +Feel free to expand on this example, changing messages, making further checks. Usage and practice are keys. + +You can quit the editor as usual with `:wq` and test it out. + +## Adding a pause in our callback + +Let's improve our callback. One thing that's worth adding would be a pause: for the time being, when we say the floor number in the elevator, the doors close and open right away. It would be better to have a pause of several seconds. More logical. + +This is a great opportunity to learn about chained events. Chained events are very useful to create pauses. Contrary to the events we have seen so far, chained events aren't called automatically. They must be called by you, and can be called after some time. + +- Chained events always have the name "chain_X". Usually, X is a number, but you can give the chained event a more explicit name. +- In our original callback, we will call our chained events in, say, 15 seconds. +- We'll also have to make sure the elevator isn't already moving. + +Other than that, a chained event can be connected to a callback as usual. We'll create a chained event in our elevator, that will only contain the code necessary to open the doors to the new floor. + + call/add here = chain_1 + +The callback is added to the "chain_1" event, an event that will not be automatically called by the system when something happens. Inside this event, you can paste the code to open the doors at the new floor. You can notice a few differences: + +```python +TO_EXIT.location = floor +TO_EXIT.destination = ELEVATOR +BACK_EXIT.location = ELEVATOR +BACK_EXIT.destination = floor +room.msg_contents("The doors of the elevator open to {floor}.", + mapping=dict(floor=floor)) +``` + +Paste this code into the editor, then use `:wq` to save and quit the editor. + +Now let's edit our callback in the "say" event. We'll have to change it a bit: + +- The callback will have to check the elevator isn't already moving. +- It must change the exits when the elevator move. +- It has to call the "chain_1" event we have defined. It should call it 15 seconds later. + +Let's see the code in our callback. + + call/edit here = say + +Remove the current code and disable auto-indentation again: + + :DD + := + +And you can paste instead the following code. Notice the differences with our first attempt: + +```python +# First let's have some constants +ELEVATOR = get(id=3) +FLOORS = { + "1": get(id=2), + "2": get(id=6), + "3": get(id=7), +} +TO_EXIT = get(id=4) +BACK_EXIT = get(id=5) + +# Now we check that the elevator isn't already at this floor +floor = FLOORS.get(message) +if floor is None: + character.msg("Which floor do you want?") +elif BACK_EXIT.location is None: + character.msg("The elevator is between floors.") +elif TO_EXIT.location is floor: + character.msg("The elevator already is at this floor.") +else: + # 'floor' contains the new room where the elevator should be + room.msg_contents("The doors of the elevator close with a clank.") + TO_EXIT.location = None + BACK_EXIT.location = None + call_event(room, "chain_1", 15) +``` + +What changed? + +1. We added a little test to make sure the elevator wasn't already moving. If it is, the `BACK_EXIT.location` (the "south" exit leading out of the elevator) should be `None`. We'll remove the exit while the elevator is moving. +2. When the doors close, we set both exits' `location` to `None`. Which "removes" them from their room but doesn't destroy them. The exits still exist but they don't connect anything. If you say "2" in the elevator and look around while the elevator is moving, you won't see any exits. +3. Instead of opening the doors immediately, we call `call_event`. We give it the object containing the event to be called (here, our elevator), the name of the event to be called (here, "chain_1") and the number of seconds from now when the event should be called (here, `15`). +4. The `chain_1` callback we have created contains the code to "re-open" the elevator doors. That is, besides displaying a message, it reset the exits' `location` and `destination`. + +If you try to say "3" in the elevator, you should see the doors closing. Look around you and you won't see any exit. Then, 15 seconds later, the doors should open, and you can leave the elevator to go to the third floor. While the elevator is moving, the exit leading to it will be inaccessible. + +> Note: we don't define the variables again in our chained event, we just call them. When we execute `call_event`, a copy of our current variables is placed in the database. These variables will be restored and accessible again when the chained event is called. + +You can use the `call/tasks` command to see the tasks waiting to be executed. For instance, say "2" in the room, notice the doors closing, and then type the `call/tasks` command. You will see a task in the elevator, waiting to call the `chain_1` event. + +## Changing exit messages + +Here's another nice little feature of events: you can modify the message of a single exit without altering the others. In this case, when someone goes north into our elevator, we'd like to see something like: "someone walks into the elevator." Something similar for the back exit would be great too. + +Inside of the elevator, you can look at the available events on the exit leading outside (south). + + call south + +You should see two interesting rows in this table: + +``` +| msg_arrive | 0 (0) | Customize the message when a character | +| | | arrives through this exit. | +| msg_leave | 0 (0) | Customize the message when a character leaves | +| | | through this exit. | +``` + +So we can change the message others see when a character leaves, by editing the "msg_leave" event. Let's do that: + + call/add south = msg_leave + +Take the time to read the help. It gives you all the information you should need. We'll need to change the "message" variable, and use custom mapping (between braces) to alter the message. We're given an example, let's use it. In the code editor, you can paste the following line: + +```python +message = "{character} walks out of the elevator." +``` + +Again, save and quit the editor by entering `:wq`. You can create a new character to see it leave. + + charcreate A beggar + tel #8 = here + +(Obviously, adapt the ID if necessary.) + + py self.search("beggar").move_to(self.search("south")) + +This is a crude way to force our beggar out of the elevator, but it allows us to test. You should see: + + A beggar(#8) walks out of the elevator. + +Great! Let's do the same thing for the exit leading inside of the elevator. Follow the beggar, then edit "msg_leave" of "north": + + call/add north = msg_leave + +```python +message = "{character} walks into the elevator." +``` + +Again, you can force our beggar to move and see the message we have just set. This modification applies to these two exits, obviously: the custom message won't be used for other exits. Since we use the same exits for every floor, this will be available no matter at what floor the elevator is, which is pretty neat! + +## Tutorial F.A.Q. + +- **Q:** what happens if the game reloads or shuts down while a task is waiting to happen? +- **A:** if your game reloads while a task is in pause (like our elevator between floors), when the game is accessible again, the task will be called (if necessary, with a new time difference to take into account the reload). If the server shuts down, obviously, the task will not be called, but will be stored and executed when the server is up again. +- **Q:** can I use all kinds of variables in my callback? Whether chained or not? +- **A:** you can use every variable type you like in your original callback. However, if you execute `call_event`, since your variables are stored in the database, they will need to respect the constraints on persistent attributes. A callback will not be stored in this way, for instance. This variable will not be available in your chained event. +- **Q:** when you say I can call my chained events something else than "chain_1", "chain_2" and such, what is the naming convention? +- **A:** chained events have names beginning by "chain_". This is useful for you and for the system. But after the underscore, you can give a more useful name, like "chain_open_doors" in our case. +- **Q:** do I have to pause several seconds to call a chained event? +- **A:** no, you can call it right away. Just leave the third parameter of `call_event` out (it will default to 0, meaning the chained event will be called right away). This will not create a task. +- **Q:** can I have chained events calling themselves? +- **A:** you can. There's no limitation. Just be careful, a callback that calls itself, particularly without delay, might be a good recipe for an infinite loop. However, in some cases, it is useful to have chained events calling themselves, to do the same repeated action every X seconds for instance. +- **Q:** what if I need several elevators, do I need to copy/paste these callbacks each time? +- **A:** not advisable. There are definitely better ways to handle this situation. One of them is to consider adding the code in the source itself. Another possibility is to call chained events with the expected behavior, which makes porting code very easy. This side of chained events will be shown in the next tutorial. + +- Previous tutorial: [Adding dialogues in events](Dialogues-in-events) diff --git a/docs/source/API-refactoring.md b/docs/source/API-refactoring.md new file mode 100644 index 0000000000..72bddb6a6f --- /dev/null +++ b/docs/source/API-refactoring.md @@ -0,0 +1,33 @@ +# API refactoring + +Building up to Evennia 1.0 and beyond, it's time to comb through the Evennia API for old cruft. This whitepage is for anyone interested to contribute with their views on what part of the API needs refactoring, cleanup or clarification (or extension!) + +Note that this is not a forum. To keep things clean, each opinion text should ideally present a clear argument or lay out a suggestion. Asking for clarification and any side-discussions should be held in chat or forum. + +--- + +### Griatch (Aug 13, 2019) + +This is how to enter an opinion. Use any markdown needed but stay within your section. Also remember to copy your text to the clipboard before saving since if someone else edited the wiki in the meantime you'll have to start over. + +### Griatch (Sept 2, 2019) + +I don't agree with removing explicit keywords as suggested by [Johnny on Aug 29 below](https://github.com/evennia/evennia/wiki/API-refactoring/_edit#reduce-usage-of-optionalpositional-arguments-aug-29-2019). Overriding such a method can still be done by `get(self, **kwargs)` if so desired, making the kwargs explicit helps IMO readability of the API. If just giving a generic `**kwargs`, one must read the docstring or even the code to see which keywords are valid. + +On the other hand, I think it makes sense to as a standard offer an extra `**kwargs` at the end of arg-lists for common methods that are expected to be over-ridden. This make the API more flexible by hinting to the dev that they could expand their own over-ridden implementation with their own keyword arguments if so desired. + +--- + +### Johnny + +#### Reduce usage of optional/positional arguments (Aug 29, 2019) +``` +# AttributeHandler +def get(self, key=None, default=None, category=None, return_obj=False, + strattr=False, raise_exception=False, accessing_obj=None, + default_access=True, return_list=False): +``` +Many classes have methods requiring lengthy positional argument lists, which are tedious and error-prone to extend and override especially in cases where not all arguments are even required. It would be useful if arguments were reserved for required inputs and anything else relegated to kwargs for easier passthrough on extension. + +--- + diff --git a/docs/source/Accounts.md b/docs/source/Accounts.md new file mode 100644 index 0000000000..a6cd5e01d8 --- /dev/null +++ b/docs/source/Accounts.md @@ -0,0 +1,105 @@ +# Accounts + + +All *users* (real people) that starts a game [Session](Sessions) on Evennia are doing so through an +object called *Account*. The Account object has no in-game representation, it represents a unique +game account. In order to actually get on the game the Account must *puppet* an [Object](Objects) +(normally a [Character](Objects#Character)). + +Exactly how many Sessions can interact with an Account and its Puppets at once is determined by +Evennia's [MULTISESSION_MODE](Sessions#Multisession-mode) setting. + +Apart from storing login information and other account-specific data, the Account object is what is +chatting on [Channels](Communications). It is also a good place to store [Permissions](Locks) to be +consistent between different in-game characters as well as configuration options. The Account +object also has its own [CmdSet](Command-Sets), the `AccountCmdSet`. + +Logged into default evennia, you can use the `ooc` command to leave your current +Objects and go into OOC mode. You are quite limited in this mode, basically it works +like a simple chat program. It acts as a staging area for switching between Characters (if your +game supports that) or as a safety mode if your Character gets deleted. Use `ic` to attempt to +(re)puppet a Character. + +Note that the Account object can have, and often does have, a different set of +[Permissions](Locks#Permissions) from the Character they control. Normally you should put your +permissions on the Account level - this will overrule permissions set on the Character level. For +the permissions of the Character to come into play the default `quell` command can be used. This +allows for exploring the game using a different permission set (but you can't escalate your +permissions this way - for hierarchical permissions like `Builder`, `Admin` etc, the *lower* of the +permissions on the Character/Account will always be used). + +## How to create your own Account types + +You will usually not want more than one Account typeclass for all new accounts (but you could in +principle create a system that changes an account's typeclass dynamically). + +An Evennia Account is, per definition, a Python class that includes `evennia.DefaultAccount` among +its parents. In `mygame/typeclasses/accounts.py` there is an empty class ready for you to modify. +Evennia defaults to using this (it inherits directly from `DefaultAccount`). + +Here's an example of modifying the default Account class in code: + +```python + # in mygame/typeclasses/accounts.py + + from evennia import DefaultAccount + + class Account(DefaultAccount): # [...] + + at_account_creation(self): "this is called only once, when account is first created" + self.db.real_name = None # this is set later self.db.real_address = None # " + self.db.config_1 = True # default config self.db.config_2 = False # " + self.db.config_3 = 1 # " + + # ... whatever else our game needs to know ``` Reload the server with `reload`. + +``` + +... However, if you use `examine *self` (the asterisk makes you examine your Account object rather +than your Character), you won't see your new Attributes yet. This is because `at_account_creation` +is only called the very *first* time the Account is called and your Account object already exists +(any new Accounts that connect will see them though). To update yourself you need to make sure to +re-fire the hook on all the Accounts you have already created. Here is an example of how to do this +using `py`: + + +``` py [account.at_account_creation() for account in evennia.managers.accounts.all()] ``` + +You should now see the Attributes on yourself. + + +> If you wanted Evennia to default to a completely *different* Account class located elsewhere, you +> must point Evennia to it. Add `BASE_ACCOUNT_TYPECLASS` to your settings file, and give the python +> path to your custom class as its value. By default this points to `typeclasses.accounts.Account`, +> the empty template we used above. + + +## Properties on Accounts + +Beyond those properties assigned to all typeclassed objects (see [Typeclasses](Typeclasses)), the +Account also has the following custom properties: + +- `user` - a unique link to a `User` Django object, representing the logged-in user. +- `obj` - an alias for `character`. +- `name` - an alias for `user.username` +- `sessions` - an instance of + [ObjectSessionHandler](https://github.com/evennia/evennia/wiki/evennia.objects.objects#objectsessionhandler) + managing all connected Sessions (physical connections) this object listens to (Note: In older + versions of Evennia, this was a list). The so-called `session-id` (used in many places) is found as + a property `sessid` on each Session instance. +- `is_superuser` (bool: True/False) - if this account is a superuser. + +Special handlers: +- `cmdset` - This holds all the current [Commands](Commands) of this Account. By default these are + the commands found in the cmdset defined by `settings.CMDSET_ACCOUNT`. +- `nicks` - This stores and handles [Nicks](Nicks), in the same way as nicks it works on Objects. + For Accounts, nicks are primarily used to store custom aliases for [Channels](Communications#Channels). + +Selection of special methods (see `evennia.DefaultAccount` for details): +- `get_puppet` - get a currently puppeted object connected to the Account and a given session id, if + any. +- `puppet_object` - connect a session to a puppetable Object. +- `unpuppet_object` - disconnect a session from a puppetable Object. +- `msg` - send text to the Account +- `execute_cmd` - runs a command as if this Account did it. +- `search` - search for Accounts. diff --git a/docs/source/Add-a-simple-new-web-page.md b/docs/source/Add-a-simple-new-web-page.md new file mode 100644 index 0000000000..da734c1476 --- /dev/null +++ b/docs/source/Add-a-simple-new-web-page.md @@ -0,0 +1,100 @@ +# Add a simple new web page + + +Evennia leverages [Django](https://docs.djangoproject.com) which is a web development framework. +Huge professional websites are made in Django and there is extensive documentation (and books) on it +. You are encouraged to at least look at the Django basic tutorials. Here we will just give a brief +introduction for how things hang together, to get you started. + +We assume you have installed and set up Evennia to run. A webserver and website comes out of the +box. You can get to that by entering `http://localhost:4001` in your web browser - you should see a +welcome page with some game statistics and a link to the web client. Let us add a new page that you +can get to by going to `http://localhost:4001/story`. + +### Create the view + +A django "view" is a normal Python function that django calls to render the HTML page you will see +in the web browser. Here we will just have it spit back the raw html, but Django can do all sorts of +cool stuff with the page in the view, like adding dynamic content or change it on the fly. Open +`mygame/web` folder and add a new module there named `story.py` (you could also put it in its own +folder if you wanted to be neat. Don't forget to add an empty `__init__.py` file if you do, to tell +Python you can import from the new folder). Here's how it looks: + +```python +# in mygame/web/story.py + +from django.shortcuts import render + +def storypage(request): + return render(request, "story.html") +``` + +This view takes advantage of a shortcut provided to use by Django, _render_. This shortcut gives the +template some information from the request, for instance, the game name, and then renders it. + +### The HTML page + +We need to find a place where Evennia (and Django) looks for html files (called *templates* in +Django parlance). You can specify such places in your settings (see the `TEMPLATES` variable in +`default_settings.py` for more info), but here we'll use an existing one. Go to +`mygame/template/overrides/website/` and create a page `story.html` there. + +This is not a HTML tutorial, so we'll go simple: + +```html +{% extends "base.html" %} +{% block content %} +
+
+

A story about a tree

+

+ This is a story about a tree, a classic tale ... +

+
+
+{% endblock %} +``` + +Since we've used the _render_ shortcut, Django will allow us to extend our base styles easily. + +If you'd rather not take advantage of Evennia's base styles, you can do something like this instead: + +```html + + +

A story about a tree

+

+ This is a story about a tree, a classic tale ... + + +``` + + +### The URL + +When you enter the address `http://localhost:4001/story` in your web browser, Django will parse that +field to figure out which page you want to go to. You tell it which patterns are relevant in the +file +[mygame/web/urls.py](https://github.com/evennia/evennia/blob/master/evennia/game_template/web/urls.py). +Open it now. + +Django looks for the variable `urlpatterns` in this file. You want to add your new pattern to the +`custom_patterns` list we have prepared - that is then merged with the default `urlpatterns`. Here's +how it could look: + +```python +from web import story + +# ... + +custom_patterns = [ + url(r'story', story.storypage, name='Story'), +] +``` + +That is, we import our story view module from where we created it earlier and then create an `url` +instance. The first argument to `url` is the pattern of the url we want to find (`"story"`) (this is +a regular expression if you are familiar with those) and then our view function we want to direct +to. + +That should be it. Reload Evennia and you should be able to browse to your new story page! diff --git a/docs/source/Add-a-wiki-on-your-website.md b/docs/source/Add-a-wiki-on-your-website.md new file mode 100644 index 0000000000..91767f6d82 --- /dev/null +++ b/docs/source/Add-a-wiki-on-your-website.md @@ -0,0 +1,211 @@ +# Add a wiki on your website + + +**Before doing this tutorial you will probably want to read the intro in +[Basic Web tutorial](Web-Tutorial).** Reading the three first parts of the +[Django tutorial](https://docs.djangoproject.com/en/1.9/intro/tutorial01/) might help as well. + +This tutorial will provide a step-by-step process to installing a wiki on your website. +Fortunately, you don't have to create the features manually, since it has been done by others, and +we can integrate their work quite easily with Django. I have decided to focus on +the [Django-wiki](http://django-wiki.readthedocs.io/). + +> Note: this article has been updated for Evennia 0.9. If you're not yet using this version, be careful, as the django wiki doesn't support Python 2 anymore. (Remove this note when enough time has passed.) + +The [Django-wiki](http://django-wiki.readthedocs.io/) offers a lot of features associated with wikis, is +actively maintained (at this time, anyway), and isn't too difficult to install in Evennia. You can +see a [demonstration of Django-wiki here](https://demo.django.wiki). + +## Basic installation + +You should begin by shutting down the Evennia server if it is running. We will run migrations and +alter the virtual environment just a bit. Open a terminal and activate your Python environment, the +one you use to run the `evennia` command. + +* On Linux: + ``` + source evenv/bin/activate + ``` +* Or Windows: + ``` + evenv\bin\activate + ``` + +### Installing with pip + +Install the wiki using pip: + + pip install wiki + +> Note: this will install the last version of Django wiki. Version >0.4 doesn't support Python 2, so install wiki 0.3 if you haven't updated to Python 3 yet. + +It might take some time, the Django-wiki having some dependencies. + +### Adding the wiki in the settings + +You will need to add a few settings to have the wiki app on your website. Open your +`server/conf/settings.py` file and add the following at the bottom (but before importing `secret_settings`). Here's what you'll find in my own setting file (add the whole Django-wiki section): + +```python +r""" +Evennia settings file. + +... + +""" + +# Use the defaults from Evennia unless explicitly overridden +from evennia.settings_default import * + +###################################################################### +# Evennia base server config +###################################################################### + +# This is the name of your game. Make it catchy! +SERVERNAME = "demowiki" + +###################################################################### +# Django-wiki settings +###################################################################### +INSTALLED_APPS += ( + 'django.contrib.humanize.apps.HumanizeConfig', + 'django_nyt.apps.DjangoNytConfig', + 'mptt', + 'sorl.thumbnail', + 'wiki.apps.WikiConfig', + 'wiki.plugins.attachments.apps.AttachmentsConfig', + 'wiki.plugins.notifications.apps.NotificationsConfig', + 'wiki.plugins.images.apps.ImagesConfig', + 'wiki.plugins.macros.apps.MacrosConfig', +) + +# Disable wiki handling of login/signup +WIKI_ACCOUNT_HANDLING = False +WIKI_ACCOUNT_SIGNUP_ALLOWED = False + +###################################################################### +# Settings given in secret_settings.py override those in this file. +###################################################################### +try: + from server.conf.secret_settings import * +except ImportError: + print("secret_settings.py file not found or failed to import.") +``` + +### Adding the new URLs + +Next we need to add two URLs in our `web/urls.py` file. Open it and compare the following output: +you will need to add two URLs in `custom_patterns` and add one import line: + +```python +from django.conf.urls import url, include +from django.urls import path # NEW! + +# default evenni a patterns +from evennia.web.urls import urlpatterns + +# eventual custom patterns +custom_patterns = [ + # url(r'/desired/url/', view, name='example'), + url('notifications/', include('django_nyt.urls')), # NEW! + url('wiki/', include('wiki.urls')), # NEW! +] + +# this is required by Django. +urlpatterns = custom_patterns + urlpatterns +``` + +You will probably need to copy line 2, 10, and 11. Be sure to place them correctly, as shown in +the example above. + +### Running migrations + +It's time to run the new migrations. The wiki app adds a few tables in our database. We'll need to +run: + + evennia migrate + +And that's it, you can start the server. If you go to http://localhost:4001/wiki , you should see +the wiki. Use your account's username and password to connect to it. That's how simple it is. + +## Customizing privileges + +A wiki can be a great collaborative tool, but who can see it? Who can modify it? Django-wiki comes +with a privilege system centered around four values per wiki page. The owner of an article can +always read and write in it (which is somewhat logical). The group of the article defines who can +read and who can write, if the user seeing the page belongs to this group. The topic of groups in +wiki pages will not be discussed here. A last setting determines which other user (that is, these +who aren't in the groups, and aren't the article's owner) can read and write. Each article has +these four settings (group read, group write, other read, other write). Depending on your purpose, +it might not be a good default choice, particularly if you have to remind every builder to keep the +pages private. Fortunately, Django-wiki gives us additional settings to customize who can read, and +who can write, a specific article. + +These settings must be placed, as usual, in your `server/conf/settings.py` file. They take a +function as argument, said function (or callback) will be called with the article and the user. +Remember, a Django user, for us, is an account. So we could check lockstrings on them if needed. +Here is a default setting to restrict the wiki: only builders can write in it, but anyone (including non-logged in users) can read it. The superuser has some additional privileges. + +```python +# In server/conf/settings.py +# ... + +def is_superuser(article, user): + """Return True if user is a superuser, False otherwise.""" + return not user.is_anonymous() and user.is_superuser + +def is_builder(article, user): + """Return True if user is a builder, False otherwise.""" + return not user.is_anonymous() and user.locks.check_lockstring(user, "perm(Builders)") + +def is_anyone(article, user): + """Return True even if the user is anonymous.""" + return True + +# Who can create new groups and users from the wiki? +WIKI_CAN_ADMIN = is_superuser +# Who can change owner and group membership? +WIKI_CAN_ASSIGN = is_superuser +# Who can change group membership? +WIKI_CAN_ASSIGN_OWNER = is_superuser +# Who can change read/write access to groups or others? +WIKI_CAN_CHANGE_PERMISSIONS = is_superuser +# Who can soft-delete an article? +WIKI_CAN_DELETE = is_builder +# Who can lock an article and permanently delete it? +WIKI_CAN_MODERATE = is_superuser +# Who can edit articles? +WIKI_CAN_WRITE = is_builder +# Who can read articles? +WIKI_CAN_READ = is_anyone +``` + +Here, we have created three functions: one to return `True` if the user is the superuser, one to +return `True` if the user is a builder, one to return `True` no matter what (this includes if the user is anonymous, E.G. if it's not logged-in). We then change settings to allow either the superuser or +each builder to moderate, read, write, delete, and more. You can, of course, add more functions, +adapting them to your need. This is just a demonstration. + +Providing the `WIKI_CAN*...` settings will bypass the original permission system. The superuser +could change permissions of an article, but still, only builders would be able to write it. If you +need something more custom, you will have to expand on the functions you use. + +### Managing wiki pages from Evennia + +Unfortunately, Django wiki doesn't provide a clear and clean entry point to read and write articles from Evennia and it doesn't seem to be a very high priority. If you really need to keep Django wiki and to create and manage wiki pages from your code, you can do so, but this article won't elaborate, as this is somewhat more technical. + +However, it is a good opportunity to present a small project that has been created more recently: [evennia-wiki](https://github.com/vincent-lg/evennia-wiki) has been created to provide a simple wiki, more tailored to Evennia and easier to connect. It doesn't, as yet, provide as many options as does Django wiki, but it's perfectly usable: + +- Pages have an inherent and much-easier to understand hierarchy based on URLs. +- Article permissions are connected to Evennia groups and are much easier to accommodate specific requirements. +- Articles can easily be created, read or updated from the Evennia code itself. +- Markdown is fully-supported with a default integration to Bootstrap to look good on an Evennia website. Tables and table of contents are supported as well as wiki links. +- The process to override wiki templates makes full use of the `template_overrides` directory. + +However evennia-wiki doesn't yet support: + +- Images in markdown and the uploading schema. If images are important to you, please consider contributing to this new project. +- Modifying permissions on a per page/setting basis. +- Moving pages to new locations. +- Viewing page history. + +Considering the list of features in Django wiki, obviously other things could be added to the list. However, these features may be the most important and useful. Additional ones might not be that necessary. If you're interested in supporting this little project, you are more than welcome to [contribute to it](https://github.com/vincent-lg/evennia-wiki). Thanks! \ No newline at end of file diff --git a/docs/source/Adding-Command-Tutorial.md b/docs/source/Adding-Command-Tutorial.md new file mode 100644 index 0000000000..4312e00dab --- /dev/null +++ b/docs/source/Adding-Command-Tutorial.md @@ -0,0 +1,163 @@ +# Adding Command Tutorial + +This is a quick first-time tutorial expanding on the [Commands](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](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](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](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](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. diff --git a/docs/source/Adding-Object-Typeclass-Tutorial.md b/docs/source/Adding-Object-Typeclass-Tutorial.md new file mode 100644 index 0000000000..82e397987c --- /dev/null +++ b/docs/source/Adding-Object-Typeclass-Tutorial.md @@ -0,0 +1,109 @@ +# 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](Typeclasses), which can be ignored for now. In +> `mygame/typeclasses` are also base typeclasses for out-of-character things, notably +> [Channels](Communications), [Accounts](Accounts) and [Scripts](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 and [Attribute](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](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](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()] + +``` diff --git a/docs/source/Administrative-Docs.md b/docs/source/Administrative-Docs.md new file mode 100644 index 0000000000..67cc94c44d --- /dev/null +++ b/docs/source/Administrative-Docs.md @@ -0,0 +1,43 @@ +# Administrative Docs + +The following pages are aimed at game administrators -- the higher-ups that possess shell access and are responsible for managing the game. + +### Installation and Early Life + +- [Choosing (and installing) an SQL Server](Choosing-An-SQL-Server) +- [Getting Started - Installing Evennia](Getting-Started) +- [Running Evennia in Docker Containers](Running-Evennia-in-Docker) +- [Starting, stopping, reloading and resetting Evennia](Start-Stop-Reload) +- [Keeping your game up to date](Updating-Your-Game) + - [Resetting your database](https://github.com/evennia/evennia/wiki/Updating%20Your%20Game#resetting-your-database) +- [Making your game available online](Online-Setup) + - [Hosting options](https://github.com/evennia/evennia/wiki/Online-Setup#hosting-options) + - [Securing your server with SSL/Let's Encrypt](https://github.com/evennia/evennia/wiki/Online-Setup#ssl) +- [Listing your game](Evennia-Game-Index) at the online [Evennia game index](http://games.evennia.com) + +### Customizing the server + +- [Changing the Settings](Server-Conf#Settings-file) + - [Available Master Settings](https://github.com/evennia/evennia/blob/master/evennia/settings_default.py) +- [Change Evennia's language](Internationalization) (internationalization) +- [Apache webserver configuration](Apache-Config) (optional) +- [Changing text encodings used by the server](Text-Encodings) +- [The Connection Screen](Connection-Screen) +- [Guest Logins](Guest-Logins) +- [How to connect Evennia to IRC channels](IRC) +- [How to connect Evennia to RSS feeds](RSS) +- [How to connect Evennia to Grapevine](Grapevine) +- [How to connect Evennia to Twitter](https://github.com/evennia/evennia/wiki/How-to-connect-Evennia-to-Twitter) + +### Administrating the running game + +- [Supported clients](Client-Support-Grid) (grid of known client issues) +- [Changing Permissions](Building-Permissions) of users +- [Banning](Banning) and deleting users + - [Summary of abuse-handling tools](https://github.com/evennia/evennia/wiki/Banning#summary-of-abuse-handling-tools) in the default cmdset + +### Working with Evennia + +- [Setting up your work environment with version control](Version-Control) +- [First steps coding with Evennia](First-Steps-Coding) +- [Setting up a continuous integration build environment](Continuous-Integration) diff --git a/docs/source/Apache-Config.md b/docs/source/Apache-Config.md new file mode 100644 index 0000000000..3a156940ac --- /dev/null +++ b/docs/source/Apache-Config.md @@ -0,0 +1,175 @@ +# Apache Config + + +**Warning**: This information is presented as a convenience, using another webserver than Evennia's +own is not directly supported and you are on your own if you want to do so. Evennia's webserver +works out of the box without any extra configuration and also runs in-process making sure to avoid +caching race conditions. The browser web client will most likely not work (at least not without +tweaking) on a third-party web server. + +One reason for wanting to use an external webserver like Apache would be to act as a *proxy* in +front of the Evennia webserver. Getting this working with TLS (encryption) requires some extra work +covered at the end of this page. + +Note that the Apache instructions below might be outdated. If something is not working right, or you +use Evennia with a different server, please let us know. Also, if there is a particular Linux distro +you would like covered, please let us know. + +## mod_wsgi Setup + +### Install mod_wsgi + +#### Fedora/RHEL +Apache HTTP Server and mod_wsgi are available in the standard package repositories for Fedora and RHEL: +``` +# dnf install httpd mod_wsgi +or +# yum install httpd mod_wsgi +``` + +#### Ubuntu/Debian +Apache HTTP Server and mod_wsgi are available in the standard package repositories for Ubuntu and Debian: +``` +# apt-get update +# apt-get install apache2 libapache2-mod-wsgi +``` + +### Copy and modify the VHOST + +After `mod_wsgi` is installed, copy the `evennia/web/utils/evennia_wsgi_apache.conf` file to your +apache2 vhosts/sites folder. On Debian/Ubuntu, this is `/etc/apache2/sites-enabled/`. Make your +modifications **after** copying the file there. + +Read the comments and change the paths to point to the appropriate locations within your setup. + +### Restart/Reload Apache + +You'll then want to reload or restart apache2 after changing the configurations. + +#### Fedora/RHEL/Ubuntu +``` +# systemctl restart httpd +``` + +#### Ubuntu/Debian +``` +# systemctl restart apache2 +``` + +### Enjoy + +With any luck, you'll be able to point your browser at your domain or subdomain that you set up in +your vhost and see the nifty default Evennia webpage. If not, read the hopefully informative error +message and work from there. Questions may be directed to our [Evennia Community +site](http://evennia.com). + +### A note on code reloading + +If your `mod_wsgi` is set up to run on daemon mode (as will be the case by default on Debian and +Ubuntu), you may tell `mod_wsgi` to reload by using the `touch` command on +`evennia/game/web/utils/apache_wsgi.conf`. When `mod_wsgi` sees that the file modification time has +changed, it will force a code reload. Any modifications to the code will not be propagated to the +live instance of your site until reloaded. + +If you are not running in daemon mode or want to force the issue, simply restart or reload apache2 to apply your changes. + +### Further notes and hints: + +If you get strange (and usually uninformative) `Permission denied` errors from Apache, make sure +that your `evennia` directory is located in a place the webserver may actually access. For example, +some Linux distributions may default to very restrictive access permissions on a user's `/home` +directory. + +One user commented that they had to add the following to their Apache config to get things to work. +Not confirmed, but worth trying if there are trouble. + + /evennia/game/web"> + Options +ExecCGI + Allow from all + + +## `mod_proxy` and `mod_ssl` setup + +Below are steps on running Evennia using a front-end proxy (Apache HTTP), `mod_proxy_http`, +`mod_proxy_wstunnel`, and `mod_ssl`. `mod_proxy_http` and `mod_proxy_wstunnel` will simply be referred to as +`mod_proxy` below. + +### Install `mod_ssl` + +#### Fedora/RHEL + +Apache HTTP Server and `mod_ssl` are available in the standard package repositories for Fedora and RHEL: + +``` +# dnf install httpd mod_ssl +or +# yum install httpd mod_ssl + +``` + +#### Ubuntu/Debian + +Apache HTTP Server and `mod_sslj`kl are installed together in the `apache2` package and available in the +standard package repositories for Ubuntu and Debian. `mod_ssl` needs to be enabled after installation: + +``` +# apt-get update +# apt-get install apache2 +# a2enmod ssl + +``` + +### TLS proxy+websocket configuration + +Below is a sample configuration for Evennia with a TLS-enabled http and websocket proxy. + +#### Apache HTTP Server Configuration + +``` + + # Always redirect to https/443 + ServerName mud.example.com + Redirect / https://mud.example.com + + + + ServerName mud.example.com + + SSLEngine On + + # Location of certificate and key + SSLCertificateFile /etc/pki/tls/certs/mud.example.com.crt + SSLCertificateKeyFile /etc/pki/tls/private/mud.example.com.key + + # Use a tool https://www.ssllabs.com/ssltest/ to scan your set after setting up. + SSLProtocol TLSv1.2 + SSLCipherSuite HIGH:!eNULL:!NULL:!aNULL + + # Proxy all websocket traffic to port 4002 in Evennia + ProxyPass /ws ws://127.0.0.1:4002/ + ProxyPassReverse /ws ws://127.0.0.1:4002/ + + # Proxy all HTTP traffic to port 4001 in Evennia + ProxyPass / http://127.0.0.1:4001/ + ProxyPassReverse / http://127.0.0.1:4001/ + + # Configure separate logging for this Evennia proxy + ErrorLog logs/evennia_error.log + CustomLog logs/evennia_access.log combined + +``` + +#### Evennia secure websocket configuration + +There is a slight trick in setting up Evennia so websocket traffic is handled correctly by the +proxy. You must set the `WEBSOCKET_CLIENT_URL` setting in your `mymud/server/conf/settings.py` file: + +``` +WEBSOCKET_CLIENT_URL = "wss://external.example.com/ws" +``` + +The setting above is what the client's browser will actually use. Note the use of `wss://` is +because our client will be communicating over an encrypted connection ("wss" indicates websocket +over SSL/TLS). Also, especially note the additional path `/ws` at the end of the URL. This is how +Apache HTTP Server identifies that a particular request should be proxied to Evennia's websocket +port but this should be applicable also to other types of proxies (like nginx). diff --git a/docs/source/Arxcode-installing-help.md b/docs/source/Arxcode-installing-help.md new file mode 100644 index 0000000000..d35b7a0d93 --- /dev/null +++ b/docs/source/Arxcode-installing-help.md @@ -0,0 +1,260 @@ +# Arxcode installing help + +## Introduction + +[Arx - After the Reckoning](http://play.arxmush.org/) is a big and very popular +[Evennia](http://www.evennia.com)-based game. Arx is heavily roleplaying-centric, relying on game +masters to drive the story. Technically it's maybe best described as "a MUSH, but with more coded +systems". In August of 2018, the game's developer, Tehom, generously released the [source code of +Arx on github](https://github.com/Arx-Game/arxcode). This is a treasure-trove for developers wanting +to pick ideas or even get a starting game to build on. These instructions are based on the Arx-code +released as of *Aug 12, 2018*. + +If you are not familiar with what Evennia is, you can read +[an introduction here](https://github.com/evennia/evennia/wiki/Evennia-Introduction). + +It's not too hard to run Arx from the sources (of course you'll start with an empty database) but +since part of Arx has grown organically, it doesn't follow standard Evennia paradigms everywhere. +This page covers one take on installing and setting things up while making your new Arx-based game +better match with the vanilla Evennia install. + +## Installing Evennia + +Firstly, set aside a folder/directory on your drive for everything to follow. + +You need to start by installing [Evennia](http://www.evennia.com) by following most of the [Getting Started +Instructions](Getting-Started) for your OS. The difference is that you need to `git clone https://github.com/TehomCD/evennia.git` instead of Evennia's repo because Arx uses TehomCD's older Evennia 0.8 [fork](https://github.com/TehomCD/evennia), notably still using Python2. This detail is important if referring to newer Evennia documentation. + +If you are new to Evennia it's *highly* recommended that you run through the +instructions in full - including initializing and starting a new empty game and connecting to it. +That way you can be sure Evennia works correctly as a base line. If you have trouble, make sure to +read the [Troubleshooting instructions](https://github.com/evennia/evennia/wiki/Getting-Started#troubleshooting) for your +operating system. You can also drop into our +[forums](https://groups.google.com/forum/#%21forum/evennia), join `#evennia` on `irc.freenode.net` +or chat from the linked [Discord Server](https://discord.gg/NecFePw). + +After installing you should have a `virtualenv` running and you should have the following file structure in your set-aside folder: + +``` +vienv/ +evennia/ +mygame/ + +``` + +Here `mygame` is the empty game you created during the Evennia install, with `evennia --init`. Go to +that and run `evennia stop` to make sure your empty game is not running. We'll instead let Evenna +run Arx, so in principle you could erase `mygame` - but it could also be good to have a clean game +to compare to. + +## Installing Arxcode + +### Clone the arxcode repo + +Cd to the root of your directory and clone the released source code from github: + + git clone https://github.com/Arx-Game/arxcode.git myarx + +A new folder `myarx` should appear next to the ones you already had. You could rename this to +something else if you want. + +Cd into `myarx`. If you wonder about the structure of the game dir, you can [read more about it here](Directory-Overview). + +### Clean up settings + +Arx has split evennia's normal settings into `base_settings.py` and `production_settings.py`. It +also has its own solution for managing 'secret' parts of the settings file. We'll keep most of Arx +way but remove the secret-handling and replace it with the normal Evennia method. + +Cd into `myarx/server/conf/` and open the file `settings.py` in a text editor. The top part (within +`"""..."""`) is just help text. Wipe everything underneath that and make it look like this instead +(don't forget to save): + +``` +from base_settings import * + +TELNET_PORTS = [4000] +SERVERNAME = "MyArx" +GAME_SLOGAN = "The cool game" + +try: + from server.conf.secret_settings import * +except ImportError: + print("secret_settings.py file not found or failed to import.") +``` + +> Note: Indents and capitalization matter in Python. Make indents 4 spaces (not tabs) for your own +> sanity. If you want a starter on Python in Evennia, [you can look here](Python-basic-introduction). + +This will import Arx' base settings and override them with the Evennia-default telnet port and give +the game a name. The slogan changes the sub-text shown under the name of your game in the website +header. You can tweak these to your own liking later. + +Next, create a new, empty file `secret_settings.py` in the same location as the `settings.py` file. +This can just contain the following: + +```python +SECRET_KEY = "sefsefiwwj3 jnwidufhjw4545_oifej whewiu hwejfpoiwjrpw09&4er43233fwefwfw" + +``` + +Replace the long random string with random ASCII characters of your own. The secret key should not +be shared. + +Next, open `myarx/server/conf/base_settings.py` in your text editor. We want to remove/comment out +all mentions of the `decouple` package, which Evennia doesn't use (we use `private_settings.py` to +hide away settings that should not be shared). + +Comment out `from decouple import config` by adding a `#` to the start of the line: `# from decouple +import config`. Then search for `config(` in the file and comment out all lines where this is used. +Many of these are specific to the server environment where the original Arx runs, so is not that +relevant to us. + +### Install Arx dependencies + +Arx has some further dependencies beyond vanilla Evennia. Start by `cd`:ing to the root of your +`myarx` folder. + +> If you run *Linux* or *Mac*: Edit `myarx/requirements.txt` and comment out the line +> `pypiwin32==219` - it's only needed on Windows and will give an error on other platforms. + +Make sure your `virtualenv` is active, then run + + pip install -r requirements.txt + +The needed Python packages will be installed for you. + +### Adding logs/ folder + +The Arx repo does not contain the `myarx/server/logs/` folder Evennia expects for storing server +logs. This is simple to add: + + # linux/mac + mkdir server/logs + # windows + mkdir server\logs + +### Setting up the database and starting + +From the `myarx` folder, run + + evennia migrate + +This creates the database and will step through all database migrations needed. + + evennia start + +If all goes well Evennia will now start up, running Arx! You can connect to it on `localhost` (or +`127.0.0.1` if your platform doesn't alias `localhost`), port `4000` using a Telnet client. +Alternatively, you can use your web browser to browse to `http://localhost:4001` to see the game's +website and get to the web client. + +When you log in you'll get the standard Evennia greeting (since the database is empty), but you can +try `help` to see that it's indeed Arx that is running. + +### Additional Setup Steps + +The first time you start Evennia after creating the database with the `evennia migrate` step above, +it should create a few starting objects for you - your superuser account, which it will prompt you +to enter, a starting room (Limbo), and a character object for you. If for some reason this does not +occur, you may have to follow the steps below. For the first time Superuser login you may have to +run steps 7-8 and 10 to create and connect to your in-came Character. + +1. Login to the game website with your Superuser account. +2. Press the `Admin` button to get into the (Django-) Admin Interface. +3. Navigate to the `Accounts` section. +4. Add a new Account named for the new staffer. Use a place holder password and dummy e-mail + address. +5. Flag account as `Staff` and apply the `Admin` permission group (This assumes you have already set + up an Admin Group in Django). +6. Add Tags named `player` and `developer`. +7. Log into the game using the web client (or a third-party telnet client) using your superuser + account. Move to where you want the new staffer character to appear. +8. In the game client, run `@create/drop :typeclasses.characters.Character`, where + `` is usually the same name you used for the Staffer account you created in the + Admin earlier (if you are creating a Character for your superuser, use your superuser account name). + This creates a new in-game Character and places it in your current location. +9. Have the new Admin player log into the game. +10. Have the new Admin puppet the character with `@ic StafferName`. +11. Have the new Admin change their password - `@password = `. + +Now that you have a Character and an Account object, there's a few additional things you may need to +do in order for some commands to function properly. You can either execute these as in-game commands +while `@ic` (controlling your character object). + +1. `@py from web.character.models import RosterEntry;RosterEntry.objects.create(player=self.player, character=self)` +2. `@py from world.dominion.models import PlayerOrNpc, AssetOwner;dompc = PlayerOrNpc.objects.create(player = self.player);AssetOwner.objects.create(player=dompc)` + +Those steps will give you a 'RosterEntry', 'PlayerOrNpc', and 'AssetOwner' objects. RosterEntry +explicitly connects a character and account object together, even while offline, and contains +additional information about a character's current presence in game (such as which 'roster' they're +in, if you choose to use an active roster of characters). PlayerOrNpc are more character extensions, +as well as support for npcs with no in-game presence and just represented by a name which can be +offscreen members of a character's family. It also allows for membership in Organizations. +AssetOwner holds information about a character or organization's money and resources. + +## Alternate guide by Pax for installing on Windows + +If for some reason you cannot use the Windows Subsystem for Linux (which would use instructions identical to the ones above), it's possible to get Evennia running under Anaconda for Windows. The process is a little bit trickier. + + Make sure you have: + * Git for Windows https://git-scm.com/download/win + * Anaconda for Windows https://www.anaconda.com/distribution/ + * VC++ Compiler for Python 2.7 http://aka.ms/vcpython27 + +conda update conda +conda create -n arx python=2.7 +source activate arx + + Set up a convenient repository place for things. + +cd ~ +mkdir Source +cd Source +mkdir Arx +cd Arx + + Replace the SSH git clone links below with your own github forks. + If you don't plan to change Evennia at all, you can use the + evennia/evennia.git repo instead of a forked one. + +git clone git@github.com:/evennia.git +git clone git@github.com:/arxcode.git + + Evennia is a package itself, so we want to install it and all of its + prerequisites, after switching to the appropriately-tagged branch for + Arxcode. + +cd evennia +git checkout tags/v0.7 -b arx-master +pip install -e . + + Arx has some dependencies of its own, so now we'll go install them + As it is not a package, we'll use the normal requirements file. + +cd ../arxcode +pip install -r requirements.txt + + The git repo doesn't include the empty log directory and Evennia is unhappy if you + don't have it, so while still in the arxcode directory... + +mkdir server/logs + + Now hit https://github.com/evennia/evennia/wiki/Arxcode-installing-help and + change the setup stuff as in the 'Clean up settings' section. + + Then we will create our default database... + +../evennia/bin/windows/evennia.bat migrate + + ...and do the first run. You need winpty because Windows does not have a TTY/PTY + by default, and so the Python console input commands (used for prompts on first + run) will fail and you will end up in an unhappy place. Future runs, you should + not need winpty. + +winpty ../evennia/bin/windows/evennia.bat start + + Once this is done, you should have your Evennia server running Arxcode up + on localhost at port 4000, and the webserver at http://localhost:4001/ + + And you are done! Huzzah! \ No newline at end of file diff --git a/docs/source/Async-Process.md b/docs/source/Async-Process.md new file mode 100644 index 0000000000..0e7a066476 --- /dev/null +++ b/docs/source/Async-Process.md @@ -0,0 +1,229 @@ +# Async Process + + +*This is considered an advanced topic.* + +## Synchronous versus Asynchronous + +Most program code operates *synchronously*. This means that each statement in your code gets +processed and finishes before the next can begin. This makes for easy-to-understand code. It is also +a *requirement* in many cases - a subsequent piece of code often depend on something calculated or +defined in a previous statement. + +Consider this piece of code in a traditional Python program: + +```python + print("before call ...") + long_running_function() + print("after call ...") + +``` + +When run, this will print `"before call ..."`, after which the `long_running_function` gets to work +for however long time. Only once that is done, the system prints `"after call ..."`. Easy and +logical to follow. Most of Evennia work in this way and often it's important that commands get +executed in the same strict order they were coded. + +Evennia, via Twisted, is a single-process multi-user server. In simple terms this means that it +swiftly switches between dealing with player input so quickly that each player feels like they do +things at the same time. This is a clever illusion however: If one user, say, runs a command +containing that `long_running_function`, *all* other players are effectively forced to wait until it +finishes. + +Now, it should be said that on a modern computer system this is rarely an issue. Very few commands +run so long that other users notice it. And as mentioned, most of the time you *want* to enforce +all commands to occur in strict sequence. + +When delays do become noticeable and you don't care in which order the command actually completes, +you can run it *asynchronously*. This makes use of the `run_async()` function in +`src/utils/utils.py`: + +```python + run_async(function, *args, **kwargs) +``` + +Where `function` will be called asynchronously with `*args` and `**kwargs`. Example: + +```python + from evennia import utils + print("before call ...") + utils.run_async(long_running_function) + print("after call ...") +``` + +Now, when running this you will find that the program will not wait around for +`long_running_function` to finish. In fact you will see `"before call ..."` and `"after call ..."` +printed out right away. The long-running function will run in the background and you (and other +users) can go on as normal. + +## Customizing asynchronous operation + +A complication with using asynchronous calls is what to do with the result from that call. What if +`long_running_function` returns a value that you need? It makes no real sense to put any lines of +code after the call to try to deal with the result from `long_running_function` above - as we saw +the `"after call ..."` got printed long before `long_running_function` was finished, making that +line quite pointless for processing any data from the function. Instead one has to use *callbacks*. + +`utils.run_async` takes reserved kwargs that won't be passed into the long-running function: + +- `at_return(r)` (the *callback*) is called when the asynchronous function (`long_running_function` + above) finishes successfully. The argument `r` will then be the return value of that function (or + `None`). + + ```python + def at_return(r): + print(r) + ``` + +- `at_return_kwargs` - an optional dictionary that will be fed as keyword arguments to the `at_return` callback. +- `at_err(e)` (the *errback*) is called if the asynchronous function fails and raises an exception. + This exception is passed to the errback wrapped in a *Failure* object `e`. If you do not supply an + errback of your own, Evennia will automatically add one that silently writes errors to the evennia + log. An example of an errback is found below: + +```python + def at_err(e): + print("There was an error:", str(e)) +``` + +- `at_err_kwargs` - an optional dictionary that will be fed as keyword arguments to the `at_err` + errback. + +An example of making an asynchronous call from inside a [Command](Commands) definition: + +```python + from evennia import utils, Command + + class CmdAsync(Command): + + key = "asynccommand" + + def func(self): + + def long_running_function(): + #[... lots of time-consuming code ...] + return final_value + + def at_return_function(r): + self.caller.msg("The final value is %s" % r) + + def at_err_function(e): + self.caller.msg("There was an error: %s" % e) + + # do the async call, setting all callbacks + utils.run_async(long_running_function, at_return=at_return_function, at_err=at_err_function) +``` + +That's it - from here on we can forget about `long_running_function` and go on with what else need +to be done. *Whenever* it finishes, the `at_return_function` function will be called and the final value will +pop up for us to see. If not we will see an error message. + +## delay + +The `delay` function is a much simpler sibling to `run_async`. It is in fact just a way to delay the +execution of a command until a future time. This is equivalent to something like `time.sleep()` +except delay is asynchronous while `sleep` would lock the entire server for the duration of the +sleep. + +```python + from evennia.utils import delay + + # [...] + # e.g. inside a Command, where `self.caller` is available + def callback(obj): + obj.msg("Returning!") + delay(10, callback, self.caller) +``` + +This will delay the execution of the callback for 10 seconds. This function is explored much more in +the [Command Duration Tutorial](Command-Duration). + +You can also try the following snippet just see how it works: + + @py from evennia.utils import delay; delay(10, lambda who: who.msg("Test!"), self) + +Wait 10 seconds and 'Test!' should be echoed back to you. + + +## The @interactive decorator + +As of Evennia 0.9, the `@interactive` [decorator](https://realpython.com/primer-on-python-decorators/) +is available. This makes any function or method possible to 'pause' and/or await player input +in an interactive way. + +```python + from evennia.utils import interactive + + @interactive + def myfunc(caller): + + while True: + caller.msg("Getting ready to wait ...") + yield(5) + caller.msg("Now 5 seconds have passed.") + + response = yield("Do you want to wait another 5 secs?") + + if response.lower() not in ("yes", "y"): + break +``` + +The `@interactive` decorator gives the function the ability to pause. The use +of `yield(seconds)` will do just that - it will asynchronously pause for the +number of seconds given before continuing. This is technically equivalent to +using `call_async` with a callback that continues after 5 secs. But the code +with `@interactive` is a little easier to follow. + +Within the `@interactive` function, the `response = yield("question")` question +allows you to ask the user for input. You can then process the input, just like +you would if you used the Python `input` function. There is one caveat to this +functionality though - _it will only work if the function/method has an +argument named exactly `caller`_. This is because internally Evennia will look +for the `caller` argument and treat that as the source of input. + +All of this makes the `@interactive` decorator very useful. But it comes with a +few caveats. Notably, decorating a function/method with `@interactive` turns it +into a Python [generator](https://wiki.python.org/moin/Generators). The most +common issue is that you cannot use `return ` from a generator (just an +empty `return` works). To return a value from a function/method you have decorated +with `@interactive`, you must instead use a special Twisted function +`twisted.internet.defer.returnValue`. Evennia also makes this function +conveniently available from `evennia.utils`: + +```python + from evennia.utils import interactive, returnValue + + @interactive + def myfunc(): + + # ... + result = 10 + + # this must be used instead of `return result` + returnValue(result) + +``` + + + +## Assorted notes + +Overall, be careful with choosing when to use asynchronous calls. It is mainly useful for large +administration operations that have no direct influence on the game world (imports and backup +operations come to mind). Since there is no telling exactly when an asynchronous call actually ends, +using them for in-game commands is to potentially invite confusion and inconsistencies (and very +hard-to-reproduce bugs). + +The very first synchronous example above is not *really* correct in the case of Twisted, which is +inherently an asynchronous server. Notably you might find that you will *not* see the first `before +call ...` text being printed out right away. Instead all texts could end up being delayed until +after the long-running process finishes. So all commands will retain their relative order as +expected, but they may appear with delays or in groups. + +## Further reading + +Technically, `run_async` is just a very thin and simplified wrapper around a +[Twisted Deferred](http://twistedmatrix.com/documents/9.0.0/core/howto/defer.html) object; the wrapper sets +up a default errback also if none is supplied. If you know what you are doing there is nothing +stopping you from bypassing the utility function, building a more sophisticated callback chain after +your own liking. diff --git a/docs/source/Attributes.md b/docs/source/Attributes.md new file mode 100644 index 0000000000..f49a533c7a --- /dev/null +++ b/docs/source/Attributes.md @@ -0,0 +1,376 @@ +# Attributes + + +When performing actions in Evennia it is often important that you store data for later. If you write +a menu system, you have to keep track of the current location in the menu tree so that the player +can give correct subsequent commands. If you are writing a combat system, you might have a +combattant's next roll get easier dependent on if their opponent failed. Your characters will +probably need to store roleplaying-attributes like strength and agility. And so on. + +[Typeclassed](Typeclasses) game entities ([Accounts](Accounts), [Objects](Objects), +[Scripts](Scripts) and [Channels](Communications)) always have *Attributes* associated with them. +Attributes are used to store any type of data 'on' such entities. This is different from storing +data in properties already defined on entities (such as `key` or `location`) - these have very +specific names and require very specific types of data (for example you couldn't assign a python +*list* to the `key` property no matter how hard you tried). `Attributes` come into play when you +want to assign arbitrary data to arbitrary names. + +## The .db and .ndb shortcuts + +To save persistent data on a Typeclassed object you normally use the `db` (DataBase) operator. Let's +try to save some data to a *Rose* (an [Object](Objects)): + +```python + # saving + rose.db.has_thorns = True + # getting it back + is_ouch = rose.db.has_thorns + +``` + +This looks like any normal Python assignment, but that `db` makes sure that an *Attribute* is +created behind the scenes and is stored in the database. Your rose will continue to have thorns +throughout the life of the server now, until you deliberately remove them. + +To be sure to save **non-persistently**, i.e. to make sure NOT to create a database entry, you use +`ndb` (NonDataBase). It works in the same way: + +```python + # saving + rose.ndb.has_thorns = True + # getting it back + is_ouch = rose.ndb.has_thorns +``` + +Technically, `ndb` has nothing to do with `Attributes`, despite how similar they look. No +`Attribute` object is created behind the scenes when using `ndb`. In fact the database is not +invoked at all since we are not interested in persistence. There is however an important reason to +use `ndb` to store data rather than to just store variables direct on entities - `ndb`-stored data +is tracked by the server and will not be purged in various cache-cleanup operations Evennia may do +while it runs. Data stored on `ndb` (as well as `db`) will also be easily listed by example the +`@examine` command. + +You can also `del` properties on `db` and `ndb` as normal. This will for example delete an `Attribute`: + +```python + del rose.db.has_thorns +``` + +Both `db` and `ndb` defaults to offering an `all()` method on themselves. This returns all +associated attributes or non-persistent properties. + +```python + list_of_all_rose_attributes = rose.db.all() + list_of_all_rose_ndb_attrs = rose.ndb.all() +``` + +If you use `all` as the name of an attribute, this will be used instead. Later deleting your custom +`all` will return the default behaviour. + +## The AttributeHandler + +The `.db` and `.ndb` properties are very convenient but if you don't know the name of the Attribute +beforehand they cannot be used. Behind the scenes `.db` actually accesses the `AttributeHandler` +which sits on typeclassed entities as the `.attributes` property. `.ndb` does the same for the +`.nattributes` property. + +The handlers have normal access methods that allow you to manage and retrieve `Attributes` and +`NAttributes`: + +- `has('attrname')` - this checks if the object has an Attribute with this key. This is equivalent + to doing `obj.db.attrname`. +- `get(...)` - this retrieves the given Attribute. Normally the `value` property of the Attribute is + returned, but the method takes keywords for returning the Attribute object itself. By supplying an + `accessing_object` to the call one can also make sure to check permissions before modifying + anything. +- `add(...)` - this adds a new Attribute to the object. An optional [lockstring](Locks) can be + supplied here to restrict future access and also the call itself may be checked against locks. +- `remove(...)` - Remove the given Attribute. This can optionally be made to check for permission + before performing the deletion. - `clear(...)` - removes all Attributes from object. +- `all(...)` - returns all Attributes (of the given category) attached to this object. + +See [this section](https://github.com/evennia/evennia/wiki/Attributes#locking-and-checking-attributes) for more about locking down Attribute +access and editing. The `Nattribute` offers no concept of access control. + +Some examples: + +```python + import evennia + obj = evennia.search_object("MyObject") + + obj.attributes.add("test", "testvalue") + print(obj.db.test) # prints "testvalue" + print(obj.attributes.get("test")) # " + print(obj.attributes.all()) # prints [] + obj.attributes.remove("test") +``` + + +## Properties of Attributes + +An Attribute object is stored in the database. It has the following properties: + +- `key` - the name of the Attribute. When doing e.g. `obj.db.attrname = value`, this property is set + to `attrname`. +- `value` - this is the value of the Attribute. This value can be anything which can be pickled - + objects, lists, numbers or what have you (see + [this section](Attributes#What_types_of_data_can_I_save_in_an_Attribute) for more info). In the example + `obj.db.attrname = value`, the `value` is stored here. +- `category` - this is an optional property that is set to None for most Attributes. Setting this + allows to use Attributes for different functionality. This is usually not needed unless you want + to use Attributes for very different functionality ([Nicks](Nicks) is an example of using Attributes + in this way). To modify this property you need to use the [Attribute Handler](Attributes#The_Attribute_Handler). +- `strvalue` - this is a separate value field that only accepts strings. This severely limits the + data possible to store, but allows for easier database lookups. This property is usually not used + except when re-using Attributes for some other purpose ([Nicks](Nicks) use it). It is only + accessible via the [Attribute Handler](Attributes#The_Attribute_Handler). + +There are also two special properties: + +- `attrtype` - this is used internally by Evennia to separate [Nicks](Nicks), from Attributes (Nicks + use Attributes behind the scenes). +- `model` - this is a *natural-key* describing the model this Attribute is attached to. This is on + the form *appname.modelclass*, like `objects.objectdb`. It is used by the Attribute and + NickHandler to quickly sort matches in the database. Neither this nor `attrtype` should normally + need to be modified. + +Non-database attributes have no equivalence to `category` nor `strvalue`, `attrtype` or `model`. + +## Persistent vs non-persistent + +So *persistent* data means that your data will survive a server reboot, whereas with +*non-persistent* data it will not ... + +... So why would you ever want to use non-persistent data? The answer is, you don't have to. Most of +the time you really want to save as much as you possibly can. Non-persistent data is potentially +useful in a few situations though. + +- You are worried about database performance. Since Evennia caches Attributes very aggressively, + this is not an issue unless you are reading *and* writing to your Attribute very often (like many + times per second). Reading from an already cached Attribute is as fast as reading any Python + property. But even then this is not likely something to worry about: Apart from Evennia's own + caching, modern database systems themselves also cache data very efficiently for speed. Our default + database even runs completely in RAM if possible, alleviating much of the need to write to disk + during heavy loads. +- A more valid reason for using non-persistent data is if you *want* to lose your state when logging + off. Maybe you are storing throw-away data that are re-initialized at server startup. Maybe you + are implementing some caching of your own. Or maybe you are testing a buggy [Script](Scripts) that + does potentially harmful stuff to your character object. With non-persistent storage you can be sure + that whatever is messed up, it's nothing a server reboot can't clear up. +- NAttributes have no restrictions at all on what they can store (see next section), since they + don't need to worry about being saved to the database - they work very well for temporary storage. +- You want to implement a fully or partly *non-persistent world*. Who are we to argue with your + grand vision! + +## What types of data can I save in an Attribute? + +> None of the following affects NAttributes, which does not invoke the database at all. There are no +> restrictions to what can be stored in a NAttribute. + +The database doesn't know anything about Python objects, so Evennia must *serialize* Attribute +values into a string representation in order to store it to the database. This is done using the +`pickle` module of Python (the only exception is if you use the `strattr` keyword of the +AttributeHandler to save to the `strvalue` field of the Attribute. In that case you can only save +*strings* which will not be pickled). + +It's important to note that when you access the data in an Attribute you are *always* de-serializing +it from the database representation every time. This is because we allow for storing +database-entities in Attributes too. If we cached it as its Python form, we might end up with +situations where the database entity was deleted since we last accessed the Attribute. +De-serializing data with a database-entity in it means querying the database for that object and +making sure it still exists (otherwise it will be set to `None`). Performance-wise this is usually +not a big deal. But if you are accessing the Attribute as part of some big loop or doing a large +amount of reads/writes you should first extract it to a temporary variable, operate on *that* and +then save the result back to the Attribute. If you are storing a more complex structure like a +`dict` or a `list` you should make sure to "disconnect" it from the database before looping over it, +as mentioned in the [Retrieving Mutable Objects](#retrieving-mutable-objects) section below. + +### Storing single objects + +With a single object, we mean anything that is *not iterable*, like numbers, strings or custom class instances without the `__iter__` method. + +* You can generally store any non-iterable Python entity that can be + [pickled](http://docs.python.org/library/pickle.html). +* Single database objects/typeclasses can be stored as any other in the Attribute. These can + normally *not* be pickled, but Evennia will behind the scenes convert them to an internal + representation using their classname, database-id and creation-date with a microsecond precision, + guaranteeing you get the same object back when you access the Attribute later. +* If you *hide* a database object inside a non-iterable custom class (like stored as a variable + inside it), Evennia will not know it's there and won't convert it safely. Storing classes with + such hidden database objects is *not* supported and will lead to errors! + +```python +# Examples of valid single-value attribute data: +obj.db.test1 = 23 +obj.db.test1 = False +# a database object (will be stored as an internal representation) +obj.db.test2 = myobj + +# example of an invalid, "hidden" dbobject +class Invalid(object): + def __init__(self, dbobj): + # no way for Evennia to know this is a dbobj + self.dbobj = dbobj +invalid = Invalid(myobj) +obj.db.invalid = invalid # will cause error! +``` + +### Storing multiple objects + +This means storing objects in a collection of some kind and are examples of *iterables*, pickle-able +entities you can loop over in a for-loop. Attribute-saving supports the following iterables: + +* [Tuples](https://docs.python.org/2/library/functions.html#tuple), like `(1,2,"test", )`. +* [Lists](https://docs.python.org/2/tutorial/datastructures.html#more-on-lists), like `[1,2,"test", ]`. +* [Dicts](https://docs.python.org/2/tutorial/datastructures.html#dictionaries), like `{1:2, "test":]`. +* [Sets](https://docs.python.org/2/tutorial/datastructures.html#sets), like `{1,2,"test",}`. +* [collections.OrderedDict](https://docs.python.org/2/library/collections.html#collections.OrderedDict), like `OrderedDict((1,2), ("test", ))`. +* [collections.Deque](https://docs.python.org/2/library/collections.html#collections.deque), like `deque((1,2,"test",))`. +* *Nestings* of any combinations of the above, like lists in dicts or an OrderedDict of tuples, each containing dicts, etc. +* All other iterables (i.e. entities with the `__iter__` method) will be converted to a *list*. + Since you can use any combination of the above iterables, this is generally not much of a + limitation. + +Any entity listed in the [Single object](Attributes#Storing-Single-Objects) section above can be stored in the iterable. + +> As mentioned in the previous section, database entities (aka typeclasses) are not possible to +> pickle. So when storing an iterable, Evennia must recursively traverse the iterable *and all its +> nested sub-iterables* in order to find eventual database objects to convert. This is a very fast +> process but for efficiency you may want to avoid too deeply nested structures if you can. + +```python +# examples of valid iterables to store +obj.db.test3 = [obj1, 45, obj2, 67] +# a dictionary +obj.db.test4 = {'str':34, 'dex':56, 'agi':22, 'int':77} +# a mixed dictionary/list +obj.db.test5 = {'members': [obj1,obj2,obj3], 'enemies':[obj4,obj5]} +# a tuple with a list in it +obj.db.test6 = (1,3,4,8, ["test", "test2"], 9) +# a set +obj.db.test7 = set([1,2,3,4,5]) +# in-situ manipulation +obj.db.test8 = [1,2,{"test":1}] +obj.db.test8[0] = 4 +obj.db.test8[2]["test"] = 5 +# test8 is now [4,2,{"test":5}] +``` + +### Retrieving Mutable objects + +A side effect of the way Evennia stores Attributes is that *mutable* iterables (iterables that can +be modified in-place after they were created, which is everything except tuples) are handled by +custom objects called `_SaverList`, `_SaverDict` etc. These `_Saver...` classes behave just like the +normal variant except that they are aware of the database and saves to it whenever new data gets +assigned to them. This is what allows you to do things like `self.db.mylist[7] = val` and be sure +that the new version of list is saved. Without this you would have to load the list into a temporary +variable, change it and then re-assign it to the Attribute in order for it to save. + +There is however an important thing to remember. If you retrieve your mutable iterable into another +variable, e.g. `mylist2 = obj.db.mylist`, your new variable (`mylist2`) will *still* be a +`_SaverList`. This means it will continue to save itself to the database whenever it is updated! + + +```python + obj.db.mylist = [1,2,3,4] + mylist = obj.db.mylist + mylist[3] = 5 # this will also update database + print(mylist) # this is now [1,2,3,5] + print(obj.db.mylist) # this is also [1,2,3,5] +``` + +To "disconnect" your extracted mutable variable from the database you simply need to convert the +`_Saver...` iterable to a normal Python structure. So to convert a `_SaverList`, you use the +`list()` function, for a `_SaverDict` you use `dict()` and so on. + +```python + obj.db.mylist = [1,2,3,4] + mylist = list(obj.db.mylist) # convert to normal list + mylist[3] = 5 + print(mylist) # this is now [1,2,3,5] + print(obj.db.mylist) # this is still [1,2,3,4] +``` + +A further problem comes with *nested mutables*, like a dict containing lists of dicts or something +like that. Each of these nested mutables would be `_Saver*` structures connected to the database and +disconnecting the outermost one of them would not disconnect those nested within. To make really +sure you disonnect a nested structure entirely from the database, Evennia provides a special +function `evennia.utils.dbserialize.deserialize`: + +``` +from evennia.utils.dbserialize import deserialize + +decoupled_mutables = deserialize(nested_mutables) + +``` + +The result of this operation will be a structure only consisting of normal Python mutables (`list` +instead of `_SaverList` and so on). + + +Remember, this is only valid for *mutable* iterables. +[Immutable](http://en.wikipedia.org/wiki/Immutable) objects (strings, numbers, tuples etc) are +already disconnected from the database from the onset. + +```python + obj.db.mytup = (1,2,[3,4]) + obj.db.mytup[0] = 5 # this fails since tuples are immutable + + # this works but will NOT update database since outermost is a tuple + obj.db.mytup[2][1] = 5 + print(obj.db.mytup[2][1]) # this still returns 4, not 5 + + mytup1 = obj.db.mytup # mytup1 is already disconnected from database since outermost + # iterable is a tuple, so we can edit the internal list as we want + # without affecting the database. +``` + +> Attributes will fetch data fresh from the database whenever you read them, so +> if you are performing big operations on a mutable Attribute property (such as looping over a list +> or dict) you should make sure to "disconnect" the Attribute's value first and operate on this +> rather than on the Attribute. You can gain dramatic speed improvements to big loops this +> way. + + +## Locking and checking Attributes + +Attributes are normally not locked down by default, but you can easily change that for individual +Attributes (like those that may be game-sensitive in games with user-level building). + +First you need to set a *lock string* on your Attribute. Lock strings are specified [Locks](Locks). The relevant lock types are + +- `attrread` - limits who may read the value of the Attribute +- `attredit` - limits who may set/change this Attribute + +You cannot use the `db` handler to modify Attribute object (such as setting a lock on them) - The +`db` handler will return the Attribute's *value*, not the Attribute object itself. Instead you use +the AttributeHandler and set it to return the object instead of the value: + +```python + lockstring = "attread:all();attredit:perm(Admins)" + obj.attributes.get("myattr", return_obj=True).locks.add(lockstring) +``` + +Note the `return_obj` keyword which makes sure to return the `Attribute` object so its LockHandler +could be accessed. + +A lock is no good if nothing checks it -- and by default Evennia does not check locks on Attributes. +You have to add a check to your commands/code wherever it fits (such as before setting an +Attribute). + +```python + # in some command code where we want to limit + # setting of a given attribute name on an object + attr = obj.attributes.get(attrname, + return_obj=True, + accessing_obj=caller, + default=None, + default_access=False) + if not attr: + caller.msg("You cannot edit that Attribute!") + return + # edit the Attribute here +``` + +The same keywords are available to use with `obj.attributes.set()` and `obj.attributes.remove()`, +those will check for the `attredit` lock type. diff --git a/docs/source/Banning.md b/docs/source/Banning.md new file mode 100644 index 0000000000..56b86968f8 --- /dev/null +++ b/docs/source/Banning.md @@ -0,0 +1,112 @@ +# Banning + + +Whether due to abuse, blatant breaking of your rules, or some other reason, you will eventually find +no other recourse but to kick out a particularly troublesome player. The default command set has +admin tools to handle this, primarily `@ban`, `@unban`, and `@boot`. + +## Creating a ban + +Say we have a troublesome player "YouSuck" - this is a person that refuses common courtesy - an abusive +and spammy account that is clearly created by some bored internet hooligan only to cause grief. You +have tried to be nice. Now you just want this troll gone. + +### Name ban + +The easiest recourse is to block the account YouSuck from ever connecting again. + + @ban YouSuck + +This will lock the name YouSuck (as well as 'yousuck' and any other capitalization combination), and next time they try to log in with this name the server will not let them! + +You can also give a reason so you remember later why this was a good thing (the banned account will never see this) + + @ban YouSuck:This is just a troll. + +If you are sure this is just a spam account, you might even consider deleting the player account outright: + + @delaccount YouSuck + +Generally, banning the name is the easier and safer way to stop the use of an account -- if you change your mind you can always remove the block later whereas a deletion is permanent. + +### IP ban + +Just because you block YouSuck's name might not mean the trolling human behind that account gives up. They can just create a new account YouSuckMore and be back at it. One way to make things harder for them is to tell the server to not allow connections from their particular IP address. + +First, when the offending account is online, check which IP address they use. This you can do with the `who` command, which will show you something like this: + + Account Name On for Idle Room Cmds Host + YouSuckMore 01:12 2m 22 212 237.333.0.223 + +The "Host" bit is the IP address from which the account is connecting. Use this to define the ban instead of the name: + + @ban 237.333.0.223 + +This will stop YouSuckMore connecting from their computer. Note however that IP address might change easily - either due to how the player's Internet Service Provider operates or by the user simply changing computers. You can make a more general ban by putting asterisks `*` as wildcards for the groups of three digits in the address. So if you figure out that !YouSuckMore mainly connects from 237.333.0.223, 237.333.0.225, and 237.333.0.256 (only changes in their subnet), it might be an idea to put down a ban like this to include any number in that subnet: + + @ban 237.333.0.* + +You should combine the IP ban with a name-ban too of course, so the account YouSuckMore is truly locked regardless of where they connect from. + +Be careful with too general IP bans however (more asterisks above). If you are unlucky you could be blocking out innocent players who just happen to connect from the same subnet as the offender. + +## Booting + +YouSuck is not really noticing all this banning yet though - and won't until having logged out and trying to log back in again. Let's help the troll along. + + @boot YouSuck + +Good riddance. You can give a reason for booting too (to be echoed to the player before getting kicked out). + + @boot YouSuck:Go troll somewhere else. + +### Lifting a ban + +Use the `@unban` (or `@ban`) command without any arguments and you will see a list of all currently active bans: + + Active bans + id name/ip date reason + 1 yousuck Fri Jan 3 23:00:22 2020 This is just a Troll. + 2 237.333.0.* Fri Jan 3 23:01:03 2020 YouSuck's IP. + +Use the `id` from this list to find out which ban to lift. + + @unban 2 + + Cleared ban 2: 237.333.0.* + +## Summary of abuse-handling tools + +Below are other useful commands for dealing with annoying players. + +- **who** -- (as admin) Find the IP of a account. Note that one account can be connected to from multiple IPs depending on what you allow in your settings. +- **examine/account thomas** -- Get all details about an account. You can also use `*thomas` to get the account. If not given, you will get the *Object* thomas if it exists in the same location, which is not what you want in this case. +- **boot thomas** -- Boot all sessions of the given account name. +- **boot 23** -- Boot one specific client session/IP by its unique id. +- **ban** -- List all bans (listed with ids) +- **ban thomas** -- Ban the user with the given account name +- **ban/ip `134.233.2.111`** -- Ban by IP +- **ban/ip `134.233.2.*`** -- Widen IP ban +- **ban/ip `134.233.*.*`** -- Even wider IP ban +- **unban 34** -- Remove ban with id #34 + +- **cboot mychannel = thomas** -- Boot a subscriber from a channel you control +- **clock mychannel = control:perm(Admin);listen:all();send:all()** -- Fine control of access to your channel using [lock definitions](https://github.com/evennia/evennia/wiki/Locks). + +Locking a specific command (like `page`) is accomplished like so: +1. Examine the source of the command. [The default `page` command class]( https://github.com/evennia/evennia/blob/master/evennia/commands/default/comms.py#L686) has the lock string **"cmd:not pperm(page_banned)"**. This means that unless the player has the 'permission' "page_banned" they can use this command. You can assign any lock string to allow finer customization in your commands. You might look for the value of an [Attribute](https://github.com/evennia/evennia/wiki/Attributes) or [Tag](https://github.com/evennia/evennia/wiki/Tags), your current location etc. +2. **perm/account thomas = page_banned** -- Give the account the 'permission' which causes (in this case) the lock to fail. + +- **perm/del/account thomas = page_banned** -- Remove the given permission + +- **tel thomas = jail** -- Teleport a player to a specified location or #dbref +- **type thomas = FlowerPot** -- Turn an annoying player into a flower pot (assuming you have a `FlowerPot` typeclass ready) +- **userpassword thomas = fooBarFoo** -- Change a user's password +- **delaccount thomas** -- Delete a player account (not recommended, use **ban** instead) + +- **server** -- Show server statistics, such as CPU load, memory usage, and how many objects are cached +- **time** -- Gives server uptime, runtime, etc +- **reload** -- Reloads the server without disconnecting anyone +- **reset** -- Restarts the server, kicking all connections +- **shutdown** -- Stops the server cold without it auto-starting again +- **py** -- Executes raw Python code, allows for direct inspection of the database and account objects on the fly. For advanced users. diff --git a/docs/source/Batch-Code-Processor.md b/docs/source/Batch-Code-Processor.md new file mode 100644 index 0000000000..b52acfe65f --- /dev/null +++ b/docs/source/Batch-Code-Processor.md @@ -0,0 +1,158 @@ +# Batch Code Processor + + +For an introduction and motivation to using batch processors, see [here](Batch-Processors). This page describes the Batch-*code* processor. The Batch-*command* one is covered [here](Batch-Command-Processor). + +## Basic Usage + +The batch-code processor is a superuser-only function, invoked by + + > @batchcode path.to.batchcodefile + +Where `path.to.batchcodefile` is the path to a *batch-code file*. Such a file should have a name ending in "`.py`" (but you shouldn't include that in the path). The path is given like a python path relative to a folder you define to hold your batch files, set by `BATCH_IMPORT_PATH` in your settings. Default folder is (assuming your game is called "mygame") `mygame/world/`. So if you want to run the example batch file in `mygame/world/batch_code.py`, you could simply use + + > @batchcode batch_code + +This will try to run through the entire batch file in one go. For more gradual, *interactive* control you can use the `/interactive` switch. The switch `/debug` will put the processor in *debug* mode. Read below for more info. + +## The batch file + +A batch-code file is a normal Python file. The difference is that since the batch processor loads and executes the file rather than importing it, you can reliably update the file, then call it again, over and over and see your changes without needing to `@reload` the server. This makes for easy testing. In the batch-code file you have also access to the following global variables: + +- `caller` - This is a reference to the object running the batchprocessor. +- `DEBUG` - This is a boolean that lets you determine if this file is currently being run in debug-mode or not. See below how this can be useful. + +Running a plain Python file through the processor will just execute the file from beginning to end. If you want to get more control over the execution you can use the processor's *interactive* mode. This runs certain code blocks on their own, rerunning only that part until you are happy with it. In order to do this you need to add special markers to your file to divide it up into smaller chunks. These take the form of comments, so the file remains valid Python. + +Here are the rules of syntax of the batch-code `*.py` file. + +- `#CODE` as the first on a line marks the start of a *code* block. It will last until the beginning of another marker or the end of the file. Code blocks contain functional python code. Each `#CODE` block will be run in complete isolation from other parts of the file, so make sure it's self-contained. +- `#HEADER` as the first on a line marks the start of a *header* block. It lasts until the next marker or the end of the file. This is intended to hold imports and variables you will need for all other blocks .All python code defined in a header block will always be inserted at the top of every `#CODE` blocks in the file. You may have more than one `#HEADER` block, but that is equivalent to having one big one. Note that you can't exchange data between code blocks, so editing a header-variable in one code block won't affect that variable in any other code block! +- `#INSERT path.to.file` will insert another batchcode (Python) file at that position. +- A `#` that is not starting a `#HEADER`, `#CODE` or `#INSERT` instruction is considered a comment. +- Inside a block, normal Python syntax rules apply. For the sake of indentation, each block acts as a separate python module. + +Below is a version of the example file found in `evennia/contrib/tutorial_examples/`. + +```python + # + # This is an example batch-code build file for Evennia. + # + + #HEADER + + # This will be included in all other #CODE blocks + + from evennia import create_object, search_object + from evennia.contrib.tutorial_examples import red_button + from typeclasses.objects import Object + + limbo = search_object('Limbo')[0] + + + #CODE + + red_button = create_object(red_button.RedButton, key="Red button", + location=limbo, aliases=["button"]) + + # caller points to the one running the script + caller.msg("A red button was created.") + + # importing more code from another batch-code file + #INSERT batch_code_insert + + #CODE + + table = create_object(Object, key="Blue Table", location=limbo) + chair = create_object(Object, key="Blue Chair", location=limbo) + + string = "A %s and %s were created." + if DEBUG: + table.delete() + chair.delete() + string += " Since debug was active, " \ + "they were deleted again." + caller.msg(string % (table, chair)) +``` + +This uses Evennia's Python API to create three objects in sequence. + +## Debug mode + +Try to run the example script with + + > @batchcode/debug tutorial_examples.example_batch_code + +The batch script will run to the end and tell you it completed. You will also get messages that the button and the two pieces of furniture were created. Look around and you should see the button there. But you won't see any chair nor a table! This is because we ran this with the `/debug` switch, which is directly visible as `DEBUG==True` inside the script. In the above example we handled this state by deleting the chair and table again. + +The debug mode is intended to be used when you test out a batchscript. Maybe you are looking for bugs in your code or try to see if things behave as they should. Running the script over and over would then create an ever-growing stack of chairs and tables, all with the same name. You would have to go back and painstakingly delete them later. + +## Interactive mode + +Interactive mode works very similar to the [batch-command processor counterpart](Batch-Command-Processor). It allows you more step-wise control over how the batch file is executed. This is useful for debugging or for picking and choosing only particular blocks to run. Use `@batchcode` with the `/interactive` flag to enter interactive mode. + + > @batchcode/interactive tutorial_examples.batch_code + +You should see the following: + + 01/02: red_button = create_object(red_button.RedButton, [...] (hh for help) + +This shows that you are on the first `#CODE` block, the first of only two commands in this batch file. Observe that the block has *not* actually been executed at this point! + +To take a look at the full code snippet you are about to run, use `ll` (a batch-processor version of `look`). + +```python + from evennia.utils import create, search + from evennia.contrib.tutorial_examples import red_button + from typeclasses.objects import Object + + limbo = search.objects(caller, 'Limbo', global_search=True)[0] + + red_button = create.create_object(red_button.RedButton, key="Red button", + location=limbo, aliases=["button"]) + + # caller points to the one running the script + caller.msg("A red button was created.") +``` + +Compare with the example code given earlier. Notice how the content of `#HEADER` has been pasted at the top of the `#CODE` block. Use `pp` to actually execute this block (this will create the button and give you a message). Use `nn` (next) to go to the next command. Use `hh` for a list of commands. + +If there are tracebacks, fix them in the batch file, then use `rr` to reload the file. You will still be at the same code block and can rerun it easily with `pp` as needed. This makes for a simple debug cycle. It also allows you to rerun individual troublesome blocks - as mentioned, in a large batch file this can be very useful (don't forget the `/debug` mode either). + +Use `nn` and `bb` (next and back) to step through the file; e.g. `nn 12` will jump 12 steps forward (without processing any blocks in between). All normal commands of Evennia should work too while working in interactive mode. + +## Limitations and Caveats + +The batch-code processor is by far the most flexible way to build a world in Evennia. There are however some caveats you need to keep in mind. + +### Safety +Or rather the lack of it. There is a reason only *superusers* are allowed to run the batch-code processor by default. The code-processor runs **without any Evennia security checks** and allows full access to Python. If an untrusted party could run the code-processor they could execute arbitrary python code on your machine, which is potentially a very dangerous thing. If you want to allow other users to access the batch-code processor you should make sure to run Evennia as a separate and very limited-access user on your machine (i.e. in a 'jail'). By comparison, the batch-command processor is much safer since the user running it is still 'inside' the game and can't really do anything outside what the game commands allow them to. + +### No communication between code blocks +Global variables won't work in code batch files, each block is executed as stand-alone environments. `#HEADER` blocks are literally pasted on top of each `#CODE` block so updating some header-variable in your block will not make that change available in another block. Whereas a python execution limitation, allowing this would also lead to very hard-to-debug code when using the interactive mode - this would be a classical example of "spaghetti code". + +The main practical issue with this is when building e.g. a room in one code block and later want to connect that room with a room you built in the current block. There are two ways to do this: + +- Perform a database search for the name of the room you created (since you cannot know in advance which dbref it got assigned). The problem is that a name may not be unique (you may have a lot of "A dark forest" rooms). There is an easy way to handle this though - use [Tags](Tags) or *Aliases*. You can assign any number of tags and/or aliases to any object. Make sure that one of those tags or aliases is unique to the room (like "room56") and you will henceforth be able to always uniquely search and find it later. +- Use the `caller` global property as an inter-block storage. For example, you could have a dictionary of room references in an `ndb`: + ```python + #HEADER + if caller.ndb.all_rooms is None: + caller.ndb.all_rooms = {} + + #CODE + # create and store the castle + castle = create_object("rooms.Room", key="Castle") + caller.ndb.all_rooms["castle"] = castle + + #CODE + # in another node we want to access the castle + castle = caller.ndb.all_rooms.get("castle") + ``` +Note how we check in `#HEADER` if `caller.ndb.all_rooms` doesn't already exist before creating the dict. Remember that `#HEADER` is copied in front of every `#CODE` block. Without that `if` statement we'd be wiping the dict every block! + +### Don't treat a batchcode file like any Python file +Despite being a valid Python file, a batchcode file should *only* be run by the batchcode processor. You should not do things like define Typeclasses or Commands in them, or import them into other code. Importing a module in Python will execute base level of the module, which in the case of your average batchcode file could mean creating a lot of new objects every time. +### Don't let code rely on the batch-file's real file path + +When you import things into your batchcode file, don't use relative imports but always import with paths starting from the root of your game directory or evennia library. Code that relies on the batch file's "actual" location *will fail*. Batch code files are read as text and the strings executed. When the code runs it has no knowledge of what file those strings where once a part of. diff --git a/docs/source/Batch-Command-Processor.md b/docs/source/Batch-Command-Processor.md new file mode 100644 index 0000000000..7f35244c17 --- /dev/null +++ b/docs/source/Batch-Command-Processor.md @@ -0,0 +1,121 @@ +# Batch Command Processor + + +For an introduction and motivation to using batch processors, see [here](Batch-Processors). This page describes the Batch-*command* processor. The Batch-*code* one is covered [here](Batch-Code-Processor). + +## Basic Usage + +The batch-command processor is a superuser-only function, invoked by + + > @batchcommand path.to.batchcmdfile + +Where `path.to.batchcmdfile` is the path to a *batch-command file* with the "`.ev`" file ending. This path is given like a python path relative to a folder you define to hold your batch files, set with `BATCH_IMPORT_PATH` in your settings. Default folder is (assuming your game is in the `mygame` folder) `mygame/world`. So if you want to run the example batch file in `mygame/world/batch_cmds.ev`, you could use + + > @batchcommand batch_cmds + +A batch-command file contains a list of Evennia in-game commands separated by comments. The processor will run the batch file from beginning to end. Note that *it will not stop if commands in it fail* (there is no universal way for the processor to know what a failure looks like for all different commands). So keep a close watch on the output, or use *Interactive mode* (see below) to run the file in a more controlled, gradual manner. + +## The batch file + +The batch file is a simple plain-text file containing Evennia commands. Just like you would write them in-game, except you have more freedom with line breaks. + +Here are the rules of syntax of an `*.ev` file. You'll find it's really, really simple: + +- All lines having the `#` (hash)-symbol *as the first one on the line* are considered *comments*. All non-comment lines are treated as a command and/or their arguments. +- Comment lines have an actual function -- they mark the *end of the previous command definition*. So never put two commands directly after one another in the file - separate them with a comment, or the second of the two will be considered an argument to the first one. Besides, using plenty of comments is good practice anyway. +- A line that starts with the word `#INSERT` is a comment line but also signifies a special instruction. The syntax is `#INSERT ` and tries to import a given batch-cmd file into this one. The inserted batch file (file ending `.ev`) will run normally from the point of the `#INSERT` instruction. +- Extra whitespace in a command definition is *ignored*. - A completely empty line translates in to a line break in texts. Two empty lines thus means a new paragraph (this is obviously only relevant for commands accepting such formatting, such as the `@desc` command). +- The very last command in the file is not required to end with a comment. +- You *cannot* nest another `@batchcommand` statement into your batch file. If you want to link many batch-files together, use the `#INSERT` batch instruction instead. You also cannot launch the `@batchcode` command from your batch file, the two batch processors are not compatible. + +Below is a version of the example file found in `evennia/contrib/tutorial_examples/batch_cmds.ev`. + +```bash + # + # This is an example batch build file for Evennia. + # + + # This creates a red button + @create button:tutorial_examples.red_button.RedButton + # (This comment ends input for @create) + # Next command. Let's create something. + @set button/desc = + This is a large red button. Now and then + it flashes in an evil, yet strangely tantalizing way. + + A big sign sits next to it. It says: + + + ----------- + + Press me! + + ----------- + + + ... It really begs to be pressed! You + know you want to! + + # This inserts the commands from another batch-cmd file named + # batch_insert_file.ev. + #INSERT examples.batch_insert_file + + + # (This ends the @set command). Note that single line breaks + # and extra whitespace in the argument are ignored. Empty lines + # translate into line breaks in the output. + # Now let's place the button where it belongs (let's say limbo #2 is + # the evil lair in our example) + @teleport #2 + # (This comments ends the @teleport command.) + # Now we drop it so others can see it. + # The very last command in the file needs not be ended with #. + drop button +``` + +To test this, run `@batchcommand` on the file: + + > @batchcommand contrib.tutorial_examples.batch_cmds + +A button will be created, described and dropped in Limbo. All commands will be executed by the user calling the command. + +> Note that if you interact with the button, you might find that its description changes, loosing your custom-set description above. This is just the way this particular object works. + +## Interactive mode + +Interactive mode allows you to more step-wise control over how the batch file is executed. This is useful for debugging and also if you have a large batch file and is only updating a small part of it -- running the entire file again would be a waste of time (and in the case of `@create`-ing objects you would to end up with multiple copies of same-named objects, for example). Use `@batchcommand` with the `/interactive` flag to enter interactive mode. + + > @batchcommand/interactive tutorial_examples.batch_cmds + +You will see this: + + 01/04: @create button:tutorial_examples.red_button.RedButton (hh for help) + +This shows that you are on the `@create` command, the first out of only four commands in this batch file. Observe that the command `@create` has *not* been actually processed at this point! + +To take a look at the full command you are about to run, use `ll` (a batch-processor version of `look`). Use `pp` to actually process the current command (this will actually `@create` the button) -- and make sure it worked as planned. Use `nn` (next) to go to the next command. Use `hh` for a list of commands. + +If there are errors, fix them in the batch file, then use `rr` to reload the file. You will still be at the same command and can rerun it easily with `pp` as needed. This makes for a simple debug cycle. It also allows you to rerun individual troublesome commands - as mentioned, in a large batch file this can be very useful. Do note that in many cases, commands depend on the previous ones (e.g. if `@create` in the example above had failed, the following commands would have had nothing to operate on). + +Use `nn` and `bb` (next and back) to step through the file; e.g. `nn 12` will jump 12 steps forward (without processing any command in between). All normal commands of Evennia should work too while working in interactive mode. + +## Limitations and Caveats + +The batch-command processor is great for automating smaller builds or for testing new commands and objects repeatedly without having to write so much. There are several caveats you have to be aware of when using the batch-command processor for building larger, complex worlds though. + +The main issue is that when you run a batch-command script you (*you*, as in your superuser character) are actually moving around in the game creating and building rooms in sequence, just as if you had been entering those commands manually, one by one. You have to take this into account when creating the file, so that you can 'walk' (or teleport) to the right places in order. + +This also means there are several pitfalls when designing and adding certain types of objects. Here are some examples: + +- *Rooms that change your [Command Set](Command-Sets)*: Imagine that you build a 'dark' room, which severely limits the cmdsets of those entering it (maybe you have to find the light switch to proceed). In your batch script you would create this room, then teleport to it - and promptly be shifted into the dark state where none of your normal build commands work ... +- *Auto-teleportation*: Rooms that automatically teleport those that enter them to another place (like a trap room, for example). You would be teleported away too. +- *Mobiles*: If you add aggressive mobs, they might attack you, drawing you into combat. If they have AI they might even follow you around when building - or they might move away from you before you've had time to finish describing and equipping them! + +The solution to all these is to plan ahead. Make sure that superusers are never affected by whatever effects are in play. Add an on/off switch to objects and make sure it's always set to *off* upon creation. It's all doable, one just needs to keep it in mind. + +## Assorted notes + +The fact that you build as 'yourself' can also be considered an advantage however, should you ever decide to change the default command to allow others than superusers to call the processor. Since normal access-checks are still performed, a malevolent builder with access to the processor should not be able to do all that much damage (this is the main drawback of the [Batch Code Processor](batch-code-processor)) + +- [GNU Emacs](https://www.gnu.org/software/emacs/) users might find it interesting to use emacs' *evennia mode*. This is an Emacs major mode found in `evennia/utils/evennia-mode.el`. It offers correct syntax highlighting and indentation with `` when editing `.ev` files in Emacs. See the header of that file for installation instructions. +- [VIM](http://www.vim.org/) users can use amfl's [vim-evennia](https://github.com/amfl/vim-evennia) mode instead, see its readme for install instructions. \ No newline at end of file diff --git a/docs/source/Batch-Processors.md b/docs/source/Batch-Processors.md new file mode 100644 index 0000000000..3ba7a3a9ea --- /dev/null +++ b/docs/source/Batch-Processors.md @@ -0,0 +1,39 @@ +# Batch Processors + + +Building a game world is a lot of work, especially when starting out. Rooms should be created, descriptions have to be written, objects must be detailed and placed in their proper places. In many traditional MUD setups you had to do all this online, line by line, over a telnet session. + +Evennia already moves away from much of this by shifting the main coding work to external Python modules. But also building would be helped if one could do some or all of it externally. Enter Evennia's *batch processors* (there are two of them). The processors allows you, as a game admin, to build your game completely offline in normal text files (*batch files*) that the processors understands. Then, when you are ready, you use the processors to read it all into Evennia (and into the database) in one go. + +You can of course still build completely online should you want to - this is certainly the easiest way to go when learning and for small build projects. But for major building work, the advantages of using the batch-processors are many: +- It's hard to compete with the comfort of a modern desktop text editor; Compared to a traditional MUD line input, you can get much better overview and many more features. Also, accidentally pressing Return won't immediately commit things to the database. +- You might run external spell checkers on your batch files. In the case of one of the batch-processors (the one that deals with Python code), you could also run external debuggers and code analyzers on your file to catch problems before feeding it to Evennia. +- The batch files (as long as you keep them) are records of your work. They make a natural starting point for quickly re-building your world should you ever decide to start over. +- If you are an Evennia developer, using a batch file is a fast way to setup a test-game after having reset the database. +- The batch files might come in useful should you ever decide to distribute all or part of your world to others. + + +There are two batch processors, the Batch-*command* processor and the Batch-*code* processor. The first one is the simpler of the two. It doesn't require any programming knowledge - you basically just list in-game commands in a text file. The code-processor on the other hand is much more powerful but also more complex - it lets you use Evennia's API to code your world in full-fledged Python code. + +- The [Batch Command Processor](Batch-command-processor) +- The [Batch Code Processor](Batch-code-processor) + +If you plan to use international characters in your batchfiles you are wise to read about *file encodings* below. + +## A note on File Encodings + +As mentioned, both the processors take text files as input and then proceed to process them. As long as you stick to the standard [ASCII](http://en.wikipedia.org/wiki/Ascii) character set (which means the normal English characters, basically) you should not have to worry much about this section. + +Many languages however use characters outside the simple `ASCII` table. Common examples are various apostrophes and umlauts but also completely different symbols like those of the greek or cyrillic alphabets. + +First, we should make it clear that Evennia itself handles international characters just fine. It (and Django) uses [unicode](http://en.wikipedia.org/wiki/Unicode) strings internally. + +The problem is that when reading a text file like the batchfile, we need to know how to decode the byte-data stored therein to universal unicode. That means we need an *encoding* (a mapping) for how the file stores its data. There are many, many byte-encodings used around the world, with opaque names such as `Latin-1`, `ISO-8859-3` or `ARMSCII-8` to pick just a few examples. Problem is that it's practially impossible to determine which encoding was used to save a file just by looking at it (it's just a bunch of bytes!). You have to *know*. + +With this little introduction it should be clear that Evennia can't guess but has to *assume* an encoding when trying to load a batchfile. The text editor and Evennia must speak the same "language" so to speak. Evennia will by default first try the international `UTF-8` encoding, but you can have Evennia try any sequence of different encodings by customizing the `ENCODINGS` list in your settings file. Evennia will use the first encoding in the list that do not raise any errors. Only if none work will the server give up and return an error message. + +You can often change the text editor encoding (this depends on your editor though), otherwise you need to add the editor's encoding to Evennia's `ENCODINGS` list. If you are unsure, write a test file with lots of non-ASCII letters in the editor of your choice, then import to make sure it works as it should. + +More help with encodings can be found in the entry [Text Encodings](Text-Encodings) and also in the Wikipedia article [here](http://en.wikipedia.org/wiki/Text_encodings). + +**A footnote for the batch-code processor**: Just because *Evennia* can parse your file and your fancy special characters, doesn't mean that *Python* allows their use. Python syntax only allows international characters inside *strings*. In all other source code only `ASCII` set characters are allowed. diff --git a/docs/source/Bootstrap-&-Evennia.md b/docs/source/Bootstrap-&-Evennia.md new file mode 100644 index 0000000000..641d4495ca --- /dev/null +++ b/docs/source/Bootstrap-&-Evennia.md @@ -0,0 +1,75 @@ +# Bootstrap & Evennia + +# What is Bootstrap? +Evennia's new default web page uses a framework called [Bootstrap](https://getbootstrap.com/). This framework is in use across the internet - you'll probably start to recognize its influence once you learn some of the common design patterns. This switch is great for web developers, perhaps like yourself, because instead of wondering about setting up different grid systems or what custom class another designer used, we have a base, a bootstrap, to work from. Bootstrap is responsive by default, and comes with some default styles that Evennia has lightly overrode to keep some of the same colors and styles you're used to from the previous design. + +For your reading pleasure, a brief overview of Bootstrap follows. For more in-depth info, please read [the documentation](https://getbootstrap.com/docs/4.0/getting-started/introduction/). +*** + +## The Layout System +Other than the basic styling Bootstrap includes, it also includes [a built in layout and grid system](https://getbootstrap.com/docs/4.0/layout/overview/). +The first part of this system is [the container](https://getbootstrap.com/docs/4.0/layout/overview/#containers). + +The container is meant to hold all your page content. Bootstrap provides two types: fixed-width and full-width. +Fixed-width containers take up a certain max-width of the page - they're useful for limiting the width on Desktop or Tablet platforms, instead of making the content span the width of the page. +``` +

+ +
+``` +Full width containers take up the maximum width available to them - they'll span across a wide-screen desktop or a smaller screen phone, edge-to-edge. +``` +
+ +
+``` + +The second part of the layout system is [the grid](https://getbootstrap.com/docs/4.0/layout/grid/). This is the bread-and-butter of the layout of Bootstrap - it allows you to change the size of elements depending on the size of the screen, without writing any media queries. We'll briefly go over it - to learn more, please read the docs or look at the source code for Evennia's home page in your browser. +> Important! Grid elements should be in a .container or .container-fluid. This will center the contents of your site. + +Bootstrap's grid system allows you to create rows and columns by applying classes based on breakpoints. The default breakpoints are extra small, small, medium, large, and extra-large. If you'd like to know more about these breakpoints, please [take a look at the documentation for them.](https://getbootstrap.com/docs/4.0/layout/overview/#responsive-breakpoints) + +To use the grid system, first create a container for your content, then add your rows and columns like so: +``` +
+
+
+ 1 of 3 +
+
+ 2 of 3 +
+
+ 3 of 3 +
+
+
+``` +This layout would create three equal-width columns. + +To specify your sizes - for instance, Evennia's default site has three columns on desktop and tablet, but reflows to single-column on smaller screens. Try it out! +``` +
+
+
+ 1 of 4 +
+
+ 2 of 4 +
+
+ 3 of 4 +
+
+ 4 of 4 +
+
+
+``` +This layout would be 4 columns on large screens, 2 columns on medium screens, and 1 column on anything smaller. + +To learn more about Bootstrap's grid, please [take a look at the docs](https://getbootstrap.com/docs/4.0/layout/grid/) +*** + +## More Bootstrap +Bootstrap also provides a huge amount of utilities, as well as styling and content elements. To learn more about them, please [read the Bootstrap docs](https://getbootstrap.com/docs/4.0/getting-started/introduction/) or read one of our other web tutorials. \ No newline at end of file diff --git a/docs/source/Bootstrap-Components-and-Utilities.md b/docs/source/Bootstrap-Components-and-Utilities.md new file mode 100644 index 0000000000..57993a1f6c --- /dev/null +++ b/docs/source/Bootstrap-Components-and-Utilities.md @@ -0,0 +1,63 @@ +# Bootstrap Components and Utilities + +Bootstrap provides many utilities and components you can use when customizing Evennia's web presence. We'll go over a few examples here that you might find useful. +> Please take a look at either [the basic web tutorial](https://github.com/evennia/evennia/wiki/Add-a-simple-new-web-page) or [the web character view tutorial](https://github.com/evennia/evennia/wiki/Web-Character-View-Tutorial) +> to get a feel for how to add pages to Evennia's website to test these examples. + +## General Styling +Bootstrap provides base styles for your site. These can be customized through CSS, but the default styles are intended to provide a consistent, clean look for sites. + +### Color +Most elements can be styled with default colors. [Take a look at the documentation](https://getbootstrap.com/docs/4.0/utilities/colors/) to learn more about these colors - suffice to say, adding a class of text-* or bg-*, for instance, text-primary, sets the text color or background color. + +### Borders +Simply adding a class of 'border' to an element adds a border to the element. For more in-depth info, please [read the documentation on borders.](https://getbootstrap.com/docs/4.0/utilities/borders/). +``` + +``` +You can also easily round corners just by adding a class. +``` + +``` + +### Spacing +Bootstrap provides classes to easily add responsive margin and padding. Most of the time, you might like to add margins or padding through CSS itself - however these classes are used in the default Evennia site. [Take a look at the docs](https://getbootstrap.com/docs/4.0/utilities/spacing/) to learn more. + +*** +## Components + +### Buttons +[Buttons](https://getbootstrap.com/docs/4.0/components/buttons/) in Bootstrap are very easy to use - button styling can be added to ` + + + +``` +### Cards +[Cards](https://getbootstrap.com/docs/4.0/components/card/) provide a container for other elements that stands out from the rest of the page. The "Accounts", "Recently Connected", and "Database Stats" on the default webpage are all in cards. Cards provide quite a bit of formatting options - the following is a simple example, but read the documentation or look at the site's source for more. +``` +
+
+

Card title

+
Card subtitle
+

Fancy, isn't it?

+ Card link +
+
+``` + +### Jumbotron +[Jumbotrons](https://getbootstrap.com/docs/4.0/components/jumbotron/) are useful for featuring an image or tagline for your game. They can flow with the rest of your content or take up the full width of the page - Evennia's base site uses the former. +``` +
+
+

Full Width Jumbotron

+

Look at the source of the default Evennia page for a regular Jumbotron

+
+
+``` + +### Forms +[Forms](https://getbootstrap.com/docs/4.0/components/forms/) are highly customizable with Bootstrap. For a more in-depth look at how to use forms and their styles in your own Evennia site, please read over [the web character gen tutorial.](https://github.com/evennia/evennia/wiki/Web-Character-Generation) \ No newline at end of file diff --git a/docs/source/Builder-Docs.md b/docs/source/Builder-Docs.md new file mode 100644 index 0000000000..014dc75c31 --- /dev/null +++ b/docs/source/Builder-Docs.md @@ -0,0 +1,26 @@ +# Builder Docs + +This section contains information useful to world builders. + +### Building basics + +- [Default in-game commands](Default-Command-Help) +- [Building Quick-start](Building-Quickstart) +- [Giving build permissions to others](Building-Permissions) +- [Adding text tags](TextTags) + - [Colored text](https://github.com/evennia/evennia/wiki/TextTags#coloured-text) + - [Clickable links](https://github.com/evennia/evennia/wiki/TextTags#clickable-links) + - [Inline functions](https://github.com/evennia/evennia/wiki/TextTags#inline-functions) +- [Customizing the connection screen](Connection-Screen) + +### Advanced building and World building + +- [Overview of batch processors](Batch-Processors) + - [Batch-command processor](Batch-Command-Processor) + - [Batch-code processor](Batch-Code-Processor) +- [Using the Spawner for individualizing objects](https://github.com/evennia/evennia/wiki/Spawner-and-Prototypes) +- [Adding Zones](Zones) + +### The Tutorial world + +- [Introduction and setup](Tutorial-World-Introduction) diff --git a/docs/source/Building-Permissions.md b/docs/source/Building-Permissions.md new file mode 100644 index 0000000000..91c30f7851 --- /dev/null +++ b/docs/source/Building-Permissions.md @@ -0,0 +1,38 @@ +# Building Permissions + + +*OBS: This gives only a brief introduction to the access system. Locks and permissions are fully detailed* [here](Locks). + +## The super user + +There are strictly speaking two types of users in Evennia, the *super user* and everyone else. The superuser is the first user you create, object `#1`. This is the all-powerful server-owner account. Technically the superuser not only has access to everything, it *bypasses* the permission checks entirely. This makes the superuser impossible to lock out, but makes it unsuitable to actually play-test the game's locks and restrictions with (see `@quell` below). Usually there is no need to have but one superuser. + +## Assigning permissions + +Whereas permissions can be used for anything, those put in `settings.PERMISSION_HIERARCHY` will have a ranking relative each other as well. We refer to these types of permissions as *hierarchical permissions*. When building locks to check these permissions, the `perm()` [lock function](Locks) is used. By default Evennia creates the following hierarchy (spelled exactly like this): + +1. **Developers** basically have the same access as superusers except that they do *not* sidestep the Permission system. Assign only to really trusted server-admin staff since this level gives access both to server reload/shutdown functionality as well as (and this may be more critical) gives access to the all-powerful `@py` command that allows the execution of arbitrary Python code on the command line. +1. **Admins** can do everything *except* affecting the server functions themselves. So an Admin couldn't reload or shutdown the server for example. They also cannot execute arbitrary Python code on the console or import files from the hard drive. +1. **Builders** - have all the build commands, but cannot affect other accounts or mess with the server. +1. **Helpers** are almost like a normal *Player*, but they can also add help files to the database. +1. **Players** is the default group that new players end up in. A new player have permission to use tells and to use and create new channels. + +A user having a certain level of permission automatically have access to locks specifying access of a lower level. + +To assign a new permission from inside the game, you need to be able to use the `@perm` command. This is an *Developer*-level command, but it could in principle be made lower-access since it only allows assignments equal or lower to your current level (so you cannot use it to escalate your own permission level). So, assuming you yourself have *Developer* access (or is superuser), you assign a new account "Tommy" to your core staff with the command + + @perm/account Tommy = Developer + +or + + @perm *Tommy = Developer + +We use a switch or the `*name` format to make sure to put the permission on the *Account* and not on any eventual *Character* that may also be named "Tommy". This is usually what you want since the Account will then remain an Developer regardless of which Character they are currently controlling. To limit permission to a per-Character level you should instead use *quelling* (see below). Normally permissions can be any string, but for these special hierarchical permissions you can also use plural ("Developer" and "Developers" both grant the same powers). + +## Quelling your permissions + +When developing it can be useful to check just how things would look had your permission-level been lower. For this you can use *quelling*. Normally, when you puppet a Character you are using your Account-level permission. So even if your Character only has *Accounts* level permissions, your *Developer*-level Account will take precedence. With the `@quell` command you can change so that the Character's permission takes precedence instead: + + @quell + +This will allow you to test out the game using the current Character's permission level. A developer or builder can thus in principle maintain several test characters, all using different permission levels. Note that you cannot escalate your permissions this way; If the Character happens to have a *higher* permission level than the Account, the *Account's* (lower) permission will still be used. diff --git a/docs/source/Building-Quickstart.md b/docs/source/Building-Quickstart.md new file mode 100644 index 0000000000..33cd106986 --- /dev/null +++ b/docs/source/Building-Quickstart.md @@ -0,0 +1,193 @@ +# Building Quickstart + + +The [default command](Default-Command-Help) definitions coming with Evennia +follows a style [similar](Using-MUX-as-a-Standard) to that of MUX, so the +commands should be familiar if you used any such code bases before. + +> Throughout the larger documentation you may come across commands prefixed +> with `@`. This is just an optional marker used in some places to make a +> command stand out. Evennia defaults to ignoring the use of `@` in front of +> your command (so entering `dig` is the same as entering `@dig`). + +The default commands have the following style (where `[...]` marks optional parts): + + command[/switch/switch...] [arguments ...] + +A _switch_ is a special, optional flag to the command to make it behave differently. It is always put directly after the command name, and begins with a forward slash (`/`). The _arguments_ are one or more inputs to the commands. It's common to use an equal sign (`=`) when assigning something to an object. + +Below are some examples of commands you can try when logged in to the game. Use `help ` for learning more about each command and their detailed options. + +## Stepping Down From Godhood + +If you just installed Evennia, your very first player account is called user #1, also known as the _superuser_ or _god user_. This user is very powerful, so powerful that it will override many game restrictions such as locks. This can be useful, but it also hides some functionality that you might want to test. + +To temporarily step down from your superuser position you can use the `quell` command in-game: + + quell + +This will make you start using the permission of your current character's level instead of your superuser level. If you didn't change any settings your game Character should have an _Developer_ level permission - high as can be without bypassing locks like the superuser does. This will work fine for the examples on this page. Use `unquell` to get back to superuser status again afterwards. + +## Creating an Object + +Basic objects can be anything -- swords, flowers and non-player characters. They are created using the `create` command: + + create box + +This created a new 'box' (of the default object type) in your inventory. Use the command `inventory` (or `i`) to see it. Now, 'box' is a rather short name, let's rename it and tack on a few aliases. + + name box = very large box;box;very;crate + +We now renamed the box to _very large box_ (and this is what we will see when looking at it), but we will also recognize it by any of the other names we give - like _crate_ or simply _box_ as before. We could have given these aliases directly after the name in the `create` command, this is true for all creation commands - you can always tag on a list of `;`-separated aliases to the name of your new object. If you had wanted to not change the name itself, but to only add aliases, you could have used the `alias` command. + +We are currently carrying the box. Let's drop it (there is also a short cut to create and drop in one go by using the `/drop` switch, for example `create/drop box`). + + drop box + +Hey presto - there it is on the ground, in all its normality. + + examine box + +This will show some technical details about the box object. For now we will ignore what this information means. + +Try to `look` at the box to see the (default) description. + + look box + You see nothing special. + +The description you get is not very exciting. Let's add some flavor. + + describe box = This is a large and very heavy box. + +If you try the `get` command we will pick up the box. So far so good, but if we really want this to be a large and heavy box, people should _not_ be able to run off with it that easily. To prevent this we need to lock it down. This is done by assigning a _Lock_ to it. Make sure the box was dropped in the room, then try this: + + lock box = get:false() + +Locks represent a rather [big topic](Locks), but for now that will do what we want. This will lock the box so noone can lift it. The exception is superusers, they override all locks and will pick it up anyway. Make sure you are quelling your superuser powers and try to get the box now: + + > get box + You can't get that. + +Think thís default error message looks dull? The `get` command looks for an [Attribute](Attributes) named `get_err_msg` for returning a nicer error message (we just happen to know this, you would need to peek into the [code](https://github.com/evennia/evennia/blob/master/evennia/commands/default/general.py#L235) for the `get` command to find out.). You set attributes using the `set` command: + + set box/get_err_msg = It's way too heavy for you to lift. + +Try to get it now and you should see a nicer error message echoed back to you. + +You create new Commands (or modify existing ones) in Python outside the game. See the [Adding Commands tutorial](https://github.com/evennia/evennia/wiki/Adding%20Command%20Tutorial) for help with creating your first own Command. + +## Get a Personality + +[Scripts](Scripts) are powerful out-of-character objects useful for many "under the hood" things. One of their optional abilities is to do things on a timer. To try out a first script, let's put one on ourselves. There is an example script in `evennia/contrib/tutorial_examples/bodyfunctions.py` that is called `BodyFunctions`. To add this to us we will use the `script` command: + + script self = tutorial_examples.bodyfunctions.BodyFunctions + +(note that you don't have to give the full path as long as you are pointing to a place inside the `contrib` directory, it's one of the places Evennia looks for Scripts). Wait a while and you will notice yourself starting making random observations. + + script self + +This will show details about scripts on yourself (also `examine` works). You will see how long it is until it "fires" next. Don't be alarmed if nothing happens when the countdown reaches zero - this particular script has a randomizer to determine if it will say something or not. So you will not see output every time it fires. + +When you are tired of your character's "insights", kill the script with + + script/stop self = tutorial_examples.bodyfunctions.BodyFunctions + +You create your own scripts in Python, outside the game; the path you give to `script` is literally the Python path to your script file. The [Scripts](Scripts) page explains more details. + +## Pushing Your Buttons + +If we get back to the box we made, there is only so much fun you can do with it at this point. It's just a dumb generic object. If you renamed it to `stone` and changed its description noone would be the wiser. However, with the combined use of custom [Typeclasses](Typeclasses), [Scripts](Scripts) and object-based [Commands](Commands), you could expand it and other items to be as unique, complex and interactive as you want. + +Let's take an example. So far we have only created objects that use the default object typeclass named simply `Object`. Let's create an object that is a little more interesting. Under `evennia/contrib/tutorial_examples` there is a module `red_button.py`. It contains the enigmatic `RedButton` typeclass. + +Let's make us one of _those_! + + create/drop button:tutorial_examples.red_button.RedButton + +We import the RedButton python class the same way you would import it in Python except Evennia makes sure to look in`evennia/contrib/` so you don't have to write the full path every time. There you go - one red button. + +The RedButton is an example object intended to show off a few of Evennia's features. You will find that the [Typeclass](Typeclasses) and [Commands](Commands) controlling it are inside `evennia/contrib/tutorial_examples/`. + +If you wait for a while (make sure you dropped it!) the button will blink invitingly. Why don't you try to push it ...? Surely a big red button is meant to be pushed. You know you want to. + +## Making Yourself a House + +The main command for shaping the game world is `dig`. For example, if you are standing in Limbo you can dig a route to your new house location like this: + + dig house = large red door;door;in,to the outside;out + +This will create a new room named 'house'. Spaces at the start/end of names and aliases are ignored so you could put more air if you wanted. This call will directly create an exit from your current location named 'large red door' and a corresponding exit named 'to the outside' in the house room leading back to Limbo. We also define a few aliases to those exits, so people don't have to write the full thing all the time. + +If you wanted to use normal compass directions (north, west, southwest etc), you could do that with `dig` too. But Evennia also has a limited version of `dig` that helps for compass directions (and also up/down and in/out). It's called `tunnel`: + + tunnel sw = cliff + +This will create a new room "cliff" with an exit "southwest" leading there and a path "northeast" leading back from the cliff to your current location. + +You can create new exits from where you are using the `open` command: + + open north;n = house + +This opens an exit `north` (with an alias `n`) to the previously created room `house`. + +If you have many rooms named `house` you will get a list of matches and have to select which one you want to link to. You can also give its database (#dbref) number, which is unique to every object. This can be found with the `examine` command or by looking at the latest constructions with `objects`. + +Follow the north exit to your 'house' or `teleport` to it: + + north + +or: + + teleport house + +To manually open an exit back to Limbo (if you didn't do so with the `dig` command): + + open door = limbo + +(or give limbo's dbref which is #2) + +## Reshuffling the World + +You can find things using the `find` command. Assuming you are back at `Limbo`, let's teleport the _large box to our house_. + + > teleport box = house + very large box is leaving Limbo, heading for house. + Teleported very large box -> house. + +We can still find the box by using find: + + > find box + One Match(#1-#8): + very large box(#8) - src.objects.objects.Object + +Knowing the `#dbref` of the box (#8 in this example), you can grab the box and get it back here without actually yourself going to `house` first: + + teleport #8 = here + +(You can usually use `here` to refer to your current location. To refer to yourself you can use `self` or `me`). The box should now be back in Limbo with you. + +We are getting tired of the box. Let's destroy it. + + destroy box + +You can destroy many objects in one go by giving a comma-separated list of objects (or their #dbrefs, if they are not in the same location) to the command. + +## Adding a Help Entry + +An important part of building is keeping the help files updated. You can add, delete and append to existing help entries using the `sethelp` command. + + sethelp/add MyTopic = This help topic is about ... + +## Adding a World + +After this brief introduction to building you may be ready to see a more fleshed-out example. Evennia comes with a tutorial world for you to explore. + +First you need to switch back to _superuser_ by using the `unquell` command. Next, place yourself in `Limbo` and run the following command: + + batchcommand tutorial_world.build + +This will take a while (be patient and don't re-run the command). You will see all the commands used to build the world scroll by as the world is built for you. + +You will end up with a new exit from Limbo named _tutorial_. Apart from being a little solo-adventure in its own right, the tutorial world is a good source for learning Evennia building (and coding). + +Read [the batch file](https://github.com/evennia/evennia/blob/master/evennia/contrib/tutorial_world/build.ev) to see exactly how it's built, step by step. See also more info about the tutorial world [here](Tutorial-World-Introduction). diff --git a/docs/source/Building-a-mech-tutorial.md b/docs/source/Building-a-mech-tutorial.md new file mode 100644 index 0000000000..180c3ad846 --- /dev/null +++ b/docs/source/Building-a-mech-tutorial.md @@ -0,0 +1,183 @@ +# Building a mech tutorial + +> This page was adapted from the article "Building a Giant Mech in Evennia" by Griatch, published in Imaginary Realities Volume 6, issue 1, 2014. The original article is no longer available online, this is a version adopted to be compatible with the latest Evennia. + +## Creating the Mech + +Let us create a functioning giant mech using the Python MUD-creation system Evennia. Everyone likes a giant mech, right? Start in-game as a character with build privileges (or the superuser). + + @create/drop Giant Mech ; mech + +Boom. We created a Giant Mech Object and dropped it in the room. We also gave it an alias *mech*. Let’s describe it. + + @desc mech = This is a huge mech. It has missiles and stuff. + +Next we define who can “puppet” the mech object. + + @lock mech = puppet:all() + +This makes it so that everyone can control the mech. More mechs to the people! (Note that whereas Evennia’s default commands may look vaguely MUX-like, you can change the syntax to look like whatever interface style you prefer.) + +Before we continue, let’s make a brief detour. Evennia is very flexible about its objects and even more flexible about using and adding commands to those objects. Here are some ground rules well worth remembering for the remainder of this article: + +- The [Account](Accounts) represents the real person logging in and has no game-world existence. +- Any [Object](Objects) can be puppeted by an Account (with proper permissions). +- [Characters](Objects#characters), [Rooms](Objects#rooms), and [Exits](Objects#exits) are just children of normal Objects. +- Any Object can be inside another (except if it creates a loop). +- Any Object can store custom sets of commands on it. Those commands can: + - be made available to the puppeteer (Account), + - be made available to anyone in the same location as the Object, and + - be made available to anyone “inside” the Object + - Also Accounts can store commands on themselves. Account commands are always available unless commands on a puppeted Object explicitly override them. + +In Evennia, using the `@ic` command will allow you to puppet a given Object (assuming you have puppet-access to do so). As mentioned above, the bog-standard Character class is in fact like any Object: it is auto-puppeted when logging in and just has a command set on it containing the normal in-game commands, like look, inventory, get and so on. + + @ic mech + +You just jumped out of your Character and *are* now the mech! If people look at you in-game, they will look at a mech. The problem at this point is that the mech Object has no commands of its own. The usual things like look, inventory and get sat on the Character object, remember? So at the moment the mech is not quite as cool as it could be. + + @ic + +You just jumped back to puppeting your normal, mundane Character again. All is well. + +> (But, you ask, where did that `@ic` command come from, if the mech had no commands on it? The answer is that it came from the Account's command set. This is important. Without the Account being the one with the `@ic` command, we would not have been able to get back out of our mech again.) + + +### Arming the Mech + +Let us make the mech a little more interesting. In our favorite text editor, we will create some new mech-suitable commands. In Evennia, commands are defined as Python classes. + +```python +# in a new file mygame/commands/mechcommands.py + +from evennia import Command + +class CmdShoot(Command): + """ + Firing the mech’s gun + + Usage: + shoot [target] + + This will fire your mech’s main gun. If no + target is given, you will shoot in the air. + """ + key = "shoot" + aliases = ["fire", "fire!"] + + def func(self): + "This actually does the shooting" + + caller = self.caller + location = caller.location + + if not self.args: + # no argument given to command - shoot in the air + message = “BOOM! The mech fires its gun in the air!” + location.msg_contents(message) + return + + # we have an argument, search for target + target = caller.search(self.args) + if target: + message = "BOOM! The mech fires its gun at %s" % target.key + location.msg_contents(message) + +class CmdLaunch(Command): + # make your own 'launch'-command here as an exercise! + # (it's very similar to the 'shoot' command above). +``` + +This is saved as a normal Python module (let’s call it `mechcommands.py`), in a place Evennia looks for such modules (`mygame/commands/`). This command will trigger when the player gives the command “shoot”, “fire,” or even “fire!” with an exclamation mark. The mech can shoot in the air or at a target if you give one. In a real game the gun would probably be given a chance to hit and give damage to the target, but this is enough for now. + +We also make a second command for launching missiles (`CmdLaunch`). To save +space we won’t describe it here; it looks the same except it returns a text +about the missiles being fired and has different `key` and `aliases`. We leave +that up to you to create as an exercise. You could have it print "WOOSH! The +mech launches missiles against !", for example. + +Now we shove our commands into a command set. A [Command Set](Command-Sets) (CmdSet) is a container holding any number of commands. The command set is what we will store on the mech. + +```python +# in the same file mygame/commands/mechcommands.py + +from evennia import CmdSet +from evennia import default_cmds + +class MechCmdSet(CmdSet): + """ + This allows mechs to do do mech stuff. + """ + key = "mechcmdset" + + def at_cmdset_creation(self): + "Called once, when cmdset is first created" + self.add(CmdShoot()) + self.add(CmdLaunch()) +``` + +This simply groups all the commands we want. We add our new shoot/launch commands. Let’s head back into the game. For testing we will manually attach our new CmdSet to the mech. + + @py self.search("mech").cmdset.add("commands.mechcommands.MechCmdSet") + +This is a little Python snippet (run from the command line as an admin) that searches for the mech in our current location and attaches our new MechCmdSet to it. What we add is actually the Python path to our cmdset class. Evennia will import and initialize it behind the scenes. + + @ic mech + +We are back as the mech! Let’s do some shooting! + + fire! + BOOM! The mech fires its gun in the air! + +There we go, one functioning mech. Try your own `launch` command and see that it works too. We can not only walk around as the mech — since the CharacterCmdSet is included in our MechCmdSet, the mech can also do everything a Character could do, like look around, pick up stuff, and have an inventory. We could now shoot the gun at a target or try the missile launch command. Once you have your own mech, what else do you need? + +> Note: You'll find that the mech's commands are available to you by just standing in the same location (not just by puppeting it). We'll solve this with a *lock* in the next section. + +## Making a Mech production line + +What we’ve done so far is just to make a normal Object, describe it and put some commands on it. This is great for testing. The way we added it, the MechCmdSet will even go away if we reload the server. Now we want to make the mech an actual object “type” so we can create mechs without those extra steps. For this we need to create a new Typeclass. + +A [Typeclass](Typeclasses) is a near-normal Python class that stores its existence to the database behind the scenes. A Typeclass is created in a normal Python source file: + +```python +# in the new file mygame/typeclasses/mech.py + +from typeclasses.objects import Object +from commands.mechcommands import MechCmdSet +from evennia import default_cmds + +class Mech(Object): + """ + This typeclass describes an armed Mech. + """ + def at_object_creation(self): + "This is called only when object is first created" + self.cmdset.add_default(default_cmds.CharacterCmdSet) + self.cmdset.add(MechCmdSet, permanent=True) + self.locks.add("puppet:all();call:false()") + self.db.desc = "This is a huge mech. It has missiles and stuff." +``` + +For convenience we include the full contents of the default `CharacterCmdSet` in there. This will make a Character’s normal commands available to the mech. We also add the mech-commands from before, making sure they are stored persistently in the database. The locks specify that anyone can puppet the meck and no-one can "call" the mech's Commands from 'outside' it - you have to puppet it to be able to shoot. + +That’s it. When Objects of this type are created, they will always start out with the mech’s command set and the correct lock. We set a default description, but you would probably change this with `@desc` to individualize your mechs as you build them. + +Back in the game, just exit the old mech (`@ic` back to your old character) then do + + @create/drop The Bigger Mech ; bigmech : mech.Mech + +We create a new, bigger mech with an alias bigmech. Note how we give the python-path to our Typeclass at the end — this tells Evennia to create the new object based on that class (we don't have to give the full path in our game dir `typeclasses.mech.Mech` because Evennia knows to look in the `typeclasses` folder already). A shining new mech will appear in the room! Just use + + @ic bigmech + +to take it on a test drive. + +## Future Mechs + +To expand on this you could add more commands to the mech and remove others. Maybe the mech shouldn’t work just like a Character after all. Maybe it makes loud noises every time it passes from room to room. Maybe it cannot pick up things without crushing them. Maybe it needs fuel, ammo and repairs. Maybe you’ll lock it down so it can only be puppeted by emo teenagers. + +Having you puppet the mech-object directly is also just one way to implement a giant mech in Evennia. + +For example, you could instead picture a mech as a “vehicle” that you “enter” as your normal Character (since any Object can move inside another). In that case the “insides” of the mech Object could be the “cockpit”. The cockpit would have the `MechCommandSet` stored on itself and all the shooting goodness would be made available to you only when you enter it. + +And of course you could put more guns on it. And make it fly. diff --git a/docs/source/Building-menus.md b/docs/source/Building-menus.md new file mode 100644 index 0000000000..3ae8fdcccd --- /dev/null +++ b/docs/source/Building-menus.md @@ -0,0 +1,1065 @@ +# Building menus + + +# The building_menu contrib + +This contrib allows you to write custom and easy to use building menus. As the name implies, these menus are most useful for building things, that is, your builders might appreciate them, although you can use them for your players as well. + +Building menus are somewhat similar to `EvMenu` although they don't use the same system at all and are intended to make building easier. They replicate what other engines refer to as "building editors", which allow to you to build in a menu instead of having to enter a lot of complex commands. Builders might appreciate this simplicity, and if the code that was used to create them is simple as well, coders could find this contrib useful. + +## A simple menu + +Before diving in, there are some things to point out: + +- Building menus work on an object. This object will be edited by manipulations in the menu. So you can create a menu to add/edit a room, an exit, a character and so on. +- Building menus are arranged in layers of choices. A choice gives access to an option or to a sub-menu. Choices are linked to commands (usually very short). For instance, in the example shown below, to edit the room key, after opening the building menu, you can type `k`. That will lead you to the key choice where you can enter a new key for the room. Then you can enter `@` to leave this choice and go back to the entire menu. (All of this can be changed). +- To open the menu, you will need something like a command. This contrib offers a basic command for demonstration, but we will override it in this example, using the same code with more flexibility. + +So let's add a very basic example to begin with. + +### A generic editing command + +Let's begin by adding a new command. You could add or edit the following file (there's no trick here, feel free to organize the code differently): + +```python +# file: commands/building.py +from evennia.contrib.building_menu import BuildingMenu +from commands.command import Command + +class EditCmd(Command): + + """ + Editing command. + + Usage: + @edit [object] + + Open a building menu to edit the specified object. This menu allows to + specific information about this object. + + Examples: + @edit here + @edit self + @edit #142 + + """ + + key = "@edit" + locks = "cmd:id(1) or perm(Builders)" + help_category = "Building" + + def func(self): + if not self.args.strip(): + self.msg("|rYou should provide an argument to this function: the object to edit.|n") + return + + obj = self.caller.search(self.args.strip(), global_search=True) + if not obj: + return + + if obj.typename == "Room": + Menu = RoomBuildingMenu + else: + self.msg("|rThe object {} cannot be edited.|n".format(obj.get_display_name(self.caller))) + return + + menu = Menu(self.caller, obj) + menu.open() +``` + +This command is rather simple in itself: + +1. It has a key `@edit` and a lock to only allow builders to use it. +2. In its `func` method, it begins by checking the arguments, returning an error if no argument is specified. +3. It then searches for the given argument. We search globally. The `search` method used in this way will return the found object or `None`. It will also send the error message to the caller if necessary. +4. Assuming we have found an object, we check the object `typename`. This will be used later when we want to display several building menus. For the time being, we only handle `Room`. If the caller specified something else, we'll display an error. +5. Assuming this object is a `Room`, we have defined a `Menu` object containing the class of our building menu. We build this class (creating an instance), giving it the caller and the object to edit. +6. We then open the building menu, using the `open` method. + +The end might sound a bit surprising at first glance. But the process is still very simple: we create an instance of our building menu and call its `open` method. Nothing more. + +> Where is our building menu? + +If you go ahead and add this command and test it, you'll get an error. We haven't defined `RoomBuildingMenu` yet. + +To add this command, edit `commands/default_cmdsets.py`. Import our command, adding an import line at the top of the file: + +```python +""" +... +""" + +from evennia import default_cmds + +# The following line is to be added +from commands.building import EditCmd +``` + +And in the class below (`CharacterCmdSet`), add the last line of this code: + +```python +class CharacterCmdSet(default_cmds.CharacterCmdSet): + """ + The `CharacterCmdSet` contains general in-game commands like `look`, + `get`, etc available on in-game Character objects. It is merged with + the `AccountCmdSet` when an Account puppets a Character. + """ + key = "DefaultCharacter" + + def at_cmdset_creation(self): + """ + Populates the cmdset + """ + super().at_cmdset_creation() + # + # any commands you add below will overload the default ones. + # + self.add(EditCmd()) +``` + +### Our first menu + +So far, we can't use our building menu. Our `@edit` command will throw an error. We have to define the `RoomBuildingMenu` class. Open the `commands/building.py` file and add to the end of the file: + +```python +# ... at the end of commands/building.py +# Our building menu + +class RoomBuildingMenu(BuildingMenu): + + """ + Building menu to edit a room. + + For the time being, we have only one choice: key, to edit the room key. + + """ + + def init(self, room): + self.add_choice("key", "k", attr="key") +``` + +Save these changes, reload your game. You can now use the `@edit` command. Here's what we get (notice that the commands we enter into the game are prefixed with `> `, though this prefix will probably not appear in your MUD client): + +``` +> look +Limbo(#2) +Welcome to your new Evennia-based game! Visit http://www.evennia.com if you need +help, want to contribute, report issues or just join the community. +As Account #1 you can create a demo/tutorial area with @batchcommand tutorial_world.build. + +> @edit here +Building menu: Limbo + + [K]ey: Limbo + [Q]uit the menu + +> q +Closing the building menu. + +> @edit here +Building menu: Limbo + + [K]ey: Limbo + [Q]uit the menu + +> k +------------------------------------------------------------------------------- +key for Limbo(#2) + +You can change this value simply by entering it. + +Use @ to go back to the main menu. + +Current value: Limbo + +> A beautiful meadow +------------------------------------------------------------------------------- + +key for A beautiful meadow(#2) + +You can change this value simply by entering it. + +Use @ to go back to the main menu. + +Current value: A beautiful meadow + +> @ +Building menu: A beautiful meadow + + [K]ey: A beautiful meadow + [Q]uit the menu + +> q + +Closing the building menu. + +> look +A beautiful meadow(#2) +Welcome to your new Evennia-based game! Visit http://www.evennia.com if you need +help, want to contribute, report issues or just join the community. +As Account #1 you can create a demo/tutorial area with @batchcommand tutorial_world.build. +``` + +Before diving into the code, let's examine what we have: + +- When we use the `@edit here` command, a building menu for this room appears. +- This menu has two choices: + - Enter `k` to edit the room key. You will go into a choice where you can simply type the key room key (the way we have done here). You can use `@` to go back to the menu. + - You can use `q` to quit the menu. + +We then check, with the `look` command, that the menu has modified this room key. So by adding a class, with a method and a single line of code within, we've added a menu with two choices. + +### Code explanation + +Let's examine our code again: + +```python +class RoomBuildingMenu(BuildingMenu): + + """ + Building menu to edit a room. + + For the time being, we have only one choice: key, to edit the room key. + + """ + + def init(self, room): + self.add_choice("key", "k", attr="key") +``` + +- We first create a class inheriting from `BuildingMenu`. This is usually the case when we want to create a building menu with this contrib. +- In this class, we override the `init` method, which is called when the menu opens. +- In this `init` method, we call `add_choice`. This takes several arguments, but we've defined only three here: + - The choice name. This is mandatory and will be used by the building menu to know how to display this choice. + - The command key to access this choice. We've given a simple `"k"`. Menu commands usually are pretty short (that's part of the reason building menus are appreciated by builders). You can also specify additional aliases, but we'll see that later. + - We've added a keyword argument, `attr`. This tells the building menu that when we are in this choice, the text we enter goes into this attribute name. It's called `attr`, but it could be a room attribute or a typeclass persistent or non-persistent attribute (we'll see other examples as well). + +> We've added the menu choice for `key` here, why is another menu choice defined for `quit`? + +Our building menu creates a choice at the end of our choice list if it's a top-level menu (sub-menus don't have this feature). You can, however, override it to provide a different "quit" message or to perform some actions. + +I encourage you to play with this code. As simple as it is, it offers some functionalities already. + +## Customizing building menus + +This somewhat long section explains how to customize building menus. There are different ways depending on what you would like to achieve. We'll go from specific to more advanced here. + +### Generic choices + +In the previous example, we've used `add_choice`. This is one of three methods you can use to add choices. The other two are to handle more generic actions: + +- `add_choice_edit`: this is called to add a choice which points to the `EvEditor`. It is used to edit a description in most cases, although you could edit other things. We'll see an example shortly. `add_choice_edit` uses most of the `add_choice` keyword arguments we'll see, but usually we specify only two (sometimes three): + - The choice title as usual. + - The choice key (command key) as usual. + - Optionally, the attribute of the object to edit, with the `attr` keyword argument. By default, `attr` contains `db.desc`. It means that this persistent data attribute will be edited by the `EvEditor`. You can change that to whatever you want though. +- `add_choice_quit`: this allows to add a choice to quit the editor. Most advisable! If you don't do it, the building menu will do it automatically, except if you really tell it not to. Again, you can specify the title and key of this menu. You can also call a function when this menu closes. + +So here's a more complete example (you can replace your `RoomBuildingMenu` class in `commands/building.py` to see it): + +```python +class RoomBuildingMenu(BuildingMenu): + + """ + Building menu to edit a room. + """ + + def init(self, room): + self.add_choice("key", "k", attr="key") + self.add_choice_edit("description", "d") + self.add_choice_quit("quit this editor", "q") +``` + +So far, our building menu class is still thin... and yet we already have some interesting feature. See for yourself the following MUD client output (again, the commands are prefixed with `> ` to distinguish them): + +``` +> @reload + +> @edit here +Building menu: A beautiful meadow + + [K]ey: A beautiful meadow + [D]escription: + Welcome to your new Evennia-based game! Visit http://www.evennia.com if you need +help, want to contribute, report issues or just join the community. +As Account #1 you can create a demo/tutorial area with @batchcommand tutorial_world.build. + [Q]uit this editor + +> d + +----------Line Editor [editor]---------------------------------------------------- +01| Welcome to your new |wEvennia|n-based game! Visit http://www.evennia.com if you need +02| help, want to contribute, report issues or just join the community. +03| As Account #1 you can create a demo/tutorial area with |w@batchcommand tutorial_world.build|n. + +> :DD + +----------[l:03 w:034 c:0247]------------(:h for help)---------------------------- +Cleared 3 lines from buffer. + +> This is a beautiful meadow. But so beautiful I can't describe it. + +01| This is a beautiful meadow. But so beautiful I can't describe it. + +> :wq +Building menu: A beautiful meadow + + [K]ey: A beautiful meadow + [D]escription: + This is a beautiful meadow. But so beautiful I can't describe it. + [Q]uit this editor + +> q +Closing the building menu. + +> look +A beautiful meadow(#2) +This is a beautiful meadow. But so beautiful I can't describe it. +``` + +So by using the `d` shortcut in our building menu, an `EvEditor` opens. You can use the `EvEditor` commands (like we did here, `:DD` to remove all, `:wq` to save and quit). When you quit the editor, the description is saved (here, in `room.db.desc`) and you go back to the building menu. + +Notice that the choice to quit has changed too, which is due to our adding `add_choice_quit`. In most cases, you will probably not use this method, since the quit menu is added automatically. + +### `add_choice` options + +`add_choice` and the two methods `add_choice_edit` and `add_choice_quit` take a lot of optional arguments to make customization easier. Some of these options might not apply to `add_choice_edit` or `add_choice_quit` however. + +Below are the options of `add_choice`, specify them as arguments: + +- The first positional, mandatory argument is the choice title, as we have seen. This will influence how the choice appears in the menu. +- The second positional, mandatory argument is the command key to access to this menu. It is best to use keyword arguments for the other arguments. +- The `aliases` keyword argument can contain a list of aliases that can be used to access to this menu. For instance: `add_choice(..., aliases=['t'])` +- The `attr` keyword argument contains the attribute to edit when this choice is selected. It's a string, it has to be the name, from the object (specified in the menu constructor) to reach this attribute. For instance, a `attr` of `"key"` will try to find `obj.key` to read and write the attribute. You can specify more complex attribute names, for instance, `attr="db.desc"` to set the `desc` persistent attribute, or `attr="ndb.something"` so use a non-persistent data attribute on the object. +- The `text` keyword argument is used to change the text that will be displayed when the menu choice is selected. Menu choices provide a default text that you can change. Since this is a long text, it's useful to use multi-line strings (see an example below). +- The `glance` keyword argument is used to specify how to display the current information while in the menu, when the choice hasn't been opened. If you examine the previous examples, you will see that the current (`key` or `db.desc`) was shown in the menu, next to the command key. This is useful for seeing at a glance the current value (hence the name). Again, menu choices will provide a default glance if you don't specify one. +- The `on_enter` keyword argument allows to add a callback to use when the menu choice is opened. This is more advanced, but sometimes useful. +- The `on_nomatch` keyword argument is called when, once in the menu, the caller enters some text that doesn't match any command (including the `@` command). By default, this will edit the specified `attr`. +- The `on_leave` keyword argument allows to specify a callback used when the caller leaves the menu choice. This can be useful for cleanup as well. + +These are a lot of possibilities, and most of the time you won't need them all. Here is a short example using some of these arguments (again, replace the `RoomBuildingMenu` class in `commands/building.py` with the following code to see it working): + +```python +class RoomBuildingMenu(BuildingMenu): + + """ + Building menu to edit a room. + + For the time being, we have only one choice: key, to edit the room key. + + """ + + def init(self, room): + self.add_choice("title", key="t", attr="key", glance="{obj.key}", text=""" + ------------------------------------------------------------------------------- + Editing the title of {{obj.key}}(#{{obj.id}}) + + You can change the title simply by entering it. + Use |y{back}|n to go back to the main menu. + + Current title: |c{{obj.key}}|n + """.format(back="|n or |y".join(self.keys_go_back))) + self.add_choice_edit("description", "d") +``` + +Reload your game and see it in action: + +``` +> @edit here +Building menu: A beautiful meadow + + [T]itle: A beautiful meadow + [D]escription: + This is a beautiful meadow. But so beautiful I can't describe it. + [Q]uit the menu + +> t +------------------------------------------------------------------------------- + +Editing the title of A beautiful meadow(#2) + +You can change the title simply by entering it. +Use @ to go back to the main menu. + +Current title: A beautiful meadow + +> @ + +Building menu: A beautiful meadow + + [T]itle: A beautiful meadow + [D]escription: + This is a beautiful meadow. But so beautiful I can't describe it. + [Q]uit the menu + +> q +Closing the building menu. +``` + +The most surprising part is no doubt the text. We use the multi-line syntax (with `"""`). Excessive spaces will be removed from the left for each line automatically. We specify some information between braces... sometimes using double braces. What might be a bit odd: + +- `{back}` is a direct format argument we'll use (see the `.format` specifiers). +- `{{obj...}} refers to the object being edited. We use two braces, because `.format` will remove them. + +In `glance`, we also use `{obj.key}` to indicate we want to show the room's key. + +### Everything can be a function + +The keyword arguments of `add_choice` are often strings (type `str`). But each of these arguments can also be a function. This allows for a lot of customization, since we define the callbacks that will be executed to achieve such and such an operation. + +To demonstrate, we will try to add a new feature. Our building menu for rooms isn't that bad, but it would be great to be able to edit exits too. So we can add a new menu choice below description... but how to actually edit exits? Exits are not just an attribute to set: exits are objects (of type `Exit` by default) which stands between two rooms (object of type `Room`). So how can we show that? + +First let's add a couple of exits in limbo, so we have something to work with: + +``` +@tunnel n +@tunnel s +``` + +This should create two new rooms, exits leading to them from limbo and back to limbo. + +``` +> look +A beautiful meadow(#2) +This is a beautiful meadow. But so beautiful I can't describe it. +Exits: north(#4) and south(#7) +``` + +We can access room exits with the `exits` property: + +``` +> @py here.exits +[, ] +``` + +So what we need is to display this list in our building menu... and to allow to edit it would be great. Perhaps even add new exits? + +First of all, let's write a function to display the `glance` on existing exits. Here's the code, it's explained below: + +```python +class RoomBuildingMenu(BuildingMenu): + + """ + Building menu to edit a room. + + """ + + def init(self, room): + self.add_choice("title", key="t", attr="key", glance="{obj.key}", text=""" + ------------------------------------------------------------------------------- + Editing the title of {{obj.key}}(#{{obj.id}}) + + You can change the title simply by entering it. + Use |y{back}|n to go back to the main menu. + + Current title: |c{{obj.key}}|n + """.format(back="|n or |y".join(self.keys_go_back))) + self.add_choice_edit("description", "d") + self.add_choice("exits", "e", glance=glance_exits, attr="exits") + + +# Menu functions +def glance_exits(room): + """Show the room exits.""" + if room.exits: + glance = "" + for exit in room.exits: + glance += "\n |y{exit}|n".format(exit=exit.key) + + return glance + + return "\n |gNo exit yet|n" +``` + +When the building menu opens, it displays each choice to the caller. A choice is displayed with its title (rendered a bit nicely to show the key as well) and the glance. In the case of the `exits` choice, the glance is a function, so the building menu calls this function giving it the object being edited (the room here). The function should return the text to see. + +``` +> @edit here +Building menu: A beautiful meadow + + [T]itle: A beautiful meadow + [D]escription: + This is a beautiful meadow. But so beautiful I can't describe it. + [E]xits: + north + south + [Q]uit the menu + +> q +Closing the editor. +``` + +> How do I know the parameters of the function to give? + +The function you give can accept a lot of different parameters. This allows for a flexible approach but might seem complicated at first. Basically, your function can accept any parameter, and the building menu will send only the parameter based on their names. If your function defines an argument named `caller` for instance (like `def func(caller):` ), then the building menu knows that the first argument should contain the caller of the building menu. Here are the arguments, you don't have to specify them (if you do, they need to have the same name): + +- `menu`: if your function defines an argument named `menu`, it will contain the building menu itself. +- `choice`: if your function defines an argument named `choice`, it will contain the `Choice` object representing this menu choice. +- `string`: if your function defines an argument named `string`, it will contain the user input to reach this menu choice. This is not very useful, except on `nomatch` callbacks which we'll see later. +- `obj`: if your function defines an argument named `obj`, it will contain the building menu edited object. +- `caller`: if your function defines an argument named `caller`, it will contain the caller of the building menu. +- Anything else: any other argument will contain the object being edited by the building menu. + +So in our case: + +```python +def glance_exits(room): +``` + +The only argument we need is `room`. It's not present in the list of possible arguments, so the editing object of the building menu (the room, here) is given. + +> Why is it useful to get the menu or choice object? + +Most of the time, you will not need these arguments. In very rare cases, you will use them to get specific data (like the default attribute that was set). This tutorial will not elaborate on these possibilities. Just know that they exist. + +We should also define a text callback, so that we can enter our menu to see the room exits. We'll see how to edit them in the next section but this is a good opportunity to show a more complete callback. To see it in action, as usual, replace the class and functions in `commands/building.py`: + +```python +# Our building menu + +class RoomBuildingMenu(BuildingMenu): + + """ + Building menu to edit a room. + + """ + + def init(self, room): + self.add_choice("title", key="t", attr="key", glance="{obj.key}", text=""" + ------------------------------------------------------------------------------- + Editing the title of {{obj.key}}(#{{obj.id}}) + + You can change the title simply by entering it. + Use |y{back}|n to go back to the main menu. + + Current title: |c{{obj.key}}|n + """.format(back="|n or |y".join(self.keys_go_back))) + self.add_choice_edit("description", "d") + self.add_choice("exits", "e", glance=glance_exits, attr="exits", text=text_exits) + + +# Menu functions +def glance_exits(room): + """Show the room exits.""" + if room.exits: + glance = "" + for exit in room.exits: + glance += "\n |y{exit}|n".format(exit=exit.key) + + return glance + + return "\n |gNo exit yet|n" + +def text_exits(caller, room): + """Show the room exits in the choice itself.""" + text = "-" * 79 + text += "\n\nRoom exits:" + text += "\n Use |y@c|n to create a new exit." + text += "\n\nExisting exits:" + if room.exits: + for exit in room.exits: + text += "\n |y@e {exit}|n".format(exit=exit.key) + if exit.aliases.all(): + text += " (|y{aliases}|n)".format(aliases="|n, |y".join( + alias for alias in exit.aliases.all())) + if exit.destination: + text += " toward {destination}".format(destination=exit.get_display_name(caller)) + else: + text += "\n\n |gNo exit has yet been defined.|n" + + return text +``` + +Look at the second callback in particular. It takes an additional argument, the caller (remember, the argument names are important, their order is not relevant). This is useful for displaying destination of exits accurately. Here is a demonstration of this menu: + +``` +> @edit here +Building menu: A beautiful meadow + + [T]itle: A beautiful meadow + [D]escription: + This is a beautiful meadow. But so beautiful I can't describe it. + [E]xits: + north + south + [Q]uit the menu + +> e +------------------------------------------------------------------------------- + +Room exits: + Use @c to create a new exit. + +Existing exits: + @e north (n) toward north(#4) + @e south (s) toward south(#7) + +> @ +Building menu: A beautiful meadow + + [T]itle: A beautiful meadow + [D]escription: + This is a beautiful meadow. But so beautiful I can't describe it. + [E]xits: + north + south + [Q]uit the menu + +> q +Closing the building menu. +``` + +Using callbacks allows a great flexibility. We'll now see how to handle sub-menus. + +### Sub-menus for complex menus + +A menu is relatively flat: it has a root (where you see all the menu choices) and individual choices you can go to using the menu choice keys. Once in a choice you can type some input or go back to the root menu by entering the return command (usually `@`). + +Why shouldn't individual exits have their own menu though? Say, you edit an exit and can change its key, description or aliases... perhaps even destination? Why ever not? It would make building much easier! + +The building menu system offers two ways to do that. The first is nested keys: nested keys allow to go beyond just one menu/choice, to have menus with more layers. Using them is quick but might feel a bit counter-intuitive at first. Another option is to create a different menu class and redirect from the first to the second. This option might require more lines but is more explicit and can be re-used for multiple menus. Adopt one of them depending of your taste. + +#### Nested menu keys + +So far, we've only used menu keys with one letter. We can add more, of course, but menu keys in their simple shape are just command keys. Press "e" to go to the "exits" choice. + +But menu keys can be nested. Nested keys allow to add choices with sub-menus. For instance, type "e" to go to the "exits" choice, and then you can type "c" to open a menu to create a new exit, or "d" to open a menu to delete an exit. The first menu would have the "e.c" key (first e, then c), the second menu would have key as "e.d". + +That's more advanced and, if the following code doesn't sound very friendly to you, try the next section which provides a different approach of the same problem. + +So we would like to edit exits. That is, you can type "e" to go into the choice of exits, then enter `@e` followed by the exit name to edit it... which will open another menu. In this sub-menu you could change the exit key or description. + +So we have a menu hierarchy similar to that: + +``` +t Change the room title +d Change the room description +e Access the room exits + [exit name] Access the exit name sub-menu + [text] Change the exit key +``` + +Or, if you prefer an example output: + +``` +> look +A beautiful meadow(#2) +This is a beautiful meadow. But so beautiful I can't describe it. +Exits: north(#4) and south(#7) + +> @edit here +Building menu: A beautiful meadow + + [T]itle: A beautiful meadow + [D]escription: + This is a beautiful meadow. But so beautiful I can't describe it. + [E]xits: + north + south + [Q]uit the menu + +> e +------------------------------------------------------------------------------- + +Room exits : + Use @c to create a new exit. + +Existing exits: + @e north (n) toward north(#4) + @e south (s) toward south(#7) + +> @e north +Editing: north +Exit north: +Enter the exit key to change it, or @ to go back. + +New exit key: + +> door + +Exit door: +Enter the exit key to change it, or @ to go back. + +New exit key: + +> @ + +------------------------------------------------------------------------------- + +Room exits : + Use @c to create a new exit. + +Existing exits: + @e door (n) toward door(#4) + @e south (s) toward south(#7) + +> @ +Building menu: A beautiful meadow + + [T]itle: A beautiful meadow + [D]escription: + This is a beautiful meadow. But so beautiful I can't describe it. + [E]xits: + door + south + [Q]uit the menu + +> q +Closing the building menu. +``` + +This needs a bit of code and a bit of explanation. So here we go... the code first, the explanations next! + +```python +# ... from commands/building.py +# Our building menu + +class RoomBuildingMenu(BuildingMenu): + + """ + Building menu to edit a room. + + For the time being, we have only one choice: key, to edit the room key. + + """ + + def init(self, room): + self.add_choice("title", key="t", attr="key", glance="{obj.key}", text=""" + ------------------------------------------------------------------------------- + Editing the title of {{obj.key}}(#{{obj.id}}) + + You can change the title simply by entering it. + Use |y{back}|n to go back to the main menu. + + Current title: |c{{obj.key}}|n + """.format(back="|n or |y".join(self.keys_go_back))) + self.add_choice_edit("description", "d") + self.add_choice("exits", "e", glance=glance_exits, text=text_exits, on_nomatch=nomatch_exits) + + # Exit sub-menu + self.add_choice("exit", "e.*", text=text_single_exit, on_nomatch=nomatch_single_exit) + + + +# Menu functions +def glance_exits(room): + """Show the room exits.""" + if room.exits: + glance = "" + for exit in room.exits: + glance += "\n |y{exit}|n".format(exit=exit.key) + + return glance + + return "\n |gNo exit yet|n" + +def text_exits(caller, room): + """Show the room exits in the choice itself.""" + text = "-" * 79 + text += "\n\nRoom exits:" + text += "\n Use |y@c|n to create a new exit." + text += "\n\nExisting exits:" + if room.exits: + for exit in room.exits: + text += "\n |y@e {exit}|n".format(exit=exit.key) + if exit.aliases.all(): + text += " (|y{aliases}|n)".format(aliases="|n, |y".join( + alias for alias in exit.aliases.all())) + if exit.destination: + text += " toward {destination}".format(destination=exit.get_display_name(caller)) + else: + text += "\n\n |gNo exit has yet been defined.|n" + + return text + +def nomatch_exits(menu, caller, room, string): + """ + The user typed something in the list of exits. Maybe an exit name? + """ + string = string[3:] + exit = caller.search(string, candidates=room.exits) + if exit is None: + return + + # Open a sub-menu, using nested keys + caller.msg("Editing: {}".format(exit.key)) + menu.move(exit) + return False + +# Exit sub-menu +def text_single_exit(menu, caller): + """Show the text to edit single exits.""" + exit = menu.keys[1] + if exit is None: + return "" + + return """ + Exit {exit}: + + Enter the exit key to change it, or |y@|n to go back. + + New exit key: + """.format(exit=exit.key) + +def nomatch_single_exit(menu, caller, room, string): + """The user entered something in the exit sub-menu. Replace the exit key.""" + # exit is the second key element: keys should contain ['e', ] + exit = menu.keys[1] + if exit is None: + caller.msg("|rCannot find the exit.|n") + menu.move(back=True) + return False + + exit.key = string + return True +``` + +> That's a lot of code! And we only handle editing the exit key! + +That's why at some point you might want to write a real sub-menu, instead of using simple nested keys. But you might need both to build pretty menus too! + +1. The first thing new is in our menu class. After creating a `on_nomatch` callback for the exits menu (that shouldn't be a surprised), we need to add a nested key. We give this menu a key of `"e.*"`. That's a bit odd! "e" is our key to the exits menu, . is the separator to indicate a nested menu, and * means anything. So basically, we create a nested menu that is contains within the exits menu and anything. We'll see what this "anything" is in practice. +2. The `glance_exits` and `text_exits` are basically the same. +3. The `nomatch_exits` is short but interesting. It's called when we enter some text in the "exits" menu (that is, in the list of exits). We have said that the user should enter `@e` followed by the exit name to edit it. So in the `nomatch_exits` callbac, we check for that input. If the entered text begins by `@e`, we try to find the exit in the room. If we do... +4. We call the `menu.move` method. That's where things get a bit complicated with nested menus: we need to use `menu.move` to change from layer to layer. Here, we are in the choice of exits (the exits menu, of key "e"). We need to go down one layer to edit an exit. So we call `menu.move` and give it an exit object. The menu system remembers what position the user is based on the keys she has entered: when the user opens the menu, there is no key. If she selects the exits choice, the menu key being "e", the position of the user is `["e"]` (a list with the menu keys). If we call `menu.move`, whatever we give to this method will be appended to the list of keys, so that the user position becomes `["e", ]`. +5. In the menu class, we have defined the menu "e.*", meaning "the menu contained in the exits choice plus anything". The "anything" here is an exit: we have called `menu.move(exit)`, so the `"e.*"` menu choice is chosen. +6. In this menu, the text is set to a callback. There is also a `on_nomatch` callback that is called whenever the user enters some text. If so, we change the exit name. + +Using `menu.move` like this is a bit confusing at first. Sometimes it's useful. In this case, if we want a more complex menu for exits, it makes sense to use a real sub-menu, not nested keys like this. But sometimes, you will find yourself in a situation where you don't need a full menu to handle a choice. + +#### Full sub-menu as separate classes + +The best way to handle individual exits is to create two separate classes: + +- One for the room menu. +- One for the individual exit menu. + +The first one will have to redirect on the second. This might be more intuitive and flexible, depending on what you want to achieve. So let's build two menus: + +```python +# Still in commands/building.py, replace the menu class and functions by... +# Our building menus + +class RoomBuildingMenu(BuildingMenu): + + """ + Building menu to edit a room. + """ + + def init(self, room): + self.add_choice("title", key="t", attr="key", glance="{obj.key}", text=""" + ------------------------------------------------------------------------------- + Editing the title of {{obj.key}}(#{{obj.id}}) + + You can change the title simply by entering it. + Use |y{back}|n to go back to the main menu. + + Current title: |c{{obj.key}}|n + """.format(back="|n or |y".join(self.keys_go_back))) + self.add_choice_edit("description", "d") + self.add_choice("exits", "e", glance=glance_exits, text=text_exits, on_nomatch=nomatch_exits) + + +# Menu functions +def glance_exits(room): + """Show the room exits.""" + if room.exits: + glance = "" + for exit in room.exits: + glance += "\n |y{exit}|n".format(exit=exit.key) + + return glance + + return "\n |gNo exit yet|n" + +def text_exits(caller, room): + """Show the room exits in the choice itself.""" + text = "-" * 79 + text += "\n\nRoom exits:" + text += "\n Use |y@c|n to create a new exit." + text += "\n\nExisting exits:" + if room.exits: + for exit in room.exits: + text += "\n |y@e {exit}|n".format(exit=exit.key) + if exit.aliases.all(): + text += " (|y{aliases}|n)".format(aliases="|n, |y".join( + alias for alias in exit.aliases.all())) + if exit.destination: + text += " toward {destination}".format(destination=exit.get_display_name(caller)) + else: + text += "\n\n |gNo exit has yet been defined.|n" + + return text + +def nomatch_exits(menu, caller, room, string): + """ + The user typed something in the list of exits. Maybe an exit name? + """ + string = string[3:] + exit = caller.search(string, candidates=room.exits) + if exit is None: + return + + # Open a sub-menu, using nested keys + caller.msg("Editing: {}".format(exit.key)) + menu.open_submenu("commands.building.ExitBuildingMenu", exit, parent_keys=["e"]) + return False + +class ExitBuildingMenu(BuildingMenu): + + """ + Building menu to edit an exit. + + """ + + def init(self, exit): + self.add_choice("key", key="k", attr="key", glance="{obj.key}") + self.add_choice_edit("description", "d") +``` + +The code might be much easier to read. But before detailing it, let's see how it behaves in the game: + +``` +> @edit here +Building menu: A beautiful meadow + + [T]itle: A beautiful meadow + [D]escription: + This is a beautiful meadow. But so beautiful I can't describe it. + [E]xits: + door + south + [Q]uit the menu + +> e +------------------------------------------------------------------------------- + +Room exits: + Use @c to create a new exit. + +Existing exits: + @e door (n) toward door(#4) + @e south (s) toward south(#7) + +Editing: door + +> @e door +Building menu: door + + [K]ey: door + [D]escription: + None + +> k +------------------------------------------------------------------------------- +key for door(#4) + +You can change this value simply by entering it. + +Use @ to go back to the main menu. + +Current value: door + +> north + +------------------------------------------------------------------------------- +key for north(#4) + +You can change this value simply by entering it. + +Use @ to go back to the main menu. + +Current value: north + +> @ +Building menu: north + + [K]ey: north + [D]escription: + None + +> d +----------Line Editor [editor]---------------------------------------------------- +01| None +----------[l:01 w:001 c:0004]------------(:h for help)---------------------------- + +> :DD +Cleared 1 lines from buffer. + +> This is the northern exit. Cool huh? +01| This is the northern exit. Cool huh? + +> :wq +Building menu: north + [K]ey: north + [D]escription: + This is the northern exit. Cool huh? + +> @ +------------------------------------------------------------------------------- +Room exits: + Use @c to create a new exit. + +Existing exits: + @e north (n) toward north(#4) + @e south (s) toward south(#7) + +> @ +Building menu: A beautiful meadow + + [T]itle: A beautiful meadow + [D]escription: + This is a beautiful meadow. But so beautiful I can't describe it. + [E]xits: + north + south + [Q]uit the menu + +> q +Closing the building menu. + +> look +A beautiful meadow(#2) +This is a beautiful meadow. But so beautiful I can't describe it. +Exits: north(#4) and south(#7) +> @py here.exits[0] +>>> here.exits[0] +north +> @py here.exits[0].db.desc +>>> here.exits[0].db.desc +This is the northern exit. Cool huh? +``` + +Very simply, we created two menus and bridged them together. This needs much less callbacks. There is only one line in the `nomatch_exits` to add: + +```python + menu.open_submenu("commands.building.ExitBuildingMenu", exit, parent_keys=["e"]) +``` + +We have to call `open_submenu` on the menu object (which opens, as its name implies, a sub menu) with three arguments: + +- The path of the menu class to create. It's the Python class leading to the menu (notice the dots). +- The object that will be edited by the menu. Here, it's our exit, so we give it to the sub-menu. +- The keys of the parent to open when the sub-menu closes. Basically, when we're in the root of the sub-menu and press `@`, we'll open the parent menu, with the parent keys. So we specify `["e"]`, since the parent menus is the "exits" choice. + +And that's it. The new class will be automatically created. As you can see, we have to create a `on_nomatch` callback to open the sub-menu, but once opened, it automatically close whenever needed. + +### Generic menu options + +There are some options that can be set on any menu class. These options allow for greater customization. They are class attributes (see the example below), so just set them in the class body: + +- `keys_go_back` (default to `["@"]`): the keys to use to go back in the menu hierarchy, from choice to root menu, from sub-menu to parent-menu. By default, only a `@` is used. You can change this key for one menu or all of them. You can define multiple return commands if you want. +- `sep_keys` (default `"."`): this is the separator for nested keys. There is no real need to redefine it except if you really need the dot as a key, and need nested keys in your menu. +- `joker_key` (default to `"*"`): used for nested keys to indicate "any key". Again, you shouldn't need to change it unless you want to be able to use the @*@ in a command key, and also need nested keys in your menu. +- `min_shortcut` (default to `1`): although we didn't see it here, one can create a menu choice without giving it a key. If so, the menu system will try to "guess" the key. This option allows to change the minimum length of any key for security reasons. + +To set one of them just do so in your menu class(es): + +```python +class RoomBuildingMenu(BuildingMenu): + keys_go_back = ["/"] + min_shortcut = 2 +``` + +## Conclusion + +Building menus mean to save you time and create a rich yet simple interface. But they can be complicated to learn and require reading the source code to find out how to do such and such a thing. This documentation, however long, is an attempt at describing this system, but chances are you'll still have questions about it after reading it, especially if you try to push this system to a great extent. Do not hesitate to read the documentation of this contrib, it's meant to be exhaustive but user-friendly. \ No newline at end of file diff --git a/docs/source/Choosing-An-SQL-Server.md b/docs/source/Choosing-An-SQL-Server.md new file mode 100644 index 0000000000..1a2d073620 --- /dev/null +++ b/docs/source/Choosing-An-SQL-Server.md @@ -0,0 +1,180 @@ +# Choosing An SQL Server + + +This page gives an overview of the supported SQL databases as well as instructions on install: + + - SQLite3 (default) + - PostgreSQL + - MySQL / MariaDB + +Since Evennia uses [Django](http://djangoproject.com), most of our notes are based off of what we know from the community and their documentation. While the information below may be useful, you can always find the most up-to-date and "correct" information at Django's [Notes about supported Databases](http://docs.djangoproject.com/en/dev/ref/databases/#ref-databases) page. + +## SQLite3 + +[SQLite3](https://sqlite.org/) is a light weight single-file database. It is our default database and Evennia will set this up for you automatically if you give no other options. SQLite stores the database in a single file (`mygame/server/evennia.db3`). This means it's very easy to reset this database - just delete (or move) that `evennia.db3` file and run `evennia migrate` again! No server process is needed and the administrative overhead and resource consumption is tiny. It is also very fast since it's run in-memory. For the vast majority of Evennia installs it will probably be all that's ever needed. + +SQLite will generally be much faster than MySQL/PostgreSQL but its performance comes with two drawbacks: + +* SQLite [ignores length constraints by design](https://www.sqlite.org/faq.html#q9); it is possible to store very large strings and numbers in fields that technically should not accept them. This is not something you will notice; your game will read and write them and function normally, but this *can* create some data migration problems requiring careful thought if you do need to change databases later. +* SQLite can scale well to storage of millions of objects, but if you end up with a thundering herd of users trying to access your MUD and web site at the same time, or you find yourself writing long-running functions to update large numbers of objects on a live game, either will yield errors and interference. SQLite does not work reliably with multiple concurrent threads or processes accessing its records. This has to do with file-locking clashes of the database file. So for a production server making heavy use of process- or thread pools (or when using a third-party webserver like Apache), a proper database is a more appropriate choice. + +### Install of SQlite3 + +This is installed and configured as part of Evennia. The database file is created as `mygame/server/evennia.db3` when you run + + evennia migrate + +without changing any database options. An optional requirement is the `sqlite3` client program - this is required if you want to inspect the database data manually. A shortcut for using it with the evennia database is `evennia dbshell`. Linux users should look for the `sqlite3` package for their distro while Mac/Windows should get the [sqlite-tools package from this page](https://sqlite.org/download.html). + +To inspect the default Evennia database (once it's been created), go to your game dir and do + +```bash + sqlite3 server/evennia.db3 + # or + evennia dbshell +``` + +This will bring you into the sqlite command line. Use `.help` for instructions and `.quit` to exit. See [here](https://gist.github.com/vincent178/10889334) for a cheat-sheet of commands. + +## PostgreSQL + +[PostgreSQL](https://www.postgresql.org/) is an open-source database engine, recommended by Django. While not as fast as SQLite for normal usage, it will scale better than SQLite, especially if your game has an very large database and/or extensive web presence through a separate server process. + +### Install and initial setup of PostgreSQL + +First, install the posgresql server. Version `9.6` is tested with Evennia. Packages are readily available for all distributions. You need to also get the `psql` client (this is called `postgresql-client` on debian-derived systems). Windows/Mac users can [find what they need on the postgresql download page](https://www.postgresql.org/download/). You should be setting up a password for your database-superuser (always called `postgres`) when you install. + +For interaction with Evennia you need to also install `psycopg2` to your Evennia install (`pip install psycopg2-binary` in your virtualenv). This acts as the python bridge to the database server. + +Next, start the postgres client: + +```bash + psql -U postgres --password +``` +> :warning: **Warning:** With the `--password` argument, Postgres should prompt you for a password. +If it won't, replace that with `-p yourpassword` instead. Do not use the `-p` argument unless you have to since the resulting command, and your password, will be logged in the shell history. + +This will open a console to the postgres service using the psql client. + +On the psql command line: + +```sql +CREATE USER evennia WITH PASSWORD 'somepassword'; +CREATE DATABASE evennia; + +# Postgres-specific optimizations +# https://docs.djangoproject.com/en/dev/ref/databases/#optimizing-postgresql-s-configuration +ALTER ROLE evennia SET client_encoding TO 'utf8'; +ALTER ROLE evennia SET default_transaction_isolation TO 'read committed'; +ALTER ROLE evennia SET timezone TO 'UTC'; + +GRANT ALL PRIVILEGES ON DATABASE evennia TO evennia; +\l # list all databases and permissions +\q # exit +``` +[Here](https://gist.github.com/Kartones/dd3ff5ec5ea238d4c546) is a cheat-sheet for psql commands. + +We create a database user 'evennia' and a new database named `evennia` (you can call them whatever you want though). We then grant the 'evennia' user full privileges to the new database so it can read/write etc to it. +If you in the future wanted to completely wipe the database, an easy way to do is to log in as the `postgres` superuser again, then do `DROP DATABASE evennia;`, then `CREATE` and `GRANT` steps above again to recreate the database and grant privileges. + +### Evennia PostgreSQL configuration + +Edit `mygame/server/conf/secret_settings.py and add the following section: + +```python +# +# PostgreSQL Database Configuration +# +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'NAME': 'evennia', + 'USER': 'evennia', + 'PASSWORD': 'somepassword', + 'HOST': 'localhost', + 'PORT': '' # use default + }} +``` + +If you used some other name for the database and user, enter those instead. Run + + evennia migrate + +to populate your database. Should you ever want to inspect the database directly you can from now on also use + + evennia dbshell + +as a shortcut to get into the postgres command line for the right database and user. + +With the database setup you should now be able to start start Evennia normally with your new database. + +## MySQL / MariaDB + +[MySQL](https://www.mysql.com/) is a commonly used proprietary database system, on par with PostgreSQL. There is an open-source alternative called [MariaDB](https://mariadb.org/) that mimics all functionality and command syntax of the former. So this section covers both. + +### Installing and initial setup of MySQL/MariaDB + +First, install and setup MariaDB or MySQL for your specific server. Linux users should look for the `mysql-server` or `mariadb-server` packages for their respective distributions. Windows/Mac users will find what they need from the [MySQL downloads](https://www.mysql.com/downloads/) or [MariaDB downloads](https://mariadb.org/download/) pages. You also need the respective database clients (`mysql`, `mariadb-client`), so you can setup the database itself. When you install the server you should usually be asked to set up the database root user and password. + +You will finally also need a Python interface to allow Evennia to talk to the database. Django recommends the `mysqlclient` one. Install this into the evennia virtualenv with `pip install mysqlclient`. + +Start the database client (this is named the same for both mysql and mariadb): + +```bash +mysql -u root -p +``` + +You should get to enter your database root password (set this up when you installed the database server). + +Inside the database client interface: + +```sql +CREATE USER 'evennia'@'localhost' IDENTIFIED BY 'somepassword'; +CREATE DATABASE evennia; +ALTER DATABASE `evennia` CHARACTER SET utf8; # note that it's `evennia` not 'evennia'! +GRANT ALL PRIVILEGES ON evennia.* TO 'evennia'@'localhost'; +FLUSH PRIVILEGES; +exit +``` +[Here](https://gist.github.com/hofmannsven/9164408) is a mysql command cheat sheet. + +Above we created a new local user and database (we called both 'evennia' here, you can name them what you prefer). We set the character set to `utf8` to avoid an issue with prefix character length that can pop up on some installs otherwise. Next we grant the 'evennia' user all privileges on the `evennia` database and make sure the privileges are applied. Exiting the client brings us back to the normal terminal/console. + +> Note: If you are not using MySQL for anything else you might consider granting the 'evennia' user full privileges with `GRANT ALL PRIVILEGES ON *.* TO 'evennia'@'localhost';`. If you do, it means you can use `evennia dbshell` later to connect to mysql, drop your database and re-create it as a way of easy reset. Without this extra privilege you will be able to drop the database but not re-create it without first switching to the database-root user. + +## Add MySQL configuration to Evennia + +To tell Evennia to use your new database you need to edit `mygame/server/conf/settings.py` (or `secret_settings.py` if you don't want your db info passed around on git repositories). + +> Note: The Django documentation suggests using an external `db.cnf` or other external conf-formatted file. Evennia users have however found that this leads to problems (see e.g. [issue #1184](https://git.io/vQdiN)). To avoid trouble we recommend you simply put the configuration in your settings as below. + +```python + # + # MySQL Database Configuration + # + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', + 'NAME': 'evennia', + 'USER': 'evennia', + 'PASSWORD': 'somepassword', + 'HOST': 'localhost', # or an IP Address that your DB is hosted on + 'PORT': '', # use default port + } + } +``` +Change this to fit your database setup. Next, run: + + evennia migrate + +to populate your database. Should you ever want to inspect the database directly you can from now on also use + + evennia dbshell + +as a shortcut to get into the postgres command line for the right database and user. + +With the database setup you should now be able to start start Evennia normally with your new database. + +## Others + +No testing has been performed with Oracle, but it is also supported through Django. There are community maintained drivers for [MS SQL](http://code.google.com/p/django-mssql/) and possibly a few others. If you try other databases out, consider expanding this page with instructions. \ No newline at end of file diff --git a/docs/source/Client-Support-Grid.md b/docs/source/Client-Support-Grid.md new file mode 100644 index 0000000000..d6c249c5e9 --- /dev/null +++ b/docs/source/Client-Support-Grid.md @@ -0,0 +1,79 @@ +# Client Support Grid + +This grid tries to gather Evennia-specific knowledge about the various clients and protocols used. Everyone's welcome to report their findings. + +##### Legend: + + - **Name**: The name of the client. If it's only available for a specific OS, it should be noted here too. + - **Version**: Which version or range of client versions were tested. + - **Comments**: Any comments or quirks on using this client with Evennia should be added here. Also note if some other protocol than Telnet is used (like Websockets, SSH etc). + +## Client Grid + +Name | Version | Comments +-----------------------|:----------:|------------- +[Evennia webclient][1] | 0.6 | Uses WS/AJAX. [Current client issues][2] +[tintin++][3] | 2.0+ | No MXP support +[tinyfugue][4] | 5.0+ | No UTF-8 support +[MUSHclient][5](Win) | 4.94 | NAWS reports full text area +[Zmud][6](Win) | 7.21 | *UNTESTED* +[Cmud][7](Win) | v3 | *UNTESTED* +[Potato][8] | 2.0.0b16 | No MXP, MCCP support. Win 32bit does not understand "localhost", must use `127.0.0.1`. [Newline issue](https://github.com/evennia/evennia/issues/1131). *Won't send a single blank line on Enter press. +[Mudlet][9] | 3.4+ | No known issues. Some older versions showed <> as html under MXP. +[SimpleMU][10](Win) | full | *UNTESTED*. Discontinued. NAWS reports pixel size. +[Atlantis][11](Mac) | 0.9.9.4 | No known issues. +[GMUD][12] | 0.0.1 | Can't handle any telnet handshakes. Not recommended. +[BeipMU][13](Win) | 3.0.255 | No MXP support. Best to enable "MUD prompt handling", disable "Handle HTML tags". +[MudRammer][14](IOS) | 1.8.7 | Bad Telnet Protocol compliance: displays spurious characters. +[MUDMaster][15](IOS) | 1.3.1 | *UNTESTED* +[BlowTorch][16](Andr) | 1.1.3 | *Telnet NOP displays as spurious character. +[Mukluk][17](Andr) | 2015.11.20| *Telnet NOP displays as spurious character. Has UTF-8/Emoji support. +[Gnome-MUD][18](Unix) | 0.11.2 | Telnet handshake errors. First (only) attempt at logging in fails. +[Spyrit][19] | 0.4 | No MXP, OOB support. +[JamochaMUD][20] | 5.2 | Does not support ANSI within MXP text. +[DuckClient][21](Chrome)| 4.2 | No MXP support. Displays Telnet Go-Ahead and WILL SUPPRESS-GO-AHEAD as ù character. Also seems to run the `version` command on connection, which will not work in `MULTISESSION_MODES` above 1. +[KildClient][22] | 2.11.1 | No known issues. + +[1]: https://github.com/evennia/evennia/wiki/Web%20features#web-client +[2]: https://github.com/evennia/evennia/issues?utf8=%E2%9C%93&q=client+status%3Dopen+] +[3]: http://tintin.sourceforge.net/ +[4]: http://tinyfugue.sourceforge.net/ +[5]: http://mushclient.com/ +[6]: http://forums.zuggsoft.com/index.php?page=4&action=file&file_id=65 +[7]: http://forums.zuggsoft.com/index.php?page=4&action=category&cat_id=11 +[8]: http://www.potatomushclient.com/ +[9]: http://www.mudlet.org/ +[10]: https://archive.org/details/tucows_196173_SimpleMU_MU_Client +[11]: http://www.riverdark.net/atlantis/ +[12]: https://sourceforge.net/projects/g-mud/ +[13]: http://www.beipmu.com/ +[14]: https://itunes.apple.com/us/app/mudrammer-a-modern-mud-client/id597157072 +[15]: https://itunes.apple.com/us/app/mudmaster/id341160033 +[16]: http://bt.happygoatstudios.com/ +[17]: https://play.google.com/store/apps/details?id=com.crap.mukluk +[18]: https://github.com/GNOME/gnome-mud +[19]: https://spyrit.ierne.eu.org/ +[20]: http://jamochamud.org/ +[21]: http://duckclient.com/ +[22]: https://www.kildclient.org/ + +## Workarounds for client issues: + +### Issue: Telnet NOP displays as spurious character. + +#### Known Clients +* [BlowTorch][16](Andr) +* [Mukluk][17](Andr) + +#### Workarounds +* Set the command in game to `@option NOPKEEPALIVE=off` for the session, or use the `/save` parameter to disable it for that Evennian account permanently. +* Client-side: Set a gag-type trigger on the NOP character to make it invisible to the client. + + +### Issue: Won't send blank line on Enter key press. + +#### Known Clients +* [Potato][8] + +#### Workaround +* Press Control Enter, then Enter key again to send blank line. \ No newline at end of file diff --git a/docs/source/Coding-FAQ.md b/docs/source/Coding-FAQ.md new file mode 100644 index 0000000000..3ed214401c --- /dev/null +++ b/docs/source/Coding-FAQ.md @@ -0,0 +1,312 @@ +# Coding FAQ + +*This FAQ page is for users to share their solutions to coding problems. Keep it brief and link to the docs if you can rather than too lengthy explanations. Don't forget to check if an answer already exists before answering - maybe you can clarify that answer rather than to make a new Q&A section.* + + +## Table of Contents + +- [Removing default commands](#removing-default-commands) +- [Preventing character from moving based on a condition](#preventing-character-from-moving-based-on-a-condition) +- [Reference initiating object in an EvMenu command](#reference-initiating-object-in-an-evmenu-command) +- [Adding color to default Evennia Channels](#adding-color-to-default-evennia-channels) +- [Selectively turn off commands in a room](#selectively-turn-off-commands-in-a-room) +- [Select Command based on a condition](#select-command-based-on-a-condition) +- [Automatically updating code when reloading](#automatically-updating-code-when-reloading) +- [Changing all exit messages](#changing-all-exit-messages) +- [Add parsing with the "to" delimiter](#add-parsing-with-the-to-delimiter) +- [Store last used session IP address](#store-last-used-session-ip-address) +- [Use wide characters with EvTable](#non-latin-characters-in-evtable) + +## Removing default commands +**Q:** How does one *remove* (not replace) e.g. the default `get` [Command](Commands) from the Character [Command Set](Command-Sets)? + +**A:** Go to `mygame/commands/default_cmdsets.py`. Find the `CharacterCmdSet` class. It has one method named `at_cmdset_creation`. At the end of that method, add the following line: `self.remove(default_cmds.CmdGet())`. See the [Adding Commands Tutorial](Adding-Command-Tutorial) for more info. + +## Preventing character from moving based on a condition +**Q:** How does one keep a character from using any exit, if they meet a certain condition? (I.E. in combat, immobilized, etc.) + +**A:** The `at_before_move` hook is called by Evennia just before performing any move. If it returns `False`, the move is aborted. Let's say we want to check for an [Attribute](Attributes) `cantmove`. Add the following code to the `Character` class: + +```python +def at_before_move(self, destination): + "Called just before trying to move" + if self.db.cantmove: # replace with condition you want to test + self.msg("Something is preventing you from moving!") + return False + return True +``` + +## Reference initiating object in an EvMenu command. +**Q:** An object has a Command on it starts up an EvMenu instance. How do I capture a reference to that object for use in the menu? + +**A:** When an [EvMenu](EvMenu) is started, the menu object is stored as `caller.ndb._menutree`. This is a good place to store menu-specific things since it will clean itself up when the menu closes. When initiating the menu, any additional keywords you give will be available for you as properties on this menu object: + +```python +class MyObjectCommand(Command): + # A Command stored on an object (the object is always accessible from + # the Command as self.obj) + def func(self): + # add the object as the stored_obj menu property + EvMenu(caller, ..., stored_obj=self.obj) + +``` + +Inside the menu you can now access the object through `caller.ndb._menutree.stored_obj`. + + +## Adding color to default Evennia Channels +**Q:** How do I add colors to the names of Evennia channels? + +**A:** The Channel typeclass' `channel_prefix` method decides what is shown at the beginning of a channel send. Edit `mygame/typeclasses/channels.py` (and then `@reload`): + +```python +# define our custom color names +CHANNEL_COLORS = {'public': '|015Public|n', + 'newbie': '|550N|n|551e|n|552w|n|553b|n|554i|n|555e|n', + 'staff': '|010S|n|020t|n|030a|n|040f|n|050f|n'} + +# Add to the Channel class + # ... + def channel_prefix(self, msg, emit=False): + prefix_string = "" + if self.key in COLORS: + prefix_string = "[%s] " % CHANNEL_COLORS.get(self.key.lower()) + else: + prefix_string = "[%s] " % self.key.capitalize() + return prefix_string +``` +Additional hint: To make colors easier to change from one place you could instead put the `CHANNEL_COLORS` dict in your settings file and import it as `from django.conf.settings import CHANNEL_COLORS`. + + +## Selectively turn off commands in a room +**Q:** I want certain commands to turn off in a given room. They should still work normally for staff. + +**A:** This is done using a custom cmdset on a room [locked with the 'call' lock type](Locks). Only if this lock is passed will the commands on the room be made available to an object inside it. Here is an example of a room where certain commands are disabled for non-staff: + +```python +# in mygame/typeclasses/rooms.py + +from evennia import default_commands, CmdSet + +class CmdBlocking(default_commands.MuxCommand): + # block commands give, get, inventory and drop + key = "give" + aliases = ["get", "inventory", "drop"] + def func(self): + self.caller.msg("You cannot do that in this room.") + +class BlockingCmdSet(CmdSet): + key = "blocking_cmdset" + # default commands have prio 0 + priority = 1 + def at_cmdset_creation(self): + self.add(CmdBlocking()) + +class BlockingRoom(Room): + def at_object_creation(self): + self.cmdset.add(BlockingCmdSet, permanent=True) + # only share commands with players in the room that + # are NOT Builders or higher + self.locks.add("call:not perm(Builders)") +``` +After `@reload`, make some `BlockingRooms` (or switch a room to it with `@typeclass`). Entering one will now replace the given commands for anyone that does not have the `Builders` or higher permission. Note that the 'call' lock is special in that even the superuser will be affected by it (otherwise superusers would always see other player's cmdsets and a game would be unplayable for superusers). + +## Select Command based on a condition +**Q:** I want a command to be available only based on a condition. For example I want the "werewolf" command to only be available on a full moon, from midnight to three in-game time. + +**A:** This is easiest accomplished by putting the "werewolf" command on the Character as normal, but to [lock](Locks) it with the "cmd" type lock. Only if the "cmd" lock type is passed will the command be available. + +```python +# in mygame/commands/command.py + +from evennia import Command + +class CmdWerewolf(Command): + key = "werewolf" + # lock full moon, between 00:00 (midnight) and 03:00. + locks = "cmd:is_full_moon(0, 3)" + def func(self): + # ... +``` +Add this to the [default cmdset as usual](Adding-Command-Tutorial). The `is_full_moon` [lock function](https://github.com/evennia/evennia/wiki/Locks#lock-functions) does not yet exist. We must create that: + +```python +# in mygame/server/conf/lockfuncs.py + +def is_full_moon(accessing_obj, accessed_obj, + starthour, endhour, *args, **kwargs): + # calculate if the moon is full here and + # if current game time is between starthour and endhour + # return True or False + +``` +After a `@reload`, the `werewolf` command will be available only at the right time, that is when the `is_full_moon` lock function returns True. + +## Automatically updating code when reloading +**Q:** I have a development server running Evennia. Can I have the server update its code-base when I reload? + +**A:** Having a development server that pulls updated code whenever you reload it can be really useful if you have limited shell access to your server, or want to have it done automatically. If you have your project in a configured Git environment, it's a matter of automatically calling `git pull` when you reload. And that's pretty straightforward: + +In `/server/conf/at_server_startstop.py`: + +```python +import subprocess + +# ... other hooks ... + +def at_server_reload_stop(): + """ + This is called only time the server stops before a reload. + """ + print("Pulling from the game repository...") + process = subprocess.call(["git", "pull"], shell=False) +``` + +That's all. We call `subprocess` to execute a shell command (that code works on Windows and Linux, assuming the current directory is your game directory, which is probably the case when you run Evennia). `call` waits for the process to complete, because otherwise, Evennia would reload on partially-modified code, which would be problematic. + +Now, when you enter `@reload` on your development server, the game repository is updated from the configured remote repository (Github, for instance). Your development cycle could resemble something like: + +1. Coding on the local machine. +2. Testing modifications. +3. Committing once, twice or more (being sure the code is still working, unittests are pretty useful here). +4. When the time comes, login to the development server and run `@reload`. + +The reloading might take one or two additional seconds, since Evennia will pull from your remote Git repository. But it will reload on it and you will have your modifications ready, without needing connecting to your server using SSH or something similar. + +## Changing all exit messages +**Q:** How can I change the default exit messages to something like "XXX leaves east" or "XXX arrives from the west"? + +**A:** the default exit messages are stored in two hooks, namely `announce_move_from` and `announce_move_to`, on the `Character` typeclass (if what you want to change is the message other characters will see when a character exits). + +These two hooks provide some useful features to easily update the message to be displayed. They take both the default message and mapping as argument. You can easily call the parent hook with these information: + +* The message represents the string of characters sent to characters in the room when a character leaves. +* The mapping is a dictionary containing additional mappings (you will probably not need it for simple customization). + +It is advisable to look in the [code of both hooks](https://github.com/evennia/evennia/tree/master/evennia/objects/objects.py), and read the hooks' documentation. The explanations on how to quickly update the message are shown below: + +```python +# In typeclasses/characters.py +""" +Characters + +""" +from evennia import DefaultCharacter + +class Character(DefaultCharacter): + """ + The default character class. + + ... + """ + + def announce_move_from(self, destination, msg=None, mapping=None): + """ + Called if the move is to be announced. This is + called while we are still standing in the old + location. + + Args: + destination (Object): The place we are going to. + msg (str, optional): a replacement message. + mapping (dict, optional): additional mapping objects. + + You can override this method and call its parent with a + message to simply change the default message. In the string, + you can use the following as mappings (between braces): + object: the object which is moving. + exit: the exit from which the object is moving (if found). + origin: the location of the object before the move. + destination: the location of the object after moving. + + """ + super().announce_move_from(destination, msg="{object} leaves {exit}.") + + def announce_move_to(self, source_location, msg=None, mapping=None): + """ + Called after the move if the move was not quiet. At this point + we are standing in the new location. + + Args: + source_location (Object): The place we came from + msg (str, optional): the replacement message if location. + mapping (dict, optional): additional mapping objects. + + You can override this method and call its parent with a + message to simply change the default message. In the string, + you can use the following as mappings (between braces): + object: the object which is moving. + exit: the exit from which the object is moving (if found). + origin: the location of the object before the move. + destination: the location of the object after moving. + + """ + super().announce_move_to(source_location, msg="{object} arrives from the {exit}.") +``` + +We override both hooks, but call the parent hook to display a different message. If you read the provided docstrings, you will better understand why and how we use mappings (information between braces). You can provide additional mappings as well, if you want to set a verb to move, for instance, or other, extra information. + +## Add parsing with the "to" delimiter + +**Q:** How do I change commands to undestand say `give obj to target` as well as the default `give obj = target`? + +**A:** You can make change the default `MuxCommand` parent with your own class making a small change in its `parse` method: + +```python + # in mygame/commands/command.py + from evennia import default_cmds + class MuxCommand(default_cmds.MuxCommand): + def parse(self): + """Implement an additional parsing of 'to'""" + super().parse() + if " to " in self.args: + self.lhs, self.rhs = self.args.split(" to ", 1) +``` +Next you change the parent of the default commands in settings: + +```python + COMMAND_DEFAULT_CLASS = "commands.command.MuxCommand" +``` + +Do a `@reload` and all default commands will now use your new tweaked parent class. A copy of the +MuxCommand class is also found commented-out in the `mygame/commands/command.py` file. + +## Store last used session IP address + +**Q:** If a user has already logged out of an Evennia account, their IP is no longer visible to staff that wants to ban-by-ip (instead of the user) with `@ban/ip`? + +**A:** One approach is to write the IP from the last session onto the "account" account object. + +`typeclasses/accounts.py` +```python + def at_post_login(self, session=None, **kwargs): + super().at_post_login(session=session, **kwargs) + self.db.lastsite = self.sessions.all()[-1].address +``` +Adding timestamp for login time and appending to a list to keep the last N login IP addresses and timestamps is possible, also. Additionally, if you don't want the list to grow beyond a `do_not_exceed` length, conditionally pop a value after you've added it, if the length has grown too long. + +**NOTE:** You'll need to add `import time` to generate the login timestamp. +```python + def at_post_login(self, session=None, **kwargs): + super().at_post_login(session=session, **kwargs) + do_not_exceed = 24 # Keep the last two dozen entries + session = self.sessions.all()[-1] # Most recent session + if not self.db.lastsite: + self.db.lastsite = [] + self.db.lastsite.insert(0, (session.address, int(time.time()))) + if len(self.db.lastsite) > do_not_exceed: + self.db.lastsite.pop() +``` +This only stores the data. You may want to interface the `@ban` command or make a menu-driven viewer for staff to browse the list and display how long ago the login occurred. + +## Non-latin characters in EvTable + +**Q:** When using e.g. Chinese characters in EvTable, some lines appear to be too wide, for example +``` ++------+------+ +| | | +| 测试 | 测试 | +| | | ++~~~~~~+~~~~~~+ +``` +**A:** The reason for this is because certain non-latin characters are *visually* much wider than their len() suggests. There is little Evennia can (reliably) do about this. If you are using such characters, you need to make sure to use a suitable mono-spaced font where are width are equal. You can set this in your web client and need to recommend it for telnet-client users. See [this discussion](https://github.com/evennia/evennia/issues/1522) where some suitable fonts are suggested. diff --git a/docs/source/Coding-Introduction.md b/docs/source/Coding-Introduction.md new file mode 100644 index 0000000000..08dff706f4 --- /dev/null +++ b/docs/source/Coding-Introduction.md @@ -0,0 +1,62 @@ +# Coding Introduction + + +Evennia allows for a lot of freedom when designing your game - but to code efficiently you still need to adopt some best practices as well as find a good place to start to learn. + +Here are some pointers to get you going. + +### Python + +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](https://github.com/evennia/evennia/wiki/Python-basic-introduction). + +### Explore Evennia interactively + +When new to Evennia it can be hard to find things or figure out what is available. Evennia offers a special interactive python shell that allows you to experiment and try out things. It's recommended to use [ipython](http://ipython.org/) for this since the vanilla python prompt is very limited. Here are some simple commands to get started: + + # [open a new console/terminal] + # [activate your evennia virtualenv in this console/terminal] + pip install ipython # [only needed the first time] + cd mygame + evennia shell + +This will open an Evennia-aware python shell (using ipython). From within this shell, try + + import evennia + 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 info on how to explore it efficiently. + +You can complement your exploration by peeking at the sections of the much more detailed [Developer Central](Developer-Central). The [Tutorials](Tutorials) section also contains a growing collection of system- or implementation-specific help. + +### Use a python syntax checker + +Evennia works by importing your own modules and running them as part of the server. Whereas Evennia should just gracefully tell you what errors it finds, it can nevertheless be a good idea for you to check your code for simple syntax errors *before* you load it into the running server. There are many python syntax checkers out there. A fast and easy one is [pyflakes](https://pypi.python.org/pypi/pyflakes), a more verbose one is [pylint](http://www.pylint.org/). You can also check so that your code looks up to snuff using [pep8](https://pypi.python.org/pypi/pep8). Even with a syntax checker you will not be able to catch every possible problem - some bugs or problems will only appear when you actually run the code. But 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) page. It might hopefully help you avoid some common pitfalls and time sinks. + +### Code in your game folder, not in the evennia/ repository + +As part of the Evennia setup you will create a game folder to host your game code. This is your home. You should *never* need to modify anything in the `evennia` library (anything you download from us, really). You import useful functionality from here and if you see code you like, copy&paste 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 your changes upstream! + +### Learn to read tracebacks + +Python is very good at reporting when and where things go wrong. A *traceback* shows everything you need to know about crashing code. The text can be pretty long, but you usually are only interested in the last bit, where it says what the error is and at which module and line number it happened - armed with this info you can resolve most problems. + +Evennia will usually not show the full traceback in-game though. Instead the server outputs errors to the terminal/console from which you started Evennia in the first place. If you want more to show in-game you can add `IN_GAME_ERRORS = True` to your settings file. This will echo most (but not all) tracebacks both in-game as well as to the terminal/console. This is a potential security problem though, so don't keep this active when your game goes into production. + +> A common confusing error is finding that objects in-game are suddenly of the type `DefaultObject` rather than your custom typeclass. This happens when you introduce a critical Syntax error to the module holding your custom class. Since such a module is not valid Python, Evennia can't load it at all. Instead of crashing, Evennia will then print the full traceback to the terminal/console and temporarily fall back to the safe `DefaultObject` until you fix the problem and reload. + +### Docs are here to help you + +Some people find reading documentation extremely dull and shun it out of principle. That's your call, but reading docs really *does* help you, promise! Evennia's documentation is pretty thorough and knowing what is possible can often give you a lot of new cool game ideas. That said, if you can't find the answer in the docs, don't be shy to ask questions! The [discussion group](https://sites.google.com/site/evenniaserver/discussions) and the [irc chat](http://webchat.freenode.net/?channels=evennia) are also there for you. + +### The most important point + +And finally, of course, have fun! + +[feature-request]: (https://github.com/evennia/evennia/issues/new?title=Feature+Request%3a+%3Cdescriptive+title+here%3E&body=%23%23%23%23+Description+of+the+suggested+feature+and+how+it+is+supposed+to+work+for+the+admin%2fend+user%3a%0D%0A%0D%0A%0D%0A%23%23%23%23+A+list+of+arguments+for+why+you+think+this+new+feature+should+be+included+in+Evennia%3a%0D%0A%0D%0A1.%0D%0A2.%0D%0A%0D%0A%23%23%23%23+Extra+information%2c+such+as+requirements+or+ideas+on+implementation%3a%0D%0A%0D%0A +[bug]: https://github.com/evennia/evennia/issues/new?title=Bug%3a+%3Cdescriptive+title+here%3E&body=%23%23%23%23+Steps+to+reproduce+the+issue%3a%0D%0A%0D%0A1.+%0D%0A2.+%0D%0A3.+%0D%0A%0D%0A%23%23%23%23+What+I+expect+to+see+and+what+I+actually+see+%28tracebacks%2c+error+messages+etc%29%3a%0D%0A%0D%0A%0D%0A%0D%0A%23%23%23%23+Extra+information%2c+such+as+Evennia+revision%2frepo%2fbranch%2c+operating+system+and+ideas+for+how+to+solve%3a%0D%0A%0D%0A diff --git a/docs/source/Coding-Utils.md b/docs/source/Coding-Utils.md new file mode 100644 index 0000000000..28e006189d --- /dev/null +++ b/docs/source/Coding-Utils.md @@ -0,0 +1,236 @@ +# Coding Utils + + +Evennia comes with many utilities to help with common coding tasks. Most are accessible directly from the flat API, otherwise you can find them in the `evennia/utils/` folder. + +## Searching + +A common thing to do is to search for objects. There it's easiest to use the `search` method defined on all objects. This will search for objects in the same location and inside the self object: + +```python + obj = self.search(objname) +``` + +The most common time one needs to do this is inside a command body. `obj = self.caller.search(objname)` will search inside the caller's (typically, the character that typed the command) `.contents` (their "inventory") and `.location` (their "room"). + +Give the keyword `global_search=True` to extend search to encompass entire database. Aliases will also be matched by this search. You will find multiple examples of this functionality in the default command set. + +If you need to search for objects in a code module you can use the functions in `evennia.utils.search`. You can access these as shortcuts `evennia.search_*`. + +```python + from evennia import search_object + obj = search_object(objname) +``` + +- [evennia.search_account](../wiki/evennia.accounts.manager#accountdbmanagersearch_account) +- [evennia.search_object](../wiki/evennia.objects.manager#objectdbmanagersearch_object) +- [evennia.search_object_by_tag](../wiki/evennia.utils.search#search_object_by_tag) +- [evennia.search_script](../wiki/evennia.scripts.manager#scriptdbmanagersearch_script) +- [evennia.search_channel](../wiki/evennia.comms.managers#channeldbmanagersearch_channel) +- [evennia.search_message](../wiki/evennia.comms.managers#msgmanagersearch_message) +- [evennia.search_help](../wiki/evennia.help.manager#helpentrymanagersearch_help) + +Note that these latter methods will always return a `list` of results, even if the list has one or zero entries. + +## Create + +Apart from the in-game build commands (`@create` etc), you can also build all of Evennia's game entities directly in code (for example when defining new create commands). +```python + import evennia + + myobj = evennia.create_objects("game.gamesrc.objects.myobj.MyObj", key="MyObj") +``` + +- [evennia.create_account](../wiki/evennia.utils.create#create_account) +- [evennia.create_object](../wiki/evennia.utils.create#create_object) +- [evennia.create_script](../wiki/evennia.utils.create#create_script) +- [evennia.create_channel](../wiki/evennia.utils.create#create_channel) +- [evennia.create_help_entry](../wiki/evennia.utils.create#create_help_entry) +- [evennia.create_message](../wiki/evennia.utils.create#create_message) + +Each of these create-functions have a host of arguments to further customize the created entity. See `evennia/utils/create.py` for more information. + +## Logging + +Normally you can use Python `print` statements to see output to the terminal/log. The `print` statement should only be used for debugging though. For producion output, use the `logger` which will create proper logs either to terminal or to file. + +```python + from evennia import logger + # + logger.log_err("This is an Error!") + logger.log_warn("This is a Warning!") + logger.log_info("This is normal information") + logger.log_dep("This feature is deprecated") +``` + +There is a special log-message type, `log_trace()` that is intended to be called from inside a traceback - this can be very useful for relaying the traceback message back to log without having it kill the server. + +```python + try: + # [some code that may fail...] + except Exception: + logger.log_trace("This text will show beneath the traceback itself.") +``` + +The `log_file` logger, finally, is a very useful logger for outputting arbitrary log messages. This is a heavily optimized asynchronous log mechanism using [threads](https://en.wikipedia.org/wiki/Thread_%28computing%29) to avoid overhead. You should be able to use it for very heavy custom logging without fearing disk-write delays. + +```python + logger.log_file(message, filename="mylog.log") +``` + +If not an absolute path is given, the log file will appear in the `mygame/server/logs/` directory. If the file already exists, it will be appended to. Timestamps on the same format as the normal Evennia logs will be automatically added to each entry. If a filename is not specified, output will be written to a file `game/logs/game.log`. + +## Time Utilities +### Game time + +Evennia tracks the current server time. You can access this time via the `evennia.gametime` shortcut: + +```python +from evennia import gametime + +# all the functions below return times in seconds). + +# total running time of the server +runtime = gametime.runtime() +# time since latest hard reboot (not including reloads) +uptime = gametime.uptime() +# server epoch (its start time) +server_epoch = gametime.server_epoch() + +# in-game epoch (this can be set by `settings.TIME_GAME_EPOCH`. +# If not, the server epoch is used. +game_epoch = gametime.game_epoch() +# in-game time passed since time started running +gametime = gametime.gametime() +# in-game time plus game epoch (i.e. the current in-game +# time stamp) +gametime = gametime.gametime(absolute=True) +# reset the game time (back to game epoch) +gametime.reset_gametime() + +``` + +The setting `TIME_FACTOR` determines how fast/slow in-game time runs compared to the real world. The setting `TIME_GAME_EPOCH` sets the starting game epoch (in seconds). The functions from the `gametime` module all return their times in seconds. You can convert this to whatever units of time you desire for your game. You can use the `@time` command to view the server time info. + +You can also *schedule* things to happen at specific in-game times using the [gametime.schedule](https://github.com/evennia/evennia/wiki/evennia.utils.gametime#schedule) function: + +```python +import evennia + +def church_clock: + limbo = evennia.search_object(key="Limbo") + limbo.msg_contents("The church clock chimes two.") + +gametime.schedule(church_clock, hour=2) +``` + +### utils.time_format() + +This function takes a number of seconds as input (e.g. from the `gametime` module above) and converts it to a nice text output in days, hours etc. It's useful when you want to show how old something is. It converts to four different styles of output using the *style* keyword: + +- style 0 - `5d:45m:12s` (standard colon output) +- style 1 - `5d` (shows only the longest time unit) +- style 2 - `5 days, 45 minutes` (full format, ignores seconds) +- style 3 - `5 days, 45 minutes, 12 seconds` (full format, with seconds) + +### utils.delay() + +```python +from evennia import utils + +def _callback(obj, text): + obj.msg(text) + +# wait 10 seconds before sending "Echo!" to obj (which we assume is defined) +deferred = utils.delay(10, _callback, obj, "Echo!", persistent=False) + +# code here will run immediately, not waiting for the delay to fire! + +``` + +This creates an asynchronous delayed call. It will fire the given callback function after the given number of seconds. This is a very light wrapper over a Twisted [Deferred](https://twistedmatrix.com/documents/current/core/howto/defer.html). Normally this is run non-persistently, which means that if the server is `@reload`ed before the delay is over, the callback will never run (the server forgets it). If setting `persistent` to True, the delay will be stored in the database and survive a `@reload` - but for this to work it is susceptible to the same limitations incurred when saving to an [Attribute](Attributes). + +The `deferred` return object can usually be ignored, but calling its `.cancel()` method will abort the delay prematurely. + +`utils.delay` is the lightest form of delayed call in Evennia. For other way to create time-bound tasks, see the [TickerHandler](TickerHandler) and [Scripts](Scripts). + +> Note that many delayed effects can be achieved without any need for an active timer. For example if you have a trait that should recover a point every 5 seconds you might just need its value when it's needed, but checking the current time and calculating on the fly what value it should have. + +## Object Classes +### utils.inherits_from() + +This useful function takes two arguments - an object to check and a parent. It returns `True` if object inherits from parent *at any distance* (as opposed to Python's in-built `is_instance()` that will only catch immediate dependence). This function also accepts as input any combination of classes, instances or python-paths-to-classes. + +Note that Python code should usually work with [duck typing](http://en.wikipedia.org/wiki/Duck_typing). But in Evennia's case it can sometimes be useful to check if an object inherits from a given [Typeclass](Typeclasses) as a way of identification. Say for example that we have a typeclass *Animal*. This has a subclass *Felines* which in turn has a subclass *HouseCat*. Maybe there are a bunch of other animal types too, like horses and dogs. Using `inherits_from` will allow you to check for all animals in one go: + +```python + from evennia import utils + if (utils.inherits_from(obj, "typeclasses.objects.animals.Animal"): + obj.msg("The bouncer stops you in the door. He says: 'No talking animals allowed.'") +``` + + + +## Text utilities + +In a text game, you are naturally doing a lot of work shuffling text back and forth. Here is a *non-complete* selection of text utilities found in `evennia/utils/utils.py` (shortcut `evennia.utils`). If nothing else it can be good to look here before starting to develop a solution of your own. + +### utils.fill() + +This flood-fills a text to a given width (shuffles the words to make each line evenly wide). It also indents as needed. + +```python + outtxt = fill(intxt, width=78, indent=4) +``` + +### utils.crop() + +This function will crop a very long line, adding a suffix to show the line actually continues. This can be useful in listings when showing multiple lines would mess up things. + +```python + intxt = "This is a long text that we want to crop." + outtxt = crop(intxt, width=19, suffix="[...]") + # outtxt is now "This is a long text[...]" +``` + +### utils.dedent() + +This solves what may at first glance appear to be a trivial problem with text - removing indentations. It is used to shift entire paragraphs to the left, without disturbing any further formatting they may have. A common case for this is when using Python triple-quoted strings in code - they will retain whichever indentation they have in the code, and to make easily-readable source code one usually don't want to shift the string to the left edge. + +```python + #python code is entered at a given indentation + intxt = """ + This is an example text that will end + up with a lot of whitespace on the left. + It also has indentations of + its own.""" + outtxt = dedent(intxt) + # outtxt will now retain all internal indentation + # but be shifted all the way to the left. +``` + +Normally you do the dedent in the display code (this is for example how the help system homogenizes help entries). + +### to_str() and to_bytes() + +Evennia supplies two utility functions for converting text to the correct +encodings. `to_str()` and `to_bytes()`. Unless you are adding a custom protocol and +need to send byte-data over the wire, `to_str` is the only one you'll need. + +The difference from Python's in-built `str()` and `bytes()` operators are that +the Evennia ones makes use of the `ENCODINGS` setting and will try very hard to +never raise a traceback but instead echo errors through logging. See +[here](Text-Encodings) for more info. + +### Ansi Coloring Tools +- [evennia.ansi](../wiki/evennia.utils.ansi) + +## Display utilities +### Making ascii tables + +The [EvTable](../wiki/evennia.utils.evtable#evtable) class (`evennia/utils/evtable.py`) can be used to create correctly formatted text tables. There is also [EvForm](../wiki/evennia.utils.evform#evform) (`evennia/utils/evform.py`). This reads a fixed-format text template from a file in order to create any level of sophisticated ascii layout. Both evtable and evform have lots of options and inputs so see the header of each module for help. + +The third-party [PrettyTable](https://code.google.com/p/prettytable/) module is also included in Evennia. PrettyTable is considered deprecated in favor of EvTable since PrettyTable cannot handle ANSI colour. PrettyTable can be found in `evennia/utils/prettytable/`. See its homepage above for instructions. + +### Menus +- [evennia.EvMenu](../wiki/evennia.utils.evmenu#evmenu) \ No newline at end of file diff --git a/docs/source/Command-Cooldown.md b/docs/source/Command-Cooldown.md new file mode 100644 index 0000000000..2dc6f6152a --- /dev/null +++ b/docs/source/Command-Cooldown.md @@ -0,0 +1,98 @@ +# Command Cooldown + + +Some types of games want to limit how often a command can be run. If a +character casts the spell *Firestorm*, you might not want them to spam that +command over and over. Or in an advanced combat system, a massive swing may +offer a chance of lots of damage at the cost of not being able to re-do it for +a while. Such effects are called *cooldowns*. + +This page exemplifies a very resource-efficient way to do cooldowns. A more +'active' way is to use asynchronous delays as in the [command duration +tutorial](Command-Duration#Blocking-Commands), the two might be useful to +combine if you want to echo some message to the user after the cooldown ends. + +## Non-persistent cooldown + +This little recipe will limit how often a particular command can be run. Since +Commands are class instances, and those are cached in memory, a command +instance will remember things you store on it. So just store the current time +of execution! Next time the command is run, it just needs to check if it has +that time stored, and compare it with the current time to see if a desired +delay has passed. + +```python +import time +from evennia import default_cmds + +class CmdSpellFirestorm(default_cmds.MuxCommand): + """ + Spell - Firestorm + + Usage: + cast firestorm + + This will unleash a storm of flame. You can only release one + firestorm every five minutes (assuming you have the mana). + """ + key = "cast firestorm" + locks = "cmd:isFireMage()" + + def func(self): + "Implement the spell" + + # check cooldown (5 minute cooldown) + now = time.time() + if hasattr(self, "lastcast") and \ + now - self.lastcast < 5 * 60: + message = "You cannot cast this spell again yet." + self.caller.msg(message) + return + + #[the spell effect is implemented] + + # if the spell was successfully cast, store the casting time + self.lastcast = now +``` + +We just check the `lastcast` flag, and update it if everything works out. +Simple and very effective since everything is just stored in memory. The +drawback of this simple scheme is that it's non-persistent. If you do +`@reload`, the cache is cleaned and all such ongoing cooldowns will be +forgotten. It is also limited only to this one command, other commands cannot +(easily) check for this value. + +## Persistent cooldown + +This is essentially the same mechanism as the simple one above, except we use +the database to store the information which means the cooldown will survive a +server reload/reboot. Since commands themselves have no representation in the +database, you need to use the caster for the storage. + +```python + # inside the func() of CmdSpellFirestorm as above + + # check cooldown (5 minute cooldown) + + now = time.time() + lastcast = self.caller.db.firestorm_lastcast + + if lastcast and now - lastcast < 5 * 60: + message = "You need to wait before casting this spell again." + self.caller.msg(message) + return + + #[the spell effect is implemented] + + # if the spell was successfully cast, store the casting time + self.caller.db.firestorm_lastcast = now +``` + +Since we are storing as an [Attribute](Attributes), we need to identify the +variable as `firestorm_lastcast` so we are sure we get the right one (we'll + likely have other skills with cooldowns after all). But this method of +using cooldowns also has the advantage of working *between* commands - you can +for example let all fire-related spells check the same cooldown to make sure +the casting of *Firestorm* blocks all fire-related spells for a while. Or, in +the case of taking that big swing with the sword, this could now block all +other types of attacks for a while before the warrior can recover. diff --git a/docs/source/Command-Duration.md b/docs/source/Command-Duration.md new file mode 100644 index 0000000000..403553a015 --- /dev/null +++ b/docs/source/Command-Duration.md @@ -0,0 +1,351 @@ +# Command Duration + + +Before reading this tutorial, if you haven't done so already, you might want to +read [the documentation on commands](Commands) to get a basic understanding of +how commands work in Evennia. + +In some types of games a command should not start and finish immediately. +Loading a crossbow might take a bit of time to do - time you don't have when +the enemy comes rushing at you. Crafting that armour will not be immediate +either. For some types of games the very act of moving or changing pose all +comes with a certain time associated with it. + +## The simple way to pause commands with yield + +Evennia allows a shortcut in syntax to create simple pauses in commands. This +syntax uses the `yield` keyword. The `yield` keyword is used in Python to +create generators, although you don't need to know what generators are to use +this syntax. A short example will probably make it clear: + +```python +class CmdTest(Command): + + """ + A test command just to test waiting. + + Usage: + test + + """ + + key = "test" + locks = "cmd:all()" + + def func(self): + self.msg("Before ten seconds...") + yield 10 + self.msg("Afterwards.") +``` +> Important: The `yield` functionality will *only* work in the `func` method of +> Commands. It only works because Evennia has especially +> catered for it in Commands. If you want the same functionality elsewhere you +> must use the [interactive decorator](Async-Process#The-@interactive-decorator). + +The important line is the `yield 10`. It tells Evennia to "pause" the command +and to wait for 10 seconds to execute the rest. If you add this command and +run it, you'll see the first message, then, after a pause of ten seconds, the +next message. You can use `yield` several times in your command. + +This syntax will not "freeze" all commands. While the command is "pausing", + you can execute other commands (or even call the same command again). And + other players aren't frozen either. + +> Note: this will not save anything in the database. If you reload the game +> while a command is "paused", it will not resume after the server has +> reloaded. + + +## The more advanced way with utils.delay + +The `yield` syntax is easy to read, easy to understand, easy to use. But it's not that flexible if you want more advanced options. Learning to use alternatives might be much worth it in the end. + +Below is a simple command example for adding a duration for a command to finish. + +```python +from evennia import default_cmds, utils + +class CmdEcho(default_cmds.MuxCommand): + """ + wait for an echo + + Usage: + echo + + Calls and waits for an echo + """ + key = "echo" + locks = "cmd:all()" + + def func(self): + """ + This is called at the initial shout. + """ + self.caller.msg("You shout '%s' and wait for an echo ..." % self.args) + # this waits non-blocking for 10 seconds, then calls self.echo + utils.delay(10, self.echo) # call echo after 10 seconds + + def echo(self): + "Called after 10 seconds." + shout = self.args + string = "You hear an echo: %s ... %s ... %s" + string = string % (shout.upper(), shout.capitalize(), shout.lower()) + self.caller.msg(string) +``` + +Import this new echo command into the default command set and reload the server. You will find that it will take 10 seconds before you see your shout coming back. You will also find that this is a *non-blocking* effect; you can issue other commands in the interim and the game will go on as usual. The echo will come back to you in its own time. + +### About utils.delay() + +`utils.delay(timedelay, callback, persistent=False, *args, **kwargs)` is a useful function. It will wait `timedelay` seconds, then call the `callback` function, optionally passing to it the arguments provided to utils.delay by way of *args and/or **kwargs`. + +> Note: The callback argument should be provided with a python path to the desired function, for instance `my_object.my_function` instead of `my_object.my_function()`. Otherwise my_function would get called and run immediately upon attempting to pass it to the delay function. +If you want to provide arguments for utils.delay to use, when calling your callback function, you have to do it separatly, for instance using the utils.delay *args and/or **kwargs, as mentioned above. + +> If you are not familiar with the syntax `*args` and `**kwargs`, [see the Python documentation here](https://docs.python.org/2/tutorial/controlflow.html#arbitrary-argument-lists). + +Looking at it you might think that `utils.delay(10, callback)` in the code above is just an alternative to some more familiar thing like `time.sleep(10)`. This is *not* the case. If you do `time.sleep(10)` you will in fact freeze the *entire server* for ten seconds! The `utils.delay()`is a thin wrapper around a Twisted [Deferred](http://twistedmatrix.com/documents/11.0.0/core/howto/defer.html) that will delay execution until 10 seconds have passed, but will do so asynchronously, without bothering anyone else (not even you - you can continue to do stuff normally while it waits to continue). + +The point to remember here is that the `delay()` call will not "pause" at that point when it is called (the way `yield` does in the previous section). The lines after the `delay()` call will actually execute *right away*. What you must do is to tell it which function to call *after the time has passed* (its "callback"). This may sound strange at first, but it is normal practice in asynchronous systems. You can also link such calls together as seen below: + +```python +from evennia import default_cmds, utils + +class CmdEcho(default_cmds.MuxCommand): + """ + waits for an echo + + Usage: + echo + + Calls and waits for an echo + """ + key = "echo" + locks = "cmd:all()" + + def func(self): + "This sets off a chain of delayed calls" + self.caller.msg("You shout '%s', waiting for an echo ..." % self.args) + + # wait 2 seconds before calling self.echo1 + utils.delay(2, self.echo1) + + # callback chain, started above + def echo1(self): + "First echo" + self.caller.msg("... %s" % self.args.upper()) + # wait 2 seconds for the next one + utils.delay(2, self.echo2) + + def echo2(self): + "Second echo" + self.caller.msg("... %s" % self.args.capitalize()) + # wait another 2 seconds + utils.delay(2, callback=self.echo3) + + def echo3(self): + "Last echo" + self.caller.msg("... %s ..." % self.args.lower()) +``` + +The above version will have the echoes arrive one after another, each separated by a two second delay. + + > echo Hello! + ... HELLO! + ... Hello! + ... hello! ... + +## Blocking commands + +As mentioned, a great thing about the delay introduced by `yield` or `utils.delay()` is that it does not block. It just goes on in the background and you are free to play normally in the interim. In some cases this is not what you want however. Some commands should simply "block" other commands while they are running. If you are in the process of crafting a helmet you shouldn't be able to also start crafting a shield at the same time, or if you just did a huge power-swing with your weapon you should not be able to do it again immediately. + +The simplest way of implementing blocking is to use the technique covered in the [Command Cooldown](Command-Cooldown) tutorial. In that tutorial we implemented cooldowns by having the Command store the current time. Next time the Command was called, we compared the current time to the stored time to determine if enough time had passed for a renewed use. This is a *very* efficient, reliable and passive solution. The drawback is that there is nothing to tell the Player when enough time has passed unless they keep trying. + +Here is an example where we will use `utils.delay` to tell the player when the cooldown has passed: + +```python +from evennia import utils, default_cmds + +class CmdBigSwing(default_cmds.MuxCommand): + """ + swing your weapon in a big way + + Usage: + swing + + Makes a mighty swing. Doing so will make you vulnerable + to counter-attacks before you can recover. + """ + key = "bigswing" + locks = "cmd:all()" + + def func(self): + "Makes the swing" + + if self.caller.ndb.off_balance: + # we are still off-balance. + self.caller.msg("You are off balance and need time to recover!") + return + + # [attack/hit code goes here ...] + self.caller.msg("You swing big! You are off balance now.") + + # set the off-balance flag + self.caller.ndb.off_balance = True + + # wait 8 seconds before we can recover. During this time + # we won't be able to swing again due to the check at the top. + utils.delay(8, self.recover) + + def recover(self): + "This will be called after 8 secs" + del self.caller.ndb.off_balance + self.caller.msg("You regain your balance.") +``` + +Note how, after the cooldown, the user will get a message telling them they are now ready for another swing. + +By storing the `off_balance` flag on the character (rather than on, say, the Command instance itself) it can be accessed by other Commands too. Other attacks may also not work when you are off balance. You could also have an enemy Command check your `off_balance` status to gain bonuses, to take another example. + +## Abortable commands + +One can imagine that you will want to abort a long-running command before it has a time to finish. If you are in the middle of crafting your armor you will probably want to stop doing that when a monster enters your smithy. + +You can implement this in the same way as you do the "blocking" command above, just in reverse. Below is an example of a crafting command that can be aborted by starting a fight: + +```python +from evennia import utils, default_cmds + +class CmdCraftArmour(default_cmds.MuxCommand): + """ + Craft armour + + Usage: + craft + + This will craft a suit of armour, assuming you + have all the components and tools. Doing some + other action (such as attacking someone) will + abort the crafting process. + """ + key = "craft" + locks = "cmd:all()" + + def func(self): + "starts crafting" + + if self.caller.ndb.is_crafting: + self.caller.msg("You are already crafting!") + return + if self._is_fighting(): + self.caller.msg("You can't start to craft " + "in the middle of a fight!") + return + + # [Crafting code, checking of components, skills etc] + + # Start crafting + self.caller.ndb.is_crafting = True + self.caller.msg("You start crafting ...") + utils.delay(60, self.step1) + + def _is_fighting(self): + "checks if we are in a fight." + if self.caller.ndb.is_fighting: + del self.caller.ndb.is_crafting + return True + + def step1(self): + "first step of armour construction" + if self._is_fighting(): + return + self.msg("You create the first part of the armour.") + utils.delay(60, callback=self.step2) + + def step2(self): + "second step of armour construction" + if self._is_fighting(): + return + self.msg("You create the second part of the armour.") + utils.delay(60, step3) + + def step3(self): + "last step of armour construction" + if self._is_fighting(): + return + + # [code for creating the armour object etc] + + del self.caller.ndb.is_crafting + self.msg("You finalize your armour.") + + +# example of a command that aborts crafting + +class CmdAttack(default_cmds.MuxCommand): + """ + attack someone + + Usage: + attack + + Try to cause harm to someone. This will abort + eventual crafting you may be currently doing. + """ + key = "attack" + aliases = ["hit", "stab"] + locks = "cmd:all()" + + def func(self): + "Implements the command" + + self.caller.ndb.is_fighting = True + + # [...] +``` + +The above code creates a delayed crafting command that will gradually create the armour. If the `attack` command is issued during this process it will set a flag that causes the crafting to be quietly canceled next time it tries to update. + +## Persistent delays + +In the latter examples above we used `.ndb` storage. This is fast and easy but it will reset all cooldowns/blocks/crafting etc if you reload the server. If you don't want that you can replace `.ndb` with `.db`. But even this won't help because the `yield` keyword is not persisent and nor is the use of `delay` shown above. To resolve this you can use `delay` with the `persistent=True` keyword. But wait! Making something persistent will add some extra complications, because now you must make sure Evennia can properly store things to the database. + +Here is the original echo-command reworked to function with persistence: +```python +from evennia import default_cmds, utils + +# this is now in the outermost scope and takes two args! +def echo(caller, args): + "Called after 10 seconds." + shout = args + string = "You hear an echo: %s ... %s ... %s" + string = string % (shout.upper(), shout.capitalize(), shout.lower()) + caller.msg(string) + +class CmdEcho(default_cmds.MuxCommand): + """ + wait for an echo + + Usage: + echo + + Calls and waits for an echo + """ + key = "echo" + locks = "cmd:all()" + + def func(self): + """ + This is called at the initial shout. + """ + self.caller.msg("You shout '%s' and wait for an echo ..." % self.args) + # this waits non-blocking for 10 seconds, then calls echo(self.caller, self.args) + utils.delay(10, echo, self.caller, self.args, persistent=True) # changes! + +``` + +Above you notice two changes: +- The callback (`echo`) was moved out of the class and became its own stand-alone function in the outermost scope of the module. It also now takes `caller` and `args` as arguments (it doesn't have access to them directly since this is now a stand-alone function). +- `utils.delay` specifies the `echo` function (not `self.echo` - it's no longer a method!) and sends `self.caller` and `self.args` as arguments for it to use. We also set `persistent=True`. + +The reason for this change is because Evennia needs to `pickle` the callback into storage and it cannot do this correctly when the method sits on the command class. Now this behave the same as the first version except if you reload (or even shut down) the server mid-delay it will still fire the callback when the server comes back up (it will resume the countdown and ignore the downtime). diff --git a/docs/source/Command-Prompt.md b/docs/source/Command-Prompt.md new file mode 100644 index 0000000000..aea0024fcc --- /dev/null +++ b/docs/source/Command-Prompt.md @@ -0,0 +1,113 @@ +# Command Prompt + + +A *prompt* is quite common in MUDs. The prompt display useful details about your character that you are likely to want to keep tabs on at all times, such as health, magical power etc. It might also show things like in-game time, weather and so on. Many modern MUD clients (including Evennia's own webclient) allows for identifying the prompt and have it appear in a correct location (usually just above the input line). Usually it will remain like that until it is explicitly updated. + +## Sending a prompt + +A prompt is sent using the `prompt` keyword to the `msg()` method on objects. The prompt will be sent without any line breaks. + +```python + self.msg(prompt="HP: 5, MP: 2, SP: 8") +``` +You can combine the sending of normal text with the sending (updating of the prompt): + +```python + self.msg("This is a text", prompt="This is a prompt") +``` + +You can update the prompt on demand, this is normally done using [OOB](OOB)-tracking of the relevant Attributes (like the character's health). You could also make sure that attacking commands update the prompt when they cause a change in health, for example. + +Here is a simple example of the prompt sent/updated from a command class: + +```python + from evennia import Command + + class CmdDiagnose(Command): + """ + see how hurt your are + + Usage: + diagnose [target] + + This will give an estimate of the target's health. Also + the target's prompt will be updated. + """ + key = "diagnose" + + def func(self): + if not self.args: + target = self.caller + else: + target = self.search(self.args) + if not target: + return + # try to get health, mana and stamina + hp = target.db.hp + mp = target.db.mp + sp = target.db.sp + + if None in (hp, mp, sp): + # Attributes not defined + self.caller.msg("Not a valid target!") + return + + text = "You diagnose %s as having " \ + "%i health, %i mana and %i stamina." \ + % (hp, mp, sp) + prompt = "%i HP, %i MP, %i SP" % (hp, mp, sp) + self.caller.msg(text, prompt=prompt) +``` +## A prompt sent with every command + +The prompt sent as described above uses a standard telnet instruction (the Evennia web client gets a special flag). Most MUD telnet clients will understand and allow users to catch this and keep the prompt in place until it updates. So *in principle* you'd not need to update the prompt every command. + +However, with a varying user base it can be unclear which clients are used and which skill level the users have. So sending a prompt with every command is a safe catch-all. You don't need to manually go in and edit every command you have though. Instead you edit the base command class for your custom commands (like `MuxCommand` in your `mygame/commands/command.py` folder) and overload the `at_post_cmd()` hook. This hook is always called *after* the main `func()` method of the Command. + +```python +from evennia import default_cmds + +class MuxCommand(default_cmds.MuxCommand): + # ... + def at_post_cmd(self): + "called after self.func()." + caller = self.caller + prompt = "%i HP, %i MP, %i SP" % (caller.db.hp, + caller.db.mp, + caller.db.sp) + caller.msg(prompt=prompt) + +``` + +### Modifying default commands + +If you want to add something small like this to Evennia's default commands without modifying them directly the easiest way is to just wrap those with a multiple inheritance to your own base class: + +```python +# in (for example) mygame/commands/mycommands.py + +from evennia import default_cmds +# our custom MuxCommand with at_post_cmd hook +from commands.command import MuxCommand + +# overloading the look command +class CmdLook(default_cmds.CmdLook, MuxCommand): + pass +``` + +The result of this is that the hooks from your custom `MuxCommand` will be mixed into the default `CmdLook` through multiple inheritance. Next you just add this to your default command set: + +```python +# in mygame/commands/default_cmdsets.py + +from evennia import default_cmds +from commands import mycommands + +class CharacterCmdSet(default_cmds.CharacterCmdSet): + # ... + def at_cmdset_creation(self): + # ... + self.add(mycommands.CmdLook()) +``` + +This will automatically replace the default `look` command in your game with your own version. \ No newline at end of file diff --git a/docs/source/Command-Sets.md b/docs/source/Command-Sets.md new file mode 100644 index 0000000000..dfaef14746 --- /dev/null +++ b/docs/source/Command-Sets.md @@ -0,0 +1,216 @@ +# Command Sets + + +Command Sets are intimately linked with [Commands](Commands) and you should be familiar with Commands before reading this page. The two pages were split for ease of reading. + +A *Command Set* (often referred to as a CmdSet or cmdset) is the basic unit for storing one or more *Commands*. A given Command can go into any number of different command sets. Storing Command classes in a command set is the way to make commands available to use in your game. + +When storing a CmdSet on an object, you will make the commands in that command set available to the object. An example is the default command set stored on new Characters. This command set contains all the useful commands, from `look` and `inventory` to `@dig` and `@reload` ([permissions](Locks#Permissions) then limit which players may use them, but that's a separate topic). + +When an account enters a command, cmdsets from the Account, Character, its location, and elsewhere are pulled together into a *merge stack*. This stack is merged together in a specific order to create a single "merged" cmdset, representing the pool of commands available at that very moment. + +An example would be a `Window` object that has a cmdset with two commands in it: `look through window` and `open window`. The command set would be visible to players in the room with the window, allowing them to use those commands only there. You could imagine all sorts of clever uses of this, like a `Television` object which had multiple commands for looking at it, switching channels and so on. The tutorial world included with Evennia showcases a dark room that replaces certain critical commands with its own versions because the Character cannot see. + +If you want a quick start into defining your first commands and using them with command sets, you can head over to the [Adding Command Tutorial](Adding-Command-Tutorial) which steps through things without the explanations. + +## Defining Command Sets + +A CmdSet is, as most things in Evennia, defined as a Python class inheriting from the correct parent (`evennia.CmdSet`, which is a shortcut to `evennia.commands.cmdset.CmdSet`). The CmdSet class only needs to define one method, called `at_cmdset_creation()`. All other class parameters are optional, but are used for more advanced set manipulation and coding (see the [merge rules](#merge-rules) section). + +```python +# file mygame/commands/mycmdset.py + +from evennia import CmdSet + +# this is a theoretical custom module with commands we +# created previously: mygame/commands/mycommands.py +from commands import mycommands + +class MyCmdSet(CmdSet): + def at_cmdset_creation(self): + """ + The only thing this method should need + to do is to add commands to the set. + """ + self.add(mycommands.MyCommand1()) + self.add(mycommands.MyCommand2()) + self.add(mycommands.MyCommand3()) +``` + +The CmdSet's `add()` method can also take another CmdSet as input. In this case all the commands from that CmdSet will be appended to this one as if you added them line by line: + +```python + def at_cmdset_creation(): + ... + self.add(AdditionalCmdSet) # adds all command from this set + ... +``` + +If you added your command to an existing cmdset (like to the default cmdset), that set is already loaded into memory. You need to make the server aware of the code changes: + +``` +@reload +``` + +You should now be able to use the command. + +If you created a new, fresh cmdset, this must be added to an object in order to make the commands within available. A simple way to temporarily test a cmdset on yourself is use the `@py` command to execute a python snippet: + +```python +@py self.cmdset.add('commands.mycmdset.MyCmdSet') +``` + +This will stay with you until you `@reset` or `@shutdown` the server, or you run + +```python +@py self.cmdset.delete('commands.mycmdset.MyCmdSet') +``` + +In the example above, a specific Cmdset class is removed. Calling `delete` without arguments will remove the latest added cmdset. + +> Note: Command sets added using `cmdset.add` are, by default, *not* persistent in the database. + +If you want the cmdset to survive a reload, you can do: + +``` +@py self.cmdset.add(commands.mycmdset.MyCmdSet, permanent=True) +``` + +Or you could add the cmdset as the *default* cmdset: + +``` +@py self.cmdset.add_default(commands.mycmdset.MyCmdSet) +``` + +An object can only have one "default" cmdset (but can also have none). This is meant as a safe fall-back even if all other cmdsets fail or are removed. It is always persistent and will not be affected by `cmdset.delete()`. To remove a default cmdset you must explicitly call `cmdset.remove_default()`. + +Command sets are often added to an object in its `at_object_creation` method. For more examples of adding commands, read the [Step by step tutorial](Adding-Command-Tutorial). Generally you can customize which command sets are added to your objects by using `self.cmdset.add()` or `self.cmdset.add_default()`. + +> Important: Commands are identified uniquely by key *or* alias (see [Commands](Commands)). If any overlap exists, two commands are considered identical. Adding a Command to a command set that already has an identical command will *replace* the previous command. This is very important. You must take this behavior into account when attempting to overload any default Evennia commands with your own. Otherwise, you may accidentally "hide" your own command in your command set when adding a new one that has a matching alias. + +### Properties on Command Sets + +There are several extra flags that you can set on CmdSets in order to modify how they work. All are optional and will be set to defaults otherwise. Since many of these relate to *merging* cmdsets, you might want to read the [Adding and Merging Command Sets](https://github.com/evennia/evennia/wiki/Command-Sets#adding-and-merging-command-sets) section for some of these to make sense. + +- `key` (string) - an identifier for the cmdset. This is optional, but should be unique. It is used for display in lists, but also to identify special merging behaviours using the `key_mergetype` dictionary below. - `mergetype` (string) - allows for one of the following string values: "*Union*", "*Intersect*", "*Replace*", or "*Remove*". +- `priority` (int) - This defines the merge order of the merge stack - cmdsets will merge in rising order of priority with the highest priority set merging last. During a merger, the commands from the set with the higher priority will have precedence (just what happens depends on the [merge type](#adding-and-merging-command-sets)). If priority is identical, the order in the merge stack determines preference. The priority value must be greater or equal to `-100`. Most in-game sets should usually have priorities between `0` and `100`. Evennia default sets have priorities as follows (these can be changed if you want a different distribution): + - EmptySet: `-101` (should be lower than all other sets) + - SessionCmdSet: `-20` + - AccountCmdSet: `-10` + - CharacterCmdSet: `0` + - ExitCmdSet: ` 101` (generally should always be available) + - ChannelCmdSet: `101` (should usually always be available) - since exits never accept arguments, there is no collision between exits named the same as a channel even though the commands "collide". +- `key_mergetype` (dict) - a dict of `key:mergetype` pairs. This allows this cmdset to merge differently with certain named cmdsets. If the cmdset to merge with has a `key` matching an entry in `key_mergetype`, it will not be merged according to the setting in `mergetype` but according to the mode in this dict. Please note that this is more complex than it may seem due to the [merge order](#adding-and-merging-command-sets) of command sets. Please review that section before using `key_mergetype`. +- `duplicates` (bool/None default `None`) - this determines what happens when merging same-priority cmdsets containing same-key commands together. The`dupicate` option will *only* apply when merging the cmdset with this option onto one other cmdset with the same priority. The resulting cmdset will *not* retain this `duplicate` setting. + - `None` (default): No duplicates are allowed and the cmdset being merged "onto" the old one will take precedence. The result will be unique commands. *However*, the system will assume this value to be `True` for cmdsets on Objects, to avoid dangerous clashes. This is usually the safe bet. + - `False`: Like `None` except the system will not auto-assume any value for cmdsets defined on Objects. + - `True`: Same-named, same-prio commands will merge into the same cmdset. This will lead to a multimatch error (the user will get a list of possibilities in order to specify which command they meant). This is is useful e.g. for on-object cmdsets (example: There is a `red button` and a `green button` in the room. Both have a `press button` command, in cmdsets with the same priority. This flag makes sure that just writing `press button` will force the Player to define just which object's command was intended). +- `no_objs` this is a flag for the cmdhandler that builds the set of commands available at every moment. It tells the handler not to include cmdsets from objects around the account (nor from rooms or inventory) when building the merged set. Exit commands will still be included. This option can have three values: + - `None` (default): Passthrough of any value set explicitly earlier in the merge stack. If never set explicitly, this acts as `False`. + - `True`/`False`: Explicitly turn on/off. If two sets with explicit `no_objs` are merged, priority determines what is used. +- `no_exits` - this is a flag for the cmdhandler that builds the set of commands available at every moment. It tells the handler not to include cmdsets from exits. This flag can have three values: + - `None` (default): Passthrough of any value set explicitly earlier in the merge stack. If never set explicitly, this acts as `False`. + - `True`/`False`: Explicitly turn on/off. If two sets with explicit `no_exits` are merged, priority determines what is used. +- `no_channels` (bool) - this is a flag for the cmdhandler that builds the set of commands available at every moment. It tells the handler not to include cmdsets from available in-game channels. This flag can have three values: + - `None` (default): Passthrough of any value set explicitly earlier in the merge stack. If never set explicitly, this acts as `False`. + - `True`/`False`: Explicitly turn on/off. If two sets with explicit `no_channels` are merged, priority determines what is used. + +## Command Sets Searched + +When a user issues a command, it is matched against the [merged](#adding-and-merging-command-sets) command sets available to the player at the moment. Which those are may change at any time (such as when the player walks into the room with the `Window` object described earlier). + +The currently valid command sets are collected from the following sources: + +- The cmdsets stored on the currently active [Session](Sessions). Default is the empty `SessionCmdSet` with merge priority `-20`. +- The cmdsets defined on the [Account](Accounts). Default is the AccountCmdSet with merge priority `-10`. +- All cmdsets on the Character/Object (assuming the Account is currently puppeting such a Character/Object). Merge priority `0`. +- The cmdsets of all objects carried by the puppeted Character (checks the `call` lock). Will not be included if `no_objs` option is active in the merge stack. +- The cmdsets of the Character's current location (checks the `call` lock). Will not be included if `no_objs` option is active in the merge stack. +- The cmdsets of objects in the current location (checks the `call` lock). Will not be included if `no_objs` option is active in the merge stack. +- The cmdsets of Exits in the location. Merge priority `+101`. Will not be included if `no_exits` *or* `no_objs` option is active in the merge stack. +- The [channel](Communications) cmdset containing commands for posting to all channels the account or character is currently connected to. Merge priority `+101`. Will not be included if `no_channels` option is active in the merge stack. + +Note that an object does not *have* to share its commands with its surroundings. A Character's cmdsets should not be shared for example, or all other Characters would get multi-match errors just by being in the same room. The ability of an object to share its cmdsets is managed by its `call` [lock](Locks). For example, [Character objects](Objects) defaults to `call:false()` so that any cmdsets on them can only be accessed by themselves, not by other objects around them. Another example might be to lock an object with `call:inside()` to only make their commands available to objects inside them, or `cmd:holds()` to make their commands available only if they are held. + +## Adding and Merging Command Sets + +*Note: This is an advanced topic. It's very useful to know about, but you might want to skip it if this is your first time learning about commands.* + +CmdSets have the special ability that they can be *merged* together into new sets. Which of the ingoing commands end up in the merged set is defined by the *merge rule* and the relative *priorities* of the two sets. Removing the latest added set will restore things back to the way it was before the addition. + +CmdSets are non-destructively stored in a stack inside the cmdset handler on the object. This stack is parsed to create the "combined" cmdset active at the moment. CmdSets from other sources are also included in the merger such as those on objects in the same room (like buttons to press) or those introduced by state changes (such as when entering a menu). The cmdsets are all ordered after priority and then merged together in *reverse order*. That is, the higher priority will be merged "onto" lower-prio ones. By defining a cmdset with a merge-priority between that of two other sets, you will make sure it will be merged in between them. +The very first cmdset in this stack is called the *Default cmdset* and is protected from accidental deletion. Running `obj.cmdset.delete()` will never delete the default set. Instead one should add new cmdsets on top of the default to "hide" it, as described below. Use the special `obj.cmdset.delete_default()` only if you really know what you are doing. + +CmdSet merging is an advanced feature useful for implementing powerful game effects. Imagine for example a player entering a dark room. You don't want the player to be able to find everything in the room at a glance - maybe you even want them to have a hard time to find stuff in their backpack! You can then define a different CmdSet with commands that override the normal ones. While they are in the dark room, maybe the `look` and `inv` commands now just tell the player they cannot see anything! Another example would be to offer special combat commands only when the player is in combat. Or when being on a boat. Or when having taken the super power-up. All this can be done on the fly by merging command sets. + +### Merge Rules + +Basic rule is that command sets are merged in *reverse priority order*. That is, lower-prio sets are merged first and higher prio sets are merged "on top" of them. Think of it like a layered cake with the highest priority on top. + +To further understand how sets merge, we need to define some examples. Let's call the first command set **A** and the second **B**. We assume **B** is the command set already active on our object and we will merge **A** onto **B**. In code terms this would be done by `object.cdmset.add(A)`. Remember, B is already active on `object` from before. + +We let the **A** set have higher priority than **B**. A priority is simply an integer number. As seen in the list above, Evennia's default cmdsets have priorities in the range `-101` to `120`. You are usually safe to use a priority of `0` or `1` for most game effects. + +In our examples, both sets contain a number of commands which we'll identify by numbers, like `A1, A2` for set **A** and `B1, B2, B3, B4` for **B**. So for that example both sets contain commands with the same keys (or aliases) "1" and "2" (this could for example be "look" and "get" in the real game), whereas commands 3 and 4 are unique to **B**. To describe a merge between these sets, we would write `A1,A2 + B1,B2,B3,B4 = ?` where `?` is a list of commands that depend on which merge type **A** has, and which relative priorities the two sets have. By convention, we read this statement as "New command set **A** is merged onto the old command set **B** to form **?**". + +Below are the available merge types and how they work. Names are partly borrowed from [Set theory](http://en.wikipedia.org/wiki/Set_theory). + +- **Union** (default) - The two cmdsets are merged so that as many commands as possible from each cmdset ends up in the merged cmdset. Same-key commands are merged by priority. + + # Union + A1,A2 + B1,B2,B3,B4 = A1,A2,B3,B4 + +- **Intersect** - Only commands found in *both* cmdsets (i.e. which have the same keys) end up in the merged cmdset, with the higher-priority cmdset replacing the lower one's commands. + + # Intersect + A1,A3,A5 + B1,B2,B4,B5 = A1,A5 + +- **Replace** - The commands of the higher-prio cmdset completely replaces the lower-priority cmdset's commands, regardless of if same-key commands exist or not. + + # Replace + A1,A3 + B1,B2,B4,B5 = A1,A3 + +- **Remove** - The high-priority command sets removes same-key commands from the lower-priority cmdset. They are not replaced with anything, so this is a sort of filter that prunes the low-prio set using the high-prio one as a template. + + # Remove + A1,A3 + B1,B2,B3,B4,B5 = B2,B4,B5 + +Besides `priority` and `mergetype`, a command-set also takes a few other variables to control how they merge: + +- `duplicates` (bool) - determines what happens when two sets of equal priority merge. Default is that the new set in the merger (i.e. **A** above) automatically takes precedence. But if *duplicates* is true, the result will be a merger with more than one of each name match. This will usually lead to the player receiving a multiple-match error higher up the road, but can be good for things like cmdsets on non-player objects in a room, to allow the system to warn that more than one 'ball' in the room has the same 'kick' command defined on it and offer a chance to select which ball to kick ... Allowing duplicates only makes sense for *Union* and *Intersect*, the setting is ignored for the other mergetypes. +- `key_mergetypes` (dict) - allows the cmdset to define a unique mergetype for particular cmdsets, identified by their cmdset `key`. Format is `{CmdSetkey:mergetype}`. Example: `{'Myevilcmdset','Replace'}` which would make sure for this set to always use 'Replace' on the cmdset with the key `Myevilcmdset` only, no matter what the main `mergetype` is set to. + +> Warning: The `key_mergetypes` dictionary *can only work on the cmdset we merge onto*. When using `key_mergetypes` it is thus important to consider the merge priorities - you must make sure that you pick a priority *between* the cmdset you want to detect and the next higher one, if any. That is, if we define a cmdset with a high priority and set it to affect a cmdset that is far down in the merge stack, we would not "see" that set when it's time for us to merge. Example: Merge stack is `A(prio=-10), B(prio=-5), C(prio=0), D(prio=5)`. We now merge a cmdset `E(prio=10)` onto this stack, with a `key_mergetype={"B":"Replace"}`. But priorities dictate that we won't be merged onto B, we will be merged onto E (which is a merger of the lower-prio sets at this point). Since we are merging onto E and not B, our `key_mergetype` directive won't trigger. To make sure it works we must make sure we merge onto B. Setting E's priority to, say, -4 will make sure to merge it onto B and affect it appropriately. + +More advanced cmdset example: + +```python +from commands import mycommands + +class MyCmdSet(CmdSet): + + key = "MyCmdSet" + priority = 4 + mergetype = "Replace" + key_mergetypes = {'MyOtherCmdSet':'Union'} + + def at_cmdset_creation(self): + """ + The only thing this method should need + to do is to add commands to the set. + """ + self.add(mycommands.MyCommand1()) + self.add(mycommands.MyCommand2()) + self.add(mycommands.MyCommand3()) +``` + +### Assorted Notes + +It is very important to remember that two commands are compared *both* by their `key` properties *and* by their `aliases` properties. If either keys or one of their aliases match, the two commands are considered the *same*. So consider these two Commands: + + - A Command with key "kick" and alias "fight" + - A Command with key "punch" also with an alias "fight" + +During the cmdset merging (which happens all the time since also things like channel commands and exits are merged in), these two commands will be considered *identical* since they share alias. It means only one of them will remain after the merger. Each will also be compared with all other commands having any combination of the keys and/or aliases "kick", "punch" or "fight". + +... So avoid duplicate aliases, it will only cause confusion. \ No newline at end of file diff --git a/docs/source/Command-System.md b/docs/source/Command-System.md new file mode 100644 index 0000000000..c45e9659f6 --- /dev/null +++ b/docs/source/Command-System.md @@ -0,0 +1,9 @@ +# Command System + +- [Commands](Commands) +- [Command Sets](Command-Sets) +- [Command Auto-help](Help-System#command-auto-help-system) + +See also: +- [Default Command Help](Default-Command-Help) +- [Adding Command Tutorial](Adding-Command-Tutorial) \ No newline at end of file diff --git a/docs/source/Commands.md b/docs/source/Commands.md new file mode 100644 index 0000000000..881e4a1d06 --- /dev/null +++ b/docs/source/Commands.md @@ -0,0 +1,452 @@ +# Commands + + +Commands are intimately linked to [Command Sets](Command-Sets) and you need to read that page too to be familiar with how the command system works. The two pages were split for easy reading. + +The basic way for users to communicate with the game is through *Commands*. These can be commands directly related to the game world such as *look*, *get*, *drop* and so on, or administrative commands such as *examine* or *@dig*. + +The [default commands](Default-Command-Help) coming with Evennia are 'MUX-like' in that they use @ for admin commands, support things like switches, syntax with the '=' symbol etc, but there is nothing that prevents you from implementing a completely different command scheme for your game. You can find the default commands in `evennia/commands/default`. You should not edit these directly - they will be updated by the Evennia team as new features are added. Rather you should look to them for inspiration and inherit your own designs from them. + +There are two components to having a command running - the *Command* class and the [Command Set](Command-Sets) (command sets were split into a separate wiki page for ease of reading). + +1. A *Command* is a python class containing all the functioning code for what a command does - for example, a *get* command would contain code for picking up objects. +1. A *Command Set* (often referred to as a CmdSet or cmdset) is like a container for one or more Commands. A given Command can go into any number of different command sets. Only by putting the command set on a character object you will make all the commands therein available to use by that character. You can also store command sets on normal objects if you want users to be able to use the object in various ways. Consider a "Tree" object with a cmdset defining the commands *climb* and *chop down*. Or a "Clock" with a cmdset containing the single command *check time*. + +This page goes into full detail about how to use Commands. To fully use them you must also read the page detailing [Command Sets](Command-Sets). There is also a step-by-step [Adding Command Tutorial](Adding-Command-Tutorial) that will get you started quickly without the extra explanations. + +## Defining Commands + +All commands are implemented as normal Python classes inheriting from the base class `Command` (`evennia.Command`). You will find that this base class is very "bare". The default commands of Evennia actually inherit from a child of `Command` called `MuxCommand` - this is the class that knows all the mux-like syntax like `/switches`, splitting by "=" etc. Below we'll avoid mux-specifics and use the base `Command` class directly. + +```python + # basic Command definition + from evennia import Command + + class MyCmd(Command): + """ + This is the help-text for the command + """ + key = "mycommand" + def parse(self): + # parsing the command line here + def func(self): + # executing the command here +``` + +Here is a minimalistic command with no custom parsing: + +```python + from evennia import Command + + class CmdEcho(Command): + key = "echo" + + def func(self): + # echo the caller's input back to the caller + self.caller.msg("Echo: {}".format(self.args) + +``` + +You define a new command by assigning a few class-global properties on your inherited class and overloading one or two hook functions. The full gritty mechanic behind how commands work are found towards the end of this page; for now you only need to know that the command handler creates an instance of this class and uses that instance whenever you use this command - it also dynamically assigns the new command instance a few useful properties that you can assume to always be available. + +### Who is calling the command? + +In Evennia there are three types of objects that may call the command. It is important to be aware of this since this will also assign appropriate `caller`, `session`, `sessid` and `account` properties on the command body at runtime. Most often the calling type is `Session`. + +* A [Session](Sessions). This is by far the most common case when a user is entering a command in their client. + * `caller` - this is set to the puppeted [Object](Objects) if such an object exists. If no puppet is found, `caller` is set equal to `account`. Only if an Account is not found either (such as before being logged in) will this be set to the Session object itself. + * `session` - a reference to the [Session](Sessions) object itself. + * `sessid` - `sessid.id`, a unique integer identifier of the session. + * `account` - the [Account](Accounts) object connected to this Session. None if not logged in. +* An [Account](Accounts). This only happens if `account.execute_cmd()` was used. No Session information can be obtained in this case. + * `caller` - this is set to the puppeted Object if such an object can be determined (without Session info this can only be determined in `MULTISESSION_MODE=0` or `1`). If no puppet is found, this is equal to `account`. + * `session` - `None*` + * `sessid` - `None*` + * `account` - Set to the Account object. +* An [Object](Objects). This only happens if `object.execute_cmd()` was used (for example by an NPC). + * `caller` - This is set to the calling Object in question. + * `session` - `None*` + * `sessid` - `None*` + * `account` - `None` + +> `*)`: There is a way to make the Session available also inside tests run directly on Accounts and Objects, and that is to pass it to `execute_cmd` like so: `account.execute_cmd("...", session=)`. Doing so *will* make the `.session` and `.sessid` properties available in the command. + +### Properties assigned to the command instance at run-time + +Let's say account *Bob* with a character *BigGuy* enters the command *look at sword*. After the system having successfully identified this as the "look" command and determined that BigGuy really has access to a command named `look`, it chugs the `look` command class out of storage and either loads an existing Command instance from cache or creates one. After some more checks it then assigns it the following properties: + +- `caller` - The character BigGuy, in this example. This is a reference to the object executing the command. The value of this depends on what type of object is calling the command; see the previous section. +- `session` - the [Session](Sessions) Bob uses to connect to the game and control BigGuy (see also previous section). +- `sessid` - the unique id of `self.session`, for quick lookup. +- `account` - the [Account](Accounts) Bob (see previous section). +- `cmdstring` - the matched key for the command. This would be *look* in our example. +- `args` - this is the rest of the string, except the command name. So if the string entered was *look at sword*, `args` would be " *at sword*". Note the space kept - Evennia would correctly interpret `lookat sword` too. This is useful for things like `/switches` that should not use space. In the `MuxCommand` class used for default commands, this space is stripped. Also see the `arg_regex` property if you want to enforce a space to make `lookat sword` give a command-not-found error. +- `obj` - the game [Object](Objects) on which this command is defined. This need not be the caller, but since `look` is a common (default) command, this is probably defined directly on *BigGuy* - so `obj` will point to BigGuy. Otherwise `obj` could be an Account or any interactive object with commands defined on it, like in the example of the "check time" command defined on a "Clock" object. +- `cmdset` - this is a reference to the merged CmdSet (see below) from which this command was matched. This variable is rarely used, it's main use is for the [auto-help system](Help-System#command-auto-help-system) (*Advanced note: the merged cmdset need NOT be the same as `BigGuy.cmdset`. The merged set can be a combination of the cmdsets from other objects in the room, for example*). +- `raw_string` - this is the raw input coming from the user, without stripping any surrounding whitespace. The only thing that is stripped is the ending newline marker. + +#### Other useful utility methods: + +- `.get_help(caller, cmdset)` - Get the help entry for this command. By default the arguments are not + used, but they could be used to implement alternate help-display systems. +- `.client_width()` - Shortcut for getting the client's screen-width. Note that not all clients will + truthfully report this value - that case the `settings.DEFAULT_SCREEN_WIDTH` will be returned. +- `.styled_table(*args, **kwargs)` - This returns an [EvTable](EvTable) [styled](OptionStyles) based on the + session calling this command. The args/kwargs are the same as for EvTable, except styling defaults are set. +- `.styled_header`, `_footer`, `separator` - These will produce [styled](OptionStyles) decorations for + display to the user. They are useful for creating listings and forms with colors adjustable per-user. + +### Defining your own command classes + +Beyond the properties Evennia always assigns to the command at run-time (listed above), your job is to define the following class properties: + +- `key` (string) - the identifier for the command, like `look`. This should (ideally) be unique. A key can consist of more than one word, like "press button" or "pull left lever". Note that *both* `key` and `aliases` below determine the identity of a command. So two commands are considered if either matches. This is important for merging cmdsets described below. +- `aliases` (optional list) - a list of alternate names for the command (`["glance", "see", "l"]`). Same name rules as for `key` applies. +- `locks` (string) - a [lock definition](Locks), usually on the form `cmd:`. Locks is a rather big topic, so until you learn more about locks, stick to giving the lockstring `"cmd:all()"` to make the command available to everyone (if you don't provide a lock string, this will be assigned for you). +- `help_category` (optional string) - setting this helps to structure the auto-help into categories. If none is set, this will be set to *General*. +- `save_for_next` (optional boolean). This defaults to `False`. If `True`, a copy of this command object (along with any changes you have done to it) will be stored by the system and can be accessed by the next command by retrieving `self.caller.ndb.last_cmd`. The next run command will either clear or replace the storage. +- `arg_regex` (optional raw string): Used to force the parser to limit itself and tell it when the command-name ends and arguments begin (such as requiring this to be a space or a /switch). This is done with a regular expression. [See the arg_regex section](#on-arg_regex) for the details. +- `auto_help` (optional boolean). Defaults to `True`. This allows for turning off the [auto-help system](Help-System#command-auto-help-system) on a per-command basis. This could be useful if you either want to write your help entries manually or hide the existence of a command from `help`'s generated list. +- `is_exit` (bool) - this marks the command as being used for an in-game exit. This is, by default, set by all Exit objects and you should not need to set it manually unless you make your own Exit system. It is used for optimization and allows the cmdhandler to easily disregard this command when the cmdset has its `no_exits` flag set. +- `is_channel` (bool)- this marks the command as being used for an in-game channel. This is, by default, set by all Channel objects and you should not need to set it manually unless you make your own Channel system. is used for optimization and allows the cmdhandler to easily disregard this command when its cmdset has its `no_channels` flag set. +- `msg_all_sessions` (bool): This affects the behavior of the `Command.msg` method. If unset (default), calling `self.msg(text)` from the Command will always only send text to the Session that actually triggered this Command. If set however, `self.msg(text)` will send to all Sessions relevant to the object this Command sits on. Just which Sessions receives the text depends on the object and the server's `MULTISESSION_MODE`. + +You should also implement at least two methods, `parse()` and `func()` (You could also implement `perm()`, but that's not needed unless you want to fundamentally change how access checks work). + +- `at_pre_cmd()` is called very first on the command. If this function returns anything that evaluates to `True` the command execution is aborted at this point. +- `parse()` is intended to parse the arguments (`self.args`) of the function. You can do this in any way you like, then store the result(s) in variable(s) on the command object itself (i.e. on `self`). To take an example, the default mux-like system uses this method to detect "command switches" and store them as a list in `self.switches`. Since the parsing is usually quite similar inside a command scheme you should make `parse()` as generic as possible and then inherit from it rather than re-implementing it over and over. In this way, the default `MuxCommand` class implements a `parse()` for all child commands to use. +- `func()` is called right after `parse()` and should make use of the pre-parsed input to actually do whatever the command is supposed to do. This is the main body of the command. The return value from this method will be returned from the execution as a Twisted Deferred. +- `at_post_cmd()` is called after `func()` to handle eventual cleanup. + +Finally, you should always make an informative [doc string](http://www.python.org/dev/peps/pep-0257/#what-is-a-docstring) (`__doc__`) at the top of your class. This string is dynamically read by the [Help System](Help-system) to create the help entry for this command. You should decide on a way to format your help and stick to that. + +Below is how you define a simple alternative "`smile`" command: + +```python +from evennia import Command + +class CmdSmile(Command): + """ + A smile command + + Usage: + smile [at] [] + grin [at] [] + + Smiles to someone in your vicinity or to the room + in general. + + (This initial string (the __doc__ string) + is also used to auto-generate the help + for this command) + """ + + key = "smile" + aliases = ["smile at", "grin", "grin at"] + locks = "cmd:all()" + help_category = "General" + + def parse(self): + "Very trivial parser" + target = self.args.strip() + + def func(self): + "This actually does things" + caller = self.caller + + if not self.target or self.target == "here": + string = f"{caller.key} smiles" + else: + target = caller.search(self.target) + if not target: + return + string = f"{caller.key} smiles at {target.key}" + + caller.location.msg_contents(string) + +``` + +The power of having commands as classes and to separate `parse()` and `func()` +lies in the ability to inherit functionality without having to parse every +command individually. For example, as mentioned the default commands all +inherit from `MuxCommand`. `MuxCommand` implements its own version of `parse()` +that understands all the specifics of MUX-like commands. Almost none of the +default commands thus need to implement `parse()` at all, but can assume the +incoming string is already split up and parsed in suitable ways by its parent. + +Before you can actually use the command in your game, you must now store it +within a *command set*. See the [Command Sets](Command-Sets) page. + +### On arg_regex + +The command parser is very general and does not require a space to end your command name. This means that the alias `:` to `emote` can be used like `:smiles` without modification. It also means `getstone` will get you the stone (unless there is a command specifically named `getstone`, then that will be used). If you want to tell the parser to require a certain separator between the command name and its arguments (so that `get stone` works but `getstone` gives you a 'command not found' error) you can do so with the `arg_regex` property. + +The `arg_regex` is a [raw regular expression string](http://docs.python.org/library/re.html). The regex will be compiled by the system at runtime. This allows you to customize how the part *immediately following* the command name (or alias) must look in order for the parser to match for this command. Some examples: + +- `commandname argument` (`arg_regex = r"\s.+"`): This forces the parser to require the command name to be followed by one or more spaces. Whatever is entered after the space will be treated as an argument. However, if you'd forget the space (like a command having no arguments), this would *not* match `commandname`. +- `commandname` or `commandname argument` (`arg_regex = r"\s.+|$"`): This makes both `look` and `look me` work but `lookme` will not. +- `commandname/switches arguments` (`arg_regex = r"(?:^(?:\s+|\/).*$)|^$"`. If you are using Evennia's `MuxCommand` Command parent, you may wish to use this since it will allow `/switche`s to work as well as having or not having a space. + +The `arg_regex` allows you to customize the behavior of your commands. You can put it in the parent class of your command to customize all children of your Commands. However, you can also change the base default behavior for all Commands by modifying `settings.COMMAND_DEFAULT_ARG_REGEX`. + +## Exiting a command + +Normally you just use `return` in one of your Command class' hook methods to exit that method. That will however still fire the other hook methods of the Command in sequence. That's usually what you want but sometimes it may be useful to just abort the command, for example if you find some unacceptable input in your parse method. To exit the command this way you can raise `evennia.InterruptCommand`: + +```python +from evennia import InterruptCommand + +class MyCommand(Command): + + # ... + + def parse(self): + # ... + # if this fires, `func()` and `at_post_cmd` will not + # be called at all + raise InterruptCommand() + +``` + +## Pauses in commands + +Sometimes you want to pause the execution of your command for a little while before continuing - maybe you want to simulate a heavy swing taking some time to finish, maybe you want the echo of your voice to return to you with an ever-longer delay. Since Evennia is running asynchronously, you cannot use `time.sleep()` in your commands (or anywhere, really). If you do, the *entire game* will be frozen for everyone! So don't do that. Fortunately, Evennia offers a really quick syntax for making pauses in commands. + +In your `func()` method, you can use the `yield` keyword. This is a Python keyword that will freeze the current execution of your command and wait for more before processing. + +> Note that you *cannot* just drop `yield` into any code and expect it to pause. Evennia will only pause for you if you `yield` inside the Command's `func()` method. Don't expect it to work anywhere else. + +Here's an example of a command using a small pause of five seconds between messages: + +```python +from evennia import Command + +class CmdWait(Command): + """ + A dummy command to show how to wait + + Usage: + wait + + """ + + key = "wait" + locks = "cmd:all()" + help_category = "General" + + def func(self): + """Command execution.""" + self.msg("Starting to wait ...") + yield 5 + self.msg("... This shows after 5 seconds. Waiting ...") + yield 2 + self.msg("... And now another 2 seconds have passed.") +``` + +The important line is the `yield 5` and `yield 2` lines. It will tell Evennia to pause execution here and not continue until the number of seconds given has passed. + +There are two things to remember when using `yield` in your Command's `func` method: + +1. The paused state produced by the `yield` is not saved anywhere. So if the server reloads in the middle of your command pausing, it will *not* resume when the server comes back up - the remainder of the command will never fire. So be careful that you are not freezing the character or account in a way that will not be cleared on reload. +2. If you use `yield` you may not also use `return ` in your `func` method. You'll get an error explaining this. This is due to how Python generators work. You can however use a "naked" `return` just fine. Usually there is no need for `func` to return a value, but if you ever do need to mix `yield` with a final return value in the same `func`, look at [twisted.internet.defer.returnValue](https://twistedmatrix.com/documents/current/api/twisted.internet.defer.html#returnValue). + +## Asking for user input + +The `yield` keyword can also be used to ask for user input. Again you can't +use Python's `input` in your command, for it would freeze Evennia for +everyone while waiting for that user to input their text. Inside a Command's +`func` method, the following syntax can also be used: + +```python +answer = yield("Your question") +``` + +Here's a very simple example: + +```python +class CmdConfirm(Command): + + """ + A dummy command to show confirmation. + + Usage: + confirm + + """ + + key = "confirm" + + def func(self): + answer = yield("Are you sure you want to go on?") + if answer.strip().lower() in ("yes", "y"): + self.msg("Yes!") + else: + self.msg("No!") +``` + +This time, when the user enters the 'confirm' command, she will be asked if she wants to go on. Entering 'yes' or "y" (regardless of case) will give the first reply, otherwise the second reply will show. + +> Note again that the `yield` keyword does not store state. If the game reloads while waiting for the user to answer, the user will have to start over. It is not a good idea to use `yield` for important or complex choices, a persistent [EvMenu](https://github.com/evennia/evennia/wiki/EvMenu) might be more appropriate in this case. + +## System commands + +*Note: This is an advanced topic. Skip it if this is your first time learning about commands.* + +There are several command-situations that are exceptional in the eyes of the server. What happens if the account enters an empty string? What if the 'command' given is infact the name of a channel the user wants to send a message to? Or if there are multiple command possibilities? + +Such 'special cases' are handled by what's called *system commands*. A system command is defined in the same way as other commands, except that their name (key) must be set to one reserved by the engine (the names are defined at the top of `evennia/commands/cmdhandler.py`). You can find (unused) implementations of the system commands in `evennia/commands/default/system_commands.py`. Since these are not (by default) included in any `CmdSet` they are not actually used, they are just there for show. When the special situation occurs, Evennia will look through all valid `CmdSet`s for your custom system command. Only after that will it resort to its own, hard-coded implementation. + +Here are the exceptional situations that triggers system commands. You can find the command keys they use as properties on `evennia.syscmdkeys`: + +- No input (`syscmdkeys.CMD_NOINPUT`) - the account just pressed return without any input. Default is to do nothing, but it can be useful to do something here for certain implementations such as line editors that interpret non-commands as text input (an empty line in the editing buffer). +- Command not found (`syscmdkeys.CMD_NOMATCH`) - No matching command was found. Default is to display the "Huh?" error message. +- Several matching commands where found (`syscmdkeys.CMD_MULTIMATCH`) - Default is to show a list of matches. +- User is not allowed to execute the command (`syscmdkeys.CMD_NOPERM`) - Default is to display the "Huh?" error message. +- Channel (`syscmdkeys.CMD_CHANNEL`) - This is a [Channel](Communications) name of a channel you are subscribing to - Default is to relay the command's argument to that channel. Such commands are created by the Comm system on the fly depending on your subscriptions. +- New session connection (`syscmdkeys.CMD_LOGINSTART`). This command name should be put in the `settings.CMDSET_UNLOGGEDIN`. Whenever a new connection is established, this command is always called on the server (default is to show the login screen). + +Below is an example of redefining what happens when the account doesn't provide any input (e.g. just presses return). Of course the new system command must be added to a cmdset as well before it will work. + +```python + from evennia import syscmdkeys, Command + + class MyNoInputCommand(Command): + "Usage: Just press return, I dare you" + key = syscmdkeys.CMD_NOINPUT + def func(self): + self.caller.msg("Don't just press return like that, talk to me!") +``` + +## Dynamic Commands + +*Note: This is an advanced topic.* + +Normally Commands are created as fixed classes and used without modification. There are however situations when the exact key, alias or other properties is not possible (or impractical) to pre-code ([Exits](Commands#Exits) is an example of this). + +To create a command with a dynamic call signature, first define the command body normally in a class (set your `key`, `aliases` to default values), then use the following call (assuming the command class you created is named `MyCommand`): + +```python + cmd = MyCommand(key="newname", + aliases=["test", "test2"], + locks="cmd:all()", + ...) +``` + +*All* keyword arguments you give to the Command constructor will be stored as a property on the command object. This will overload existing properties defined on the parent class. + +Normally you would define your class and only overload things like `key` and `aliases` at run-time. But you could in principle also send method objects (like `func`) as keyword arguments in order to make your command completely customized at run-time. + +## Exits + +*Note: This is an advanced topic.* + +Exits are examples of the use of a [Dynamic Command](Commands#Dynamic_Commands). + +The functionality of [Exit](Objects) objects in Evennia is not hard-coded in the engine. Instead Exits are normal [typeclassed](Typeclasses) objects that auto-create a [CmdSet](Commands#CmdSets) on themselves when they load. This cmdset has a single dynamically created Command with the same properties (key, aliases and locks) as the Exit object itself. When entering the name of the exit, this dynamic exit-command is triggered and (after access checks) moves the Character to the exit's destination. +Whereas you could customize the Exit object and its command to achieve completely different behaviour, you will usually be fine just using the appropriate `traverse_*` hooks on the Exit object. But if you are interested in really changing how things work under the hood, check out `evennia/objects/objects.py` for how the `Exit` typeclass is set up. + +## Command instances are re-used + +*Note: This is an advanced topic that can be skipped when first learning about Commands.* + +A Command class sitting on an object is instantiated once and then re-used. So if you run a command from object1 over and over you are in fact running the same command instance over and over (if you run the same command but sitting on object2 however, it will be a different instance). This is usually not something you'll notice, since every time the Command-instance is used, all the relevant properties on it will be overwritten. But armed with this knowledge you can implement some of the more exotic command mechanism out there, like the command having a 'memory' of what you last entered so that you can back-reference the previous arguments etc. + +> Note: On a server reload, all Commands are rebuilt and memory is flushed. + +To show this in practice, consider this command: + +```python +class CmdTestID(Command): + key = "testid" + + def func(self): + + if not hasattr(self, "xval"): + self.xval = 0 + self.xval += 1 + + self.caller.msg("Command memory ID: {} (xval={})".format(id(self), self.xval)) + +``` + +Adding this to the default character cmdset gives a result like this in-game: + +``` +> testid +Command memory ID: 140313967648552 (xval=1) +> testid +Command memory ID: 140313967648552 (xval=2) +> testid +Command memory ID: 140313967648552 (xval=3) +``` + +Note how the in-memory address of the `testid` command never changes, but `xval` keeps ticking up. + +## Dynamically created commands + +*This is also an advanced topic.* + +Commands can also be created and added to a cmdset on the fly. Creating a class instance with a keyword argument, will assign that keyword argument as a property on this paricular command: + +``` +class MyCmdSet(CmdSet): + + def at_cmdset_creation(self): + + self.add(MyCommand(myvar=1, foo="test") + +``` + +This will start the `MyCommand` with `myvar` and `foo` set as properties (accessable as `self.myvar` and `self.foo`). How they are used is up to the Command. Remember however the discussion from the previous section - since the Command instance is re-used, those properties will *remain* on the command as long as this cmdset and the object it sits is in memory (i.e. until the next reload). Unless `myvar` and `foo` are somehow reset when the command runs, they can be modified and that change will be remembered for subsequent uses of the command. + + +## How commands actually work + +*Note: This is an advanced topic mainly of interest to server developers.* + +Any time the user sends text to Evennia, the server tries to figure out if the text entered corresponds to a known command. This is how the command handler sequence looks for a logged-in user: + +1. A user enters a string of text and presses enter. +2. The user's Session determines the text is not some protocol-specific control sequence or OOB command, but sends it on to the command handler. +3. Evennia's *command handler* analyzes the Session and grabs eventual references to Account and eventual puppeted Characters (these will be stored on the command object later). The *caller* property is set appropriately. +4. If input is an empty string, resend command as `CMD_NOINPUT`. If no such command is found in cmdset, ignore. +5. If command.key matches `settings.IDLE_COMMAND`, update timers but don't do anything more. +6. The command handler gathers the CmdSets available to *caller* at this time: + - The caller's own currently active CmdSet. + - CmdSets defined on the current account, if caller is a puppeted object. + - CmdSets defined on the Session itself. + - The active CmdSets of eventual objects in the same location (if any). This includes commands on [Exits](Objects#Exits). + - Sets of dynamically created *System commands* representing available [Communications](Communications.md#Channels). +7. All CmdSets *of the same priority* are merged together in groups. Grouping avoids order-dependent issues of merging multiple same-prio sets onto lower ones. +8. All the grouped CmdSets are *merged* in reverse priority into one combined CmdSet according to each set's merge rules. +9. Evennia's *command parser* takes the merged cmdset and matches each of its commands (using its key and aliases) against the beginning of the string entered by *caller*. This produces a set of candidates. +10. The *cmd parser* next rates the matches by how many characters they have and how many percent matches the respective known command. Only if candidates cannot be separated will it return multiple matches. + - If multiple matches were returned, resend as `CMD_MULTIMATCH`. If no such command is found in cmdset, return hard-coded list of matches. + - If no match was found, resend as `CMD_NOMATCH`. If no such command is found in cmdset, give hard-coded error message. +11. If a single command was found by the parser, the correct command object is plucked out of storage. This usually doesn't mean a re-initialization. +12. It is checked that the caller actually has access to the command by validating the *lockstring* of the command. If not, it is not considered as a suitable match and `CMD_NOMATCH` is triggered. +13. If the new command is tagged as a channel-command, resend as `CMD_CHANNEL`. If no such command is found in cmdset, use hard-coded implementation. +14. Assign several useful variables to the command instance (see previous sections). +15. Call `at_pre_command()` on the command instance. +16. Call `parse()` on the command instance. This is fed the remainder of the string, after the name of the command. It's intended to pre-parse the string into a form useful for the `func()` method. +17. Call `func()` on the command instance. This is the functional body of the command, actually doing useful things. +18. Call `at_post_command()` on the command instance. + +## Assorted notes + +The return value of `Command.func()` is a Twisted +[deferred](http://twistedmatrix.com/documents/current/core/howto/defer.html). +Evennia does not use this return value at all by default. If you do, you must +thus do so asynchronously, using callbacks. + +```python + # in command class func() + def callback(ret, caller): + caller.msg("Returned is %s" % ret) + deferred = self.execute_command("longrunning") + deferred.addCallback(callback, self.caller) +``` + +This is probably not relevant to any but the most advanced/exotic designs (one might use it to create a "nested" command structure for example). + +The `save_for_next` class variable can be used to implement state-persistent commands. For example it can make a command operate on "it", where it is determined by what the previous command operated on. diff --git a/docs/source/Communications.md b/docs/source/Communications.md new file mode 100644 index 0000000000..96afc62e86 --- /dev/null +++ b/docs/source/Communications.md @@ -0,0 +1,70 @@ +# Communications + + +Apart from moving around in the game world and talking, players might need other forms of communication. This is offered by Evennia's `Comm` system. Stock evennia implements a 'MUX-like' system of channels, but there is nothing stopping you from changing things to better suit your taste. + +Comms rely on two main database objects - `Msg` and `Channel`. There is also the `TempMsg` which mimics the API of a `Msg` but has no connection to the database. + +## Msg + +The `Msg` object is the basic unit of communication in Evennia. A message works a little like an e-mail; it always has a sender (a [Account](Accounts)) and one or more recipients. The recipients may be either other Accounts, or a *Channel* (see below). You can mix recipients to send the message to both Channels and Accounts if you like. + +Once created, a `Msg` is normally not changed. It is peristently saved in the database. This allows for comprehensive logging of communications. This could be useful for allowing senders/receivers to have 'mailboxes' with the messages they want to keep. + +### Properties defined on `Msg` + +- `senders` - this is a reference to one or many [Account](Accounts) or [Objects](Objects) (normally *Characters*) sending the message. This could also be an *External Connection* such as a message coming in over IRC/IMC2 (see below). There is usually only one sender, but the types can also be mixed in any combination. +- `receivers` - a list of target [Accounts](Accounts), [Objects](Objects) (usually *Characters*) or *Channels* to send the message to. The types of receivers can be mixed in any combination. +- `header` - this is a text field for storing a title or header for the message. +- `message` - the actual text being sent. +- `date_sent` - when message was sent (auto-created). +- `locks` - a [lock definition](Locks). +- `hide_from` - this can optionally hold a list of objects, accounts or channels to hide this `Msg` from. This relationship is stored in the database primarily for optimization reasons, allowing for quickly post-filter out messages not intended for a given target. There is no in-game methods for setting this, it's intended to be done in code. + +You create new messages in code using `evennia.create_message` (or `evennia.utils.create.create_message.`) + +## TempMsg + +`evennia.comms.models` also has `TempMsg` which mimics the API of `Msg` but is not connected to the database. TempMsgs are used by Evennia for channel messages by default. They can be used for any system expecting a `Msg` but when you don't actually want to save anything. + +## Channels + +Channels are [Typeclassed](Typeclasses) entities, which mean they can be easily extended and their functionality modified. To change which channel typeclass Evennia uses, change settings.BASE_CHANNEL_TYPECLASS. + +Channels act as generic distributors of messages. Think of them as "switch boards" redistributing `Msg` or `TempMsg` objects. Internally they hold a list of "listening" objects and any `Msg` (or `TempMsg`) sent to the channel will be distributed out to all channel listeners. Channels have [Locks](Locks) to limit who may listen and/or send messages through them. + +The *sending* of text to a channel is handled by a dynamically created [Command](Commands) that always have the same name as the channel. This is created for each channel by the global `ChannelHandler`. The Channel command is added to the Account's cmdset and normal command locks are used to determine which channels are possible to write to. When subscribing to a channel, you can then just write the channel name and the text to send. + +The default ChannelCommand (which can be customized by pointing `settings.CHANNEL_COMMAND_CLASS` to your own command), implements a few convenient features: + + - It only sends `TempMsg` objects. Instead of storing individual entries in the database it instead dumps channel output a file log in `server/logs/channel_.log`. This is mainly for practical reasons - we find one rarely need to query individual Msg objects at a later date. Just stupidly dumping the log to a file also means a lot less database overhead. + - It adds a `/history` switch to view the 20 last messages in the channel. These are read from the end of the log file. One can also supply a line number to start further back in the file (but always 20 entries at a time). It's used like this: + + > public/history + > public/history 35 + + +There are two default channels created in stock Evennia - `MudInfo` and `Public`. `MudInfo` receives server-related messages meant for Admins whereas `Public` is open to everyone to chat on (all new accounts are automatically joined to it when logging in, it is useful for asking questions). The default channels are defined by the `DEFAULT_CHANNELS` list (see `evennia/settings_default.py` for more details). + +You create new channels with `evennia.create_channel` (or `evennia.utils.create.create_channel`). + +In code, messages are sent to a channel using the `msg` or `tempmsg` methods of channels: + + channel.msg(msgobj, header=None, senders=None, persistent=True) + +The argument `msgobj` can be either a string, a previously constructed `Msg` or a `TempMsg` - in the latter cases all the following keywords are ignored since the message objects already contains all this information. If `msgobj` is a string, the other keywords are used for creating a new `Msg` or `TempMsg` on the fly, depending on if `persistent` is set or not. By default, a `TempMsg` is emitted for channel communication (since the default ChannelCommand instead logs to a file). + +```python + # assume we have a 'sender' object and a channel named 'mychan' + + # manually sending a message to a channel + mychan.msg("Hello!", senders=[sender]) +``` + +### Properties defined on `Channel` + +- `key` - main name for channel +- `aliases` - alternative native names for channels +- `desc` - optional description of channel (seen in listings) +- `keep_log` (bool) - if the channel should store messages (default) +- `locks` - A [lock definition](Locks). Channels normally use the access_types `send, control` and `listen`. \ No newline at end of file diff --git a/docs/source/Connection-Screen.md b/docs/source/Connection-Screen.md new file mode 100644 index 0000000000..be8234b82c --- /dev/null +++ b/docs/source/Connection-Screen.md @@ -0,0 +1,35 @@ +# Connection Screen + + +When you first connect to your game you are greeted by Evennia's default connection screen. + + + ============================================================== + Welcome to Evennia, version Beta-ra4d24e8a3cab+! + + If you have an existing account, connect to it by typing: + connect + If you need to create an account, type (without the <>'s): + create + + If you have spaces in your username, enclose it in quotes. + Enter help for more info. look will re-show this screen. + ============================================================== + +Effective, but not very exciting. You will most likely want to change this to be more unique for your game. This is simple: + +1. Edit `mygame/server/conf/connection_screens.py`. +1. [Reload](Start-Stop-Reload) Evennia. + +Evennia will look into this module and locate all *globally defined strings* in it. These strings +are used as the text in your connection screen and are shown to the user at startup. If more than +one such string/screen is defined in the module, a *random* screen will be picked from among those +available. + +### Commands available at the Connection Screen + +You can also customize the [Commands](Commands) available to use while the connection screen is +shown (`connect`, `create` etc). These commands are a bit special since when the screen is running +the account is not yet logged in. A command is made available at the login screen by adding them to +`UnloggedinCmdSet` in `mygame/commands/default_cmdset.py`. See [Commands](Commands) and the +tutorial section on how to add new commands to a default command set. diff --git a/docs/source/Continuous-Integration.md b/docs/source/Continuous-Integration.md new file mode 100644 index 0000000000..c5420d76c3 --- /dev/null +++ b/docs/source/Continuous-Integration.md @@ -0,0 +1,212 @@ +# Continuous Integration + +One of the advantages of Evennia over traditional MUSH development systems is that Evennia is +capable of integrating into enterprise level integration environments and source control. Because of +this, it can also be the subject of automation for additional convenience, allowing a more +streamlined development environment. + +## What is Continuous Integration? + +[Continuous Integration (CI)](https://www.thoughtworks.com/continuous-integration) is a development +practice that requires developers to integrate code into a shared repository several times a day. +Each check-in is then verified by an automated build, allowing teams to detect problems early. + +For Evennia, continuous integration allows an automated build process to: +* Pull down a latest build from Source Control. +* Run migrations on the backing SQL database. +* Automate additional unique tasks for that project. +* Run unit tests. +* Publish those files to the server directory +* Reload the game. + +## Preparation +To prepare a CI environment for your `MU*`, it will be necessary to set up some prerequisite software for your server. + +Among those you will need: +* A Continuous Integration Environment. + * I recommend [TeamCity](https://www.jetbrains.com/teamcity/) which has an in-depth [Setup Guide](https://confluence.jetbrains.com/display/TCD8/Installing+and+Configuring+the+TeamCity+Server) +* [Source Control](https://github.com/evennia/evennia/wiki/Version%20Control) + * This could be Git or SVN or any other available SC. + +## Linux TeamCity Setup +For this part of the guide, an example setup will be provided for administrators running a TeamCity +build integration environment on Linux. + +After meeting the preparation steps for your specific environment, log on to your teamcity interface +at `http://:8111/`. + +Create a new project named "Evennia" and in it construct a new template called continuous-integration. + +### A Quick Overview +Templates are fancy objects in TeamCity that allow an administrator to define build steps that are +shared between one or more build projects. Assigning a VCS Root (Source Control) is unnecessary at +this stage, primarily you'll be worrying about the build steps and your default parameters (both +visible on the tabs to the left.) + +### Template Setup + +In this template, you'll be outlining the steps necessary to build your specific game. (A number of +sample scripts are provided under this section below!) Click Build Steps and prepare your general +flow. For this example, we will be doing a few basic example steps: + +* Transforming the Settings.py file + * We do this to update ports or other information that make your production environment unique + from your development environment. +* Making migrations and migrating the game database. +* Publishing the game files. +* Reloading the server. + +For each step we'll being use the "Command Line Runner" (a fancy name for a shell script executor). + +* Create a build step with the name: Transform Configuration +* For the script add: + + ```bash + #!/bin/bash + # Replaces the game configuration with one + # appropriate for this deployment. + + CONFIG="%system.teamcity.build.checkoutDir%/server/conf/settings.py" + MYCONF="%system.teamcity.build.checkoutDir%/server/conf/my.cnf" + + sed -e 's/TELNET_PORTS = [4000]/TELNET_PORTS = [%game.ports%]/g' "$CONFIG" > "$CONFIG".tmp && mv "$CONFIG".tmp "$CONFIG" + sed -e 's/WEBSERVER_PORTS = [(4001, 4002)]/WEBSERVER_PORTS = [%game.webports%]/g' "$CONFIG" > "$CONFIG".tmp && mv "$CONFIG".tmp "$CONFIG" + + # settings.py MySQL DB configuration + echo Configuring Game Database... + echo "" >> "$CONFIG" + echo "######################################################################" >> "$CONFIG" + echo "# MySQL Database Configuration" >> "$CONFIG" + echo "######################################################################" >> "$CONFIG" + + echo "DATABASES = {" >> "$CONFIG" + echo " 'default': {" >> "$CONFIG" + echo " 'ENGINE': 'django.db.backends.mysql'," >> "$CONFIG" + echo " 'OPTIONS': {" >> "$CONFIG" + echo " 'read_default_file': 'server/conf/my.cnf'," >> "$CONFIG" + echo " }," >> "$CONFIG" + echo " }" >> "$CONFIG" + echo "}" >> "$CONFIG" + + # Create the My.CNF file. + echo "[client]" >> "$MYCONF" + echo "database = %mysql.db%" >> "$MYCONF" + echo "user = %mysql.user%" >> "$MYCONF" + echo "password = %mysql.pass%" >> "$MYCONF" + echo "default-character-set = utf8" >> "$MYCONF" + ``` + +If you look at the parameters side of the page after saving this script, you'll notice that some new +parameters have been populated for you. This is because we've included new teamcity configuration +parameters that are populated when the build itself is ran. When creating projects that inherit this +template, we'll be able to fill in or override those parameters for project-specific configuration. + +* Go ahead and create another build step called "Make Database Migration" + * If you're using SQLLite on your game, it will be prudent to change working directory on this step to: %game.dir% +* In this script include: + + ```bash + #!/bin/bash + # Update the DB migration + + LOGDIR="server/logs" + + . %evenv.dir%/bin/activate + + # Check that the logs directory exists. + if [ ! -d "$LOGDIR" ]; then + # Control will enter here if $LOGDIR doesn't exist. + mkdir "$LOGDIR" + fi + + evennia makemigrations + ``` + +* Create yet another build step, this time named: "Execute Database Migration": + * If you're using SQLLite on your game, it will be prudent to change working directory on this step to: %game.dir% + ```bash + #!/bin/bash + # Apply the database migration. + + LOGDIR="server/logs" + + . %evenv.dir%/bin/activate + + # Check that the logs directory exists. + if [ ! -d "$LOGDIR" ]; then + # Control will enter here if $LOGDIR doesn't exist. + mkdir "$LOGDIR" + fi + + evennia migrate + + ``` + +Our next build step is where we actually publish our build. Up until now, all work on game has been +done in a 'work' directory on TeamCity's build agent. From that directory we will now copy our files +to where our game actually exists on the local server. + +* Create a new build step called "Publish Build": + * If you're using SQLLite on your game, be sure to order this step ABOVE the Database Migration steps. The build order will matter! + ```bash + #!/bin/bash + # Publishes the build to the proper build directory. + + DIRECTORY="%game.dir%" + + if [ ! -d "$DIRECTORY" ]; then + # Control will enter here if $DIRECTORY doesn't exist. + mkdir "$DIRECTORY" + fi + + # Copy all the files. + cp -ruv %teamcity.build.checkoutDir%/* "$DIRECTORY" + chmod -R 775 "$DIRECTORY" + + ``` + +Finally the last script will reload our game for us. + +* Create a new script called "Reload Game": + * The working directory on this build step will be: %game.dir% + ```bash + #!/bin/bash + # Apply the database migration. + + LOGDIR="server/logs" + PIDDIR="server/server.pid" + + . %evenv.dir%/bin/activate + + # Check that the logs directory exists. + if [ ! -d "$LOGDIR" ]; then + # Control will enter here if $LOGDIR doesn't exist. + mkdir "$LOGDIR" + fi + + # Check that the server is running. + if [ -d "$PIDDIR" ]; then + # Control will enter here if the game is running. + evennia reload + fi + ``` + +Now the template is ready for use! It would be useful this time to revisit the parameters page and +set the evenv parameter to the directory where your virtualenv exists: IE "/srv/mush/evenv". + +### Creating the Project + +Now it's time for the last few steps to set up a CI environment. + +* Return to the Evennia Project overview/administration page. +* Create a new Sub-Project called "Production" + * This will be the category that holds our actual game. +* Create a new Build Configuration in Production with the name of your MUSH. + * Base this configuration off of the continuous-integration template we made earlier. +* In the build configuration, enter VCS roots and create a new VCS root that points to the branch/version control that you are using. +* Go to the parameters page and fill in the undefined parameters for your specific configuration. +* If you wish for the CI to run every time a commit is made, go to the VCS triggers and add one for "On Every Commit". + +And you're done! At this point, you can return to the project overview page and queue a new build +for your game. If everything was set up correctly, the build will complete successfully. Additional +build steps could be added or removed at this point, adding some features like Unit Testing or more! diff --git a/docs/source/Contributing.md b/docs/source/Contributing.md new file mode 100644 index 0000000000..2ac3fea3f2 --- /dev/null +++ b/docs/source/Contributing.md @@ -0,0 +1,87 @@ +# Contributing + + +Wanna help out? Great! Here's how. + +## Spreading the word + +Even if you are not keen on working on the server code yourself, just spreading the word is a big +help - it will help attract more people which leads to more feedback, motivation and interest. +Consider writing about Evennia on your blog or in your favorite (relevant) forum. Write a review +somewhere (good or bad, we like feedback either way). Rate it on places like [ohloh][ohloh]. Talk +about it to your friends ... that kind of thing. + +## Donations + +The best way to support Evennia is to become an [Evennia patron][patron]. Evennia is a free, +open-source project and any monetary donations you want to offer are completely voluntary. See it as +a way of announcing that you appreciate the work done - a tip of the hat! A patron donates a +(usually small) sum every month to show continued support. If this is not your thing you can also +show your appreciation via a [one-time donation][donate] (this is a PayPal link but you don't need +PayPal yourself). + +## Help with Documentation + +Evennia depends heavily on good documentation and we are always looking for extra eyes and hands to +improve it. Even small things such as fixing typos are a great help! + +The documentation is a wiki and as long as you have a GitHub account you can edit it. It can be a +good idea to discuss in the chat or forums if you want to add new pages/tutorials. Otherwise, it +goes a long way just pointing out wiki errors so we can fix them (in an Issue or just over +chat/forum). + +## Contributing through a forked repository + +We always need more eyes and hands on the code. Even if you don't feel confident with tackling a +[bug or feature][issues], just correcting typos, adjusting formatting or simply *using* the thing +and reporting when stuff doesn't make sense helps us a lot. + +The most elegant way to contribute code to Evennia is to use GitHub to create a *fork* of the +Evennia repository and make your changes to that. Refer to the [Forking Evennia][forking] version +control instructions for detailed instructions. + +Once you have a fork set up, you can not only work on your own game in a separate branch, you can +also commit your fixes to Evennia itself. Make separate branches for all Evennia additions you do - +don't edit your local `master` or `develop` branches directly. It will make your life a lot easier. +If you have a change that you think is suitable for the main Evennia repository, you issue a [Pull +Request][pullrequest]. This will let Evennia devs know you have stuff to share. Bug fixes should +generally be done against the `master` branch of Evennia, while new features/contribs should go into +the `develop` branch. If you are unsure, just pick one and we'll figure it out. + +## Contributing with Patches + +To help with Evennia development it's recommended to do so using a fork repository as described +above. But for small, well isolated fixes you are also welcome to submit your suggested Evennia +fixes/addendums as a [patch][patch]. + +You can include your patch in an Issue or a Mailing list post. Please avoid pasting the full patch +text directly in your post though, best is to use a site like [Pastebin](http://pastebin.com/) and +just supply the link. + +## Contributing with Contribs + +While Evennia's core is pretty much game-agnostic, it also has a `contrib/` directory. The `contrib` +directory contains game systems that are specialized or useful only to certain types of games. Users +are welcome to contribute to the `contrib/` directory. Such contributions should always happen via a +Forked repository as described above. + +* If you are unsure if your idea/code is suitable as a contrib, *ask the devs before putting any work into it*. This can also be a good idea in order to not duplicate efforts. This can also act as a check that your implementation idea is sound. We are, for example, unlikely to accept contribs that require large modifications of the game directory structure. +* If your code is intended *primarily* as an example or shows a concept/principle rather than a working system, it is probably not suitable for `contrib/`. You are instead welcome to use it as part of a [new tutorial][tutorials]! +* The code should ideally be contained within a single Python module. But if the contribution is large this may not be practical and it should instead be grouped in its own subdirectory (not as loose modules). +* The contribution should preferably be isolated (only make use of core Evennia) so it can easily be dropped into use. If it does depend on other contribs or third-party modules, these must be clearly documented and part of the installation instructions. +* The code itself should follow Evennia's [Code style guidelines][codestyle]. +* The code must be well documented as described in our [documentation style guide](https://github.com/evennia/evennia/blob/master/CODING_STYLE.md#doc-strings). Expect that your code will be read and should be possible to understand by others. Include comments as well as a header in all modules. If a single file, the header should include info about how to include the contrib in a game (installation instructions). If stored in a subdirectory, this info should go into a new `README.md` file within that directory. +* Within reason, your contribution should be designed as genre-agnostic as possible. Limit the amount of game-style-specific code. Assume your code will be applied to a very different game than you had in mind when creating it. +* To make the licensing situation clear we assume all contributions are released with the same [license as Evennia](Licensing). If this is not possible for some reason, talk to us and we'll handle it on a case-by-case basis. +* Your contribution must be covered by [unit tests](Unit-Testing). Having unit tests will both help make your code more stable and make sure small changes does not break it without it being noticed, it will also help us test its functionality and merge it quicker. If your contribution is a single module, you can add your unit tests to `evennia/contribs/tests.py`. If your contribution is bigger and in its own sub-directory you could just put the tests in your own `tests.py` file (Evennia will find it automatically). +* Merging of your code into Evennia is not guaranteed. Be ready to receive feedback and to be asked to make corrections or fix bugs. Furthermore, merging a contrib means the Evennia project takes on the responsibility of maintaining and supporting it. For various reasons this may be deemed to be beyond our manpower. However, if your code were to *not* be accepted for merger for some reason, we will instead add a link to your online repository so people can still find and use your work if they want. + +[ohloh]: http://www.ohloh.net/p/evennia +[patron]: https://www.patreon.com/griatch +[donate]: https://www.paypal.com/en/cgi-bin/webscr?cmd=_flow&SESSION=TWy_epDPSWqNr4UJCOtVWxl-pO1X1jbKiv_-UBBFWIuVDEZxC0M_2pM6ywO&dispatch=5885d80a13c0db1f8e263663d3faee8d66f31424b43e9a70645c907a6cbd8fb4 +[forking]: https://github.com/evennia/evennia/wiki/Version-Control#wiki-forking-from-evennia +[pullrequest]: https://github.com/evennia/evennia/pulls +[issues]: https://github.com/evennia/evennia/issues +[patch]: https://secure.wikimedia.org/wikipedia/en/wiki/Patch_%28computing%29 +[codestyle]: https://github.com/evennia/evennia/blob/master/CODING_STYLE.md +[tutorials]: https://github.com/evennia/evennia/wiki/Tutorials diff --git a/docs/source/Coordinates.md b/docs/source/Coordinates.md new file mode 100644 index 0000000000..0c0a3a2657 --- /dev/null +++ b/docs/source/Coordinates.md @@ -0,0 +1,334 @@ +# Coordinates + +# Adding room coordinates in your game + +This tutorial is moderately difficult in content. You might want to be familiar and at ease with +some Python concepts (like properties) and possibly Django concepts (like queries), although this +tutorial will try to walk you through the process and give enough explanations each time. If you don't feel very confident with math, don't hesitate to pause, go to the example section, which shows a tiny map, and try to walk around the code or read the explanation. + +Evennia doesn't have a coordinate system by default. Rooms and other objects are linked by location +and content: + +- An object can be in a location, that is, another object. Like an exit in a room. +- An object can access its content. A room can see what objects uses it as location (that would + include exits, rooms, characters and so on). + +This system allows for a lot of flexibility and, fortunately, can be extended by other systems. +Here, I offer you a way to add coordinates to every room in a way most compliant with Evennia +design. This will also show you how to use coordinates, find rooms around a given point for +instance. + +## Coordinates as tags + +The first concept might be the most surprising at first glance: we will create coordinates as +[tags](Tags). + +> Why not attributes, wouldn't that be easier? + +It would. We could just do something like `room.db.x = 3`. The advantage of using tags is that it +will be easy and effective to search. Although this might not seem like a huge advantage right now, +with a database of thousands of rooms, it might make a difference, particularly if you have a lot of +things based on coordinates. + +Rather than giving you a step-by-step process, I'll show you the code. Notice that we use +properties to easily access and update coordinates. This is a Pythonic approach. Here's our first +`Room` class, that you can modify in `typeclasses/rooms.py`: + +```python +# in typeclasses/rooms.py + +from evennia import DefaultRoom + +class Room(DefaultRoom): + """ + Rooms are like any Object, except their location is None + (which is default). They also use basetype_setup() to + add locks so they cannot be puppeted or picked up. + (to change that, use at_object_creation instead) + + See examples/object.py for a list of + properties and methods available on all Objects. + """ + + @property + def x(self): + """Return the X coordinate or None.""" + x = self.tags.get(category="coordx") + return int(x) if isinstance(x, str) else None + + @x.setter + def x(self, x): + """Change the X coordinate.""" + old = self.tags.get(category="coordx") + if old is not None: + self.tags.remove(old, category="coordx") + if x is not None: + self.tags.add(str(x), category="coordx") + + @property + def y(self): + """Return the Y coordinate or None.""" + y = self.tags.get(category="coordy") + return int(y) if isinstance(y, str) else None + + @y.setter + def y(self, y): + """Change the Y coordinate.""" + old = self.tags.get(category="coordy") + if old is not None: + self.tags.remove(old, category="coordy") + if y is not None: + self.tags.add(str(y), category="coordy") + + @property + def z(self): + """Return the Z coordinate or None.""" + z = self.tags.get(category="coordz") + return int(z) if isinstance(z, str) else None + + @z.setter + def z(self, z): + """Change the Z coordinate.""" + old = self.tags.get(category="coordz") + if old is not None: + self.tags.remove(old, category="coordz") + if z is not None: + self.tags.add(str(z), category="coordz") +``` + +If you aren't familiar with the concept of properties in Python, I encourage you to read a good +tutorial on the subject. [This article on Python properties](https://www.programiz.com/python-programming/property) +is well-explained and should help you understand the idea. + +Let's look at our properties for `x`. First of all is the read property. + +```python + @property + def x(self): + """Return the X coordinate or None.""" + x = self.tags.get(category="coordx") + return int(x) if isinstance(x, str) else None +``` + +What it does is pretty simple: + +1. It gets the tag of category `"coordx"`. It's the tag category where we store our X coordinate. + The `tags.get` method will return `None` if the tag can't be found. +2. We convert the value to an integer, if it's a `str`. Remember that tags can only contain `str`, + so we'll need to convert it. + +> I thought tags couldn't contain values? + +Well, technically, they can't: they're either here or not. But using tag categories, as we have +done, we get a tag, knowing only its category. That's the basic approach to coordinates in this +tutorial. + +Now, let's look at the method that will be called when we wish to set `x` in our room: + +```python + @x.setter + def x(self, x): + """Change the X coordinate.""" + old = self.tags.get(category="coordx") + if old is not None: + self.tags.remove(old, category="coordx") + if x is not None: + self.tags.add(str(x), category="coordx") +``` + +1. First, we remove the old X coordinate, if it exists. Otherwise, we'd end up with two tags in our + room with "coordx" as their category, which wouldn't do at all. +2. Then we add the new tag, giving it the proper category. + +> Now what? + +If you add this code and reload your game, once you're logged in with a character in a room as its +location, you can play around: + +``` +@py here.x +@py here.x = 0 +@py here.y = 3 +@py here.z = -2 +@py here.z = None +``` + +The code might not be that easy to read, but you have to admit it's fairly easy to use. + +## Some additional searches + +Having coordinates is useful for several reasons: + +1. It can help in shaping a truly logical world, in its geography, at least. +2. It can allow to look for specific rooms at given coordinates. +3. It can be good in order to quickly find the rooms around a location. +4. It can even be great in path-finding (finding the shortest path between two rooms). + +So far, our coordinate system can help with 1., but not much else. Here are some methods that we +could add to the `Room` typeclass. These methods will just be search methods. Notice that they are +class methods, since we want to get rooms. + +### Finding one room + +First, a simple one: how to find a room at a given coordinate? Say, what is the room at X=0, Y=0, Z=0? + +```python +class Room(DefaultRoom): + # ... + @classmethod + def get_room_at(cls, x, y, z): + """ + Return the room at the given location or None if not found. + + Args: + x (int): the X coord. + y (int): the Y coord. + z (int): the Z coord. + + Return: + The room at this location (Room) or None if not found. + + """ + rooms = cls.objects.filter( + db_tags__db_key=str(x), db_tags__db_category="coordx").filter( + db_tags__db_key=str(y), db_tags__db_category="coordy").filter( + db_tags__db_key=str(z), db_tags__db_category="coordz") + if rooms: + return rooms[0] + + return None +``` + +This solution includes a bit of [Django queries](https://docs.djangoproject.com/en/1.11/topics/db/queries/). +Basically, what we do is reach for the object manager and search for objects with the matching tags. +Again, don't spend too much time worrying about the mechanism, the method is quite easy to use: + +``` +Room.get_room_at(5, 2, -3) +``` + +Notice that this is a class method: you will call it from `Room` (the class), not an instance. Though you still can: + + @py here.get_room_at(3, 8, 0) + +### Finding several rooms + +Here's another useful method that allows us to look for rooms around a given coordinate. This is +more advanced search and doing some calculation, beware! Look at the following section if you're lost. + +```python +from math import sqrt + +class Room(DefaultRoom): + + # ... + + @classmethod + def get_rooms_around(cls, x, y, z, distance): + """ + Return the list of rooms around the given coordinates. + + This method returns a list of tuples (distance, room) that + can easily be browsed. This list is sorted by distance (the + closest room to the specified position is always at the top + of the list). + + Args: + x (int): the X coord. + y (int): the Y coord. + z (int): the Z coord. + distance (int): the maximum distance to the specified position. + + Returns: + A list of tuples containing the distance to the specified + position and the room at this distance. Several rooms + can be at equal distance from the position. + + """ + # Performs a quick search to only get rooms in a square + x_r = list(reversed([str(x - i) for i in range(0, distance + 1)])) + x_r += [str(x + i) for i in range(1, distance + 1)] + y_r = list(reversed([str(y - i) for i in range(0, distance + 1)])) + y_r += [str(y + i) for i in range(1, distance + 1)] + z_r = list(reversed([str(z - i) for i in range(0, distance + 1)])) + z_r += [str(z + i) for i in range(1, distance + 1)] + wide = cls.objects.filter( + db_tags__db_key__in=x_r, db_tags__db_category="coordx").filter( + db_tags__db_key__in=y_r, db_tags__db_category="coordy").filter( + db_tags__db_key__in=z_r, db_tags__db_category="coordz") + + # We now need to filter down this list to find out whether + # these rooms are really close enough, and at what distance + # In short: we change the square to a circle. + rooms = [] + for room in wide: + x2 = int(room.tags.get(category="coordx")) + y2 = int(room.tags.get(category="coordy")) + z2 = int(room.tags.get(category="coordz")) + distance_to_room = sqrt( + (x2 - x) ** 2 + (y2 - y) ** 2 + (z2 - z) ** 2) + if distance_to_room <= distance: + rooms.append((distance_to_room, room)) + + # Finally sort the rooms by distance + rooms.sort(key=lambda tup: tup[0]) + return rooms +``` + +This gets more serious. + +1. We have specified coordinates as parameters. We determine a broad range using the distance. + That is, for each coordinate, we create a list of possible matches. See the example below. +2. We then search for the rooms within this broader range. It gives us a square + around our location. Some rooms are definitely outside the range. Again, see the example below to follow the logic. +3. We filter down the list and sort it by distance from the specified coordinates. + +Notice that we only search starting at step 2. Thus, the Django search doesn't look and cache all +objects, just a wider range than what would be really necessary. This method returns a circle of +coordinates around a specified point. Django looks for a square. What wouldn't fit in the circle +is removed at step 3, which is the only part that includes systematic calculation. This method is +optimized to be quick and efficient. + +### An example + +An example might help. Consider this very simple map (a textual description follows): + +``` +4 A B C D +3 E F G H +2 I J K L +1 M N O P + 1 2 3 4 +``` + +The X coordinates are given below. The Y coordinates are given on the left. This is a simple square with 16 rooms: 4 on each line, 4 lines of them. All the rooms are identified by letters in this example: the first line at the top has rooms A to D, the second E to H, the third I to L and the fourth M to P. The bottom-left room, X=1 and Y=1, is M. The upper-right room X=4 and Y=4 is D. + +So let's say we want to find all the neighbors, distance 1, from the room J. J is at X=2, Y=2. + +So we use: + + Room.get_rooms_around(x=2, y=2, z=0, distance=1) + # we'll assume a z coordinate of 0 for simplicity + +1. First, this method gets all the rooms in a square around J. So it gets E F G, I J K, M N O. If you want, draw the square around these coordinates to see what's happening. +2. Next, we browse over this list and check the real distance between J (X=2, Y=2) and the room. The four corners of the square are not in this circle. For instance, the distance between J and M is not 1. If you draw a circle of center J and radius 1, you'll notice that the four corners of our square (E, G, M and O) are not in this circle. So we remove them. +3. We sort by distance from J. + +So in the end we might obtain something like this: + +``` +[ + (0, J), # yes, J is part of this circle after all, with a distance of 0 + (1, F), + (1, I), + (1, K), + (1, N), +] +``` + +You can try with more examples if you want to see this in action. + +### To conclude + +You can definitely use this system to map other objects, not just rooms. You can easily remove the +`Z coordinate too, if you simply need X and Y. diff --git a/docs/source/Custom-Protocols.md b/docs/source/Custom-Protocols.md new file mode 100644 index 0000000000..ef16c1207c --- /dev/null +++ b/docs/source/Custom-Protocols.md @@ -0,0 +1,222 @@ +# Custom Protocols + + +*Note: This is considered an advanced topic and is mostly of interest to users planning to implement +their own custom client protocol.* + + +A [PortalSession](Portal-and-Server-Sessions) is the basic data object representing an external +connection to the Evennia [Portal](Portal-And-Server) -- usually a human player running a mud client +of some kind. The way they connect (the language the player's client and Evennia use to talk to +each other) is called the connection *Protocol*. The most common such protocol for MUD:s is the +*Telnet* protocol. All Portal Sessions are stored and managed by the Portal's *sessionhandler*. + +It's technically sometimes hard to separate the concept of *PortalSession* from the concept of +*Protocol* since both depend heavily on the other (they are often created as the same class). When data flows through this part of the system, this is how it goes + +``` +# In the Portal +You <-> + Protocol + PortalSession <-> + PortalSessionHandler <-> + (AMP) <-> + ServerSessionHandler <-> + ServerSession <-> + InputFunc +``` + +(See the [Message Path](Messagepath) for the bigger picture of how data flows through Evennia). The parts that needs to be customized to make your own custom protocol is the `Protocol + PortalSession` (which translates between data coming in/out over the wire to/from Evennia internal representation) as well as the `InputFunc` (which handles incoming data). + +## Adding custom Protocols + +Evennia has a plugin-system that add the protocol as a new "service" to the application. + +Take a look at `evennia/server/portal/portal.py`, notably the sections towards the end of that file. +These are where the various in-built services like telnet, ssh, webclient etc are added to the +Portal (there is an equivalent but shorter list in `evennia/server/server.py`). + +To add a new service of your own (for example your own custom client protocol) to the Portal or +Server, look at `mygame/server/conf/server_services_plugins` and `portal_services_plugins`. By +default Evennia will look into these modules to find plugins. If you wanted to have it look for more +modules, you could do the following: + +```python + # add to the Server + SERVER_SERVICES_PLUGIN_MODULES.append('server.conf.my_server_plugins') + # or, if you want to add to the Portal + PORTAL_SERVICES_PLUGIN_MODULES.append('server.conf.my_portal_plugins') +``` + +When adding a new connection you'll most likely only need to add new things to the `PORTAL_SERVICES_PLUGIN_MODULES`. + +This module can contain whatever you need to define your protocol, but it *must* contain a function +`start_plugin_services(app)`. This is called by the Portal as part of its upstart. The function +`start_plugin_services` must contain all startup code the server need. The `app` argument is a +reference to the Portal/Server application itself so the custom service can be added to it. The +function should not return anything. + +This is how it looks: + +```python + # mygame/server/conf/portal_services_plugins.py + + # here the new Portal Twisted protocol is defined + class MyOwnFactory( ... ): + [...] + + # some configs + MYPROC_ENABLED = True # convenient off-flag to avoid having to edit settings all the time + MY_PORT = 6666 + + def start_plugin_services(portal): + "This is called by the Portal during startup" + if not MYPROC_ENABLED: + return + # output to list this with the other services at startup + print(" myproc: %s" % MY_PORT) + + # some setup (simple example) + factory = MyOwnFactory() + my_service = internet.TCPServer(MY_PORT, factory) + # all Evennia services must be uniquely named + my_service.setName("MyService") + # add to the main portal application + portal.services.addService(my_service) +``` + +Once the module is defined and targeted in settings, just reload the server and your new +protocol/services should start with the others. + +## Writing your own Protocol + +Writing a stable communication protocol from scratch is not something we'll cover here, it's no +trivial task. The good news is that Twisted offers implementations of many common protocols, ready +for adapting. + +Writing a protocol implementation in Twisted usually involves creating a class inheriting from an already existing Twisted protocol class and from `evennia.server.session.Session` (multiple inheritance), then overloading the methods that particular protocol uses to link them to the Evennia-specific inputs. + +Here's a example to show the concept: + +```python +# In module that we'll later add to the system through PORTAL_SERVICE_PLUGIN_MODULES + +# pseudo code +from twisted.something import TwistedClient +# this class is used both for Portal- and Server Sessions +from evennia.server.session import Session + +from evennia.server.portal.portalsessionhandler import PORTAL_SESSIONS + +class MyCustomClient(TwistedClient, Session): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.sessionhandler = PORTAL_SESSIONS + + # these are methods we must know that TwistedClient uses for + # communication. Name and arguments could vary for different Twisted protocols + def onOpen(self, *args, **kwargs): + # let's say this is called when the client first connects + + # we need to init the session and connect to the sessionhandler. The .factory + # is available through the Twisted parents + + client_address = self.getClientAddress() # get client address somehow + + self.init_session("mycustom_protocol", client_address, self.factory.sessionhandler) + self.sessionhandler.connect(self) + + def onClose(self, reason, *args, **kwargs): + # called when the client connection is dropped + # link to the Evennia equivalent + self.disconnect(reason) + + def onMessage(self, indata, *args, **kwargs): + # called with incoming data + # convert as needed here + self.data_in(data=indata) + + def sendMessage(self, outdata, *args, **kwargs): + # called to send data out + # modify if needed + super().sendMessage(self, outdata, *args, **kwargs) + + # these are Evennia methods. They must all exist and look exactly like this + # The above twisted-methods call them and vice-versa. This connects the protocol + # the Evennia internals. + + def disconnect(self, reason=None): + """ + Called when connection closes. + This can also be called directly by Evennia when manually closing the connection. + Do any cleanups here. + """ + self.sessionhandler.disconnect(self) + + def at_login(self): + """ + Called when this session authenticates by the server (if applicable) + """ + + def data_in(self, **kwargs): + """ + Data going into the server should go through this method. It + should pass data into `sessionhandler.data_in`. THis will be called + by the sessionhandler with the data it gets from the approrpriate + send_* method found later in this protocol. + """ + self.sessionhandler.data_in(self, text=kwargs['data']) + + def data_out(self, **kwargs): + """ + Data going out from the server should go through this method. It should + hand off to the protocol's send method, whatever it's called. + """ + # we assume we have a 'text' outputfunc + self.onMessage(kwargs['text']) + + # 'outputfuncs' are defined as `send_`. From in-code, they are called + # with `msg(outfunc_name=)`. + + def send_text(self, txt, *args, **kwargs): + """ + Send text, used with e.g. `session.msg(text="foo")` + """ + # we make use of the + self.data_out(text=txt) + + def send_default(self, cmdname, *args, **kwargs): + """ + Handles all outputfuncs without an explicit `send_*` method to handle them. + """ + self.data_out(**{cmdname: str(args)}) + +``` +The principle here is that the Twisted-specific methods are overridden to redirect inputs/outputs to the Evennia-specific methods. + +### Sending data out + +To send data out through this protocol, you'd need to get its Session and then you could e.g. + +```python + session.msg(text="foo") +``` + +The message will pass through the system such that the sessionhandler will dig out the session and check if it has a `send_text` method (it has). It will then pass the "foo" into that method, which in our case means sending "foo" across the network. + +### Receiving data + +Just because the protocol is there, does not mean Evennia knows what to do with it. An [Inputfunc](Inputfunc) must exist to receive it. In the case of the `text` input exemplified above, Evennia alredy handles this input - it will parse it as a Command name followed by its inputs. So handle that you need to simply add a cmdset with commands on your receiving Session (and/or the Object/Character it is puppeting). If not you may need to add your own Inputfunc (see the [Inputfunc](Inputfunc) page for how to do this. + +These might not be as clear-cut in all protocols, but the principle is there. These four basic +components - however they are accessed - links to the *Portal Session*, which is the actual common +interface between the different low-level protocols and Evennia. + +## Assorted notes + +To take two examples, Evennia supports the *telnet* protocol as well as *webclient*, via ajax or +websockets. You'll find that whereas telnet is a textbook example of a Twisted protocol as seen +above, the ajax protocol looks quite different due to how it interacts with the +webserver through long-polling (comet) style requests. All the necessary parts +mentioned above are still there, but by necessity implemented in very different +ways. diff --git a/docs/source/Customize-channels.md b/docs/source/Customize-channels.md new file mode 100644 index 0000000000..fd35ba71ab --- /dev/null +++ b/docs/source/Customize-channels.md @@ -0,0 +1,470 @@ +# Customize channels + + +# Channel commands in Evennia + +By default, Evennia's default channel commands are inspired by MUX. They all +begin with "c" followed by the action to perform (like "ccreate" or "cdesc"). +If this default seems strange to you compared to other Evennia commands that +rely on switches, you might want to check this tutorial out. + +This tutorial will also give you insight into the workings of the channel system. +So it may be useful even if you don't plan to make the exact changes shown here. + +## What we will try to do + +Our mission: change the default channel commands to have a different syntax. + +This tutorial will do the following changes: + +- Remove all the default commands to handle channels. +- Add a `+` and `-` command to join and leave a channel. So, assuming there is +a `public` channel on your game (most often the case), you could type `+public` +to join it and `-public` to leave it. +- Group the commands to manipulate channels under the channel name, after a +switch. For instance, instead of writing `cdesc public = My public channel`, + you would write `public/desc My public channel`. + + +> I listed removing the default Evennia commands as a first step in the +> process. Actually, we'll move it at the very bottom of the list, since we +> still want to use them, we might get it wrong and rely on Evennia commands +> for a while longer. + +## A command to join, another to leave + +We'll do the most simple task at first: create two commands, one to join a +channel, one to leave. + +> Why not have them as switches? `public/join` and `public/leave` for instance? + +For security reasons, I will hide channels to which the caller is not +connected. It means that if the caller is not connected to the "public" +channel, he won't be able to use the "public" command. This is somewhat +standard: if we create an administrator-only channel, we don't want players to +try (or even know) the channel command. Again, you could design it a different +way should you want to. + +First create a file named `comms.py` in your `commands` package. It's +a rather logical place, since we'll write different commands to handle +communication. + +Okay, let's add the first command to join a channel: + +```python +# in commands/comms.py +from evennia.utils.search import search_channel +from commands.command import Command + +class CmdConnect(Command): + """ + Connect to a channel. + """ + + key = "+" + help_category = "Comms" + locks = "cmd:not pperm(channel_banned)" + auto_help = False + + def func(self): + """Implement the command""" + caller = self.caller + args = self.args + if not args: + self.msg("Which channel do you want to connect to?") + return + + channelname = self.args + channel = search_channel(channelname) + if not channel: + return + + # Check permissions + if not channel.access(caller, 'listen'): + self.msg("%s: You are not allowed to listen to this channel." % channel.key) + return + + # If not connected to the channel, try to connect + if not channel.has_connection(caller): + if not channel.connect(caller): + self.msg("%s: You are not allowed to join this channel." % channel.key) + return + else: + self.msg("You now are connected to the %s channel. " % channel.key.lower()) + else: + self.msg("You already are connected to the %s channel. " % channel.key.lower()) +``` + +Okay, let's review this code, but if you're used to Evennia commands, it shouldn't be too strange: + +1. We import `search_channel`. This is a little helper function that we will use to search for channels by name and aliases, found in `evennia.utils.search`. It's just more convenient. +2. Our class `CmdConnect` contains the body of our command to join a channel. +3. Notice the key of this command is simply `"+"`. When you enter `+something` in the game, it will try to find a command key `+something`. Failing that, it will look at other potential matches. Evennia is smart enough to understand that when we type `+something`, `+` is the command key and `something` is the command argument. This will, of course, fail if you have a command beginning by `+` conflicting with the `CmdConnect` key. +4. We have altered some class attributes, like `auto_help`. If you want to know what they do and why they have changed here, you can check the [documentation on commands](https://github.com/evennia/evennia/wiki/Commands). +5. In the command body, we begin by extracting the channel name. Remember that this name should be in the command arguments (that is, in `self.args`). Following the same example, if a player enters `+something`, `self.args` should contain `"something"`. We use `search_channel` to see if this channel exists. +6. We then check the access level of the channel, to see if the caller can listen to it (not necessarily use it to speak, mind you, just listen to others speak, as these are two different locks on Evennia). +7. Finally, we connect the caller if he's not already connected to the channel. We use the channel's `connect` method to do this. Pretty straightforward eh? + +Now we'll add a command to leave a channel. It's almost the same, turned upside down: + +```python +class CmdDisconnect(Command): + """ + Disconnect from a channel. + """ + + key = "-" + help_category = "Comms" + locks = "cmd:not pperm(channel_banned)" + auto_help = False + + def func(self): + """Implement the command""" + caller = self.caller + args = self.args + if not args: + self.msg("Which channel do you want to disconnect from?") + return + + channelname = self.args + channel = search_channel(channelname) + if not channel: + return + + # If connected to the channel, try to disconnect + if channel.has_connection(caller): + if not channel.disconnect(caller): + self.msg("%s: You are not allowed to disconnect from this channel." % channel.key) + return + else: + self.msg("You stop listening to the %s channel. " % channel.key.lower()) + else: + self.msg("You are not connected to the %s channel. " % channel.key.lower()) +``` + +So far, you shouldn't have trouble following what this command does: it's +pretty much the same as the `CmdConnect` class in logic, though it accomplishes +the opposite. If you are connected to the channel `public` you could +disconnect from it using `-public`. Remember, you can use channel aliases too +(`+pub` and `-pub` will also work, assuming you have the alias `pub` on the + `public` channel). + +It's time to test this code, and to do so, you will need to add these two +commands. Here is a good time to say it: by default, Evennia connects accounts +to channels. Some other games (usually with a higher multisession mode) will +want to connect characters instead of accounts, so that several characters in +the same account can be connected to various channels. You can definitely add +these commands either in the `AccountCmdSet` or `CharacterCmdSet`, the caller +will be different and the command will add or remove accounts of characters. +If you decide to install these commands on the `CharacterCmdSet`, you might +have to disconnect your superuser account (account #1) from the channel before +joining it with your characters, as Evennia tends to subscribe all accounts +automatically if you don't tell it otherwise. + +So here's an example of how to add these commands into your `AccountCmdSet`. +Edit the file `commands/default_cmdsets.py` to change a few things: + +```python +# In commands/default_cmdsets.py +from evennia import default_cmds +from commands.comms import CmdConnect, CmdDisconnect + + +# ... Skip to the AccountCmdSet class ... + +class AccountCmdSet(default_cmds.AccountCmdSet): + """ + This is the cmdset available to the Account at all times. It is + combined with the `CharacterCmdSet` when the Account puppets a + Character. It holds game-account-specific commands, channel + commands, etc. + """ + key = "DefaultAccount" + + def at_cmdset_creation(self): + """ + Populates the cmdset + """ + super().at_cmdset_creation() + + # Channel commands + self.add(CmdConnect()) + self.add(CmdDisconnect()) +``` + +Save, reload your game, and you should be able to use `+public` and `-public` +now! + +## A generic channel command with switches + +It's time to dive a little deeper into channel processing. What happens in +Evennia when a player enters `public Hello everybody!`? + +Like exits, channels are a particular command that Evennia automatically +creates and attaches to individual channels. So when you enter `public +message` in your game, Evennia calls the `public` command. + +> But I didn't add any public command... + +Evennia will just create these commands automatically based on the existing +channels. The base command is the command we'll need to edit. + +> Why edit it? It works just fine to talk. + +Unfortunately, if we want to add switches to our channel names, we'll have to +edit this command. It's not too hard, however, we'll just start writing a +standard command with minor twitches. + +### Some additional imports + +You'll need to add a line of import in your `commands/comms.py` file. We'll +see why this import is important when diving in the command itself: + +```python +from evennia.comms.models import ChannelDB +``` + +### The class layout + +```python +# In commands/comms.py +class ChannelCommand(Command): + """ + {channelkey} channel + + {channeldesc} + + Usage: + {lower_channelkey} + {lower_channelkey}/history [start] + {lower_channelkey}/me + {lower_channelkey}/who + + Switch: + history: View 20 previous messages, either from the end or + from number of messages from the end. + me: Perform an emote on this channel. + who: View who is connected to this channel. + + Example: + {lower_channelkey} Hello World! + {lower_channelkey}/history + {lower_channelkey}/history 30 + {lower_channelkey}/me grins. + {lower_channelkey}/who + """ + # note that channeldesc and lower_channelkey will be filled + # automatically by ChannelHandler + + # this flag is what identifies this cmd as a channel cmd + # and branches off to the system send-to-channel command + # (which is customizable by admin) + is_channel = True + key = "general" + help_category = "Channel Names" + obj = None + arg_regex = "" +``` + +There are some differences here compared to most common commands. + +- There is something disconcerting in the class docstring. Some information is +between curly braces. This is a format-style which is only used for channel +commands. `{channelkey}` will be replaced by the actual channel key (like + public). `{channeldesc}` will be replaced by the channel description (like + "public channel"). And `{lower_channelkey}`. +- We have set `is_channel` to `True` in the command class variables. You +shouldn't worry too much about that: it just tells Evennia this is a special +command just for channels. +- `key` is a bit misleading because it will be replaced eventually. So we +could set it to virtually anything. +- The `obj` class variable is another one we won't detail right now. +- `arg_regex` is important: the default `arg_regex` in the channel command will +forbid to use switches (a slash just after the channel name is not allowed). +That's why we enforce it here, we allow any syntax. + +> What will become of this command? + +Well, when we'll be through with it, and once we'll add it as the default +command to handle channels, Evennia will create one per existing channel. For +instance, the public channel will receive one command of this class, with `key` +set to `public` and `aliases` set to the channel aliases (like `['pub']`). + +> Can I see it work? + +Not just yet, there's still a lot of code needed. + +Okay we have the command structure but it's rather empty. + +### The parse method + +The `parse` method is called before `func` in every command. Its job is to +parse arguments and in our case, we will analyze switches here. + +```python +# ... + def parse(self): + """ + Simple parser + """ + # channel-handler sends channame:msg here. + channelname, msg = self.args.split(":", 1) + self.switch = None + if msg.startswith("/"): + try: + switch, msg = msg[1:].split(" ", 1) + except ValueError: + switch = msg[1:] + msg = "" + + self.switch = switch.lower().strip() + + self.args = (channelname.strip(), msg.strip()) +``` + +Reading the comments we see that the channel handler will send the command in a +strange way: a string with the channel name, a colon and the actual message +entered by the player. So if the player enters "public hello", the command +`args` will contain `"public:hello"`. You can look at the way the channel name +and message are parsed, this can be used in a lot of different commands. + +Next we check if there's any switch, that is, if the message starts with a +slash. This would be the case if a player entered `public/me jumps up and +down`, for instance. If there is a switch, we save it in `self.switch`. We +alter `self.args` at the end to contain a tuple with two values: the channel +name, and the message (if a switch was used, notice that the switch will be + stored in `self.switch`, not in the second element of `self.args`). + +### The command func + +Finally, let's see the `func` method in the command class. It will have to +handle switches and also the raw message to send if no switch was used. + + +```python +# ... + def func(self): + """ + Create a new message and send it to channel, using + the already formatted input. + """ + channelkey, msg = self.args + caller = self.caller + channel = ChannelDB.objects.get_channel(channelkey) + + # Check that the channel exists + if not channel: + self.msg(_("Channel '%s' not found.") % channelkey) + return + + # Check that the caller is connected + if not channel.has_connection(caller): + string = "You are not connected to channel '%s'." + self.msg(string % channelkey) + return + + # Check that the caller has send access + if not channel.access(caller, 'send'): + string = "You are not permitted to send to channel '%s'." + self.msg(string % channelkey) + return + + # Handle the various switches + if self.switch == "me": + if not msg: + self.msg("What do you want to do on this channel?") + else: + msg = "{} {}".format(caller.key, msg) + channel.msg(msg, online=True) + elif self.switch: + self.msg("{}: Invalid switch {}.".format(channel.key, self.switch)) + elif not msg: + self.msg("Say what?") + else: + if caller in channel.mutelist: + self.msg("You currently have %s muted." % channel) + return + channel.msg(msg, senders=self.caller, online=True) +``` + +- First of all, we try to get the channel object from the channel name we have +in the `self.args` tuple. We use `ChannelDB.objects.get_channel` this time +because we know the channel name isn't an alias (that was part of the deal, + `channelname` in the `parse` method contains a command key). +- We check that the channel does exist. +- We then check that the caller is connected to the channel. Remember, if the +caller isn't connected, we shouldn't allow him to use this command (that + includes the switches on channels). +- We then check that the caller has access to the channel's `send` lock. This +time, we make sure the caller can send messages to the channel, no matter what +operation he's trying to perform. +- Finally we handle switches. We try only one switch: `me`. This switch would +be used if a player entered `public/me jumps up and down` (to do a channel + emote). +- We handle the case where the switch is unknown and where there's no switch +(the player simply wants to talk on this channel). + +The good news: The code is not too complicated by itself. The bad news is that +this is just an abridged version of the code. If you want to handle all the +switches mentioned in the command help, you will have more code to write. This +is left as an exercise. + +### End of class + +It's almost done, but we need to add a method in this command class that isn't +often used. I won't detail it's usage too much, just know that Evennia will use +it and will get angry if you don't add it. So at the end of your class, just +add: + +```python +# ... + def get_extra_info(self, caller, **kwargs): + """ + Let users know that this command is for communicating on a channel. + + Args: + caller (TypedObject): A Character or Account who has entered an ambiguous command. + + Returns: + A string with identifying information to disambiguate the object, conventionally with a preceding space. + """ + return " (channel)" +``` + +### Adding this channel command + +Contrary to most Evennia commands, we won't add our `ChannelCommand` to a +`CmdSet`. Instead we need to tell Evennia that it should use the command we +just created instead of its default channel-command. + +In your `server/conf/settings.py` file, add a new setting: + +```python +# Channel options +CHANNEL_COMMAND_CLASS = "commands.comms.ChannelCommand" +``` + +Then you can reload your game. Try to type `public hello` and `public/me jumps +up and down`. Don't forget to enter `help public` to see if your command has +truly been added. + +## Conclusion and full code + +That was some adventure! And there's still things to do! But hopefully, this +tutorial will have helped you in designing your own channel system. Here are a +few things to do: + +- Add more switches to handle various actions, like changing the description of +a channel for instance, or listing the connected participants. +- Remove the default Evennia commands to handle channels. +- Alter the behavior of the channel system so it better aligns with what you +want to do. + +As a special bonus, you can find a full, working example of a communication +system similar to the one I've shown you: this is a working example, it +integrates all switches and does ever some extra checking, but it's also very +close from the code I've provided here. Notice, however, that this resource is +external to Evennia and not maintained by anyone but the original author of +this article. + +[Read the full example on Github](https://github.com/vincent-lg/avenew/blob/master/commands/comms.py) diff --git a/docs/source/Debugging.md b/docs/source/Debugging.md new file mode 100644 index 0000000000..bbbffe099b --- /dev/null +++ b/docs/source/Debugging.md @@ -0,0 +1,290 @@ +# Debugging + + +Sometimes, an error is not trivial to resolve. A few simple `print` statements is not enough to find +the cause of the issue. Running a *debugger* can then be very helpful and save a lot of time. Debugging +means running Evennia under control of a special *debugger* program. This allows you to stop the +action at a given point, view the current state and step forward through the program to see how its +logic works. + +Evennia natively supports these debuggers: + +- [Pdb](https://docs.python.org/2/library/pdb.html) is a part of the Python distribution and + available out-of-the-box. +- [PuDB](https://pypi.org/project/pudb/) is a third-party debugger that has a slightly more + 'graphical', curses-based user interface than pdb. It is installed with `pip install pudb`. + +## Debugging Evennia + +To run Evennia with the debugger, follow these steps: + +1. Find the point in the code where you want to have more insight. Add the following line at that + point. + ```python + from evennia import set_trace;set_trace() + ``` +2. (Re-)start Evennia in interactive (foreground) mode with `evennia istart`. This is important - + without this step the debugger will not start correctly - it will start in this interactive + terminal. +3. Perform the steps that will trigger the line where you added the `set_trace()` call. The debugger + will start in the terminal from which Evennia was interactively started. + +The `evennia.set_trace` function takes the following arguments: + + +```python + evennia.set_trace(debugger='auto', term_size=(140, 40)) +``` + +Here, `debugger` is one of `pdb`, `pudb` or `auto`. If `auto`, use `pudb` if available, otherwise +use `pdb`. The `term_size` tuple sets the viewport size for `pudb` only (it's ignored by `pdb`). + + +## A simple example using pdb + +The debugger is useful in different cases, but to begin with, let's see it working in a command. +Add the following test command (which has a range of deliberate errors) and also add it to your +default cmdset. Then restart Evennia in interactive mode with `evennia istart`. + + +```python +# In file commands/command.py + + +class CmdTest(Command): + + """ + A test command just to test pdb. + + Usage: + test + + """ + + key = "test" + + def func(self): + from evennia import set_trace; set_trace() # <--- start of debugger + obj = self.search(self.args) + self.msg("You've found {}.".format(obj.get_display_name())) + +``` + +If you type `test` in your game, everything will freeze. You won't get any feedback from the game, +and you won't be able to enter any command (nor anyone else). It's because the debugger has started +in your console, and you will find it here. Below is an example with `pdb`. + +``` +... +> .../mygame/commands/command.py(79)func() +-> obj = self.search(self.args) +(Pdb) + +``` + +`pdb` notes where it has stopped execution and, what line is about to be executed (in our case, `obj += self.search(self.args)`), and ask what you would like to do. + +### Listing surrounding lines of code + +When you have the `pdb` prompt `(Pdb)`, you can type in different commands to explore the code. The first one you should know is `list` (you can type `l` for short): + +``` +(Pdb) l + 43 + 44 key = "test" + 45 + 46 def func(self): + 47 from evennia import set_trace; set_trace() # <--- start of debugger + 48 -> obj = self.search(self.args) + 49 self.msg("You've found {}.".format(obj.get_display_name())) + 50 + 51 # ------------------------------------------------------------- + 52 # + 53 # The default commands inherit from +(Pdb) +``` + +Okay, this didn't do anything spectacular, but when you become more confident with `pdb` and find +yourself in lots of different files, you sometimes need to see what's around in code. Notice that +there is a little arrow (`->`) before the line that is about to be executed. + +This is important: **about to be**, not **has just been**. You need to tell `pdb` to go on (we'll +soon see how). + +### Examining variables + +`pdb` allows you to examine variables (or really, to run any Python instruction). It is very useful +to know the values of variables at a specific line. To see a variable, just type its name (as if +you were in the Python interpreter: + +``` +(Pdb) self + +(Pdb) self.args +u'' +(Pdb) self.caller + +(Pdb) +``` + +If you try to see the variable `obj`, you'll get an error: + +``` +(Pdb) obj +*** NameError: name 'obj' is not defined +(Pdb) +``` + +That figures, since at this point, we haven't created the variable yet. + +> Examining variable in this way is quite powerful. You can even run Python code and keep on +> executing, which can help to check that your fix is actually working when you have identified an +> error. If you have variable names that will conflict with `pdb` commands (like a `list` +> variable), you can prefix your variable with `!`, to tell `pdb` that what follows is Python code. + +### Executing the current line + +It's time we asked `pdb` to execute the current line. To do so, use the `next` command. You can +shorten it by just typing `n`: + +``` +(Pdb) n +AttributeError: "'CmdTest' object has no attribute 'search'" +> .../mygame/commands/command.py(79)func() +-> obj = self.search(self.args) +(Pdb) +``` + +`Pdb` is complaining that you try to call the `search` method on a command... whereas there's no +`search` method on commands. The character executing the command is in `self.caller`, so we might +change our line: + +```python +obj = self.caller.search(self.args) +``` + +### Letting the program run + +`pdb` is waiting to execute the same instruction... it provoked an error but it's ready to try +again, just in case. We have fixed it in theory, but we need to reload, so we need to enter a +command. To tell `pdb` to terminate and keep on running the program, use the `continue` (or `c`) +command: + +``` +(Pdb) c +... +``` + +You see an error being caught, that's the error we have fixed... or hope to have. Let's reload the +game and try again. You need to run `evennia istart` again and then run `test` to get into the +command again. + +``` +> .../mygame/commands/command.py(79)func() +-> obj = self.caller.search(self.args) +(Pdb) + +``` + +`pdb` is about to run the line again. + +``` +(Pdb) n +> .../mygame/commands/command.py(80)func() +-> self.msg("You've found {}.".format(obj.get_display_name())) +(Pdb) +``` + +This time the line ran without error. Let's see what is in the `obj` variable: + +``` +(Pdb) obj +(Pdb) print obj +None +(Pdb) +``` + +We have entered the `test` command without parameter, so no object could be found in the search (`self.args` is an empty string). + +Let's allow the command to continue and try to use an object name as parameter (although, we should +fix that bug too, it would be better): + +``` +(Pdb) c +... +``` + +Notice that you'll have an error in the game this time. Let's try with a valid parameter. I have +another character, `barkeep`, in this room: + +```test barkeep``` + +And again, the command freezes, and we have the debugger opened in the console. + +Let's execute this line right away: + +``` +> .../mygame/commands/command.py(79)func() +-> obj = self.caller.search(self.args) +(Pdb) n +> .../mygame/commands/command.py(80)func() +-> self.msg("You've found {}.".format(obj.get_display_name())) +(Pdb) obj + +(Pdb) +``` + +At least this time we have found the object. Let's process... + +``` +(Pdb) n +TypeError: 'get_display_name() takes exactly 2 arguments (1 given)' +> .../mygame/commands/command.py(80)func() +-> self.msg("You've found {}.".format(obj.get_display_name())) +(Pdb) +``` + +As an exercise, fix this error, reload and run the debugger again. Nothing better than some +experimenting! + +Your debugging will often follow the same strategy: + +1. Receive an error you don't understand. +2. Put a breaking point **BEFORE** the error occurs. +3. Run the code again and see the debugger open. +4. Run the program line by line,examining variables, checking the logic of instructions. +5. Continue and try again, each step a bit further toward the truth and the working feature. + +### Stepping through a function + +`n` is useful, but it will avoid stepping inside of functions if it can. But most of the time, when +we have an error we don't understand, it's because we use functions or methods in a way that wasn't +intended by the developer of the API. Perhaps using wrong arguments, or calling the function in a +situation that would cause a bug. When we have a line in the debugger that calls a function or +method, we can "step" to examine it further. For instance, in the previous example, when `pdb` was +about to execute `obj = self.caller.search(self.args)`, we may want to see what happens inside of +the `search` method. + +To do so, use the `step` (or `s`) command. This command will show you the definition of the +function/method and you can then use `n` as before to see it line-by-line. In our little example, +stepping through a function or method isn't that useful, but when you have an impressive set of +commands, functions and so on, it might really be handy to examine some feature and make sure they +operate as planned. + +## Cheat-sheet of pdb/pudb commands + +PuDB and Pdb share the same commands. The only real difference is how it's presented. The `look` +command is not needed much in `pudb` since it displays the code directly in its user interface. + +| Pdb/PuDB command | To do what | +| ----------- | ---------- | +| list (or l) | List the lines around the point of execution (not needed for `pudb`, it will show this directly). | +| print (or p) | Display one or several variables. | +| `!` | Run Python code (using a `!` is often optional). | +| continue (or c) | Continue execution and terminate the debugger for this time. | +| next (or n) | Execute the current line and goes to the next one. | +| step (or s) | Step inside of a function or method to examine it. | +| `` | Repeat the last command (don't type `n` repeatedly, just type it once and then press `` to repeat it). | + +If you want to learn more about debugging with Pdb, you will find an [interesting tutorial on that topic here](https://pymotw.com/3/pdb/). diff --git a/docs/source/Default-Command-Help.md b/docs/source/Default-Command-Help.md new file mode 100644 index 0000000000..238848481e --- /dev/null +++ b/docs/source/Default-Command-Help.md @@ -0,0 +1,2432 @@ +# Default Command Help + + +[](Auto-generated-listing-of-default-Evennia-commands) + +> *This page is auto-generated. Do not modify - your changes will be lost. Report problems to the +[issue tracker](https://github.com/evennia/evennia/issues).* + +The full set of default Evennia commands currently contains 92 commands in 9 source +files. Our policy for adding default commands is outlined [here](Using-Mux-as-a-Standard). More +information about how commands work can be found in the documentation for [Commands](Commands). + + + +## A-Z + +- [`__unloggedin_look_command`](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-`--unloggedin-look-command`-cmdunconnectedlook) - look when in unlogged-in state +- [about](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-about-cmdabout) - show Evennia info +- [access](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-access-cmdaccess) - show your current game access +- [addcom](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-addcom-cmdaddcom) - add a channel alias and/or subscribe to a channel +- [alias](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-alias-cmdsetobjalias) - adding permanent aliases for object +- [allcom](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-allcom-cmdallcom) - perform admin operations on all channels +- [ban](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-ban-cmdban) - ban an account from the server +- [batchcode](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-batchcode-cmdbatchcode) - build from batch-code file +- [batchcommands](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-batchcommands-cmdbatchcommands) - build from batch-command file +- [boot](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-boot-cmdboot) - kick an account from the server. +- [cboot](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-cboot-cmdcboot) - kick an account from a channel you control +- [ccreate](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-ccreate-cmdchannelcreate) - create a new channel +- [cdesc](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-cdesc-cmdcdesc) - describe a channel you control +- [cdestroy](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-cdestroy-cmdcdestroy) - destroy a channel you created +- [cemit](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-cemit-cmdcemit) - send an admin message to a channel you control +- [channels](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-channels-cmdchannels) - list all channels available to you +- [charcreate](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-charcreate-cmdcharcreate) - create a new character +- [chardelete](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-chardelete-cmdchardelete) - delete a character - this cannot be undone! +- [clock](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-clock-cmdclock) - change channel locks of a channel you control +- [cmdsets](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-cmdsets-cmdlistcmdsets) - list command sets defined on an object +- [color](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-color-cmdcolortest) - testing which colors your client support +- [command](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-command-objmanipcommand) - This is a parent class for some of the defining objmanip commands +- [connect](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-connect-cmdunconnectedconnect) - connect to the game +- [copy](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-copy-cmdcopy) - copy an object and its properties +- [cpattr](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-cpattr-cmdcpattr) - copy attributes between objects +- [create](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-create-cmdunconnectedcreate) - create a new account account +- [create](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-create-cmdcreate) - create new objects +- [cwho](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-cwho-cmdcwho) - show who is listening to a channel +- [delcom](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-delcom-cmddelcom) - remove a channel alias and/or unsubscribe from channel +- [desc](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-desc-cmddesc) - describe an object or the current room. +- [destroy](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-destroy-cmddestroy) - permanently delete objects +- [dig](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-dig-cmddig) - build new rooms and connect them to the current location +- [drop](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-drop-cmddrop) - drop something +- [emit](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-emit-cmdemit) - admin command for emitting message to multiple objects +- [examine](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-examine-cmdexamine) - get detailed information about an object +- [find](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-find-cmdfind) - search the database for objects +- [force](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-force-cmdforce) - forces an object to execute a command +- [get](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-get-cmdget) - pick up something +- [give](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-give-cmdgive) - give away something to someone +- [help](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-help-cmdunconnectedhelp) - get help when in unconnected-in state +- [help](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-help-cmdhelp) - View help or a list of topics +- [home](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-home-cmdhome) - move to your character's home location +- [ic](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-ic-cmdic) - control an object you have permission to puppet +- [inventory](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-inventory-cmdinventory) - view inventory +- [irc2chan](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-irc2chan-cmdirc2chan) - Link an evennia channel to an external IRC channel +- [link](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-link-cmdlink) - link existing rooms together with exits +- [lock](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-lock-cmdlock) - assign a lock definition to an object +- [look](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-look-cmdlook) - look at location or object +- [look](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-look-cmdooclook) - look while out-of-character +- [mvattr](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-mvattr-cmdmvattr) - move attributes between objects +- [name](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-name-cmdname) - change the name and/or aliases of an object +- [nick](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-nick-cmdnick) - define a personal alias/nick by defining a string to +- [objects](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-objects-cmdobjects) - statistics on objects in the database +- [ooc](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-ooc-cmdooc) - stop puppeting and go ooc +- [open](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-open-cmdopen) - open a new exit from the current room +- [option](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-option-cmdoption) - Set an account option +- [page](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-page-cmdpage) - send a private message to another account +- [password](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-password-cmdpassword) - change your password +- [perm](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-perm-cmdperm) - set the permissions of an account/object +- [pose](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-pose-cmdpose) - strike a pose +- [py](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-py-cmdpy) - execute a snippet of python code +- [quell](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-quell-cmdquell) - use character's permissions instead of account's +- [quit](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-quit-cmdunconnectedquit) - quit when in unlogged-in state +- [quit](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-quit-cmdquit) - quit the game +- [reload](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-reload-cmdreload) - reload the server +- [reset](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-reset-cmdreset) - reset and reboot the server +- [rss2chan](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-rss2chan-cmdrss2chan) - link an evennia channel to an external RSS feed +- [say](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-say-cmdsay) - speak as your character +- [script](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-script-cmdscript) - attach a script to an object +- [scripts](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-scripts-cmdscripts) - list and manage all running scripts +- [server](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-server-cmdserverload) - show server load and memory statistics +- [service](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-service-cmdservice) - manage system services +- [sessions](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-sessions-cmdsessions) - check your connected session(s) +- [set](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-set-cmdsetattribute) - set attribute on an object or account +- [setdesc](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-setdesc-cmdsetdesc) - describe yourself +- [sethelp](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-sethelp-cmdsethelp) - Edit the help database. +- [sethome](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-sethome-cmdsethome) - set an object's home location +- [shutdown](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-shutdown-cmdshutdown) - stop the server completely +- [spawn](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-spawn-cmdspawn) - spawn objects from prototype +- [style](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-style-cmdstyle) - In-game style options +- [tag](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-tag-cmdtag) - handles the tags of an object +- [tel](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-tel-cmdteleport) - teleport object to another location +- [time](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-time-cmdtime) - show server time statistics +- [tunnel](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-tunnel-cmdtunnel) - create new rooms in cardinal directions only +- [typeclass](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-typeclass-cmdtypeclass) - set or change an object's typeclass +- [unban](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-unban-cmdunban) - remove a ban from an account +- [unlink](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-unlink-cmdunlink) - remove exit-connections between rooms +- [userpassword](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-userpassword-cmdnewpassword) - change the password of an account +- [wall](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-wall-cmdwall) - make an announcement to all +- [whisper](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-whisper-cmdwhisper) - Speak privately as your character to another +- [who](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-who-cmdwho) - list who is currently online +- [wipe](https://github.com/evennia/evennia/wiki/Default-Command-Help#wiki-wipe-cmdwipe) - clear all attributes from an object + +## A-Z by source file + +- [account.py](https://github.com/evennia/evennia/wiki/Default-Command-Help#accountpy) +- [admin.py](https://github.com/evennia/evennia/wiki/Default-Command-Help#adminpy) +- [batchprocess.py](https://github.com/evennia/evennia/wiki/Default-Command-Help#batchprocesspy) +- [building.py](https://github.com/evennia/evennia/wiki/Default-Command-Help#buildingpy) +- [comms.py](https://github.com/evennia/evennia/wiki/Default-Command-Help#commspy) +- [general.py](https://github.com/evennia/evennia/wiki/Default-Command-Help#generalpy) +- [help.py](https://github.com/evennia/evennia/wiki/Default-Command-Help#helppy) +- [system.py](https://github.com/evennia/evennia/wiki/Default-Command-Help#systempy) +- [unloggedin.py](https://github.com/evennia/evennia/wiki/Default-Command-Help#unloggedinpy) + +## Command details + +These are generated from the auto-documentation and are ordered by their source file location in +[evennia/commands/default/](https://github.com/evennia/evennia/tree/master/evennia/commands/default/) + + +### `account.py` + +[View account.py source](https://github.com/evennia/evennia/tree/master/evennia/commands/default/account.py) + + +#### charcreate (CmdCharCreate) +``` + create a new character + + Usage: + charcreate [= desc] + + Create a new character, optionally giving it a description. You + may use upper-case letters in the name - you will nevertheless + always be able to access your character using lower-case letters + if you want. +``` +- **key:** *charcreate* +- **aliases:** +- **[locks](Locks):** *"cmd:pperm(Player)"* +- **[`help_category`](Help-System):** *"General"* +- **Source:** class `CmdCharCreate` in [account.py](https://github.com/evennia/evennia/tree/master/evennia/commands/default/account.py). +Belongs to command set *'DefaultAccount'* of class `AccountCmdSet` in [cmdset_account.py](https://github.com/evennia/evennia/tree/master/evennia/commands/default/cmdset_account.py). + + +#### chardelete (CmdCharDelete) +``` + delete a character - this cannot be undone! + + Usage: + chardelete + + Permanently deletes one of your characters. +``` +- **key:** *chardelete* +- **aliases:** +- **[locks](Locks):** *"cmd:pperm(Player)"* +- **[`help_category`](Help-System):** *"General"* +- **Source:** class `CmdCharDelete` in [account.py](https://github.com/evennia/evennia/tree/master/evennia/commands/default/account.py). +Belongs to command set *'DefaultAccount'* of class `AccountCmdSet` in [cmdset_account.py](https://github.com/evennia/evennia/tree/master/evennia/commands/default/cmdset_account.py). + + +#### color (CmdColorTest) +``` + testing which colors your client support + + Usage: + color ansi||xterm256 + + Prints a color map along with in-mud color codes to use to produce + them. It also tests what is supported in your client. Choices are + 16-color ansi (supported in most muds) or the 256-color xterm256 + standard. No checking is done to determine your client supports + color - if not you will see rubbish appear. +``` +- **key:** *color* +- **aliases:** +- **[locks](Locks):** *"cmd:all()"* +- **[`help_category`](Help-System):** *"General"* +- **Source:** class `CmdColorTest` in [account.py](https://github.com/evennia/evennia/tree/master/evennia/commands/default/account.py). +Belongs to command set *'DefaultAccount'* of class `AccountCmdSet` in [cmdset_account.py](https://github.com/evennia/evennia/tree/master/evennia/commands/default/cmdset_account.py). + + +#### ic (CmdIC) +``` + control an object you have permission to puppet + + Usage: + ic + + Go in-character (IC) as a given Character. + + This will attempt to "become" a different object assuming you have + the right to do so. Note that it's the ACCOUNT character that puppets + characters/objects and which needs to have the correct permission! + + You cannot become an object that is already controlled by another + account. In principle can be any in-game object as long + as you the account have access right to puppet it. +``` +- **key:** *ic* +- **aliases:** *puppet* +- **[locks](Locks):** *"cmd:all()"* +- **[`help_category`](Help-System):** *"General"* +- **Source:** class `CmdIC` in [account.py](https://github.com/evennia/evennia/tree/master/evennia/commands/default/account.py). +Belongs to command set *'DefaultAccount'* of class `AccountCmdSet` in [cmdset_account.py](https://github.com/evennia/evennia/tree/master/evennia/commands/default/cmdset_account.py). + + +#### look (CmdOOCLook) +``` + look while out-of-character + + Usage: + look + + Look in the ooc state. +``` +- **key:** *look* +- **aliases:** *l*, *ls* +- **[locks](Locks):** *"cmd:all()"* +- **[`help_category`](Help-System):** *"General"* +- **Source:** class `CmdOOCLook` in [account.py](https://github.com/evennia/evennia/tree/master/evennia/commands/default/account.py). +Belongs to command set *'DefaultAccount'* of class `AccountCmdSet` in [cmdset_account.py](https://github.com/evennia/evennia/tree/master/evennia/commands/default/cmdset_account.py). + + +#### ooc (CmdOOC) +``` + stop puppeting and go ooc + + Usage: + ooc + + Go out-of-character (OOC). + + This will leave your current character and put you in a incorporeal OOC state. +``` +- **key:** *ooc* +- **aliases:** *unpuppet* +- **[locks](Locks):** *"cmd:pperm(Player)"* +- **[`help_category`](Help-System):** *"General"* +- **Source:** class `CmdOOC` in [account.py](https://github.com/evennia/evennia/tree/master/evennia/commands/default/account.py). +Belongs to command set *'DefaultAccount'* of class `AccountCmdSet` in [cmdset_account.py](https://github.com/evennia/evennia/tree/master/evennia/commands/default/cmdset_account.py). + + +#### option (CmdOption) +``` + Set an account option + + Usage: + option[/save] [name = value] + + Switches: + save - Save the current option settings for future logins. + clear - Clear the saved options. + + This command allows for viewing and setting client interface + settings. Note that saved options may not be able to be used if + later connecting with a client with different capabilities. +``` +- **key:** *option* +- **aliases:** *options* +- **[locks](Locks):** *"cmd:all()"* +- **[`help_category`](Help-System):** *"General"* +- **Source:** class `CmdOption` in [account.py](https://github.com/evennia/evennia/tree/master/evennia/commands/default/account.py). +Belongs to command set *'DefaultAccount'* of class `AccountCmdSet` in [cmdset_account.py](https://github.com/evennia/evennia/tree/master/evennia/commands/default/cmdset_account.py). + + +#### password (CmdPassword) +``` + change your password + + Usage: + password = + + Changes your password. Make sure to pick a safe one. +``` +- **key:** *password* +- **aliases:** +- **[locks](Locks):** *"cmd:pperm(Player)"* +- **[`help_category`](Help-System):** *"General"* +- **Source:** class `CmdPassword` in [account.py](https://github.com/evennia/evennia/tree/master/evennia/commands/default/account.py). +Belongs to command set *'DefaultAccount'* of class `AccountCmdSet` in [cmdset_account.py](https://github.com/evennia/evennia/tree/master/evennia/commands/default/cmdset_account.py). + + +#### quell (CmdQuell) +``` + use character's permissions instead of account's + + Usage: + quell + unquell + + Normally the permission level of the Account is used when puppeting a + Character/Object to determine access. This command will switch the lock + system to make use of the puppeted Object's permissions instead. This is + useful mainly for testing. + Hierarchical permission quelling only work downwards, thus an Account cannot + use a higher-permission Character to escalate their permission level. + Use the unquell command to revert back to normal operation. +``` +- **key:** *quell* +- **aliases:** *unquell* +- **[locks](Locks):** *"cmd:pperm(Player)"* +- **[`help_category`](Help-System):** *"General"* +- **Source:** class `CmdQuell` in [account.py](https://github.com/evennia/evennia/tree/master/evennia/commands/default/account.py). +Belongs to command set *'DefaultAccount'* of class `AccountCmdSet` in [cmdset_account.py](https://github.com/evennia/evennia/tree/master/evennia/commands/default/cmdset_account.py). + + +#### quit (CmdQuit) +``` + quit the game + + Usage: + quit + + Switch: + all - disconnect all connected sessions + + Gracefully disconnect your current session from the + game. Use the /all switch to disconnect from all sessions. +``` +- **key:** *quit* +- **aliases:** +- **[locks](Locks):** *"cmd:all()"* +- **[`help_category`](Help-System):** *"General"* +- **Source:** class `CmdQuit` in [account.py](https://github.com/evennia/evennia/tree/master/evennia/commands/default/account.py). +Belongs to command set *'DefaultAccount'* of class `AccountCmdSet` in [cmdset_account.py](https://github.com/evennia/evennia/tree/master/evennia/commands/default/cmdset_account.py). + + +#### sessions (CmdSessions) +``` + check your connected session(s) + + Usage: + sessions + + Lists the sessions currently connected to your account. +``` +- **key:** *sessions* +- **aliases:** +- **[locks](Locks):** *"cmd:all()"* +- **[`help_category`](Help-System):** *"General"* +- **Source:** class `CmdSessions` in [account.py](https://github.com/evennia/evennia/tree/master/evennia/commands/default/account.py). +Belongs to command set *'DefaultSession'* of class `SessionCmdSet` in [cmdset_session.py](https://github.com/evennia/evennia/tree/master/evennia/commands/default/cmdset_session.py). + + +#### style (CmdStyle) +``` + In-game style options + + Usage: + style + style