Python - ldmud/ldmud GitHub Wiki
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.
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
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.
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.
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:
- Create an empty directory
- Create a subdirectory with the name of your package (eg.
ldmud_hello
) - In the subdirectory put your implementation. You'll need at least a (possibly empty)
__init__.py
there. For example put ahello.py
there:def hello_world(): print("Hello, world!") return 1
- In the main directory (beside the
ldmud_hello
in our example) put aREADME.md
with a description and asetup.py
:Theimport 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, )
entry_points
entryldmud_efun
contains a list of all efuns that the package offers. In this case the efunhello
will be implemented by the functionhello_world
in the moduleldmud_hello.hello
. - Install the package
python3 setup.py install --user
- 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.)
Python code that is executed by LDMud can use the ldmud
module.
import ldmud
This module is not available to standalone Python programs.
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.
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
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!")
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])
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.
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.
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)