working client

This commit is contained in:
bvn13 2024-12-07 01:17:53 +03:00
commit d6a06aa697
54 changed files with 3926 additions and 0 deletions

105
.gitignore vendored Normal file
View File

@ -0,0 +1,105 @@
# Created by .ignore support plugin (hsz.mobi)
### Windows template
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
### VirtualEnv template
# Virtualenv
# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/
.Python
[Bb]in
[Ii]nclude
[Ll]ib
[Ll]ib64
[Ll]ocal
[Ss]cripts
pyvenv.cfg
.venv
env
venv
pip-selfcheck.json
### macOS template
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### Linux template
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
# Sphinx
_build
# Pycharm/VScode
.idea
.idea/**
.vscode
# Python
build
dist
miney.egg-info
__pycache__/
*.pyc
.pytest_cache/
# miney
tmp
Minetest

22
.readthedocs.yml Normal file
View File

@ -0,0 +1,22 @@
# .readthedocs.yml
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
# Build documentation in the docs/ directory with Sphinx
sphinx:
configuration: docs/conf.py
# Optionally build your docs in additional formats such as PDF and ePub
formats: all
build:
image: latest
# Optionally set the version of Python and requirements required to build your docs
python:
version: 3.8
install:
- requirements: docs/requirements.txt

165
LICENSE Normal file
View File

@ -0,0 +1,165 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.

22
README.md Normal file
View File

@ -0,0 +1,22 @@
<p align="center">
<img src="https://github.com/miney-py/miney/raw/master/docs/miney-logo.png">
</p>
# Miney - The python interface to minetest
Miney is an Python API to Minetest.
Miney connects locally, over network or internet to the [mineysocket](https://github.com/miney-py/mineysocket) mod of minetest.
## Documentation
https://miney.readthedocs.io/en/latest/
## Status
Beta, the current todo list is in the [wiki](https://github.com/miney-py/miney/wiki).
## Requirement
* Python 3.6+ (tested on 3.8)
* A minetest-server with [mineysocket](https://github.com/miney-py/mineysocket) mod

5
dev-requirements.txt Normal file
View File

@ -0,0 +1,5 @@
Sphinx
sphinx_rtd_theme
wheel
twine
pytest

64
devtools/luaconsole.py Normal file
View File

@ -0,0 +1,64 @@
import sys
import os
import socket
try:
from miney import Minetest, exceptions
except ModuleNotFoundError:
sys.path.append(os.getcwd())
from miney import Minetest, exceptions
server = sys.argv[1] if 1 < len(sys.argv) else "127.0.0.1"
port = sys.argv[2] if 2 < len(sys.argv) else 29999
playername = sys.argv[3] if 3 < len(sys.argv) else "Player"
password = sys.argv[4] if 4 < len(sys.argv) else ""
mt = Minetest(server, playername, password, port)
print("python luaconsole.py [<server> <port> <playername> <password>] - All parameter optional on localhost")
print("Press ctrl+c to quit. Start multiline mode with \"--\", run it with two empty lines, exit it with ctrl+c")
multiline_mode = False
multiline_cmd = ""
ret = ""
while True:
if mt.event_queue:
print(mt.event_queue)
try:
if not multiline_mode:
cmd = input(">> ")
multiline_cmd = ""
else:
cmd = input("-- ")
if cmd == "--":
multiline_mode = True
else:
if multiline_mode:
multiline_cmd = multiline_cmd + cmd + "\n"
if "\n\n" in multiline_cmd:
ret = mt.lua.run(multiline_cmd)
multiline_mode = False
if not isinstance(ret, type(None)): # print everything but none
print("<<", ret)
else:
ret = mt.lua.run(cmd)
if not isinstance(ret, type(None)): # print everything but none
print("<<", ret)
except exceptions.LuaError as e:
print("<<", e)
if multiline_mode:
multiline_mode = False
except (socket.timeout, ConnectionResetError) as e:
print(e)
sys.exit(-1)
except KeyboardInterrupt:
if multiline_mode:
multiline_mode = False
print("")
else:
sys.exit()

20
docs/Makefile Normal file
View File

@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

11
docs/_static/style.css vendored Normal file
View File

@ -0,0 +1,11 @@
.wy-side-nav-search {
/* Permalink - use to edit and share this gradient: https://colorzilla.com/gradient-editor/#c9de96+0,8ab66b+44,398235+100;Green+3D+%233 */
background: #c9de96; /* Old browsers */
background: -moz-linear-gradient(top, #c9de96 0%, #8ab66b 44%, #398235 100%); /* FF3.6-15 */
background: -webkit-linear-gradient(top, #c9de96 0%,#8ab66b 44%,#398235 100%); /* Chrome10-25,Safari5.1-6 */
background: linear-gradient(to bottom, #c9de96 0%,#8ab66b 44%,#398235 100%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#c9de96', endColorstr='#398235',GradientType=0 ); /* IE6-9 */
}
.miney-logo-mainpage { padding: 2em; }
.wy-nav-content { max-width: 1200px; }

13
docs/_templates/layout.html vendored Normal file
View File

@ -0,0 +1,13 @@
{% extends "!layout.html" %}
{% block extrahead %}
<link href="{{ pathto("_static/style.css", True) }}" rel="stylesheet" type="text/css">
{% endblock %}
{% block footer %}
<script type="text/javascript">
<!-- Adds target=_blank to external links -->
$(document).ready(function () {
$('a[href^="http://"], a[href^="https://"]').not('a[class*=internal]').attr('target', '_blank');
});
</script>
{% endblock %}

81
docs/conf.py Normal file
View File

@ -0,0 +1,81 @@
import re
import io
import os
# Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# http://www.sphinx-doc.org/en/master/config
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
import os
import sys
sys.path.insert(0, os.path.abspath(".."))
# -- Project information -----------------------------------------------------
project = 'Miney'
copyright = '2020, Robert Lieback'
author = 'Robert Lieback'
__version__ = re.search(
r'__version__\s*=\s*[\'"]([^\'"]*)[\'"]', # It excludes inline comment too
io.open(os.path.abspath(os.path.join("..", "miney", "__init__.py")), encoding='utf_8_sig').read()
).group(1)
# The full version, including alpha/beta/rc tags
release = __version__
# -- General configuration ---------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
"sphinx.ext.autodoc", "sphinx.ext.viewcode", "sphinx.ext.intersphinx"
]
autodoc_default_options = {
'members': True,
'member-order': 'alphabetical',
# 'special-members': '__init__',
}
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'sphinx_rtd_theme'
html_theme_options = {
'logo_only': False,
# 'collapse_navigation': False,
# 'titles_only': True
}
html_logo = "miney-logo.png"
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
intersphinx_mapping = {'python': ('https://docs.python.org/3', None)}
todo_include_todos = True

7
docs/helpers.rst Normal file
View File

@ -0,0 +1,7 @@
Helper functions
=================================
.. autofunction:: miney.doc
.. autofunction:: miney.is_miney_available
.. autofunction:: miney.run_miney_game
.. autofunction:: miney.run_minetest

81
docs/index.rst Normal file
View File

@ -0,0 +1,81 @@
.. image:: miney-slogan.png
:alt: Miney logo
:align: center
:class: miney-logo-mainpage
Welcome to the Miney documentation!
====================================
Miney provides an `Python <https://www.python.org/>`_ interface to `Minetest <https://www.minetest.net/>`_.
First goal is to have fun with a sandbox for Python.
**Do whatever you like:**
* Play and fiddle around while learning python
* Visualize data in unusual ways
* Automate things with bots
* Connect minetest to external services like twitter, ebay or whatsoever
* Do whatever you want!
.. important::
For the best way to get everything running, take a look at the :doc:`quickstart` page.
.. warning::
Miney is currently in beta, so it's usable but the API may change at any point.
Why Python?
-------------
.. image:: python-logo.png
:alt: Python logo
:align: right
Some marketing text from the `Python website <https://www.python.org/about/>`_:
| Python is powerful... and fast;
| plays well with others;
| runs everywhere;
| is friendly & easy to learn;
| is Open.
These are some of the reasons people who use Python would rather not use anything else.
And it's popular! And cause of that it has a `giant package index <https://pypi.org/>`_ filled by over 400.000 users!
Why Minetest?
---------------
.. image:: minetest-logo.png
:alt: Python logo
:align: left
Why not Minecraft? Minetest is free. Not only you don't have to pay for Minetest (consider to `donate <https://www.minetest.net/get-involved/#donate>`_!), it's also open source!
That's a big point, if you try to use this for example in a classroom.
Also modding for minecraft isn't that easy, cause there is no official API or an embedded scripting language like Lua
in Minetest. Mods have to be written in Java, but have to be recompiled on every Minecraft update.
Cause of that many attempt for APIs appeared and disappeared in recent years.
In contrast Minetest modding in Lua is easy: no compilation, a official API and all game logic is also in Lua.
Table of Contents
-----------------
.. toctree::
:maxdepth: 1
:caption: Getting started
quickstart
.. toctree::
:maxdepth: 1
:caption: Reference
objects/index
helpers
tech_background
Miney version: |release|

35
docs/make.bat Normal file
View File

@ -0,0 +1,35 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=.
set BUILDDIR=_build
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd

BIN
docs/minetest-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

183
docs/minetest.svg Normal file
View File

@ -0,0 +1,183 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="48px"
height="48px"
id="svg2856"
version="1.1"
inkscape:version="0.47 r22583"
sodipodi:docname="minetest.svg"
inkscape:export-filename="/home/erlehmann/pics/icons/minetest/minetest-icon-24x24.png"
inkscape:export-xdpi="45"
inkscape:export-ydpi="45">
<defs
id="defs2858">
<filter
inkscape:collect="always"
id="filter3864">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="0.20490381"
id="feGaussianBlur3866" />
</filter>
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="12.083333"
inkscape:cx="24"
inkscape:cy="24"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:grid-bbox="true"
inkscape:document-units="px"
inkscape:window-width="1233"
inkscape:window-height="755"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1">
<inkscape:grid
type="xygrid"
id="grid2866"
empspacing="2"
visible="true"
enabled="true"
snapvisiblegridlinesonly="true"
spacingx="0.5px"
spacingy="10px"
color="#ff0000"
opacity="0.1254902"
empcolor="#ff0000"
empopacity="0.25098039"
dotted="false" />
<inkscape:grid
type="axonomgrid"
id="grid2870"
units="px"
empspacing="1"
visible="true"
enabled="true"
snapvisiblegridlinesonly="true"
spacingy="1px"
originx="0px" />
</sodipodi:namedview>
<metadata
id="metadata2861">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer1"
inkscape:label="Layer 1"
inkscape:groupmode="layer">
<path
style="fill:#e9b96e;fill-opacity:1;stroke:#573a0d;stroke-width:1;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
d="M 6.1513775e-7,16 3.2110204e-7,28 21.035899,40.145082 l 21,-12.414519 0,-11.461126 L 20.78461,4 6.1513775e-7,16 z"
id="path3047"
transform="translate(3.4641013,6)"
sodipodi:nodetypes="ccccccc" />
<path
style="fill:#2e3436;fill-opacity:1;stroke:#2e3436;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
d="m 8.5,30.907477 -2,-1.1547 0,6 L 17.320508,42 l 0,-2 -1.732051,-1 0,-2 L 13.5,35.794229 l 0,-4 -5,-2.886752 0,2 z"
id="path3831"
sodipodi:nodetypes="ccccccccccc" />
<path
style="opacity:1;fill:#555753;fill-opacity:1;stroke:#2e3436;stroke-linejoin:miter"
d="m 6.9282032,36 3.4641018,-2 3.464101,2 1.643594,0.948929 0,2 2,1.154701 0,2 L 6.9282032,36 z"
id="path3870"
sodipodi:nodetypes="cccccccc" />
<path
style="fill:#fce94f;fill-opacity:1;stroke:#625802;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
d="M 25.980762,19 31.5,22.186533 l 0,2 L 38.09375,28 41.5625,26 45.5,23.730563 l 0,2.538874 0,-4 L 32.908965,15 25.980762,19 z"
id="path3851"
sodipodi:nodetypes="cccccccccc" />
<path
style="fill:#e9b96e;fill-opacity:1;stroke:#573a0d;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0.50000000000000000"
d="m 24.839746,18.341234 8.660254,-5 0,2 -8.660254,5 0,-2 z"
id="path5684"
sodipodi:nodetypes="ccccc" />
<path
style="fill:#73d216;fill-opacity:1;stroke:#325b09;stroke-width:1;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
d="M 25.980762,5 3.4641016,18 17.5,26.10363 31.5,18.186533 24.839746,14.341234 33.5,9.341234 25.980762,5 z"
id="path3821"
sodipodi:nodetypes="ccccccc"
transform="translate(0,4)" />
<path
style="fill:#729fcf;fill-opacity:1;stroke:#19314b;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
d="m 17.5,28.10363 0,2 1.552559,0.89637 0,2 5.447441,3.145082 12,-7.071797 0,-2.14657 2,-1.1547 0,-1.54403 -7,-4.041452 -14,7.917097 z"
id="path3825"
sodipodi:nodetypes="ccccccccccc"
transform="translate(0,4)" />
<g
id="g5691"
style="stroke-linejoin:miter">
<path
sodipodi:nodetypes="ccccc"
id="path3862"
d="m 13.856406,20 6.928204,4 -6.928204,4 -6.9282028,-4 6.9282028,-4 z"
style="fill:#2e3436;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;filter:url(#filter3864);opacity:0.25000000000000000" />
<g
id="g3858"
style="stroke-linejoin:miter">
<path
style="fill:#c17d11;fill-opacity:1;stroke:#8f5902;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
d="m 15.588457,21 1.732051,1 1.732051,-1 0,-6 -1.732051,-1 -1.732051,1 0,6 z"
id="path3833"
sodipodi:nodetypes="ccccccc"
transform="translate(-3.4641015,2)" />
<path
style="fill:#4e9a06;fill-opacity:1;stroke:#316004;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
d="M 9.9641015,13.752777 17.320508,18 l 6.643593,-3.835681 0,-8.3286385 L 17.320508,2 9.9641015,6.2472233 l 0,7.5055537 z"
id="path3837"
transform="translate(-3.4641015,2)"
sodipodi:nodetypes="ccccccc" />
</g>
</g>
<g
id="g5686"
transform="translate(-4.2591582e-7,2)"
style="stroke-linejoin:miter">
<path
transform="translate(24.248712,-2)"
style="opacity:0.25000000000000000;fill:#2e3436;fill-opacity:1;stroke:none;filter:url(#filter3864);stroke-linejoin:miter"
d="m 13.856406,20 5.196153,3 -5.196153,3 -5.196152,-3 5.196152,-3 z"
id="path3868"
sodipodi:nodetypes="ccccc" />
<path
style="fill:#4e9a06;fill-opacity:1;stroke:#316004;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
d="M 15.71539,21.073285 17.320508,22 l 1.394882,-0.805336 0,-8.389328 L 17.320508,12 l -1.605118,1.073285 0,8 z"
id="path3853"
sodipodi:nodetypes="ccccccc"
transform="translate(20.78461,0)" />
</g>
<path
style="fill:none;fill-opacity:1;stroke:#ef2929;stroke-width:0.50000000000000000;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:0.50000000000000000, 0.50000000000000000;stroke-dashoffset:0.25000000000000000"
d="M 12.124356,33 11.25833,32.5"
id="path3872"
sodipodi:nodetypes="cc" />
<path
style="fill:#888a85;stroke:#2e3436;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0.50000000000000000"
d="m 45.5,26.730563 -4,2.309401 0,1 -2,1.1547 0,2 -2,1.154701 0,4 8,-4.618802 0,-7 z"
id="path3874"
sodipodi:nodetypes="ccccccccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.1 KiB

BIN
docs/miney-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
docs/miney-slogan.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

7
docs/objects/Chat.rst Normal file
View File

@ -0,0 +1,7 @@
Chat
====
Get controls of the chat.
.. autoclass:: miney.Chat
:members:

View File

@ -0,0 +1,5 @@
Exceptions
==========
.. automodule:: miney.exceptions
:members:

View File

@ -0,0 +1,7 @@
Inventory
=========
Lua related functions
.. autoclass:: miney.Inventory
:members:

7
docs/objects/Lua.rst Normal file
View File

@ -0,0 +1,7 @@
Lua
===
Lua related functions
.. autoclass:: miney.Lua
:members:

23
docs/objects/Minetest.rst Normal file
View File

@ -0,0 +1,23 @@
Minetest
========
This is the starting point for this library. With creating a Minetest object you also connect to minetest.
In this object are all functions that targets minetest itself.
There is also some properties inside, to get other objects like players or nodes.
:Example:
>>> from miney import Minetest
>>>
>>> mt = Minetest()
>>>
>>> # We set the time to midday.
>>> mt.time_of_day = 0.5
>>>
>>> # Write to the servers log
>>> mt.log("Time is set to midday ...")
.. autoclass:: miney.Minetest
:members:

13
docs/objects/Node.rst Normal file
View File

@ -0,0 +1,13 @@
Node
====
Manipulate and get information's about nodes.
Nodes are defined as dicts in the form
>>> {'param1': 15, 'name': 'air', 'param2': 0, 'x': -158.67400138786, 'y': 3.5000000521541, 'z': -16.144999935403}
The keys "param1" and "param2" are optional storage variables.
.. autoclass:: miney.Node
:members:

7
docs/objects/Player.rst Normal file
View File

@ -0,0 +1,7 @@
Player
======
Change properties of a single player, like there view, speed or gravity.
.. autoclass:: miney.Player
:members:

20
docs/objects/index.rst Normal file
View File

@ -0,0 +1,20 @@
API
=================================
.. rubric:: Objects
.. toctree::
Minetest
Lua
Chat
Player
Node
Inventory
Exceptions
.. rubric:: Indices and tables
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

BIN
docs/python-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

59
docs/quickstart.rst Normal file
View File

@ -0,0 +1,59 @@
Quickstart
==========
Welcome in the block sandbox!
Blockgames like Minecraft or Minetest give you the ideal playground for creative playing and building just like a real sandbox.
But other than real sandboxes, you can work on very large worlds together with your friends over the internet.
And you can use (very simplified) physics, save the progress and many more.
But what about learning programming while expressing your creativity? Why not automate things? Or build even greater things?
Installation
------------
Windows
^^^^^^^
* Download the latest precompiled Miney distribution: https://github.com/miney-py/miney_distribution/releases
* Start the miney-launcher.exe and click on "Quickstart". This will open Minetest directly into a game and IDLE, the IDE shipped with python.
Linux
^^^^^
Tested under lubuntu 20.04LTS
$ sudo apt-get install minetest fonts-crosextra-caladea fonts-crosextra-carlito minetest-mod-moreblocks minetest-mod-moreores minetest-mod-pipeworks minetest-server minetestmapper
$ sudo apt-get install luajit lua-socket lua-cjson idle3 python3-pip
$ pip3 install miney
Then install the mineysocket mod in minetest
$ cd ~/.minetest/mods
$ git clone https://github.com/miney-py/mineysocket.git
Don't forget to enable the mods in the configuration tab for your new game!
MacOS
^^^^^
Untested
First lines of code
-------------------
The first lines of code with miney should be the import statement and the creation of the miney object "mt". This will
connect miney to your already running Minetest.
::
import miney
mt = miney.Minetest()
.. Important::
Whenever you see a object "mt" in the documentation, it was created with this line!

2
docs/requirements.txt Normal file
View File

@ -0,0 +1,2 @@
Sphinx==3.3.1
sphinx_rtd_theme==0.5.0

55
docs/tech_background.rst Normal file
View File

@ -0,0 +1,55 @@
.. image:: python-logo.png
:alt: Python logo
:align: right
Technical Background
=====================
This page provides an inside view of how Miney works.
Miney's basic idea is, to use `Minetest <https://www.minetest.net/>`_ with `Python <https://www.python.org/>`_.
Minetest's main programming language (besides C++) is `Lua <https://www.lua.org/>`_ and it provides an mighty Lua-API for mod programming.
But Lua isn't the ideal programming language to start programming and mod programming isn't fun,
if you just want to play around with a sandbox.
So we need something like an interface that is accessible by Python.
The interface
------------------------------
For this we've written the `Mineysocket <https://github.com/miney-py/mineysocket>`_ mod as a regular Lua mod.
This mod opens a network port and receives JSON encoded commands.
The most important command is the "lua" command, where it just executes the received Lua code and
sends any return value back.
Miney is using this lua command to execute Lua code inside Minetest.
.. note::
**And you can use Miney without knowing any Lua or even seeing a single line of Lua code.**
Mineysocket, Windows and the Miney distribution
----------------------------------------------------
Python is the language with batteries included and it ships with a very complete library for nearly everything.
In contrast Lua has the batteries explicitly excluded, so there are nearly no libraries and it misses also a
network library.
So we need a Lua extension for networking, thats `luasocket <https://github.com/diegonehab/luasocket>`_.
And an extension for JSON, thats `lua-cjson <https://luarocks.org/modules/openresty/lua-cjson>`_
Under Linux this should be no big deal, just install these packages (most distributions provide them) and you are ready to go.
Windows
^^^^^^^^^^^^
It isn't that easy for Minetest on Windows. The Minetest binary's where compiled with Visual Studio and the extension
has to be linked against minetest also with the same version of Visual Studio.
So the best way under windows is, to compile Minetest and the Lua extensions by yourself with the same Visual Studio.
And when we already do this, why not replace Lua with `lua-jit <https://luajit.org/>`_ for more speed?
And when we've done all this, why not also bundle a actual python interpreter? And why not preinstall Miney in this
interpreter? Now it would be nice to have a comfortable `launcher <https://github.com/miney-py/launcher>`_.
We've done all this for windows and we call it `Miney Distibution <https://github.com/miney-py/miney_distribution/releases>`_.

View File

@ -0,0 +1,23 @@
from miney.minetest import Minetest
mt = Minetest(server='127.0.0.1', port=29999, playername='bvn13')
print("Connected to", mt)
players = mt.player
if len(players):
mt.chat.send_to_all("I'm running the example script...")
print("Player positions:")
while True:
for player in players:
standing_position = player.position
standing_position["y"] = standing_position["y"] - 0.5 # Position of the block under my feet
print("\r", player.name, player.position, player.look_horizontal, player.look_vertical, mt.node.get(standing_position), end='')
else:
raise Exception("There is no player on the server but we need at least one...")

8
examples/Chatbot.py Normal file
View File

@ -0,0 +1,8 @@
"""
This example shows a simple chatbot, that listens on commands but also on any messages.
"""
from miney import Minetest
mt = Minetest()

73
examples/donut.py Normal file
View File

@ -0,0 +1,73 @@
"""
The MIT License (MIT)
Pycraft Mod Copyright (c) Giuseppe Menegoz (@gmenegoz) & Alessandro Norfo (@alenorfo)
RaspberryJamMod Copyright (c) 2015 Alexander R. Pruss
Lua Copyright (c) 1994<EFBFBD>2015 Lua.org, PUC-Rio.
luasocket Copyright (c) Diego Nehab (with socket.lua changed by ARP to load x64/x86 as needed, and minetest compatibility)
tools.lua adapted from lua-websockets Copyright (c) 2012 Gerhard Lipp (with base64 inactivated and minetest compatibility)
base64.lua Copyright (c) 2014 Mark Rogaski and (C) 2012 Paul Moore (changed by ARP to MIME encoding and minetest compatibility)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
import math
import miney
def draw_donut(mt, mcx, mcy, mcz, R, r, mcblock):
positions = []
for x in range(-R - r, R + r):
for y in range(-R - r, R + r):
xy_dist = math.sqrt(x ** 2 + y ** 2)
if xy_dist > 0:
ringx = x / xy_dist * R # nearest point on major ring
ringy = y / xy_dist * R
ring_dist_sq = (x - ringx) ** 2 + (y - ringy) ** 2
for z in range(-R - r, R + r):
if ring_dist_sq + z ** 2 <= r ** 2:
positions.append(
{
"x": (mcx + x),
"y": (mcz + y),
"z": (mcy + z)
}
)
print("Spawning", len(positions), "nodes of", mcblock)
print(positions)
mt.node.set(nodes=positions, name=mcblock)
if miney.is_miney_available():
mt = miney.Minetest(server='127.0.0.1', port=29999, playername='bvn13')
playerPos = mt.player[0].position
draw_donut(mt, playerPos["x"], playerPos["y"] + 1, playerPos["z"], 4, 2, 'basenodes:dirt_with_grass')
#mt.chat.send_to_all(mt.node.type.default.glass + " donut done")
#print(mt.node.type.default.glass + " donut done")
draw_donut(mt, playerPos["x"], playerPos["y"] + 1, playerPos["z"], 4, 1, 'basenodes:sand')
#mt.chat.send_to_all(mt.node.type.default.lava_source + " donut done")
#print(mt.node.type.default.lava_source + " donut done")
else:
raise miney.MinetestRunError("Please start Minetest with the miney game")

View File

@ -0,0 +1,4 @@
import miney
mt = miney.Minetest

17
miney/__init__.py Normal file
View File

@ -0,0 +1,17 @@
"""
Miney is the python interface to minetest
"""
from .minetest import Minetest
from .player import Player
from .chat import Chat
from .node import Node
from .lua import Lua
from .inventory import Inventory
from .exceptions import *
from .tool import ToolIterable
from .player import PlayerIterable
from .helper import *
__version__ = "0.2.2"
default_playername = "MineyPlayer"

31
miney/callback.py Normal file
View File

@ -0,0 +1,31 @@
import string
from random import choices
import miney
class Callback:
"""Register callbacks inside minetest, to receive events from Lua"""
def __init__(self, mt: miney.Minetest):
self.mt = mt
def activate(self, event: str, callback: callable, parameters: dict = None):
"""
Register a callback for an event.
:param event: Event to register for
:param callback: The function to be called on events
:param parameters: Some events need parameters
:return: None
"""
# Match answer to request
result_id = ''.join(choices(string.ascii_lowercase + string.ascii_uppercase + string.digits, k=6))
self.mt.send(
{
'activate_event':
{
'event': event,
},
'id': result_id
}
)

58
miney/chat.py Normal file
View File

@ -0,0 +1,58 @@
from typing import Dict
import miney
class Chat:
"""
Chat functions.
"""
def __init__(self, mt: miney.Minetest):
self.mt = mt
def __repr__(self):
return '<minetest chat functions>'
def send_to_all(self, message: str) -> None:
"""
Send a chat message to all connected players.
:param message: The chat message
:return: None
"""
self.mt.lua.run("minetest.chat_send_all('{}')".format(message.replace("\'", "\\'")))
def send_to_player(self, playername: str, message: str):
return self.mt.lua.run("return minetest.chat_send_player({}, {}})".format(playername, message))
def format_message(self, playername: str, message: str):
return self.mt.lua.run("return minetest.format_chat_message({}}, {}})".format(playername, message))
def registered_commands(self):
return self.mt.lua.run("return minetest.registered_chatcommands")
def register_command(self, name, parameter: str, callback_function, description: str = "", privileges: Dict = None):
if isinstance(callback_function, callable):
pass
elif isinstance(callback_function, str):
pass
return self.mt.lua.run(
"""
return minetest.register_chatcommand(
{name},
{{params = {params}, description={description}, privs={privs}, func={func}}
)""".format(
name=name,
params=parameter,
description=description,
privs=self.mt.lua.dumps(privileges) if privileges else "{}",
func=callback_function
)
)
def override_command(self, definition):
pass
def unregister_command(self, name: str):
return self.mt.lua.run("return minetest.register_chatcommand({})".format(name))

54
miney/exceptions.py Normal file
View File

@ -0,0 +1,54 @@
# Minetest exceptions
class MinetestRunError(Exception):
"""
Error: minetest was not found.
"""
pass
class LuaError(Exception):
"""
Error during Lua code execution.
"""
pass
class LuaResultTimeout(Exception):
"""
The answer from Lua takes to long.
"""
pass
class DataError(Exception):
"""
Malformed data received.
"""
pass
class AuthenticationError(Exception):
"""
Authentication error.
"""
pass
class SessionReconnected(Exception):
"""
We had to reconnect and reauthenticate.
"""
pass
# Player exceptions
class PlayerInvalid(Exception):
pass
class PlayerOffline(Exception):
pass
class NoValidPosition(Exception):
pass

132
miney/helper.py Normal file
View File

@ -0,0 +1,132 @@
import socket
import time
import os
import platform
import subprocess
import webbrowser
import miney
def is_miney_available(ip: str = "127.0.0.1", port: int = 29999, timeout: int = 1.0) -> bool:
"""
Check if there is a running miney game available on an optional given host and/or port.
This functions pings mineysocket and waits **timeout** seconds for a pong.
:param ip: Optional IP or hostname
:param port: Optional port
:param timeout: Optional timeout
:return: True or False
"""
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(timeout)
try:
s.connect((ip, int(port)))
s.send(b"ping\n")
reply = s.recv(4096).decode()
if reply == "pong\n":
return True
else:
return False
except (socket.timeout, ConnectionResetError, ConnectionRefusedError):
return False
finally:
s.close()
def run_miney_game():
"""
Run minetest with the miney world. Miney will look for the minetest executable in common places for itself,
but it's also possible to provide the path as parameter or as environment variable :envvar:`MINETEST_BIN`.
:return: None
"""
if is_miney_available():
raise miney.MinetestRunError("A miney game is already running")
else:
run_minetest(show_menu=False)
wait = 0
while wait < 12:
time.sleep(1)
available = is_miney_available()
if available:
time.sleep(2) # some extra time to get everything initialized
return True
wait = wait + 1
raise miney.MinetestRunError("Timeout while waiting for minetest with an open mineysocket")
def run_minetest(
minetest_path: str = None,
show_menu: bool = True,
world_path: str = "Miney",
seed: str = "746036489947438842"
) -> None:
"""
Run minetest. Miney will look for the minetest executable in common places for itself,
but it's also possible to provide the path as parameter or as environment variable 'MINETEST_BIN'.
:param minetest_path: Path to the minetest executable
:param show_menu: Start in the world or in the menu
:param world_path: Optional world path
:param seed: Optional world seed
:return: None
"""
if not minetest_path:
if os.environ.get('MINETEST_BIN') and os.path.isfile(os.environ.get('MINETEST_BIN')):
minetest_path = os.environ['MINETEST_BIN']
else:
if platform.system() == 'Windows':
exe_name = "minetest.exe"
else:
exe_name = "minetest"
# we have to guess the path
possible_paths = [
os.path.join(os.getcwd(), "Minetest", "bin"),
]
for p in possible_paths:
path = os.path.join(p, exe_name)
if os.path.isfile(path):
minetest_path = os.path.join(p, exe_name)
break
else:
raise miney.MinetestRunError("Minetest was not found")
if world_path == "Miney":
world_path = os.path.abspath(os.path.join(minetest_path, "..", "..", "worlds", "miney"))
if not os.path.isdir(world_path): # We have to create the default world
if not os.path.isdir(os.path.abspath(os.path.join(world_path, "..", "..", "worlds"))):
os.mkdir(os.path.abspath(os.path.join(world_path, "..", "..", "worlds")))
os.mkdir(world_path)
with open(os.path.join(world_path, "world.mt"), "w") as world_config_file:
world_config_file.write(
"enable_damage = true\ncreative_mode = false\ngameid = minetest\nplayer_backend = sqlite3\n"
"backend = sqlite3\nauth_backend = sqlite3\nload_mod_mineysocket = true\nserver_announce = false\n"
)
with open(os.path.join(world_path, "map_meta.txt"), "w") as world_meta_file:
world_meta_file.write(f"seed = {seed}")
if not os.path.isdir(os.path.abspath(os.path.join(minetest_path, "..", "..", "mods", "mineysocket"))):
raise miney.MinetestRunError("Mineysocket mod is not installed")
# todo: run_minetest - implementation for linux/macos
if show_menu:
subprocess.Popen(
f"{minetest_path} "
f"--world \"{world_path}\" --name {miney.default_playername} --address \"\""
)
else:
subprocess.Popen(
f"{minetest_path} "
f"--go --world \"{world_path}\" --name {miney.default_playername} --address \"\""
)
def doc() -> None:
"""
Open the documention in the webbrower. This is just a shortcut for IDLE or the python interactive console.
:return: None
"""
webbrowser.open("https://miney.readthedocs.io/en/latest/")

39
miney/inventory.py Normal file
View File

@ -0,0 +1,39 @@
import miney
class Inventory:
"""
Inventories are places to store items, like Chests or player inventories.
"""
def __init__(self, minetest: miney.Minetest, parent: object):
self.mt = minetest
self.parent = parent
def add(self, item: str, amount: int = 1) -> None:
"""
Add an item to an inventory. Possible items can be obtained from :attr:`~miney.Node.type`.
:param item: item type
:param amount: item amount
:return: None
"""
if isinstance(self.parent, miney.Player):
self.mt.lua.run(
f"minetest.get_inventory("
f"{{type = \"player\", name = \"{self.parent.name}\"}}"
f"):add_item(\"main\", ItemStack(\"{item} {amount}\"))"
)
def remove(self, item: str, amount: int = 1) -> None:
"""
Remove an item from an inventory. Possible items can be obtained from mt.node.type.
:param item: item type
:param amount: item amount
:return: None
"""
if isinstance(self.parent, miney.Player):
self.mt.lua.run(
f"minetest.get_inventory({{type = \"player\", "
f"name = \"{self.parent.name}\"}}):remove_item(\"main\", ItemStack(\"{item} {amount}\"))")

73
miney/lua.py Normal file
View File

@ -0,0 +1,73 @@
import re
import string
from random import choices
import miney
class Lua:
"""
Lua specific functions.
"""
def __init__(self, mt: miney.Minetest):
self.mt = mt
def run(self, lua_code: str, timeout: float = 10.0):
"""
Run load code on the minetest server.
:param lua_code: Lua code to run
:param timeout: How long to wait for a result
:return: The return value. Multiple values as a list.
"""
# generates nearly unique id's (under 1000 collisions in 10 million values)
result_id = ''.join(choices(string.ascii_lowercase + string.ascii_uppercase + string.digits, k=6))
self.mt.send({"lua": lua_code, "id": result_id})
try:
return self.mt.receive(result_id, timeout=timeout)
except miney.SessionReconnected:
# We rerun the code, cause he was dropped during reconnect
return self.run(lua_code, timeout=timeout)
def run_file(self, filename):
"""
Loads and runs Lua code from a file. This is useful for debugging, cause Minetest can throws errors with
correct line numbers. It's also easier usable with a Lua capable IDE.
:param filename:
:return:
"""
with open(filename, "r") as f:
return self.run(f.read())
def dumps(self, data) -> str:
"""
Convert python data type to a string with lua data type.
:param data: Python data
:return: Lua data
"""
# credits:
# https://stackoverflow.com/questions/54392760/serialize-a-dict-as-lua-table/54392761#54392761
if type(data) is str:
return '"{}"'.format(re.escape(data))
if type(data) in (int, float):
return '{}'.format(data)
if type(data) is bool:
return data and "true" or "false"
if type(data) is list:
l = "{"
l += ", ".join([self.dumps(item) for item in data])
l += "}"
return l
if type(data) is dict:
t = "{"
t += ", ".join(
[
'[\"{}\"]={}'.format(re.escape(k), self.dumps(v)) for k, v in data.items()
]
)
t += "}"
return t
raise ValueError("Unknown type {}".format(type(data)))

382
miney/minetest.py Normal file
View File

@ -0,0 +1,382 @@
import socket
import json
import math
from typing import Dict, Union
import time
import string
from random import choices
import miney
class Minetest:
"""__init__([server, playername, password, [port]])
The Minetest server object. All other objects are accessable from here. By creating an object you connect to Minetest.
**Parameters aren't required, if you run miney and minetest on the same computer.**
*If you connect over LAN or Internet to a Minetest server with installed mineysocket, you have to provide a valid
playername with a password:*
::
>>> mt = Minetest("192.168.0.2", "MyNick", "secret_password")
Account creation is done by starting Minetest and connect to a server with a username
and password. https://wiki.minetest.net/Getting_Started#Play_Online
:param str server: IP or DNS name of an minetest server with installed apisocket mod
:param str playername: A name to identify yourself to the server.
:param str password: Your password
:param int port: The apisocket port, defaults to 29999
"""
def __init__(self, server: str = "127.0.0.1", playername: str = None, password: str = "", port: int = 29999):
"""
Connect to the minetest server.
:param server: IP or DNS name of an minetest server with installed apisocket mod
:param port: The apisocket port, defaults to 29999
"""
self.server = server
self.port = port
if playername:
self.playername = playername
else:
self.playername = miney.default_playername
self.password = password
# setup connection
self.connection = None
self._connect()
self.event_queue = [] # List for collected but unprocessed events
self.result_queue = {} # List for unprocessed results
self.callbacks = {}
self.clientid = None # The clientid we got from mineysocket after successful authentication
self._authenticate()
# objects representing local properties
self._lua: miney.lua.Lua = miney.Lua(self)
self._chat: miney.chat.Chat = miney.Chat(self)
self._node: miney.node.Node = miney.Node(self)
player = self.lua.run(
"""
local players = {}
for _,player in ipairs(minetest.get_connected_players()) do
table.insert(players,player:get_player_name())
end
return players
"""
)
if player:
player = [] if len(player) == 0 else player
else:
raise miney.DataError("Received malformed player data.")
self._player = miney.PlayerIterable(self, player)
self._tools_cache = self.lua.run(
"""
local nodes = {}
for name, def in pairs(minetest.registered_tools) do
table.insert(nodes, name)
end return nodes
"""
)
self._tool = miney.ToolIterable(self, self._tools_cache)
def _connect(self):
# setup connection
self.connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.connection.settimeout(2.0)
self.connection.connect((self.server, self.port))
def _authenticate(self):
"""
Authenticate to mineysocket.
:return: None
"""
# authenticate
self.send({"playername": self.playername, "password": self.password})
result = self.receive("auth")
if result:
if "auth_ok" not in result:
raise miney.AuthenticationError("Wrong playername or password")
else:
self.clientid = result[1] # Clientid = <IP>:<Port>
else:
raise miney.DataError("Unexpected authentication result.")
def send(self, data: Dict):
"""
Send json objects to the miney-socket.
:param data:
:return:
"""
chunk_size = 4096
raw_data: bytes = str.encode(json.dumps(data) + "\n")
try:
if len(raw_data) < chunk_size:
self.connection.sendall(raw_data)
else: # we need to break the message in chunks
for i in range(0, int(math.ceil((len(raw_data)/chunk_size)))):
self.connection.sendall(
raw_data[i * chunk_size:chunk_size + (i * chunk_size)]
)
time.sleep(0.01) # Give luasocket a chance to read the buffer in time
# todo: Protocol change, that every chunked message needs a response before sending the next
except ConnectionAbortedError:
self._connect()
self.send(data)
def receive(self, result_id: str = None, timeout: float = None) -> Union[str, bool]:
"""
Receive data and events from minetest.
With an optional result_id this function waits for a result with that id by call itself until the right result
was received. **If lua.run() was used this is not necessary, cause miney already takes care of it.**
With the optional timeout the blocking waiting time for data can be changed.
:Example to receive and print all events:
>>> while True:
>>> print("Event received:", mt.receive())
:param str result_id: Wait for this result id
:param float timeout: Block further execution until data received or timeout in seconds is over.
:rtype: Union[str, bool]
:return: Data from mineysocket
"""
def format_result(result_data):
if type(result_data["result"]) in (list, dict):
if len(result_data["result"]) == 0:
return
if len(result_data["result"]) == 1: # list with single element doesn't needs a list
return result_data["result"][0]
if len(result_data["result"]) > 1:
return tuple(result_data["result"])
else:
return result_data["result"]
# Check if we have to return something received earlier
if result_id in self.result_queue:
result = format_result(self.result_queue[result_id])
del self.result_queue[result_id]
return result
# Without a result_id we run an event callback
elif not result_id and len(self.event_queue):
result = self.event_queue[0]
del self.event_queue[0]
self._run_callback(result)
try:
# Set a new timeout to prevent long waiting for a timeout
if timeout:
self.connection.settimeout(timeout)
try:
# receive the raw data and try to decode json
data_buffer = b""
while "\n" not in data_buffer.decode():
data_buffer = data_buffer + self.connection.recv(4096)
data = json.loads(data_buffer.decode())
except socket.timeout:
raise miney.LuaResultTimeout()
# process data
if "result" in data:
if result_id: # do we need a specific result?
if data["id"] == result_id: # we've got the result we wanted
return format_result(data)
# We store this for later processing
self.result_queue[data["id"]] = data
elif "error" in data:
if data["error"] == "authentication error":
if self.clientid:
# maybe a server restart or timeout. We just reauthenticate.
self._authenticate()
raise miney.SessionReconnected()
else: # the server kicked us
raise miney.AuthenticationError("Wrong playername or password")
else:
raise miney.LuaError("Lua-Error: " + data["error"])
elif "event" in data:
self._run_callback(data)
# if we don't got our result we have to receive again
if result_id:
self.receive(result_id)
except ConnectionAbortedError:
self._connect()
self.receive(result_id, timeout)
def on_event(self, name: str, callback: callable) -> None:
"""
Sets a callback function for specific events.
:param name: The name of the event
:param callback: A callback function
:return: None
"""
# Match answer to request
result_id = ''.join(choices(string.ascii_lowercase + string.ascii_uppercase + string.digits, k=6))
self.callbacks[name] = callback
self.send({'activate_event': {'event': name}, 'id': result_id})
def _run_callback(self, data: dict):
if data['event'] in self.callbacks:
# self.callbacks[data['event']](**data['event']['params'])
if type(data['params']) is dict:
self.callbacks[data['event']](**data['params'])
elif type(data['params']) is list:
self.callbacks[data['event']](*data['params'])
@property
def chat(self):
"""
Object with chat functions.
:Example:
>>> mt.chat.send_to_all("My chat message")
:return: :class:`miney.Chat`: chat object
"""
return self._chat
@property
def node(self):
"""
Manipulate and get information's about nodes.
:return: :class:`miney.Node`: Node manipulation functions
"""
return self._node
def log(self, line: str):
"""
Write a line in the servers logfile.
:param line: The log line
:return: None
"""
return self.lua.run('minetest.log("action", "{}")'.format(line))
@property
def player(self):
"""
Get a single players object.
:Examples:
Make a player 5 times faster:
>>> mt.player.MyPlayername.speed = 5
Use a playername from a variable:
>>> player = "MyPlayername"
>>> mt.player[player].speed = 5
Get a list of all players
>>> list(mt.player)
[<minetest player "MineyPlayer">, <minetest player "SecondPlayer">, ...]
:return: :class:`miney.Player`: Player related functions
"""
return self._player
@property
def lua(self):
"""
Functions to run Lua inside Minetest.
:return: :class:`miney.Lua`: Lua related functions
"""
return self._lua
@property
def time_of_day(self) -> int:
"""
Get and set the time of the day between 0 and 1, where 0 stands for midnight, 0.5 for midday.
:return: time of day as float.
"""
return self.lua.run("return minetest.get_timeofday()")
@time_of_day.setter
def time_of_day(self, value: float):
if 0 <= value <= 1:
self.lua.run("return minetest.set_timeofday({})".format(value))
else:
raise ValueError("Time value has to be between 0 and 1.")
@property
def settings(self) -> dict:
"""
Receive all server settings defined in "minetest.conf".
:return: A dict with all non-default settings.
"""
return self.lua.run("return minetest.settings:to_table()")
@property
def tool(self) -> 'miney.ToolIterable':
"""
All available tools in the game, sorted by categories. In the end it just returns the corresponding
minetest tool string, so `mt.tool.default.axe_stone` returns the string 'default:axe_stone'.
It's a nice shortcut in REPL, cause with auto completion you have only pressed 2-4 keys to get to your type.
:Examples:
Directly access a tool:
>>> mt.tool.default.pick_mese
'default:pick_mese'
Iterate over all available types:
>>> for tool_type in mt.tool:
>>> print(tool_type)
default:shovel_diamond
default:sword_wood
default:shovel_wood
... (there should be around 34 different tools)
>>> print(len(mt.tool))
34
Get a list of all types:
>>> list(mt.tool)
['default:pine_tree', 'default:dry_grass_5', 'farming:desert_sand_soil', ...
Add a diamond pick axe to the first player's inventory:
>>> mt.player[0].inventory.add(mt.tool.default.pick_diamond, 1)
:rtype: :class:`ToolIterable`
:return: :class:`ToolIterable` object with categories. Look at the examples above for usage.
"""
return self._tool
def __del__(self) -> None:
"""
Close the connection to the server.
:return: None
"""
self.connection.close()
def __repr__(self):
return '<minetest server "{}:{}">'.format(self.server, self.port)
def __delete__(self, instance):
self.connection.close()

250
miney/node.py Normal file
View File

@ -0,0 +1,250 @@
import miney
from typing import Union
from copy import deepcopy
class Node:
"""
Manipulate and get information's about nodes.
**Node manipulation is currently tested for up to 25.000 nodes, more optimization will come later**
"""
def __init__(self, mt: miney.Minetest):
self.mt = mt
self._types_cache = self.mt.lua.run(
"""
local nodes = {}
for name, def in pairs(minetest.registered_nodes) do
table.insert(nodes, name)
end return nodes
"""
)
self._types = TypeIterable(self, self._types_cache)
@property
def type(self) -> 'TypeIterable':
"""
All available node types in the game, sorted by categories. In the end it just returns the corresponding
minetest type string, so `mt.node.types.default.dirt` returns the string 'default:dirt'.
It's a nice shortcut in REPL, cause with auto completion you have only pressed 2-4 keys to get to your type.
:Examples:
Directly access a type:
>>> mt.node.type.default.dirt
'default:dirt'
Iterate over all available types:
>>> for node_type in mt.node.type:
>>> print(node_type)
default:pine_tree
default:dry_grass_5
farming:desert_sand_soil
... (there should be over 400 different types)
>>> print(len(mt.node.type))
421
Get a list of all types:
>>> list(mt.node.type)
['default:pine_tree', 'default:dry_grass_5', 'farming:desert_sand_soil', ...
Add 99 dirt to player "IloveDirt"'s inventory:
>>> mt.player.IloveDirt.inventory.add(mt.node.type.default.dirt, 99)
:rtype: :class:`TypeIterable`
:return: :class:`TypeIterable` object with categories. Look at the examples above for usage.
"""
return self._types
def set(self, nodes: Union[dict, list], name: str = None, offset: dict = None) -> None:
"""
Set a single or multiple nodes at given position to another node type
(something like mt.node.type.default.apple).
You can get a list of all available nodes with :attr:`~miney.Minetest.node.type`
A node is defined as a dict with these keys:
* "x", "y", and "z" keys to define the absolute position
* "name" for a the node type like "default:dirt" (you can also get that from mt.node.type.default.dirt).
Dicts without name will be set as "air"
* some other optional minetest parameters
**The nodes parameter can be a single dict with the above parameters
or a list of these dicts for bulk spawning.**
:Examples:
Set a single node over :
>>> mt.node.set(mt.player[0].nodes, mt.node)
:param nodes: A dict or a list of dicts with node definitions
:param name: a type name like "default:dirt" as string or from :attr:`~miney.Minetest.node.type`. This overrides
node names defined in the :attr:`nodes` dict
:param offset: A dict with "x", "y", "z" keys. All node positions will be added with this values.
"""
_nodes = deepcopy(nodes)
if offset:
if not all(pos in ['x', 'y', 'z'] for pos in offset):
raise ValueError("offset parameter dict needs y, x and z keys")
# Set a single node
if type(_nodes) is dict:
if offset:
_nodes["x"] = _nodes["x"] + offset["x"]
_nodes["y"] = _nodes["y"] + offset["y"]
_nodes["z"] = _nodes["z"] + offset["z"]
if name:
_nodes["name"] = name
self.mt.lua.run(f"minetest.set_node({self.mt.lua.dumps(_nodes)}, {{name=\"{_nodes['name']}\"}})")
# Set many blocks
elif type(_nodes) is list:
lua = ""
# Loop over nodes, modify name/type, position/offset and generate lua code
for node in _nodes:
# default name to 'air'
if "name" not in node and not name:
node["name"] = "air"
if name:
if "name" not in node or (node["name"] != "air" and node["name"] != "ignore"):
node["name"] = name
if offset:
node["x"] = node["x"] + offset["x"]
node["y"] = node["y"] + offset["y"]
node["z"] = node["z"] + offset["z"]
if node["name"] != "ignore":
lua = lua + f"minetest.set_node(" \
f"{self.mt.lua.dumps({'x': node['x'], 'y': node['y'], 'z': node['z']})}, " \
f"{{name=\"{node['name']}\"}})\n"
self.mt.lua.run(lua)
def get(self, position: dict, position2: dict = None, relative: bool = True,
offset: dict = None) -> Union[dict, list]:
"""
Get the node at given position. It returns a dict with the node definition.
This contains the "x", "y", "z", "param1", "param2" and "name" keys, where "name" is the node type like
"default:dirt".
If also position2 is given, this function returns a list of dicts with node definitions. This list contains a
cuboid of definitions with the diagonal between position and position2.
You can get a list of all available node types with :attr:`~miney.Minetest.node.type`.
:param position: A dict with x,y,z keys
:param position2: Another point, to get multiple nodes as a list
:param relative: Return relative or absolute positions
:param offset: A dict with "x", "y", "z" keys. All node positions will be added with this values.
:return: The node type on this position
"""
if type(position) is dict and not position2: # for a single node
_position = deepcopy(position)
if offset:
_position["x"] = _position["x"] + offset["x"]
_position["y"] = _position["y"] + offset["y"]
_position["z"] = _position["z"] + offset["z"]
node = self.mt.lua.run(f"return minetest.get_node({self.mt.lua.dumps(position)})")
node["x"] = position["x"]
node["y"] = position["y"]
node["z"] = position["z"]
return node
elif type(position) is dict and type(position2) is dict: # Multiple nodes
_position = deepcopy(position)
_position2 = deepcopy(position2)
if offset:
_position["x"] = _position["x"] + offset["x"]
_position["y"] = _position["y"] + offset["y"]
_position["z"] = _position["z"] + offset["z"]
_position2["x"] = _position2["x"] + offset["x"]
_position2["y"] = _position2["y"] + offset["y"]
_position2["z"] = _position2["z"] + offset["z"]
nodes_relative = """
node["x"] = x - start_x
node["y"] = y - start_y
node["z"] = z - start_z
"""
nodes_absolute = """
node["x"] = x
node["y"] = y
node["z"] = z
"""
return self.mt.lua.run(
f"""
pos1 = {self.mt.lua.dumps(_position)}
pos2 = {self.mt.lua.dumps(_position2)}
minetest.load_area(pos1, pos2)
nodes = {{}}
if pos1.x <= pos2.x then start_x = pos1.x end_x = pos2.x else start_x = pos2.x end_x = pos1.x end
if pos1.y <= pos2.y then start_y = pos1.y end_y = pos2.y else start_y = pos2.y end_y = pos1.y end
if pos1.z <= pos2.z then start_z = pos1.z end_z = pos2.z else start_z = pos2.z end_z = pos1.z end
for x = start_x, end_x do
for y = start_y, end_y do
for z = start_z, end_z do
node = minetest.get_node({{x = x, y = y, z = z}})
{nodes_relative if relative else nodes_absolute}
nodes[#nodes+1] = node -- append node
end
end
end
return nodes""", timeout=180)
def __repr__(self):
return '<minetest node functions>'
class TypeIterable:
"""Node type, implemented as iterable for easy autocomplete in the interactive shell"""
def __init__(self, parent, nodes_types=None):
self.__parent = parent
if nodes_types:
# get type categories list
type_categories = {}
for ntype in nodes_types:
if ":" in ntype:
type_categories[ntype.split(":")[0]] = ntype.split(":")[0]
for tc in dict.fromkeys(type_categories):
self.__setattr__(tc, TypeIterable(parent))
# values to categories
for ntype in nodes_types:
if ":" in ntype:
self.__getattribute__(ntype.split(":")[0]).__setattr__(ntype.split(":")[1], ntype)
else:
self.__setattr__(ntype, ntype) # for 'air' and 'ignore'
def __iter__(self):
# todo: list(mt.node.type.default) should return only default group
return iter(self.__parent._types_cache)
def __getitem__(self, item_key):
if type(self.__parent) is not type(self): # if we don't have a category below
return self.__getattribute__(item_key)
if item_key in self.__parent.node_types:
return item_key
else:
if type(item_key) == int:
return self.__parent.node_types[item_key]
raise IndexError("unknown node type")
def __len__(self):
return len(self.__parent._types_cache)

335
miney/player.py Normal file
View File

@ -0,0 +1,335 @@
from typing import Union
import miney
# todo: set modes creative/survival -> Not possible without installed minetest mods
class Player:
"""
A player of the minetest server.
"""
def __init__(self, minetest: miney.Minetest, name):
"""
Initialize the player object.
:param minetest: Parent minetest object
:param name: Player name
"""
self.mt = minetest
self.name = name
# get user data: password hash, last login, privileges
data = self.mt.lua.run("return minetest.get_auth_handler().get_auth('{}')".format(self.name))
if data and all(k in data for k in ("password", "last_login", "privileges")): # if we have all keys
self.password = data["password"]
self.last_login = data["last_login"]
self.privileges = data["privileges"]
else:
raise miney.PlayerInvalid("There is no player with that name")
self.inventory: miney.Inventory = miney.Inventory(minetest, self)
"""Manipulate player's inventory.
:Example to add 99 dirt to player "IloveDirt"'s inventory:
>>> import miney
>>> mt = miney.Minetest()
>>> mt.player.IloveDirt.inventory.add(mt.node.type.default.dirt, 99)
:Example to remove 99 dirt from player "IhateDirt"'s inventory:
>>> import miney
>>> mt = miney.Minetest()
>>> mt.player.IhateDirt.inventory.remove(mt.node.type.default.dirt, 99)
"""
def __repr__(self):
return '<minetest player "{}">'.format(self.name)
@property
def is_online(self) -> bool:
"""
Returns the online status of this player.
:return: True or False
"""
# TODO: Better check without provoke a lua error
try:
if self.name == self.mt.lua.run(
"return minetest.get_player_by_name('{}'):get_player_name()".format(self.name)):
return True
except miney.LuaError:
return False
@property
def position(self) -> dict:
"""
Get the players current position.
:return: A dict with x,y,z keys: {"x": 0, "y":1, "z":2}
"""
try:
return self.mt.lua.run("return minetest.get_player_by_name('{}'):get_pos()".format(self.name))
except miney.LuaError:
raise miney.PlayerOffline("The player has no position, he could be offline")
@position.setter
def position(self, values: dict):
"""
Set player position
:param values:
:return:
"""
if all(k in values for k in ("x", "y", "z")): # if we have all keys
self.mt.lua.run(
"return minetest.get_player_by_name('{}'):set_pos({{x = {}, y = {}, z = {}}})".format(
self.name,
values["x"],
values["y"],
values["z"],
)
)
else:
raise miney.NoValidPosition(
"A valid position need x,y,z values in an dict ({\"x\": 12, \"y\": 70, \"z\": 12}).")
@property
def speed(self) -> int:
"""
Get or set the players speed. Default is 1.
:return: Float
"""
return self.mt.lua.run(
"return minetest.get_player_by_name('{}'):get_physics_override()".format(self.name))["speed"]
@speed.setter
def speed(self, value: int):
self.mt.lua.run(
"return minetest.get_player_by_name('{}'):set_physics_override({{speed = {}}})".format(self.name, value))
@property
def jump(self):
"""
Get or set the players jump height. Default is 1.
:return: Float
"""
return self.mt.lua.run(
"return minetest.get_player_by_name('{}'):get_physics_override()".format(self.name))["jump"]
@jump.setter
def jump(self, value):
self.mt.lua.run(
"return minetest.get_player_by_name('{}'):set_physics_override({{jump = {}}})".format(self.name, value))
@property
def gravity(self):
"""
Get or set the players gravity. Default is 1.
:return: Float
"""
return self.mt.lua.run(
"return minetest.get_player_by_name('{}'):get_physics_override()".format(self.name))["gravity"]
@gravity.setter
def gravity(self, value):
self.mt.lua.run(
"return minetest.get_player_by_name('{}'):set_physics_override({{gravity = {}}})".format(self.name, value))
@property
def look(self) -> dict:
"""
Get and set look in radians. Horizontal angle is counter-clockwise from the +z direction. Vertical angle ranges
between -pi/2 (~-1.563) and pi/2 (~1.563), which are straight up and down respectively.
:return: A dict like {'v': 0.34, 'h': 2.50} where h is horizontal and v = vertical
"""
return self.mt.lua.run(
f"return {{"
f"h=minetest.get_player_by_name('{self.name}'):get_look_horizontal(), "
f"v=minetest.get_player_by_name('{self.name}'):get_look_vertical()"
f"}}"
)
@look.setter
def look(self, value: dict):
if type(value) is dict:
if "v" in value and "h" in value:
if type(value["v"]) in [int, float] and type(value["h"]) in [int, float]:
self.mt.lua.run(
f"""
local player = minetest.get_player_by_name('{self.name}')
player:set_look_horizontal({value["h"]})
player:set_look_vertical({value["v"]})
return true
"""
)
else:
raise TypeError("values for v or h aren't float or int")
else:
raise TypeError("There isn't the required v or h key in the dict")
else:
raise TypeError("The value isn't a dict, as required. Use a dict in the form: {\"h\": 1.1, \"v\": 1.1}")
@property
def look_vertical(self):
"""
Get and set pitch in radians. Angle ranges between -pi/2 (~-1.563) and pi/2 (~1.563), which are straight
up and down respectively.
:return: Pitch in radians
"""
return self.mt.lua.run("return minetest.get_player_by_name('{}'):get_look_vertical()".format(self.name))
@look_vertical.setter
def look_vertical(self, value):
self.mt.lua.run("return minetest.get_player_by_name('{}'):set_look_vertical({})".format(self.name, value))
@property
def look_horizontal(self):
"""
Get and set yaw in radians. Angle is counter-clockwise from the +z direction.
:return: Pitch in radians
"""
return self.mt.lua.run("return minetest.get_player_by_name('{}'):get_look_horizontal()".format(self.name))
@look_horizontal.setter
def look_horizontal(self, value):
self.mt.lua.run("return minetest.get_player_by_name('{}'):set_look_horizontal({})".format(self.name, value))
@property
def hp(self):
"""
Get and set the number of hitpoints (2 * number of hearts) between 0 and 20.
By setting his hitpoint to zero you instantly kill this player.
:return:
"""
return self.mt.lua.run(f"return minetest.get_player_by_name('{self.name}'):get_hp()")
@hp.setter
def hp(self, value: int):
if type(value) is int and value in range(0, 21):
self.mt.lua.run(
f"return minetest.get_player_by_name('{self.name}'):set_hp({value}, {{type=\"set_hp\"}})")
else:
raise ValueError("HP has to be between 0 and 20.")
@property
def breath(self):
return self.mt.lua.run(f"return minetest.get_player_by_name('{self.name}'):get_breath()")
@breath.setter
def breath(self, value: int):
if type(value) is int and value in range(0, 21):
self.mt.lua.run(
f"return minetest.get_player_by_name('{self.name}'):set_breath({value}, {{type=\"set_hp\"}})")
else:
raise ValueError("HP has to be between 0 and 20.")
@property
def fly(self) -> bool:
"""
Get and set the privilege to fly to this player. Press K to enable and disable fly mode.
As a shortcut you can set fly to a number instead if `True` to also changes the players speed to this number.
.. Example:
>>> mt.player.MineyPlayer.fly = True # the can player fly
>>> mt.player.MineyPlayer.fly = 5 # the can player fly 5 times faster
:return:
"""
return self.mt.lua.run(
f"""
local privs = minetest.get_player_privs(\"{self.name}\")
if privs["fly"] then
return true
else
return false
end
"""
)
@fly.setter
def fly(self, value: Union[bool, int]):
if value:
state = "true"
if type(value) is int:
if value > 0:
self.speed = value
else:
state = "false"
self.mt.lua.run(
f"""
local privs = minetest.get_player_privs(\"{self.name}\")
privs["fly"] = {state}
minetest.set_player_privs(\"{self.name}\", privs)
"""
)
@property
def creative(self) -> bool:
return self.mt.lua.run(
f"""
local privs = minetest.get_player_privs(\"{self.name}\")
if privs["creative"] then
return true
else
return false
end
"""
)
@creative.setter
def creative(self, value: bool):
if type(value) is not bool:
raise ValueError("creative needs to be true or false")
if value is True:
state = "true"
else:
state = "false"
luastring = f"""
local privs = minetest.get_player_privs(\"{self.name}\")
privs["creative"] = {state}
minetest.set_player_privs(\"{self.name}\", privs)
"""
self.mt.lua.run(
luastring
)
class PlayerIterable:
"""Player, implemented as iterable for easy autocomplete in the interactive shell"""
def __init__(self, minetest: miney.Minetest, online_players: list = None):
if online_players:
self.__online_players = online_players
self.__mt = minetest
# update list
for player in online_players:
self.__setattr__(player, miney.Player(minetest, player))
def __iter__(self):
player_object = []
for player in self.__online_players:
player_object.append(miney.Player(self.__mt, player))
return iter(player_object)
def __getitem__(self, item_key) -> Player:
if item_key in self.__online_players:
return self.__getattribute__(item_key)
else:
if type(item_key) == int:
return self.__getattribute__(self.__online_players[item_key])
raise IndexError("unknown player")
def __len__(self):
return len(self.__online_players)

37
miney/tool.py Normal file
View File

@ -0,0 +1,37 @@
class ToolIterable:
"""Tool type, implemented as iterable for easy autocomplete in the interactive shell"""
def __init__(self, parent, tool_types=None):
self.__parent = parent
if tool_types:
# get type categories list
type_categories = {}
for ntype in tool_types:
if ":" in ntype:
type_categories[ntype.split(":")[0]] = ntype.split(":")[0]
for tc in dict.fromkeys(type_categories):
self.__setattr__(tc, ToolIterable(parent))
# values to categories
for ttype in tool_types:
if ":" in ttype:
self.__getattribute__(ttype.split(":")[0]).__setattr__(ttype.split(":")[1], ttype)
else:
self.__setattr__(ttype, ttype) # for 'air' and 'ignore'
def __iter__(self):
# todo: list(mt.node.tool.default) should return only default group
return iter(self.__parent._tools_cache)
def __getitem__(self, item_key):
if item_key in self.__parent.node_types:
return item_key
else:
if type(item_key) == int:
return self.__parent.node_types[item_key]
raise IndexError("unknown node type")
def __len__(self):
return len(self.__parent._tools_cache)

65
miney/vector.py Normal file
View File

@ -0,0 +1,65 @@
# https://github.com/kirill-belousov/vector3d/blob/master/vector3d/point.py
from math import sqrt, acos, degrees
class Vector:
x = float()
y = float()
z = float()
def __init__(self, x: float = 0, y: float = 0, z: float = 0):
self.x = x
self.y = y
self.z = z
def __eq__(self, other):
if self.x == other.x and self.y == other.y and self.z == other.z:
return True
else:
return False
def __len__(self):
return int(self.length())
def __add__(self, o):
return Vector((self.x + o.x), (self.y + o.y), (self.z + o.z))
def __sub__(self, o):
return Vector((self.x - o.x), (self.y - o.y), (self.z - o.z))
def __mul__(self, o):
return (self.x * o.x) + (self.y * o.y) + (self.z * o.z)
def __iadd__(self, o):
self.x += o.x
self.y += o.y
self.z += o.z
return self
def __isub__(self, o):
self.x -= o.x
self.y -= o.y
self.z -= o.z
return self
def __neg__(self):
return Vector(-self.x, -self.y, -self.z)
def length(self):
return sqrt((self.x * self.x) + (self.y * self.y) + (self.z * self.z))
def normalize(self):
return Vector((self.x / self.length()), (self.y / self.length()), (self.z / self.length()))
def angle(a, b):
m = a.x * b.x + a.y * b.y + a.z * b.z
return degrees(acos(m / (a.length() * b.length())))
def horizontal_angle(a, b):
return angle(Vector(a.x, a.y, 0), Vector(b.x, b.y, 0))
def vertical_angle(a, b):
return angle(Vector(0, a.y, a.z), Vector(0, b.y, b.z))

1033
poetry.lock generated Normal file

File diff suppressed because it is too large Load Diff

19
pyproject.toml Normal file
View File

@ -0,0 +1,19 @@
[tool.poetry]
name = "minetest-network-api-client"
version = "0.1.0"
description = ""
authors = ["bvn13 <from.github@bvn13.me>"]
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.12"
sphinx = "^8.1.3"
sphinx-rtd-theme = "^3.0.2"
wheel = "^0.45.1"
twine = "^6.0.1"
pytest = "^8.3.4"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

2
pytest.ini Normal file
View File

@ -0,0 +1,2 @@
[pytest]
addopts = -x tests/

35
setup.py Normal file
View File

@ -0,0 +1,35 @@
import setuptools
import re, io
with open("README.md", "r") as fh:
long_description = fh.read()
# get version from package's __version__
__version__ = re.search(
r'__version__\s*=\s*[\'"]([^\'"]*)[\'"]', # It excludes inline comment too
io.open('miney/__init__.py', encoding='utf_8_sig').read()
).group(1)
setuptools.setup(
name="miney",
version=__version__,
author="Robert Lieback",
author_email="robertlieback@zetabyte.de",
description="The python interface to minetest",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://github.com/miney-py/miney",
project_urls={
"Documentation": "https://miney.readthedocs.io"
},
packages=["miney"],
classifiers=[
"Programming Language :: Python :: 3",
"Operating System :: OS Independent",
"Development Status :: 4 - Beta",
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
"Operating System :: OS Independent",
"Topic :: Games/Entertainment"
],
python_requires='>=3.6',
)

0
tests/__init__.py Normal file
View File

15
tests/conftest.py Normal file
View File

@ -0,0 +1,15 @@
"""
The place for fixtures
"""
import pytest
import miney
@pytest.fixture(scope="session")
def mt():
# if not miney.is_miney_available():
# assert miney.run_miney_game(), "Minetest with mineysocket isn't running."
mt = miney.Minetest()
assert len(mt.player) >= 1
return miney.Minetest()

109
tests/test_minetest.py Normal file
View File

@ -0,0 +1,109 @@
"""
Tests Minetest class' methods
"""
import socket
import miney
import pytest
def test_connection(mt: miney.Minetest):
"""
Test connection with some missconfigurations.
:param mt: fixture
:return: None
"""
# wrong server name
with pytest.raises(socket.gaierror) as e:
mt_fail = miney.Minetest("someunresolveableserver", "someuser", "somepass")
# wrong port / apisocket unreachable or not running
with pytest.raises((socket.timeout, ConnectionResetError)) as e:
mt_fail = miney.Minetest(port=12345)
def test_send_corrupt_data(mt: miney.Minetest):
"""
Send corrupt data, they shouldn't crash the server.
:param mt: fixture
:return: None
"""
mt.connection.sendto(
str.encode(
"}s876" + "\n"
),
("127.0.0.1", 29999)
)
with pytest.raises(miney.exceptions.LuaError):
mt.receive()
def test_minetest(mt: miney.Minetest):
"""
Test basic functionality.
:param mt: fixture
:return: None
"""
assert str(mt) == '<minetest server "{}:{}">'.format("127.0.0.1", "29999")
nodes = mt.node.type
assert "air" in nodes
assert "default:stone" in nodes
assert (mt.log("Pytest is running...")) is None
settings = mt.settings
assert "secure.trusted_mods" in settings
assert "name" in settings
assert 0 < mt.time_of_day < 1.1
mt.time_of_day = 0.99
assert 1 > mt.time_of_day > 0.95
mt.time_of_day = 0.5
assert 0.51 > mt.time_of_day > 0.49
def test_lua(mt: miney.Minetest):
"""
Test running lua code.
:param mt: fixture
:return: None
"""
with pytest.raises(miney.LuaError) as e:
mt.lua.run("thatshouldntworkatall")
# multiple return values
returnvalues = mt.lua.run(
"""
mytable = {}
mytable["var"] = 99
return 12 , "test", {8, "9"}, mytable
"""
)
assert returnvalues == (12, 'test', [8, '9'], {'var': 99})
def test_players(mt: miney.Minetest):
"""
Test player count and object creation.
:param mt: fixture
:return: None
"""
players = mt.player
assert str(type(players)) == "<class 'miney.player.PlayerIterable'>"
assert len(players) >= 1, "You should join the server for tests!"
assert (mt.chat.send_to_all("Pytest is running...")) is None
# get unknown player
with pytest.raises(AttributeError) as e:
x = mt.player.stupidname123
player = players[0]
assert isinstance(player, miney.player.Player)
assert len(player.name) > 0
assert str(player) == "<minetest player \"{}\">".format(player.name)

48
tests/test_nodes.py Normal file
View File

@ -0,0 +1,48 @@
"""
Test all node related.
"""
import miney
def test_node_types(mt: miney.Minetest):
assert len(mt.node.type) > 400
assert mt.node.type.default.dirt == "default:dirt"
assert mt.node.type["default"]["dirt"] == "default:dirt"
assert len(mt.node.type["default"]) > 400
def test_node_set_and_get(mt: miney.Minetest):
pos1 = {"x": 22, "y": 28, "z": 22}
mt.node.set(pos1, name=mt.node.type.default.dirt, offset={"x": -1, "y": -1, "z": -1})
pos1_node = mt.node.get(pos1)
assert "name" in pos1_node
assert pos1_node["name"] in mt.node.type
assert "param1" in pos1_node
assert "param2" in pos1_node
# we create a cube of dirt
nodes = []
for x in range(0, 9):
for y in range(0, 9):
for z in range(0, 9):
nodes.append({"x": pos1["x"] + x, "y": pos1["y"] + y, "z": pos1["z"] + z, "name": "default:dirt"})
# save for later restore
before = mt.node.get(nodes[0], nodes[-1], relative=False)
mt.node.set(nodes, name="default:dirt")
dirt_nodes = mt.node.get(nodes[0], nodes[-1])
assert dirt_nodes[0]["name"] in mt.node.type
assert dirt_nodes[0]["name"] == "default:dirt"
assert dirt_nodes[-1]["name"] in mt.node.type
assert dirt_nodes[-1]["name"] == "default:dirt"
assert "param1" in dirt_nodes[0]
assert "param2" in dirt_nodes[0]
mt.node.set(before, name="default:dirt")
before_nodes = mt.node.get(before[0], before[-1])
assert before_nodes[0]["name"] == before[0]["name"]
assert before_nodes[-1]["name"] == before[-1]["name"]

65
tests/test_player.py Normal file
View File

@ -0,0 +1,65 @@
import pytest
import miney
import math
from time import sleep
@pytest.fixture(scope="module")
def mt_player(mt: miney.Minetest):
return mt.player[0]
def test_player(mt: miney.Minetest, mt_player: miney.Player):
"""
Test player basics.
:param mt: fixture
:param mt_player: fixture
:return: None
"""
assert mt_player.is_online is True
position = mt_player.position
assert "x" in position
assert "y" in position
assert "z" in position
mt_player.gravity = 0
assert mt_player.gravity == 0
# set fly or player will go into free fall
mt_player.fly = True
assert mt_player.fly
mt_player.creative = True
assert mt_player.creative
mt_player.position = {"x": 12, "y": 8.5, "z": 12}
sleep(0.1) # give the value some time to get to the client
position = mt_player.position
assert 12.1 > position["x"] > 11.9
assert 9 > position["y"] > 8
assert 12.1 > position["z"] > 11.9
mt_player.gravity = 0.8
assert round(mt_player.gravity, 1) == 0.8
mt_player.speed = 2.0
assert mt_player.speed == 2.0
mt_player.jump = 2.0
assert mt_player.jump == 2.0
look_vertical = mt_player.look_vertical
assert 1.563 >= look_vertical >= -1.563 # 1.5620696544647 -1.5620696544647
mt_player.look_vertical = 1.5620696544647
sleep(0.01) # give the value some time to get to the client
assert 1.5622 > mt_player.look_vertical > 1.5616
look_horizontal = mt_player.look_horizontal
assert 6.25 > look_horizontal > 0 # 0 6.28
mt_player.look_horizontal = math.pi
sleep(0.1) # give the value some time to get to the client
assert 3.15 > mt_player.look_horizontal > 3.13
mt_player.inventory.add("default:axe_wood")