Python - ldmud/ldmud GitHub Wiki

Introduction

Since LDMud 3.5 the driver can be compiled with Python support. The primary purpose of the Python support is the implementation of additional efuns in Python. Python offers a wide range of additional libraries that can be made available to LPC code this way.

First Steps

Prerequisites

LDMud 3.5 and Python 3 (3.7 or later recommended) is needed. Python support must be explicitly activated when compiling LDMud:

./configure --enable-use-python
make

Python startup script

When LDMud is started a Python script is executed. The default script is startup.py in the mudlib directory. The default can be changed with the --with-python-script=... option for configure. A different script can also be given on the command line of ldmud:

./ldmud --python-script ../python/startup.py

The location of the script is interpreted relative to the mudlib directory.

A simple startup script would be:

import ldmud

def hello_world():
    print("Hello, world!")
    return 1

ldmud.register_efun("hello", hello_world)

This script adds an efun named hello() that will print a string on standard output of the driver (not to the player!) and return 1.

The script is executed once and only once at startup. There is no way to execute it again or start another .py file.

Organizing efuns

Dynamically finding efuns

For any long running MUDs it is desirable to load additional efuns later or reload efuns to fix bugs. For this there is a Python package ldmud-efuns that searches Python packages for LDMud entry points. This package can be installed with pip3:

pip3 install --user ldmud-efuns

This package offers a startup function that can be used in the startup.py:

from ldmudefuns.startup import startup

startup()

The startup function will search for any installed Python package that has LDMud efuns and load and register them. The package already has its own efun python_reload() that will be found and registered this way. This efun will do this process again and search for new efuns and reload any existing efuns.

Implementing own efuns in a package

If you want your own efun to be found by the startup function from the ldmud-efuns package, you'll need to create a Python package, too:

  1. Create an empty directory
  2. Create a subdirectory with the name of your package (eg. ldmud_hello)
  3. In the subdirectory put your implementation. You'll need at least a (possibly empty) __init__.py there. For example put a hello.py there:
     def hello_world():
         print("Hello, world!")
         return 1
  4. In the main directory (beside the ldmud_hello in our example) put a README.md with a description and a setup.py:
    import setuptools
    
    with open("README.md", "r") as fh:
        long_description = fh.read()
    
    setuptools.setup(
        name="ldmud-hello",
        version="0.0.1",
        author="Me",
        author_email="[email protected]"
        description="Hello World efun for LDMud",
        long_description=long_description,
        long_description_content_type="text/markdown",
        packages=setuptools.find_packages(),
        entry_points={
            'ldmud_efun': [
                  'hello = ldmud_hello.hello:hello_world',
            ]
        },
        zip_safe=False,
    )
    The entry_points entry ldmud_efun contains a list of all efuns that the package offers. In this case the efun hello will be implemented by the function hello_world in the module ldmud_hello.hello.
  5. Install the package
    python3 setup.py install --user
  6. In a running LDMud instance call the efun python_reload() that will then find the new efun. (Or reboot the mud, so the startup script will find it.)

Programming in Python

The ldmud module

Python code that is executed by LDMud can use the ldmud module.

import ldmud

This module is not available to standalone Python programs.

Types

All LPC types are mapped to Python objects. The types int, float, string and bytes are converted to Python int, float, str and bytes. The other LPC types have their own type in the ldmud module: Object, LWObject, Array, Mapping, Struct, Closure, Symbol, QuotedArray, Lvalue.

Python data types like dict or list can be converted to the corresponding LPC Mapping or Array, but this must be done explicitly. There is no automatism that does this.

import ldmud

python_list = [1, 2, 3]
lpc_array = ldmud.Array(python_list)
python_list = list(lpc_array)

The usual operations (array/mapping addition, struct member access, closure call) are available with these objects.

Efun declaration

Python efuns can be annotated with type information to offer compile and runtime type checks:

def hello(title: str) -> int:
   print("Hello, %s!" % (title,))
   return 1

Efun and Lfun calls

Python code can call efuns and lfuns. These calls are done in the context of the calling LPC code (the LPC program that has called the Python efun will be regarded as this_object() for the efun resp. previous_object() for the lfun call).

Efun calls can be called using the ldmud.efuns namespace:

import ldmud

def hello_world() -> None:
    ldmud.efuns.write("Hello, World!\n")

Lfuns can be called using an ldmud.Object. It has a .functions member that contains all functions of the object.

import ldmud

def hello_world(ob: ldmud.Object) -> None:
    ob.functions.send_message("Hello, World!")

Events

Beside registered efuns Python code can also be called by certain events. The following events can be handled with hooks:

  • Heartbeats
  • Object creation
  • Object destruction
  • Child process termination

To react on such an event a Python function has to be registered:

import ldmud

def ping():
    print("Ping")

ldmud.register_hook(ldmud.ON_HEARTBEAT, ping)

Further hook names are ON_OBJECT_CREATED, ON_OBJECT_DESTRUCTED and ON_CHILD_PROCESS_TERMINATED. For the object hooks the corresponding Python function will then get the object as an argument.

Additionally Python code can watch for non-blocking sockets:

import ldmud, os, select

pipe = os.pipe2(os.O_NONBLOCK | os.O_CLOEXEC)

def received(event):
    print("Received: %s" % (os.read(pipe[0], 1024),))

ldmud.register_socket(pipe[0], received, select.POLLIN)

Both types of event handlers will not automatically unregistered. If you're using a reload mechanism remember to unregister them when reloading. The ldmud-efuns package will call on_reload() when reloading:

def on_reload():
    ldmud.unregister_socket(pipe[0])
    os.close(pipe[0])

Advanced topics

Debugging

Debugging is complicated because the Python scripts can not be executed outside the LDMud process. Therefore you might want to have a Python console in the MUD environment. The LDMud sources contain an example for it:

You'll need to load the Python script from your startup and use python_console efun like shown in the LPC snippet.

Be warned that this efun is a security risk. It lets you execute arbitrary Python statements and therefore should only be available in a private development environment.

Multithreading

LDMud does not support multithreading, Python however does. So if you're creating additional threads in Python, you are not allowed to use any LPC routines or datastructures. So copy any needed data into pure Python data structures before using it in another thread.

Asynchronous programming

There is support for the Python asyncio module. For this you'll need to install the ldmud-asyncio package. It allows to execute Python coroutines within LDMud:

import ldmud, ldmud_asyncio, asyncio

async def do_exec(prog, cb):
    proc = await asyncio.create_subprocess_exec(prog, stdout=asyncio.subprocess.PIPE)

    async for line in proc.stdout:
        cb(line.decode())

    await proc.wait()
    cb(0)

def efun_exec(prog: str, cb: ldmud.Closure) -> None:
    # Execute <prog> and call <cb> with each line of its output.
    asyncio.run(do_exec(prog, cb))

ldmud.register_efun("py_exec", efun_exec)
⚠️ **GitHub.com Fallback** ⚠️