coder guide
=============
-.. Copyright (c) 2004-2019 mudpy authors. Permission to use, copy,
+.. Copyright (c) 2004-2020 mudpy authors. Permission to use, copy,
modify, and distribute this software is granted under terms
provided in the LICENSE file distributed with this software.
~~~~~~~~~~~~~~~~~~~~~~
Git_ is used for version control on the project, and the archive can
-be browsed or cloned anonymously from https://mudpy.org/code/mudpy .
-For now, detailed commits can be E-mailed to fungi@yuggoth.org, but
-there will most likely be a developer mailing list for more open
-presentation and discussion of patches soon.
+be browsed or cloned anonymously from the official
+https://mudpy.org/code/mudpy repository location. For now, detailed
+commits can be E-mailed to fungi@yuggoth.org, but there will most
+likely be a developer mailing list for more open presentation and
+discussion of patches eventually.
A :file:`ChangeLog` is generated automatically from repository
commit logs, and is included automatically in all sdist_ tarballs. It
regression testing
~~~~~~~~~~~~~~~~~~
-All new commits are tested using a selftest script in the
-``mudpy/tests`` directory of the source archive, to help ensure the
-software is continually usable. Any new features should be
-accompanied by suitable regression tests so that their functionality
-can be maintained properly through future releases. The selftest can
-be invoked with ``tox -e selftest`` after starting the daemon with
-the test configuration provided in the ``mudpy/tests/fixtures``
-directory.
+All new commits are tested using an included
+:mod:`mudpy.test.selftest` script, to help ensure the software is
+continually usable. Any new features should be accompanied by
+suitable regression tests so that their functionality can be
+maintained properly through future releases. The selftest can be
+invoked with :command:`tox -e py3` which will automatically start
+the daemon with the :file:`mudpy/tests/fixtures/test_daemon.yaml`
+test configuration.
style
-----
-This project follows Guido van Rossum and Barry Warsaw's `Style Guide`_
-for Python Code (a.k.a. "PEP-8"). When in need of sample code or other
-examples, any common source code file or text document file distributed
-as part of mudpy should serve as a suitable reference. Testing of all
-new patches with the flake8_ utility should be performed with ``tox
--e flake8`` to ensure adherence to preferred style conventions.
+This project follows Guido van Rossum and Barry Warsaw's `Style
+Guide`_ for Python Code (a.k.a. "PEP-8"). When in need of sample
+code or other examples, any common source code file or text document
+file distributed as part of mudpy should serve as a suitable
+reference. Testing of all new patches with the flake8_ utility
+should be performed by running :command:`tox -e flake8` from the
+repository working directory to ensure adherence to preferred style
+conventions.
.. _Style Guide: :pep:`0008`
.. _flake8: https://pypi.org/project/flake8
+.. _demo:
+
test and demo walk-through
--------------------------
The included tox configuration provides testenv definitions for a
variety of analyzers, regression tests, documentation builds and
-package generation. It also has a ``demo`` testenv which will run the
-server using the provided :file:`etc/mudpy.yaml` and other sample
-files. By default it listens on TCP port 4000 at the IPv6 loopback
-address, streams its logging to the terminal via stdout, and grants
-administrative rights automatically to an account named ``admin``
-(once created).
-
-Because all the dependencies besides the ``python3`` interpreter itself
-are available from PyPI, installing them should be fairly similar
-across most GNU/Linux distributions. For example, on Debian 10 (a.k.a.
-"Buster") you need to expressly install the ``pip`` and ``venv`` modules
-since they're packaged separately from the rest of the Python standard
-library. Once that's done, you can perform local installs of ``tox`` and
-``tox-venv`` as a normal non-root user. We're also going to install
-system packages for the ``git`` revision control toolset and an
-extensible console-based MUD client called ``tf5`` (TinyFugue version
-5)::
+package generation. It also has a ``demo`` testenv which will run
+the server using the provided :file:`etc/mudpy.yaml` and other
+sample files. By default it listens on TCP port 4000 at the IPv6
+loopback address, streams its logging to the terminal via stdout,
+and grants administrative rights automatically to an account named
+``admin`` (once created).
+
+Because all the dependencies besides the :command:`python3`
+interpreter itself are available from PyPI, installing them should
+be fairly similar across most GNU/Linux distributions. For example,
+on Debian 10 (a.k.a. *Buster*) you need to expressly install the
+``pip`` and ``venv`` modules since they're packaged separately from
+the rest of the Python standard library. Once that's done, you can
+perform local installs of ``tox`` and ``tox-venv`` as a normal
+non-root user. We're also going to install system packages for the
+``git`` revision control toolset and an extensible console-based MUD
+client called ``tf5`` (TinyFugue version 5)::
sudo apt install git python3-pip python3-venv tf5
pip install --user tox tox-venv
exit
-The reason for exiting is that, if this is the first time you've ever
-used pip's ``--user`` option, when you log back in your ``~/.profile``
-should see that there's now a ``~/.local/bin`` directory and add it to
-your ``$PATH`` environment variable automatically from that point on.
-Next, retrieve the project source code and switch your current working
-directory to where you've cloned it::
+The reason for exiting is that, if this is the first time you've
+ever used pip's ``--user`` option, when you log back in your
+``~/.profile`` should see that there's now a ``~/.local/bin``
+directory and add it to your ``$PATH`` environment variable
+automatically from that point on. Next, retrieve the project source
+code and switch your current working directory to where you've
+cloned it::
git clone https://mudpy.org/code/mudpy
cd mudpy
Now you should be able to invoke any tox testenv you like. Just
-running ``tox`` without any additional options will go through the
-defalt battery of checks and is a good way to make sure everything is
-installed and working. Once you're ready to try out the server
-interactively, launch it like this::
+running :command:`tox` without any additional options will go
+through the defalt battery of checks and is a good way to make sure
+everything is installed and working. Once you're ready to try out
+the server interactively, launch it like this::
tox -e demo
-Now in another terminal/session (because the one you've been using is
-busy displaying the server's logs) connect using a MUD client::
+Now in another terminal/session (because the one you've been using
+is busy displaying the server's logs) connect using a MUD client
+(such as :command:`tf5` which we installed above)::
tf5 ip6-localhost 4000
-Log in as ``admin`` creating an account and then an avatar and awaken
-it. Try out the ``help`` command and make sure you see some command
-words in red (you're using a color terminal, right?) since those are
-admin-only commands and being able to see them confirms you're an
-administrator. When you're ready to terminate the service you can
-either give the ``halt`` command in your MUD client terminal or press
-the ``control`` and ``c`` keys together in the terminal where you ran
-tox. To exit the MUD client, give it the ``/quit`` command.
+Log in as ``admin`` creating an account and then an avatar and
+awaken it. Try out the :command:`help` command and make sure you see
+some command words in red (you're using a color terminal, right?)
+since those are admin-only commands and being able to see them
+confirms you're an administrator. When you're ready to terminate the
+service you can either give the :command:`halt` command in your MUD
+client terminal or press the ``control`` and ``c`` keys together in
+the terminal where you ran tox. To exit the tf5 MUD client, give it
+the :command:`/quit` command.
+
+miscellanea
+-----------
+
+This section is a collection of various coding-related discussions
+and treatises, mostly here because there's not a better place, and
+so they don't get lost in random E-mail threads.
+
+avatar names
+~~~~~~~~~~~~
+
+It comes up fairly often, so bears mentioning, **there is no
+assumption avatar names will be globally unique**. This is part of
+the reason the default :code:`choose_name` menu just runs
+:func:`mudpy.misc.random_name`, to make impersonation a bit harder.
+The idea is to make sure to be able to support realistic settings
+where multiple people are often given the same names and don't
+really have much choice as to what their parents decided to name
+them at birth, but can still choose a name they like later (perhaps
+through some cultural rite of passage quest, attaining a particular
+guild rank, or just through a command they're allowed to run as soon
+as they awaken that avatar for the first time).
+
+It may be possible to force globally-unique avatar names, but the
+need to treat avatars similarly to non-player characters (actors
+which aren't associated with an account and may be driven by
+scripted routines instead) means it may also be desirable to prevent
+a user from choosing an avatar name which duplicates the name of any
+existing actor (whether or not that actor is a user's avatar). This
+would entail scanning the full dataset to identify actors with
+similar names so they could be rejected or excluded for a new
+avatar, but then raises the question as to what to do when some new
+content adds NPCs with a name which is already in use.
+
+The best way to side-step this challenge is to not rely on avatar
+names for programmatic interaction and instead reference the
+corresponding ID for an avatar's element in the universe contents.
+IDs **are** already guaranteed to be globally unique, so there is no
+ambiguity when using them (an avatar's element ID is constructed
+from the owning account name plus an index integer). Exactly what
+variables you have to work from will depend on the context where
+your hypothetical routine is called.
+
+Taking as an example, let's say what you want is to be able to have
+an area's owner permit a specific avatar to pass through the portals
+(doorways, gates, whatever) which connect it to other areas. This is
+similar to how *guild houses* work in some classical MUDs. Here's
+how I'd go about implementing it:
+
+1. Extend the area element to have two new facets: *owners* (type
+ list) and *visitors* (type list). The first will contain
+ references to the element IDs for the avatars who are allowed to
+ alter the entries of both lists, while the second will be the
+ element IDs for avatars who are allowed to enter the area (maybe
+ also allow owners to enter an area so they don't need to be
+ duplicated in both lists).
+
+2. Implement a command which allows someone to see the corresponding
+ ID for an element (alternatively make it an acquirable ability,
+ skill, spell, item, or however finding that information would
+ best fit into your setting). This is stored in the element's
+ :code:`.key` attribute, and you can play around with it on the
+ command line with :command:`show result` like this::
+
+ > look
+ Center Sample Location
+ This is the Center Sample Location. It is merely provided as an
+ example of what an area might look like.
+ [ Exits: down, east, north, south, up, west ]
+ A sample prop sits here.
+ Utso is here.
+
+ > show result [
+ a.key.split('_', 1)[1]
+ for a in actor.universe.contents[
+ actor.get('location')
+ ].contents.values()
+ if a.key.startswith('actor.avatar_')
+ and a.get('name') == 'Utso'
+ ][0]
+
+ 'luser0_0'
+
+ [Put the command on one line, It's merely wrapped here for
+ readability.] In short, this list comprehension takes the
+ internal IDs for elements present in the calling actor's current
+ location, filtered by whether they're avatars and a specific
+ actor name, then splits the group and prefix off (relying on the
+ fact that it uses ``_`` as a prefix separator) and returns the
+ first result. In a non-prototype implementation, the command
+ would probably include a routine which looked something like:
+
+ .. code-block:: python
+
+ location_id = actor.get("location")
+ also_here = actor.universe.contents[location_id].contents
+ for who in also_here.values():
+ if (who.key.startswith("actor.avatar_")
+ and who.get("name") == parameters[0]:
+ message += "Avatar ID: %s" % .key.split("_", 1)[1]
+ break
+
+ That's a rough approximation, and not terribly immersive, but
+ hopefully you get the idea. We could also improve the element
+ attributes with pre-populated backlink references for some of
+ these relationships to reduce code complexity.
+
+3. Implement a command which allows a user to add and remove entries
+ in the owners and visitors lists for their current location,
+ which takes this avatar ID as a parameter.
+
+4. Hook in the :meth:`mudpy.misc.Element.go_to` method to test the
+ owners and visitors lists for the area parameter to make sure
+ self is in one of those lists. Probably also stick in an override
+ to make sure that if the actor's owner account is flagged as
+ administrative then they're also allowed even if they're not on
+ the allowed list.
+
+The resulting workflow this would enable is that after an avatar *A*
+has been set as an owner for area *X* (perhaps by an admin or a
+world builder), *A* when in another area *Y* at the same time as
+some avatar *B* whom they would like to be allowed to visit *X*
+could run the identification command from part #2 above, then they
+would later travel to *X* and run the safe passage command from part
+#3 adding *B* to the visitors list for *X*. After that, *B* is able
+freely enter *X* until *A* runs a similar command to remove them
+from the visitors list for *X* again.
+
+To tackle the immersion problem, an optional substitution cipher
+could be implemented to (reversibly) turn those IDs into something
+more mystical-looking. Also the identification command (or ability,
+skill, spell, tool, whatever) could be limited to only work when the
+area you're running it in is flagged a certain way. Taking this
+concept further, all sorts of elements could have access lists for
+which avatars own them and which avatars are allowed to interact
+with them (in which case maybe the term *visitor* for the latter is
+too area-specific and it needs a slightly more general term?).
+
+However, a far more immersive solution would be to get closable and
+lockable elements (and inventory management) implemented, have a
+means of crafting keys which unlock specific elements, and then when
+creating an area you want to restrict to specific avatars, put
+locked doors for all its portals and give keys for them to anyone
+you want to be able to visit. Or implement non-avatar actors, then
+create guards you can hire or summon or construct to police your
+doorways and check whether visitors are on a list. That list itself
+could even be a piece of paper or a book (a prop), created with
+appropriate materials and some skill which allows the actor to write
+and edit paper notes, given to the guard and held by it as an
+inventory item.
+
+custom commands
+~~~~~~~~~~~~~~~
+
+Command definitions are split into metadata and procedure. The
+metadata needs to be in an element like the basic ones shipped in
+the :file:`share/command.yaml` file, and then a handler function
+added to the :mod:`mudpy.command` module. There's not yet a plugin
+layer to allow those to be added to a separate module. The function
+name needs to match the element base name, or you have to add an
+action facet to the element indicating the name of the function you
+want it to call; the ``command.set`` element has an example of this,
+so that we avoid shadowing Python's built-in :func:`set` function
+and call :func:`mudpy.command.c_set` instead.
+
+The :meth:`mudpy.misc.Element.set` method takes two parameters, the
+name of the facet and the value to pass into it. An example of it in
+action is :meth:`mudpy.misc.User.authenticate` where the user's
+*administrator* facet is set to the value *True* if their username
+is in the ``.mudpy.limit.admins`` list used to bootstrap
+administrators:
+
+.. code-block:: python
+ :emphasize-lines: 9
+
+ def authenticate(self):
+ """Flag the user as authenticated and disconnect duplicates."""
+ if self.state != "authenticated":
+ self.authenticated = True
+ log("User %s authenticated for account %s." % (
+ self, self.account.subkey), 2)
+ if ("mudpy.limit" in universe.contents and self.account.subkey in
+ universe.contents["mudpy.limit"].get("admins")):
+ self.account.set("administrator", True)
+ log("Account %s is an administrator." % (
+ self.account.subkey), 2)