luanti-network-api-client/miney/minetest.py

383 lines
13 KiB
Python
Raw Normal View History

2024-12-07 01:17:53 +03:00
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()