minestrapper 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4 @@
1
+ from .server import Server
2
+ from .state_handler import ServerState
3
+
4
+ __all__ = ["Server", "ServerState"]
@@ -0,0 +1,50 @@
1
+ import os
2
+ import json
3
+ from enum import Enum
4
+
5
+ class FileType(Enum):
6
+ CONFIG = "config"
7
+ SERVER_PROPERTIES = "server_properties"
8
+
9
+ def key_value_to_dict(unpacked_string: list) -> dict:
10
+ dict = {}
11
+
12
+ for line in unpacked_string:
13
+ key_value = line.strip().split('=')
14
+ if len(key_value) == 2:
15
+ dict[key_value[0]] = key_value[1]
16
+
17
+ return dict
18
+
19
+ def dict_to_key_value(dictionary: dict) -> str:
20
+ key_value_string = ""
21
+ for key, value in dictionary.items():
22
+ key_value_string += f"{key}={value}\n"
23
+ return key_value_string
24
+
25
+ class ConfigHandler:
26
+ def __init__(self, path: str):
27
+ self.path = path
28
+ self.config_json = os.path.join(self.path, "minestrapper", "config.json")
29
+ self.server_properties_file = os.path.join(self.path, "server.properties")
30
+
31
+ def __open(self, file_type: FileType):
32
+ if file_type == FileType.CONFIG:
33
+ self.__config = json.load(open(self.config_json, 'r'))
34
+ elif file_type == FileType.SERVER_PROPERTIES:
35
+ with open(self.server_properties_file, 'r') as f:
36
+ self.__server_properties = key_value_to_dict(f.readlines())
37
+
38
+ def get_config(self):
39
+ self.__open(FileType.CONFIG)
40
+ return self.__config
41
+
42
+ def get_server_properties(self):
43
+ self.__open(FileType.SERVER_PROPERTIES)
44
+ return self.__server_properties
45
+
46
+ def set_server_properties(self, key : str, value : str):
47
+ self.__open(FileType.SERVER_PROPERTIES)
48
+ self.__server_properties[key] = value
49
+ with open(self.server_properties_file, 'w') as f:
50
+ f.write(dict_to_key_value(self.__server_properties))
File without changes
@@ -0,0 +1,13 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ if TYPE_CHECKING:
4
+ from ..server import Server
5
+
6
+ class Feature:
7
+ def __init__(self, name: str, description: str, server: "Server"):
8
+ self.name = name
9
+ self.description = description
10
+ self.server = server
11
+
12
+ def run(self):
13
+ raise NotImplementedError("Feature must implement the run method")
@@ -0,0 +1,35 @@
1
+ import os
2
+ import sys
3
+
4
+ from typing import TYPE_CHECKING
5
+
6
+ from .state_styles.state_styles import StateStyle
7
+ from .server_resource_pack.server_resource_pack import ServerResourcePack
8
+
9
+ if TYPE_CHECKING:
10
+ from ..server import Server
11
+
12
+ FEATURES = {
13
+ "state_styles": {
14
+ "class": StateStyle,
15
+ "enabled_by_default": True
16
+ },
17
+ "server_resource_pack": {
18
+ "class": ServerResourcePack,
19
+ "enabled_by_default": False
20
+ }
21
+ }
22
+
23
+ def handle_built_in_features(server: "Server"):
24
+ for feature_key, feature_info in FEATURES.items():
25
+ target_feature = server.config_handler.get_config()["features"].get(feature_key)
26
+ if target_feature is None: continue
27
+
28
+ if target_feature.get("feature_enabled", feature_info["enabled_by_default"]):
29
+ feature_class = feature_info["class"]
30
+ feature_instance = feature_class(server)
31
+ server.logger.info(f"Feature '{feature_instance.name}' has been started.")
32
+ try:
33
+ feature_instance.run()
34
+ except Exception as e:
35
+ server.logger.error(f"An error occurred while running the feature '{feature_instance.name}': {e}")
File without changes
@@ -0,0 +1,48 @@
1
+ import ipaddress
2
+ import socket
3
+
4
+ def is_valid_ip(ip_address: str) -> bool:
5
+ try:
6
+ ipaddress.ip_address(ip_address)
7
+ except ValueError:
8
+ raise ValueError(f"Invalid IP address for server resource pack: {ip_address}")
9
+
10
+ return True
11
+
12
+ def get_port_integer(port: str) -> int:
13
+ try:
14
+ return int(port)
15
+ except (TypeError, ValueError):
16
+ raise ValueError(f"Port must be an integer, got {port}")
17
+
18
+ def is_valid_port(port: int) -> bool:
19
+ if (port < 1 or port > 65535):
20
+ raise ValueError(f"Port must be between 1 and 65535, got {port}")
21
+
22
+ return True
23
+
24
+ def is_port_free(ip: str, port: str) -> bool:
25
+ candidate_port = int(port)
26
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
27
+ return s.connect_ex((ip, candidate_port)) != 0
28
+
29
+ def get_clean_address(ip: str, port: str) -> tuple[str, int]:
30
+ cleaned_ip = ip
31
+ cleaned_port = port
32
+
33
+ if (ip == "localhost"):
34
+ cleaned_ip = "127.0.0.1"
35
+ if (ip in [None, ""]):
36
+ cleaned_ip = "0.0.0.0"
37
+
38
+ if (not is_valid_ip(cleaned_ip)):
39
+ raise ValueError(f"Invalid IP address for server resource pack: {cleaned_ip}")
40
+
41
+ port_int = get_port_integer(port)
42
+ if (not is_valid_port(port_int)):
43
+ raise ValueError(f"Invalid port for server resource pack: {port}")
44
+
45
+ if (not is_port_free(cleaned_ip, port)):
46
+ cleaned_port = 0
47
+
48
+ return cleaned_ip, int(cleaned_port)
@@ -0,0 +1,26 @@
1
+ from pathlib import Path
2
+
3
+ def resolve_server_path(path_str: str, server_root: str | Path) -> Path:
4
+ if (not path_str):
5
+ raise ValueError(
6
+ "Resource pack path is not set in config."
7
+ "Did you mean to disable the server_resource_pack feature?"
8
+ )
9
+
10
+ path = Path(path_str)
11
+
12
+ if not path.is_absolute():
13
+ path = Path(server_root) / path
14
+
15
+ path = path.resolve()
16
+
17
+ if (not path.exists()):
18
+ raise ValueError(f"Resource pack path {path} does not exist.")
19
+
20
+ if (not path.is_file()):
21
+ raise ValueError(f"Resource pack path {path} is not a file.")
22
+
23
+ if (path.suffix.lower() != ".zip"):
24
+ raise ValueError(f"Resource pack path {path} is not a zip file.")
25
+
26
+ return path
@@ -0,0 +1,87 @@
1
+ import threading
2
+
3
+ import os
4
+ from http.server import BaseHTTPRequestHandler, HTTPServer
5
+
6
+ from minestrapper.logger import LogLevel
7
+
8
+ from pathlib import Path
9
+
10
+ from minestrapper.features.feature import Feature
11
+
12
+ from typing import TYPE_CHECKING
13
+
14
+ from minestrapper.features.server_resource_pack.path_resolver import resolve_server_path
15
+ if TYPE_CHECKING:
16
+ from ....minestrapper.server import Server
17
+
18
+ from .address_validator import get_clean_address
19
+
20
+ import hashlib
21
+
22
+ class ServerResourcePack(Feature):
23
+ def __init__(self, server: "Server"):
24
+ super().__init__(
25
+ "Server Resource Pack",
26
+ "An HTTP server to serve a custom resource pack for your Minecraft Server.",
27
+ server
28
+ )
29
+
30
+ self.config = self.server.config_handler.get_config()['features']['server_resource_pack']
31
+
32
+ self.path = resolve_server_path(self.config["path"], self.server.path)
33
+ self.ip, self.port = get_clean_address(self.config.get("ip", ""), self.config.get("port", ""))
34
+
35
+ def log(self, level: LogLevel, message: str):
36
+ self.server.logger.log(level, f"Resource Pack Server: {message}")
37
+
38
+ def get_sha1(self, file_path: Path) -> str:
39
+ sha1 = hashlib.sha1()
40
+
41
+ with open(file_path, "rb") as f:
42
+ while chunk := f.read(65536):
43
+ sha1.update(chunk)
44
+
45
+ return sha1.hexdigest()
46
+
47
+ def start_resource_pack_server(self):
48
+ file_path = self.path
49
+ server_instance = self.server
50
+ log_method = self.log
51
+
52
+ class ResourcePackRequestHandler(BaseHTTPRequestHandler):
53
+ def log_message(self, format, *args):
54
+ log_level = LogLevel.INFO
55
+ if (len(args) >= 2):
56
+ try:
57
+ code = int(args[1])
58
+ if (code >= 500):
59
+ log_level = LogLevel.ERROR
60
+ elif (code >= 400):
61
+ log_level = LogLevel.WARNING
62
+ except Exception as e:
63
+ pass
64
+ log_method(log_level, format % args)
65
+
66
+ def do_GET(self):
67
+ self.send_response(200)
68
+ self.send_header("Content-Type", "application/zip")
69
+ self.send_header("Content-Length", str(os.path.getsize(file_path)))
70
+ self.end_headers()
71
+
72
+ with open(file_path, "rb") as resource_pack_file:
73
+ while chunk := resource_pack_file.read(65536):
74
+ self.wfile.write(chunk)
75
+
76
+ server = HTTPServer((self.ip, self.port), ResourcePackRequestHandler)
77
+ threading.Thread(target=server.serve_forever, daemon=True).start()
78
+
79
+ return server
80
+
81
+ def run(self):
82
+ self.resource_pack_server = self.start_resource_pack_server()
83
+ resource_pack_url = f"http://{self.ip}:{self.port}/"
84
+
85
+ self.server.logger.info(f"Started resource pack server at {resource_pack_url}.")
86
+ self.server.config_handler.set_server_properties("resource-pack", resource_pack_url)
87
+ self.server.config_handler.set_server_properties("resource-pack-sha1", self.get_sha1(self.path))
File without changes
@@ -0,0 +1,59 @@
1
+ import os
2
+ import ctypes
3
+
4
+ from typing import TYPE_CHECKING, Callable
5
+
6
+ from minestrapper.features.feature import Feature
7
+ from minestrapper.util.ansi_colors import ANSI_COLORS
8
+ from minestrapper.state_handler import ServerState
9
+
10
+ if TYPE_CHECKING:
11
+ from ....minestrapper.server import Server
12
+
13
+ class StateStyle(Feature):
14
+ def __init__(self, server: "Server"):
15
+ super().__init__("State Styles", "Provider for colored text and titles based on different server states", server)
16
+ self.state_styles_config = self.server.config_handler.get_config()['features']['state_styles']
17
+ self.styles = self.state_styles_config["styles"]
18
+ self.title_enabled = self.state_styles_config.get("title_enabled", False)
19
+ self.color_enabled = self.state_styles_config.get("color_enabled", False)
20
+
21
+ def get_colored_text(self, text: str, state: ServerState) -> str:
22
+ map = {
23
+ ServerState.STARTING: ANSI_COLORS.get(self.styles["starting"]["color"], ANSI_COLORS["reset"]),
24
+ ServerState.RUNNING: ANSI_COLORS.get(self.styles["running"]["color"], ANSI_COLORS["reset"]),
25
+ ServerState.STOPPING: ANSI_COLORS.get(self.styles["stopping"]["color"], ANSI_COLORS["reset"]),
26
+ ServerState.STOPPED: ANSI_COLORS.get(self.styles["stopped"]["color"], ANSI_COLORS["reset"]),
27
+ ServerState.PAUSED: ANSI_COLORS.get(self.styles["paused"]["color"], ANSI_COLORS["reset"]),
28
+ }
29
+
30
+ color = map.get(state, ANSI_COLORS["reset"])
31
+
32
+ return f"{color}{text}{ANSI_COLORS['reset']}"
33
+
34
+ def get_title(self, state: ServerState) -> str:
35
+ map = {
36
+ ServerState.STARTING: self.styles["starting"].get("title", "Starting"),
37
+ ServerState.RUNNING: self.styles["running"].get("title", "Running"),
38
+ ServerState.STOPPING: self.styles["stopping"].get("title", "Stopping"),
39
+ ServerState.STOPPED: self.styles["stopped"].get("title", "Stopped"),
40
+ ServerState.PAUSED: self.styles["paused"].get("title", "Paused"),
41
+ }
42
+
43
+ return map.get(state, "Unknown")
44
+
45
+ def run(self):
46
+ if (self.color_enabled):
47
+ @self.server.logger.add_line_transformer
48
+ def state_styles_transformer(line: str):
49
+ return self.get_colored_text(line, self.server.state_handler.get())
50
+
51
+ if (self.title_enabled):
52
+ @self.server.state_handler.on_state_change
53
+ def set_terminal_title():
54
+ title = self.get_title(self.server.state_handler.get())
55
+ if (os.name == "nt"):
56
+ ctypes.windll.kernel32.SetConsoleTitleW(title)
57
+ else:
58
+ sys.stdout.write(f'\x1b]0;{title}\x07')
59
+ sys.stdout.flush()
minestrapper/logger.py ADDED
@@ -0,0 +1,76 @@
1
+ import os
2
+ import threading
3
+
4
+ from enum import Enum
5
+ from pathlib import Path
6
+ from datetime import datetime
7
+
8
+ from typing import TYPE_CHECKING, Callable
9
+
10
+ if TYPE_CHECKING:
11
+ from .server import Server
12
+
13
+ class LogLevel(Enum):
14
+ INFO = "INFO"
15
+ WARNING = "WARNING"
16
+ ERROR = "ERROR"
17
+
18
+ class Logger:
19
+ def __init__(self, server: "Server"):
20
+ self.server = server
21
+ self.line_transformers: list[Callable[[str], str]] = []
22
+
23
+ logs_folder = Path(server.path) / "logs"
24
+ logs_folder.mkdir(parents=True, exist_ok=True)
25
+ self.log_file = logs_folder / "latest.log"
26
+ self.log_file.parent.mkdir(parents=True, exist_ok=True)
27
+
28
+ self.minestrapper_log_file = logs_folder / "latest-minestrapper.log"
29
+ self.minestrapper_log_file.parent.mkdir(parents=True, exist_ok=True)
30
+ self.minestrapper_log_file.touch(exist_ok=True)
31
+ with open(self.minestrapper_log_file, "w", encoding="utf-8") as log:
32
+ log.write("")
33
+
34
+ self.info("Initialized Minestrapper Logger successfully.")
35
+
36
+ def add_line_transformer(self, transformer: Callable[[str], str]):
37
+ self.line_transformers.append(transformer)
38
+ return transformer
39
+
40
+ def __write_line_log(self, line: str):
41
+ with open(self.minestrapper_log_file, "a", encoding="utf-8") as f:
42
+ f.write(line + "\n")
43
+
44
+ def print_with_transformation(self, message: str):
45
+ for transformer in self.line_transformers:
46
+ message = transformer(message)
47
+
48
+ print(message)
49
+
50
+ def forwarded_log(self, message: str):
51
+ self.print_with_transformation(message)
52
+ self.__write_line_log(message)
53
+
54
+ def log(self, level: LogLevel, message: str):
55
+ timestamp = f"[{datetime.now().strftime('%H:%M:%S')}]"
56
+ log_level_label = f"[Minestrapper/{level.value}]:"
57
+ full_line = f"{timestamp} {log_level_label} {message}"
58
+
59
+ self.print_with_transformation(full_line)
60
+ self.__write_line_log(full_line)
61
+
62
+ def publish_minestrapper_log(self):
63
+ with open(self.minestrapper_log_file, "r", encoding="utf-8") as f:
64
+ minestrapper_log_content = f.read()
65
+
66
+ with open(self.log_file, "w", encoding="utf-8") as f:
67
+ f.write(minestrapper_log_content)
68
+
69
+ def info(self, message: str):
70
+ self.log(LogLevel.INFO, message)
71
+
72
+ def warning(self, message: str):
73
+ self.log(LogLevel.WARNING, message)
74
+
75
+ def error(self, message: str):
76
+ self.log(LogLevel.ERROR, message)
minestrapper/server.py ADDED
@@ -0,0 +1,124 @@
1
+ from pathlib import Path
2
+ import subprocess
3
+ from typing import Callable
4
+ import threading
5
+
6
+ import os
7
+
8
+ from .logger import Logger
9
+
10
+ from .config_handler import ConfigHandler
11
+ from .state_handler import ServerState, StateHandler
12
+
13
+ from minestrapper.util.get_state_from_line import get_state_from_line
14
+
15
+ from minestrapper.features.handle_built_in_features import handle_built_in_features
16
+
17
+ from time import sleep as wait
18
+
19
+ class Server:
20
+ def __init__(self, path: str | Path):
21
+ self.path : str = (str(path) if isinstance(path, Path) else path)
22
+ self.config_handler : ConfigHandler = ConfigHandler(self.path)
23
+ self.state_handler : StateHandler = StateHandler(ServerState.STARTING)
24
+ self.periodic_callbacks : list[Callable] = []
25
+ self.new_line_callbacks : list[Callable] = []
26
+ self.server_process : subprocess.Popen | None = None
27
+
28
+ self.logger = Logger(self)
29
+
30
+ os.chdir(self.path)
31
+
32
+ def __start_server_process(self):
33
+ config = self.config_handler.get_config()
34
+
35
+ jar_path = f"{config['server']['jar_name']}.jar"
36
+
37
+ full_command = [
38
+ "java"
39
+ ]
40
+ java_args = [
41
+ f"-Xms{config['server']['min_ram']}",
42
+ f"-Xmx{config['server']['max_ram']}",
43
+ ]
44
+
45
+ if "other_args" in config['server']:
46
+ java_args.extend(config['server']['other_args'])
47
+
48
+ full_command.extend(java_args)
49
+ full_command.extend([
50
+ "-jar", jar_path, "nogui"
51
+ ])
52
+
53
+ server_process = subprocess.Popen(
54
+ full_command,
55
+ stdout=subprocess.PIPE,
56
+ stderr=subprocess.STDOUT,
57
+ text=True,
58
+ bufsize=1,
59
+ encoding="utf-8",
60
+ errors="replace"
61
+ )
62
+
63
+ return server_process
64
+
65
+ def start_server(self):
66
+ self.server_process = self.__start_server_process()
67
+
68
+ def loop():
69
+ while self.state_handler.get() != ServerState.STOPPED and self.server_process is not None and self.server_process.poll() is None:
70
+ self.__on_periodic()
71
+
72
+ if (self.periodic_callbacks):
73
+ for callback in self.periodic_callbacks:
74
+ callback()
75
+ wait(0.020)
76
+
77
+ self.periodic_thread = threading.Thread(target=loop, daemon=True)
78
+ self.periodic_thread.start()
79
+
80
+ def output_loop():
81
+ assert self.server_process is not None
82
+ if (self.server_process.stdout is not None):
83
+ for line in self.server_process.stdout:
84
+ self.__on_new_line(line.strip("\n"))
85
+
86
+ self.output_thread = threading.Thread(target=output_loop, daemon=True)
87
+ self.output_thread.start()
88
+
89
+ self.logger.info("Server process started successfully.")
90
+ handle_built_in_features(self)
91
+
92
+ def wait_loop(self):
93
+ assert self.server_process is not None
94
+ self.server_process.wait()
95
+
96
+ # If the server process has stopped, this code should now run.
97
+ self.state_handler.set(ServerState.STOPPED)
98
+ self.logger.info("Server process has stopped.")
99
+ self.logger.publish_minestrapper_log()
100
+ input("Press Enter to exit...")
101
+
102
+ def __on_new_line(self, line : str):
103
+ new_state = get_state_from_line(line)
104
+ if (new_state is not None and new_state != self.state_handler.get()):
105
+ self.state_handler.set(new_state)
106
+
107
+ self.logger.forwarded_log(line)
108
+
109
+ if self.new_line_callbacks:
110
+ for callback in self.new_line_callbacks:
111
+ callback(line)
112
+
113
+ def on_new_line(self, function: Callable):
114
+ self.new_line_callbacks.append(function)
115
+ return function
116
+
117
+ def __on_periodic(self):
118
+ for callback in self.periodic_callbacks:
119
+ callback()
120
+
121
+ def on_periodic(self, function: Callable):
122
+ self.periodic_callbacks.append(function)
123
+
124
+ return function
@@ -0,0 +1,44 @@
1
+ from enum import Enum
2
+ from typing import Callable
3
+
4
+ class ServerState(Enum):
5
+ STARTING = "starting"
6
+ RUNNING = "running"
7
+ PAUSED = "paused"
8
+ STOPPING = "stopping"
9
+ STOPPED = "stopped"
10
+
11
+ class StateHandler:
12
+ def __init__(self, initial_state: ServerState):
13
+ self.__server_state = initial_state
14
+ self.state_callbacks: dict[ServerState | None, list[Callable]] = {}
15
+
16
+ def set(self, state: ServerState):
17
+ if (state == self.__server_state):
18
+ return
19
+
20
+ self.__server_state = state
21
+
22
+ for callback in self.state_callbacks.get(state, []):
23
+ callback()
24
+
25
+ for callback in self.state_callbacks.get(None, []):
26
+ callback()
27
+
28
+ def on_state_change(self, function: Callable, state: ServerState | None = None) -> Callable:
29
+ def decorator():
30
+ if state is None or self.get() == state:
31
+ function()
32
+
33
+ if (state is not None and not isinstance(state, ServerState)):
34
+ raise NameError(f"State {state} is not a valid server state.")
35
+
36
+ if state not in self.state_callbacks:
37
+ self.state_callbacks[state] = []
38
+ self.state_callbacks[state].append(function)
39
+
40
+ decorator() # Call the function immediately if we're already in the correct state
41
+ return decorator
42
+
43
+ def get(self) -> ServerState:
44
+ return self.__server_state
File without changes
@@ -0,0 +1,19 @@
1
+ ANSI_COLORS = {
2
+ "reset": "\033[0m",
3
+ "red": "\033[31m",
4
+ "bright_red": "\033[91m",
5
+ "green": "\033[32m",
6
+ "bright_green": "\033[92m",
7
+ "yellow": "\033[33m",
8
+ "bright_yellow": "\033[93m",
9
+ "blue": "\033[34m",
10
+ "bright_blue": "\033[94m",
11
+ "magenta": "\033[35m",
12
+ "bright_magenta": "\033[95m",
13
+ "cyan": "\033[36m",
14
+ "bright_cyan": "\033[96m",
15
+ "white": "\033[37m",
16
+ "bright_white": "\033[97m",
17
+ "black": "\033[30m",
18
+ "bright_black": "\033[90m",
19
+ }
@@ -0,0 +1,30 @@
1
+ import re
2
+ from ..state_handler import ServerState
3
+
4
+ # These regex statements were vibe coded 🤤:
5
+ # "[HH:MM:SS] [Thread/Level]:"
6
+ LOG_PREFIX = re.compile(r"^\[\d{2}:\d{2}:\d{2}\] \[[^\]]+\]: ")
7
+ # "Playername joined the game"
8
+ PLAYER_JOINED_REGEX = r"^[^\s]+ joined the game"
9
+
10
+ def __strip_prefix(line: str) -> str:
11
+ return LOG_PREFIX.sub("", line, count=1)
12
+
13
+ def get_state_from_line(line: str):
14
+ msg = __strip_prefix(line)
15
+
16
+ state_table = {
17
+ msg.startswith("Done ("): ServerState.RUNNING,
18
+ msg.startswith("Stopping the server"): ServerState.STOPPING,
19
+ msg.startswith("Server empty for 60 seconds, pausing"): ServerState.PAUSED,
20
+ re.match(PLAYER_JOINED_REGEX, msg): ServerState.RUNNING
21
+ }
22
+
23
+ new_state = None
24
+ for condition in state_table:
25
+ new_state_value = state_table[condition]
26
+ if condition:
27
+ new_state = new_state_value
28
+ break
29
+
30
+ return new_state
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: minestrapper
3
+ Version: 0.1.0
4
+ Summary: Minestrapper is a bootstrapper for Minecraft servers that lets you easily add custom features to your server without needing a full mod/plugin loader.
5
+ Author: Faizaan J
6
+ Requires-Python: >=3.9
7
+ License-File: LICENSE
8
+ Dynamic: license-file
@@ -0,0 +1,22 @@
1
+ minestrapper/__init__.py,sha256=DlUQbX9W9H2yzQeLfTtka4CD9C-3ZQqoOJioPuukzo8,105
2
+ minestrapper/config_handler.py,sha256=JDn10xeuJKLehmSbt0pdYExxaCpMZO1HVPPuWrYmc9A,1726
3
+ minestrapper/logger.py,sha256=VZ8K9kt6S2CzXj6EQRBvo_Efp4us8ieVXDzwwNegzqc,2568
4
+ minestrapper/server.py,sha256=qzsNNebEXH5hJ8uvjs_GVXW4tmEEJ9wnDHUgGMxk7_U,4062
5
+ minestrapper/state_handler.py,sha256=qsiVrV9mONIuI_LP8l90zwVMX9WasoCgAdgMU1fbEwo,1432
6
+ minestrapper/features/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ minestrapper/features/feature.py,sha256=tVvyzEHTgakuzbx8PRsqUXN798qITvV5rxM2SQxY0KY,370
8
+ minestrapper/features/handle_built_in_features.py,sha256=lqGxnZBBeKPSXXnmUpCKwFiE_nECHnWn60z7TgEZFEQ,1204
9
+ minestrapper/features/server_resource_pack/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ minestrapper/features/server_resource_pack/address_validator.py,sha256=yI_3AaLfqInMo_8JPxoQpYhdYvgS8LPgcIWvboCaGgU,1466
11
+ minestrapper/features/server_resource_pack/path_resolver.py,sha256=NyITlpAlXP_KctMNdOpF_NXZGa-f9_XwF2554Ai9Ync,781
12
+ minestrapper/features/server_resource_pack/server_resource_pack.py,sha256=nxkYnY4y62i4mzQKc5G71dG8BNwlNZt5NV6wzDDccXg,3275
13
+ minestrapper/features/state_styles/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ minestrapper/features/state_styles/state_styles.py,sha256=Q7iPcGib5po0dEsWLfr1a6z5By7iFF6xnpr7hjXdjLU,2819
15
+ minestrapper/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
+ minestrapper/util/ansi_colors.py,sha256=ifAe1cSPFt2r9gyga3RnSZbYpk4PCDQk1AkggEBprvM,515
17
+ minestrapper/util/get_state_from_line.py,sha256=MSVzgBDlOMIRiR62ugcXWON01i_XvmIqvei7A0DpsP8,962
18
+ minestrapper-0.1.0.dist-info/licenses/LICENSE,sha256=hDJHxQ6zTzazAoRB6Z-rdxVVzl3j1YHjc9K0652RS34,1085
19
+ minestrapper-0.1.0.dist-info/METADATA,sha256=9NoMy33yp4XfjPg1nKb8AfDXevBhTvzmIZuwErtVT18,309
20
+ minestrapper-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
21
+ minestrapper-0.1.0.dist-info/top_level.txt,sha256=11eT8b6b6DLNCsPQaGDP4YRPJcVPo5vjulySPw62gds,13
22
+ minestrapper-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Faizaan J
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ minestrapper