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()