Code Examples

This section contains practical examples demonstrating how to use the Miney library. The code is included directly from the source files, so it’s always up-to-date.

Setup Checker (check_setup.py)

This script is the best starting point to verify that your Miney installation and server connection are working correctly.

View Code
 1"""
 2Miney Setup Checker
 3===================
 4
 5This script connects to a Luanti server to verify that both the server and
 6the Miney library are correctly configured. It gathers and displays key
 7information, such as the server version, online players, and the number of
 8registered nodes and tools.
 9
10This is the recommended first script to run after installing Miney to ensure
11your environment is ready.
12
13How to Run:
141. Make sure the `miney` mod is installed and enabled on your Luanti server.
152. Run this script from your terminal, providing connection details if needed:
16   python examples/check_setup.py [server] [port] [playername] [password]
17"""
18import logging
19import sys
20
21from miney import Luanti, LuantiConnectionError
22
23# --- Logger Setup ---
24# Configure a simple logger for clean and informative output
25logging.basicConfig(
26    level=logging.INFO,
27    format="%(asctime)s | %(levelname)-8s | %(message)s",
28    datefmt="%Y-%m-%d %H:%M:%S",
29)
30logger = logging.getLogger(__name__)
31
32
33if __name__ == "__main__":
34    # --- Connection Details ---
35    server = sys.argv[1] if len(sys.argv) > 1 else "127.0.0.1"
36    port = int(sys.argv[2]) if len(sys.argv) > 2 else 30000
37    username = sys.argv[3] if len(sys.argv) > 3 else "miney"
38    password = sys.argv[4] if len(sys.argv) > 4 else "ChangeThePassword!"
39
40    logger.info(f"Attempting to connect to {server}:{port} as '{username}'...")
41
42    try:
43        # Use a 'with' statement for automatic connection management
44        with Luanti(server=server, playername=username, password=password, port=port) as lt:
45            logger.info("Connection successful!")
46            print("-" * 40)
47            logger.info("--- Luanti & Miney Setup Check ---")
48
49            # 1. Get Server Information
50            server_version = lt.version
51            logger.info(f"Luanti Server Version: {server_version}")
52
53            # 2. Get Player Information
54            players = list(lt.players)
55            player_count = len(players)
56            player_names = ", ".join([p.name for p in players]) if players else "None"
57            logger.info(f"Players Online ({player_count}): {player_names}")
58
59            # 3. Get World Content Information
60            # This confirms that Miney can access the game's content database
61            node_count = len(list(lt.nodes.names))
62            tool_count = len(list(lt.tool))
63            logger.info(f"Registered Node Types: {node_count}")
64            logger.info(f"Registered Tool Types: {tool_count}")
65
66            print("-" * 40)
67
68            # 4. Final Verification
69            if node_count > 10 and tool_count > 0:
70                logger.info("✅ Verification successful. Miney appears to be correctly set up!")
71            else:
72                logger.warning("⚠️ Verification complete, but with warnings.")
73                logger.warning("Low node/tool count may indicate an issue with the server or mod.")
74
75    except LuantiConnectionError as e:
76        logger.critical(f"❌ Connection Failed: {e}")
77        logger.critical("Please check the following:")
78        logger.critical("  - Is the Luanti server running?")
79        logger.critical("  - Are the server address and port correct?")
80        logger.critical("  - Is the 'miney' mod installed and enabled on the server?")
81        logger.critical("  - Is the password correct?")
82    except KeyboardInterrupt:
83        logger.info("\nScript interrupted by user.")
84    except Exception as e:
85        logger.critical(f"An unexpected error occurred: {e}", exc_info=True)
86    finally:
87        logger.info("Script finished.")

Lua Console (luaconsole.py)

A powerful, interactive Read-Eval-Print Loop (REPL) for executing Lua code on the server directly from your terminal.

View Code
  1import sys
  2import os
  3import socket
  4import pprint
  5
  6try:
  7    from miney import Luanti, exceptions
  8except ModuleNotFoundError:
  9    # Allow running the script from the project's root directory
 10    sys.path.append(os.getcwd())
 11    from miney import Luanti, exceptions
 12
 13
 14def print_welcome():
 15    """Prints the welcome message and instructions."""
 16    print("Miney Lua Console")
 17    print("=================")
 18    print("Usage: python luaconsole.py [<server> <port> <playername> <password>]")
 19    print("\nClient-side commands:")
 20    print("  !help          - Show this help message.")
 21    print("  !clear         - Clear the console screen.")
 22    print("  !run <filename>  - Execute a local Lua script file on the server.")
 23    print("  !exit or !quit - Exit the console.")
 24    print("\nLua Interaction:")
 25    print("  - Enter any Lua code to execute it on the server.")
 26    print("  - Your code runs inside a function. Use 'return <value>' to get a result.")
 27    print("    (For simple expressions like '1+1', 'return' is added automatically).")
 28    print("  - Start multiline mode with '--'. Submit with two empty lines.")
 29    print("  - Press Ctrl+C to exit multiline mode or the console itself.")
 30    print("-" * 20)
 31
 32
 33def handle_client_command(cmd: str, lt: Luanti, pp: pprint.PrettyPrinter) -> bool:
 34    """
 35    Handles client-side commands that start with '!'.
 36
 37    :param cmd: The command string.
 38    :param lt: The Luanti instance for executing code.
 39    :param pp: The PrettyPrinter for displaying results.
 40    :return: True if the console should exit, False otherwise.
 41    """
 42    if cmd in ("!exit", "!quit"):
 43        return True
 44    elif cmd == "!help":
 45        print_welcome()
 46    elif cmd == "!clear":
 47        # Works on Windows (cls) and Unix-like systems (clear)
 48        os.system('cls' if os.name == 'nt' else 'clear')
 49    elif cmd.startswith("!run "):
 50        parts = cmd.split(" ", 1)
 51        if len(parts) < 2 or not parts[1].strip():
 52            print("<< Usage: !run <filename.lua>")
 53        else:
 54            filename = parts[1].strip()
 55            try:
 56                with open(filename, 'r', encoding='utf-8') as f:
 57                    lua_code = f.read()
 58                print(f">> Executing '{filename}'...")
 59                result = lt.lua.run(lua_code)
 60                if result is not None:
 61                    print("<<")
 62                    pp.pprint(result)
 63            except FileNotFoundError:
 64                print(f"<< Error: File not found: {filename}")
 65            except Exception as e:
 66                print(f"<< Error reading or executing file: {e}")
 67    else:
 68        print(f"<< Unknown client command: {cmd}. Type '!help' for instructions.")
 69    return False
 70
 71
 72def main():
 73    """Main function to run the Lua console."""
 74    # --- Connection Details ---
 75    server = sys.argv[1] if len(sys.argv) > 1 else "127.0.0.1"
 76    port = int(sys.argv[2]) if len(sys.argv) > 2 else 30000
 77    playername = sys.argv[3] if len(sys.argv) > 3 else "miney"
 78    password = sys.argv[4] if len(sys.argv) > 4 else "ChangeThePassword!"
 79
 80    # --- Connect to Server ---
 81    try:
 82        lt = Luanti(server, playername, password, port)
 83        print(f"Successfully connected to {server}:{port} as '{playername}'.")
 84    except (exceptions.LuantiConnectionError, socket.timeout) as e:
 85        print(f"Error: Could not connect to server. {e}", file=sys.stderr)
 86        sys.exit(1)
 87
 88    print_welcome()
 89
 90    # --- REPL Setup ---
 91    multiline_mode = False
 92    multiline_cmd = ""
 93    # Pretty printer for nicely formatting Lua tables (returned as Python dicts/lists)
 94    pp = pprint.PrettyPrinter(indent=2)
 95
 96    # --- Main Loop ---
 97    try:
 98        while True:
 99            try:
100                prompt = "-- " if multiline_mode else ">> "
101                cmd = input(prompt)
102
103                if multiline_mode:
104                    multiline_cmd += cmd + "\n"
105                    if "\n\n" in multiline_cmd:
106                        # Execute multiline command
107                        ret = lt.lua.run(multiline_cmd)
108                        multiline_mode = False
109                        if ret is not None:
110                            print("<<")
111                            pp.pprint(ret)
112                else:
113                    if cmd.startswith('!'):
114                        if handle_client_command(cmd, lt, pp):
115                            break  # Exit loop
116                        continue
117
118                    if cmd == "--":
119                        multiline_mode = True
120                        multiline_cmd = ""
121                        continue
122
123                    # Execute single-line command only if it's not empty
124                    if cmd.strip():
125                        processed_cmd = cmd.strip()
126                        # A client-side heuristic to automatically add 'return' for expressions.
127                        # If the command starts with a common statement keyword, we assume it's
128                        # a full statement and don't modify it. Otherwise, we treat it as
129                        # an expression and add 'return' for convenience.
130                        LUA_STATEMENT_KEYWORDS = (
131                            "return", "local", "if", "for", "while", "function", "do"
132                        )
133                        if not processed_cmd.startswith(LUA_STATEMENT_KEYWORDS):
134                            processed_cmd = "return " + processed_cmd
135
136                        ret = lt.lua.run(processed_cmd)
137                        if ret is not None:
138                            print("<<")
139                            pp.pprint(ret)
140
141            except exceptions.LuaError as e:
142                print(f"<< [LUA ERROR] {e}")
143                if multiline_mode:
144                    multiline_mode = False
145            except KeyboardInterrupt:
146                if multiline_mode:
147                    multiline_mode = False
148                    print("\nExited multiline mode.")
149                else:
150                    print("\nExiting...")
151                    break
152            except (socket.timeout, ConnectionResetError) as e:
153                print(f"\nConnection lost: {e}", file=sys.stderr)
154                break
155
156    finally:
157        # --- Cleanup ---
158        lt.disconnect()
159        print("Disconnected.")
160
161
162if __name__ == "__main__":
163    main()

Treasure Hunt Game (treasure_hunt.py)

A complete, multiplayer treasure hunt game that showcases many of Miney’s features in action, including world interaction and player management.

View Code
  1"""
  2Treasure Hunt Example
  3=====================
  4
  5This script demonstrates a simple, interactive treasure hunt game within Luanti
  6using the Miney library. It showcases several key features:
  7
  8- **Multiplayer Gameplay:** The game is announced to all players, and anyone can win.
  9- **Efficient World Interaction:** Uses ranged ``nodes.get`` to quickly scan for the surface.
 10- **World Modification:** Placing a node (:meth:`~miney.Nodes.set`) to hide the treasure chest.
 11- **Player Interaction:** Sending global chat messages to all players.
 12- **Player State:** Reading player positions (:attr:`~miney.Player.position`) to check for a winner.
 13- **Linear Script Flow:** The script runs from start to finish without threads or callbacks.
 14- **World Cleanup:** Restores the original blocks when the script ends or is interrupted.
 15
 16How to Run:
 171. Make sure the `miney` mod is installed and enabled on your Luanti server. This game is optimized for the minetest
 18   game, and there is a very high chance it will not work in other games.
 192. Run this script from your terminal, providing connection details if needed:
 20   python examples/treasure_hunt.py [server] [port] [playername] [password]
 213. The game will start immediately for all online players. The first to find the
 22   chest wins.
 23"""
 24import logging
 25import random
 26import sys
 27import time
 28from typing import Optional, List
 29
 30from miney import Luanti
 31from miney.node import Node
 32from miney.player import Player
 33from miney.point import Point
 34
 35# --- Configuration ---
 36# You can change these values to customize the game
 37SEARCH_RADIUS: int = 100  # How far from a player the chest can be hidden
 38SCAN_HEIGHT_START: int = 120  # Y-level to start scanning down from to find the surface
 39BURY_DEPTH: int = 1  # How many blocks below the surface to bury the chest
 40WIN_DISTANCE: float = 2.0  # How close a player must be to the chest to win
 41HINT_INTERVAL: int = 5  # Seconds between hints
 42ROUND_TIME_LIMIT: int = 300  # 5 minutes in seconds for a round
 43
 44# --- Logger Setup ---
 45# Configure a simple logger for script output
 46logging.basicConfig(
 47    level=logging.INFO,
 48    format="%(asctime)s | %(levelname)-8s | %(message)s",
 49    datefmt="%Y-%m-%d %H:%M:%S",
 50)
 51logger = logging.getLogger(__name__)
 52
 53
 54def find_surface(lt: Luanti, x: int, z: int) -> Optional[Point]:
 55    """
 56    Finds the first non-air block by scanning a column of nodes at once.
 57
 58    :param lt: The Luanti instance.
 59    :param x: The X coordinate.
 60    :param z: The Z coordinate.
 61    :return: A Point object representing the surface location, or None if no surface is found.
 62    """
 63    logger.info(f"Scanning for surface at X={x}, Z={z} from Y={SCAN_HEIGHT_START}.")
 64    # Define the top and bottom of the column to scan
 65    p_top = Point(x, SCAN_HEIGHT_START, z)
 66    p_bottom = Point(x, -30, z)
 67
 68    try:
 69        # Get all nodes in the column in a single, efficient call
 70        nodes_in_column = lt.nodes.get((p_bottom, p_top))
 71
 72        # Iterate backwards from the top (highest Y) to find the first solid block
 73        for node in reversed(nodes_in_column):
 74            if node.name != "air":
 75                logger.info(f"Surface found at {node.position} with node '{node.name}'.")
 76                return node.position
 77
 78        logger.warning(f"No surface found at X={x}, Z={z} (column is all air).")
 79        return None
 80
 81    except Exception as e:
 82        logger.error(f"Error getting node column at ({x}, {z}): {e}")
 83        return None
 84
 85
 86def hide_treasure(lt: Luanti, players: List[Player]) -> tuple[Optional[Point], list[Node], Optional[str]]:
 87    """
 88    Hides a treasure chest at a random location near a random player.
 89
 90    :param lt: The Luanti instance.
 91    :param players: A list of online players to choose from.
 92    :return: A tuple containing the treasure's Point, a list of original nodes to restore, and the target player's name.
 93    """
 94    # Pick a random player to center the search area on
 95    target_player = random.choice(players)
 96    start_pos = target_player.position
 97    logger.info(f"Centering search area on player '{target_player.name}' at {start_pos}.")
 98
 99    # Try a few times to find a suitable location
100    for _ in range(50):  # 10 attempts to find a good spot
101        rand_x = start_pos.x + random.randint(-SEARCH_RADIUS, SEARCH_RADIUS)
102        rand_z = start_pos.z + random.randint(-SEARCH_RADIUS, SEARCH_RADIUS)
103
104        surface_pos = find_surface(lt, int(rand_x), int(rand_z))
105
106        if surface_pos:
107            # The surface_pos from find_surface is already a Node object, so we use it directly.
108            surface_node = surface_pos
109            # Check for "water" in the node name to exclude water source and flowing water
110            if "water" in surface_node.name:
111                logger.warning(f"Surface at {surface_pos} is water ('{surface_node.name}'). Retrying.")
112                continue
113
114            chest_pos = surface_pos - Point(0, BURY_DEPTH, 0)
115
116            # Get the original nodes before overwriting them
117            try:
118                # Fetch the column of nodes that will be affected.
119                original_blocks = lt.nodes.get([chest_pos, surface_pos])
120                # The number of nodes in the column from the surface to the chest is BURY_DEPTH + 1.
121                if not (isinstance(original_blocks, list) and len(original_blocks) == BURY_DEPTH + 1):
122                    logger.warning(f"Failed to retrieve original nodes. Retrying.")
123                    continue
124            except Exception as e:
125                logger.error(f"Error getting original nodes: {e}")
126                continue
127
128            logger.info(f"Hiding treasure chest at {chest_pos}.")
129
130            try:
131                # Place a chest with a sign on top for a hint
132                lt.nodes.set([
133                    Node(chest_pos.x, chest_pos.y, chest_pos.z, name="default:chest"),
134                    Node(surface_pos.x, surface_pos.y, surface_pos.z, name="default:mese_post_light_acacia_wood")
135                ])
136
137                # Get the chest node object to access its inventory
138                chest_node = lt.nodes.get(chest_pos)
139                if chest_node:
140                    # Create a pool of items to choose from, excluding some non-stackable/undesirable ones
141                    item_pool = [
142                        item for item in list(lt.nodes.names)
143                        if "air" not in item and "water" not in item and "lava" not in item and "ignore" not in item
144                    ]
145                    # Pick a random item from the pool
146                    random_item = random.choice(item_pool)
147                    # Add the random item to the chest's inventory
148                    chest_node.inventory.add(random_item)
149                    logger.info(f"Placed random item '{random_item}' in the treasure chest.")
150                else:
151                    logger.warning(f"Could not get chest node at {chest_pos} to add item.")
152
153                return chest_pos, original_blocks, target_player.name
154            except Exception as e:
155                logger.error(f"Failed to place chest at {chest_pos} or add item: {e}", exc_info=True)
156                # If setting fails, nothing was placed, so no cleanup is needed.
157                return None, [], None
158
159    logger.error("Could not find a suitable location to hide the treasure after 10 attempts.")
160    return None, [], None
161
162
163if __name__ == "__main__":
164    # --- Connection Details ---
165    server = sys.argv[1] if len(sys.argv) > 1 else "127.0.0.1"
166    port = int(sys.argv[2]) if len(sys.argv) > 2 else 30000
167    username = sys.argv[3] if len(sys.argv) > 3 else "miney"
168    password = sys.argv[4] if len(sys.argv) > 4 else "ChangeThePassword!"
169
170    logger.info(f"Connecting to {server}:{port} as '{username}'...")
171
172    try:
173        with Luanti(server, username, password, port) as lt:
174            logger.info("Connection successful. Starting treasure hunt loop.")
175
176            lt.players[lt.playername].invisible = True
177
178            while True:  # Main loop to run the game continuously
179                original_blocks = []
180                try:
181                    # 1. Get all online players and filter out the script's own player
182                    game_players = [p for p in list(lt.players) if p.name != username]
183
184                    if not game_players:
185                        logger.info("No players online. Waiting for players to join...")
186                        time.sleep(30)  # Wait before starting a new round
187                        continue
188
189                    logger.info(f"Starting a new round with players: {[p.name for p in game_players]}")
190
191                    # 2. Announce the game to everyone
192                    lt.chat.send_to_all("A new treasure hunt is starting!")
193                    lt.chat.send_to_all(f"A chest is buried {BURY_DEPTH} blocks below the surface.")
194                    lt.chat.send_to_all("The first to find it wins. Good luck!")
195
196                    # 3. Hide the treasure and get the original blocks for cleanup
197                    treasure_location, original_blocks, target_player_name = hide_treasure(lt, game_players)
198
199                    if not treasure_location:
200                        lt.chat.send_to_all("Sorry, I couldn't find a place to hide the treasure. Game cancelled.")
201                        time.sleep(30)  # Wait before trying again
202                        continue
203
204                    logger.info(f"Treasure hidden at {treasure_location} (target: {target_player_name}). The hunt begins!")
205
206                    round_start_time = time.time()
207
208                    # 4. Main game loop to provide hints and check for a winner
209                    winner = None
210                    while winner is None:
211                        elapsed_time = time.time() - round_start_time
212                        if elapsed_time > ROUND_TIME_LIMIT:
213                            logger.info("Round time limit exceeded. Ending the round.")
214                            lt.chat.send_to_all("Time's up! The treasure was not found.")
215                            lt.chat.send_to_all(f"The treasure was at {treasure_location}.")
216                            break
217
218                        current_players = [p for p in list(lt.players) if p.name != username]
219                        if not current_players:
220                            logger.warning("All players have logged out. Ending current round.")
221                            break
222
223                        # Check if the target player is still online
224                        current_player_names = [p.name for p in current_players]
225                        if target_player_name not in current_player_names:
226                            logger.warning(f"The target player '{target_player_name}' has left the game. Ending round.")
227                            lt.chat.send_to_all(f"The treasure hunt is cancelled because the target player left.")
228                            break
229
230                        for player in current_players:
231                            try:
232                                distance = player.position.distance(treasure_location)
233                                if distance < WIN_DISTANCE:
234                                    winner = player
235                                    break
236
237                                # 5. Construct and send a personalized hint
238                                dist_int = int(distance)
239                                hint = ""
240                                if distance > 100:
241                                    hint = f"You are very far from the treasure, about {dist_int} blocks away."
242                                elif distance > 50:
243                                    hint = f"You're still quite a ways off, around {dist_int} blocks to go."
244                                elif distance > 25:
245                                    hint = f"You are getting warmer! The treasure is about {dist_int} blocks from here."
246                                elif distance > 10:
247                                    hint = f"You're hot on the trail! Only {dist_int} blocks now."
248                                else:
249                                    hint = f"You are extremely close! Less than {dist_int + 1} blocks away!"
250
251                                lt.chat.send_to_player(player.name, hint)
252                            except Exception as e:
253                                logger.warning(f"Could not process player {player.name}: {e}", exc_info=True)
254
255                        if winner:
256                            break
257                        time.sleep(HINT_INTERVAL)
258
259                    # 6. Announce winner and give them time to loot
260                    if winner:
261                        lt.chat.send_to_all(f"★★ {winner.name} found the treasure! Congratulations! ★★")
262                        logger.info(f"Winner found: {winner.name}")
263
264                        # Announce the contents of the chest
265                        chest_node = lt.nodes.get(treasure_location)
266                        if chest_node:
267                            try:
268                                chest_contents = chest_node.inventory.get_list("main")
269                                # Filter out empty slots (None) and create a list of item strings
270                                non_empty_items = [item for item in chest_contents if item]
271                                if non_empty_items:
272                                    lt.chat.send_to_all(f"The chest contained: {', '.join(non_empty_items)}")
273                                else:
274                                    lt.chat.send_to_all("The chest was empty!")
275                            except Exception as e:
276                                logger.warning(f"Could not read chest inventory: {e}")
277
278                        lt.chat.send_to_all("The chest will be removed in 30 seconds. A new round will begin shortly.")
279                        time.sleep(30)
280                    else:
281                        lt.chat.send_to_all("The round has ended.")
282                        time.sleep(10)  # Short pause before next round check
283
284                finally:
285                    # This block runs at the end of each round
286                    if original_blocks:
287                        logger.info("Restoring original blocks to clean up the world...")
288                        try:
289                            lt.nodes.set(original_blocks)
290                            logger.info("World restored successfully.")
291                        except Exception as e:
292                            logger.error(f"Failed to restore world state: {e}")
293
294    except KeyboardInterrupt:
295        logger.info("Script stopped by user.")
296    except Exception as e:
297        logger.critical(f"A critical error occurred: {e}", exc_info=True)
298    finally:
299        logger.info("Script shutting down.")

Chat Callbacks (chat.py)

A compact example of non-blocking chat callbacks. It shows how to subscribe to chat messages and register a simple chat command that is processed by the Python client.

View Code
  1import logging
  2import signal
  3import sys
  4import time
  5
  6from miney.luanti import Luanti
  7
  8# Import specific event classes
  9from miney.events import (
 10    ChatCommandEvent,
 11    ChatMessageEvent,
 12    PlayerJoinsEvent,
 13    PlayerLeavesEvent,
 14)
 15
 16logger = logging.getLogger(__name__)
 17
 18
 19def setup_logging() -> None:
 20    handler = logging.StreamHandler()
 21    formatter = logging.Formatter(
 22        "%(asctime)s | %(levelname)-8s | %(module)s.%(funcName)s | %(message)s",
 23        datefmt="%Y-%m-%d %H:%M:%S",
 24    )
 25    handler.setFormatter(formatter)
 26    root = logging.getLogger()
 27    root.setLevel(logging.INFO)
 28    root.handlers.clear()
 29    root.addHandler(handler)
 30
 31
 32def main() -> int:
 33    setup_logging()
 34    logger.info("Starting non-blocking callbacks example")
 35
 36    lt = Luanti(server="127.0.0.1", playername="miney", password="ChangeThePassword!", port=30000)
 37
 38    # Use the flattened event attributes
 39    @lt.chat.on(event="chat_message")
 40    def on_chat_message(event: ChatMessageEvent) -> None:
 41        logger.info("chat_message from %s: %s", event.sender_name, event.message)
 42
 43    # Register a handler for players leaving the game
 44    @lt.callbacks.on("player_leaves")
 45    def on_player_leaves(event: PlayerLeavesEvent) -> None:
 46        reason = "timed out" if event.timed_out else "disconnected"
 47        logger.info("Player %s has left the game (%s).", event.player_name, reason)
 48
 49    # Register a handler for players joining the game
 50    @lt.callbacks.on("player_joins")
 51    def on_player_joins(event: PlayerJoinsEvent) -> None:
 52        player_name = event.player_name
 53        # The payload contains the name; get the full Player object via the players list
 54        player_obj = lt.players[player_name]
 55
 56        if event.last_login:
 57            # Format the datetime object for logging
 58            last_login_str = event.last_login.strftime('%Y-%m-%d %H:%M:%S')
 59            logger.info("Player %s just joined. Last login was at %s.", player_name, last_login_str)
 60            lt.chat.send_to_player(player_obj, f"Welcome back, {player_name}!")
 61        else:
 62            logger.info("New player %s just joined for the first time!", player_name)
 63            lt.chat.send_to_player(player_obj, f"Welcome to the server, {player_name}!")
 64
 65    # Register a simple chat command handled in Python using the decorator
 66    @lt.chat.command("hello", description="Says hi back to the issuer.")
 67    def hello_cmd(event: ChatCommandEvent) -> None:
 68        logger.info(f"Received /hello command from {event.issuer}, {event.param}")
 69        lt.chat.send_to_player(event.issuer, "Hi from Miney Python callbacks! 👋")
 70
 71    # It's also possible to register and unregister callbacks procedurally
 72    def procedural_chat_handler(event: ChatMessageEvent) -> None:
 73        # We only want to log messages from a specific player, for example 'singleplayer'
 74        if event.sender_name == "singleplayer":
 75            logger.info(
 76                "(Procedural handler) singleplayer said: %s", event.message
 77            )
 78
 79    lt.on_event("chat_message", procedural_chat_handler)
 80
 81    stop = False
 82
 83    def handle_sigint(signum, frame):
 84        nonlocal stop
 85        stop = True
 86
 87    signal.signal(signal.SIGINT, handle_sigint)
 88
 89    try:
 90        logger.info("Press Ctrl+C to exit. Try typing '/hello' in-game.")
 91        while not stop:
 92            time.sleep(0.2)
 93    finally:
 94        # The disconnect method handles unregistering all callbacks and commands automatically.
 95        lt.disconnect()
 96        logger.info("Shut down cleanly")
 97    return 0
 98
 99
100if __name__ == "__main__":
101    sys.exit(main())

Move Showcase (move_showcase.py)

Demonstrates smooth, scripted player movements using the Player.move() API.

View Code
  1import logging
  2import math
  3import random
  4import time
  5
  6from miney import Luanti, Point, Player
  7
  8# Configure logger for the example
  9logger = logging.getLogger(__name__)
 10handler = logging.StreamHandler()
 11formatter = logging.Formatter("%(asctime)s | %(levelname)-8s | %(module)s.%(funcName)s | %(message)s")
 12handler.setFormatter(formatter)
 13logger.addHandler(handler)
 14logger.setLevel(logging.INFO)
 15
 16
 17def print_welcome():
 18    """
 19    Prints a welcome message to the console.
 20    """
 21    print("=" * 50)
 22    print("Miney Move Showcase")
 23    print("=" * 50)
 24    print("This script demonstrates the player movement capabilities.")
 25    print("You can watch the miney player in the game to see the movements.")
 26    print("-" * 50)
 27
 28
 29def showcase_moves(player: Player):
 30    """
 31    Demonstrates various movement capabilities of the Player.move() method.
 32
 33    :param player: The player object to control.
 34    """
 35    print("\nStarting move showcase...")
 36    logger.info(f"Controlling player: {player.name}")
 37
 38    # Store initial state to restore it later
 39    initial_pos = player.position
 40    initial_noclip = player.noclip
 41    logger.info(f"Initial position: {initial_pos}")
 42
 43    try:
 44        # Enable noclip for smooth air movement, otherwise the player would fall
 45        player.noclip = True
 46        time.sleep(0.5)  # Give the server a moment to apply the setting
 47
 48        # 1. Simple move to a new position
 49        print("\n1. Moving 10 blocks forward (smoothly).")
 50        target_pos = initial_pos + Point(10, 0, 0)
 51        player.move(destination=target_pos, smooth=True, duration=2)
 52        time.sleep(2.5)  # Wait for the movement to complete
 53
 54        # 2. Move back to start while looking at the previous target
 55        print("\n2. Moving back to start while looking at the last position.")
 56        player.move(destination=initial_pos, look_at=target_pos, smooth=True, duration=2)
 57        time.sleep(2.5)
 58
 59        # 3. Circle movement
 60        print("\n3. Performing a circular flight pattern.")
 61        radius = 8
 62        steps = 40
 63        # The center of the circle is in front of the player's starting position
 64        center = player.position + Point(radius, 0, 0)
 65        for i in range(steps + 1):
 66            # Calculate the next point on the circle
 67            angle = (i / steps) * 2 * math.pi
 68            x = center.x - radius * math.cos(angle)
 69            z = center.z + radius * math.sin(angle)
 70            y = initial_pos.y + 3  # Fly a bit above the ground
 71
 72            # The player will always look at the center of the circle
 73            player.move(
 74                destination=Point(x, y, z),
 75                look_at=center,
 76                smooth=True,
 77                duration=0.2
 78            )
 79            time.sleep(0.2)
 80
 81        # 4. "Dance" routine with random movements
 82        print("\n4. Let's dance! Performing some random moves.")
 83        for i in range(10):
 84            # Random offset from the starting position
 85            offset = Point(
 86                random.uniform(-3, 3),
 87                random.uniform(0, 2),
 88                random.uniform(-3, 3)
 89            )
 90            # Random point to look at
 91            look_offset = Point(
 92                random.uniform(-10, 10),
 93                random.uniform(-5, 5),
 94                random.uniform(-10, 10)
 95            )
 96            player.move(
 97                destination=initial_pos + offset,
 98                look_at=initial_pos + look_offset,
 99                smooth=True,
100                duration=0.4
101            )
102            time.sleep(0.5)
103
104        print("\nShowcase finished!")
105
106    finally:
107        # Clean up: move back to the starting point and reset noclip
108        print("\nReturning to initial position and restoring state.")
109        player.move(destination=initial_pos, smooth=True, duration=1.5)
110        time.sleep(2)
111        player.noclip = initial_noclip
112        logger.info("Player state restored.")
113
114
115def main():
116    """
117    Main function to run the move showcase.
118    """
119    print_welcome()
120
121    try:
122        with Luanti() as lt:
123            # For this example, we control the 'miney' player
124            player = lt.players["miney"]
125            showcase_moves(player)
126
127    except Exception as e:
128        logger.error(f"An unexpected error occurred: {e}", exc_info=True)
129
130
131if __name__ == "__main__":
132    main()

Choreography Showcase (choreography.py)

Demonstrates a multi-client solar-system choreography using multiple Luanti clients with Player.move(). Ensure the server is running. Clients will connect as ‘dancer_1’ to ‘dancer_<N>’. On first connection, you may need to grant them ‘miney’ and ‘noclip’ privileges.

View Code
  1"""
  2A multi-client choreography example.
  3
  4This script demonstrates how to connect multiple clients to a Minetest server
  5and have them perform a synchronized dance routine. It uses Python's
  6threading module to simulate multiple clients, each running in its own thread.
  7
  8The choreography consists of two main parts:
  91. All dancers line up in a row.
 102. They perform a synchronized circular flight pattern, always looking towards
 11   the center of the circle.
 12"""
 13import logging
 14import math
 15import threading
 16import time
 17from typing import List
 18
 19from miney import Luanti, Point
 20
 21# --- Configuration ---
 22NUM_DANCERS = 4
 23SERVER = "127.0.0.1"
 24PASSWORD = "ChangeThis"
 25BASE_PLAYER_NAME = "dancer"
 26
 27# --- Choreography Parameters ---
 28# Center of the entire solar system, and the point the "planet" orbits
 29SYSTEM_CENTER = Point(0, 20, 0)
 30
 31# Planet (the first dancer, index 0) parameters
 32PLANET_ORBIT_RADIUS = 15.0
 33PLANET_ORBIT_SPEED_MULTIPLIER = 1.0  # Revolutions per full animation cycle
 34
 35# Moon (other dancers) parameters
 36MOON_BASE_ORBIT_RADIUS = 4.0
 37MOON_RADIUS_INCREMENT = 2.0
 38MOON_ORBIT_SPEED_MULTIPLIER = 12.0  # Moons orbit the planet faster
 39
 40# General animation parameters
 41TOTAL_STEPS = 240  # Total number of animation steps for one full planet orbit
 42STEP_DURATION = 0.2  # Duration of each small step in seconds
 43
 44# Synchronization primitive to make all dancers start the main routine together.
 45# We need NUM_DANCERS + 1 parties: one for each dancer and one for the main thread.
 46barrier = threading.Barrier(NUM_DANCERS + 1)
 47
 48# --- Logging Setup ---
 49logger = logging.getLogger(__name__)
 50handler = logging.StreamHandler()
 51formatter = logging.Formatter(
 52    "%(asctime)s | %(levelname)-8s | %(threadName)-12s | %(message)s"
 53)
 54handler.setFormatter(formatter)
 55logger.addHandler(handler)
 56logger.setLevel(logging.INFO)
 57
 58
 59def dance_routine(player_name: str, dancer_index: int):
 60    """
 61    The routine each dancer will perform.
 62
 63    This function simulates a small solar system:
 64    - Dancer 0 is the "planet," orbiting a central point.
 65    - Other dancers are "moons," orbiting the planet.
 66
 67    :param player_name: The name of the player for this client.
 68    :param dancer_index: The index of the dancer (0 for planet, >0 for moons).
 69    """
 70    logger.info("Connecting to the server...")
 71    initial_noclip_state = False
 72
 73    try:
 74        with Luanti(server=SERVER, playername=player_name, password=PASSWORD) as m:
 75            player = m.players[player_name]
 76            initial_noclip_state = player.noclip
 77
 78            # --- Phase 1: Move to initial position ---
 79            logger.info("Moving to initial position in the formation.")
 80            player.noclip = True
 81            time.sleep(0.2)  # Give noclip time to apply
 82
 83            # Calculate initial positions (at step i=0)
 84            planet_pos_initial = SYSTEM_CENTER + Point(PLANET_ORBIT_RADIUS, 0, 0)
 85            my_initial_pos: Point
 86
 87            if dancer_index == 0:  # I am the planet
 88                my_initial_pos = planet_pos_initial
 89            else:  # I am a moon
 90                moon_index = dancer_index - 1
 91                num_moons = NUM_DANCERS - 1 if NUM_DANCERS > 1 else 1
 92
 93                moon_phase_offset = (moon_index / num_moons) * 2 * math.pi
 94                my_moon_radius = MOON_BASE_ORBIT_RADIUS + (moon_index * MOON_RADIUS_INCREMENT)
 95
 96                moon_orbit_offset = Point(
 97                    my_moon_radius * math.cos(moon_phase_offset),
 98                    0,
 99                    my_moon_radius * math.sin(moon_phase_offset)
100                )
101                my_initial_pos = planet_pos_initial + moon_orbit_offset
102
103            player.move(destination=my_initial_pos, duration=3, smooth=True)
104            time.sleep(3.5)  # Wait for movement
105
106            logger.info("In position. Waiting for other dancers.")
107            barrier.wait()
108
109            # --- Phase 2: Planetary Dance ---
110            logger.info("Starting planetary dance routine.")
111            last_moon_yaw = None  # For unwrapping the moon's yaw angle to prevent spinning
112
113            for i in range(1, TOTAL_STEPS + 1):
114                # Everyone calculates the planet's position for this step
115                planet_angle = (i / TOTAL_STEPS) * 2 * math.pi * PLANET_ORBIT_SPEED_MULTIPLIER
116                planet_pos = SYSTEM_CENTER + Point(
117                    PLANET_ORBIT_RADIUS * math.cos(planet_angle),
118                    0,
119                    PLANET_ORBIT_RADIUS * math.sin(planet_angle)
120                )
121
122                my_target_pos: Point
123                move_kwargs = {
124                    "smooth": True,
125                    "duration": STEP_DURATION
126                }
127
128                if dancer_index == 0:  # I am the planet
129                    my_target_pos = planet_pos
130                    move_kwargs["look_at"] = SYSTEM_CENTER
131                else:  # I am a moon
132                    moon_index = dancer_index - 1
133                    num_moons = NUM_DANCERS - 1 if NUM_DANCERS > 1 else 1
134
135                    moon_angle = (i / TOTAL_STEPS) * 2 * math.pi * MOON_ORBIT_SPEED_MULTIPLIER
136                    moon_phase_offset = (moon_index / num_moons) * 2 * math.pi
137                    final_moon_angle = moon_angle + moon_phase_offset
138
139                    my_moon_radius = MOON_BASE_ORBIT_RADIUS + (moon_index * MOON_RADIUS_INCREMENT)
140
141                    moon_orbit_offset = Point(
142                        my_moon_radius * math.cos(final_moon_angle),
143                        0,
144                        my_moon_radius * math.sin(final_moon_angle)
145                    )
146                    my_target_pos = planet_pos + moon_orbit_offset
147
148                    # To prevent spinning, we calculate yaw manually and unwrap it.
149                    # The direction to look is from the moon to the planet (-moon_orbit_offset).
150                    direction_to_planet = -moon_orbit_offset
151                    current_yaw = math.atan2(direction_to_planet.x, direction_to_planet.z)
152
153                    # Unwrap the angle to ensure the shortest rotation path.
154                    if last_moon_yaw is not None:
155                        while current_yaw - last_moon_yaw > math.pi:
156                            current_yaw -= 2 * math.pi
157                        while current_yaw - last_moon_yaw < -math.pi:
158                            current_yaw += 2 * math.pi
159
160                    move_kwargs["yaw"] = current_yaw
161                    last_moon_yaw = current_yaw
162
163                player.move(destination=my_target_pos, **move_kwargs)
164                time.sleep(STEP_DURATION)
165
166            logger.info("Finished dancing.")
167
168    except Exception as e:
169        logger.error(f"An error occurred: {e}", exc_info=True)
170    finally:
171        # --- Cleanup ---
172        try:
173            # Use a new connection for cleanup if the main one failed
174            with Luanti(server=SERVER, playername=player_name, password=PASSWORD) as m:
175                player = m.players[player_name]
176                logger.info("Restoring noclip state.")
177                player.noclip = initial_noclip_state
178        except Exception as e:
179            logger.error(f"Cleanup failed: {e}")
180
181        logger.info("Disconnecting.")
182
183
184def main():
185    """
186    Main function to set up and run the choreography.
187    """
188    print("--- Multi-Client Choreography Showcase: Solar System ---")
189    print(f"Starting {NUM_DANCERS} dancers (1 planet, {NUM_DANCERS - 1} moons)...")
190    print("Make sure the Minetest server is running and accessible.")
191    print(f"Dancers will connect with names '{BASE_PLAYER_NAME}_1' to '{BASE_PLAYER_NAME}_{NUM_DANCERS}'.")
192    print("On first connection, you may need to grant them 'miney' and 'noclip' privileges.")
193    print("------------------------------------------")
194
195    threads: List[threading.Thread] = []
196    for i in range(NUM_DANCERS):
197        player_name = f"{BASE_PLAYER_NAME}_{i + 1}"
198        thread = threading.Thread(
199            target=dance_routine, args=(player_name, i), name=player_name
200        )
201        threads.append(thread)
202        thread.start()
203        time.sleep(0.2)  # Stagger connections
204
205    try:
206        logger.info("Main thread waiting for all dancers to get in position...")
207        barrier.wait(timeout=60)
208        logger.info("All dancers are ready! The show begins now.")
209    except threading.BrokenBarrierError:
210        logger.error("Not all dancers reached their position in time. Aborting.")
211        return
212
213    for thread in threads:
214        thread.join()
215
216    logger.info("Choreography showcase finished.")
217
218
219if __name__ == "__main__":
220    main()