luanti-network-api-client/miney/node.py
2024-12-07 01:17:53 +03:00

251 lines
9.7 KiB
Python

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)