working client
This commit is contained in:
commit
d6a06aa697
105
.gitignore
vendored
Normal file
105
.gitignore
vendored
Normal 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
22
.readthedocs.yml
Normal 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
165
LICENSE
Normal 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
22
README.md
Normal 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
5
dev-requirements.txt
Normal file
@ -0,0 +1,5 @@
|
||||
Sphinx
|
||||
sphinx_rtd_theme
|
||||
wheel
|
||||
twine
|
||||
pytest
|
64
devtools/luaconsole.py
Normal file
64
devtools/luaconsole.py
Normal 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
20
docs/Makefile
Normal 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
11
docs/_static/style.css
vendored
Normal 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
13
docs/_templates/layout.html
vendored
Normal 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
81
docs/conf.py
Normal 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
7
docs/helpers.rst
Normal 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
81
docs/index.rst
Normal 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
35
docs/make.bat
Normal 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
BIN
docs/minetest-logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
183
docs/minetest.svg
Normal file
183
docs/minetest.svg
Normal 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
BIN
docs/miney-logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
BIN
docs/miney-slogan.png
Normal file
BIN
docs/miney-slogan.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 49 KiB |
7
docs/objects/Chat.rst
Normal file
7
docs/objects/Chat.rst
Normal file
@ -0,0 +1,7 @@
|
||||
Chat
|
||||
====
|
||||
|
||||
Get controls of the chat.
|
||||
|
||||
.. autoclass:: miney.Chat
|
||||
:members:
|
5
docs/objects/Exceptions.rst
Normal file
5
docs/objects/Exceptions.rst
Normal file
@ -0,0 +1,5 @@
|
||||
Exceptions
|
||||
==========
|
||||
|
||||
.. automodule:: miney.exceptions
|
||||
:members:
|
7
docs/objects/Inventory.rst
Normal file
7
docs/objects/Inventory.rst
Normal file
@ -0,0 +1,7 @@
|
||||
Inventory
|
||||
=========
|
||||
|
||||
Lua related functions
|
||||
|
||||
.. autoclass:: miney.Inventory
|
||||
:members:
|
7
docs/objects/Lua.rst
Normal file
7
docs/objects/Lua.rst
Normal file
@ -0,0 +1,7 @@
|
||||
Lua
|
||||
===
|
||||
|
||||
Lua related functions
|
||||
|
||||
.. autoclass:: miney.Lua
|
||||
:members:
|
23
docs/objects/Minetest.rst
Normal file
23
docs/objects/Minetest.rst
Normal 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
13
docs/objects/Node.rst
Normal 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
7
docs/objects/Player.rst
Normal 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
20
docs/objects/index.rst
Normal 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
BIN
docs/python-logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
59
docs/quickstart.rst
Normal file
59
docs/quickstart.rst
Normal 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
2
docs/requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
Sphinx==3.3.1
|
||||
sphinx_rtd_theme==0.5.0
|
55
docs/tech_background.rst
Normal file
55
docs/tech_background.rst
Normal 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>`_.
|
23
examples/02_player_locator.py
Normal file
23
examples/02_player_locator.py
Normal 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
8
examples/Chatbot.py
Normal 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
73
examples/donut.py
Normal 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")
|
4
examples/spawn_a_house.py
Normal file
4
examples/spawn_a_house.py
Normal file
@ -0,0 +1,4 @@
|
||||
import miney
|
||||
|
||||
mt = miney.Minetest
|
||||
|
17
miney/__init__.py
Normal file
17
miney/__init__.py
Normal 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
31
miney/callback.py
Normal 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
58
miney/chat.py
Normal 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
54
miney/exceptions.py
Normal 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
132
miney/helper.py
Normal 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
39
miney/inventory.py
Normal 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
73
miney/lua.py
Normal 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
382
miney/minetest.py
Normal 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
250
miney/node.py
Normal 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
335
miney/player.py
Normal 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
37
miney/tool.py
Normal 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
65
miney/vector.py
Normal 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
1033
poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
pyproject.toml
Normal file
19
pyproject.toml
Normal 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
2
pytest.ini
Normal file
@ -0,0 +1,2 @@
|
||||
[pytest]
|
||||
addopts = -x tests/
|
35
setup.py
Normal file
35
setup.py
Normal 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
0
tests/__init__.py
Normal file
15
tests/conftest.py
Normal file
15
tests/conftest.py
Normal 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
109
tests/test_minetest.py
Normal 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
48
tests/test_nodes.py
Normal 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
65
tests/test_player.py
Normal 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")
|
Loading…
Reference in New Issue
Block a user