pyTrialmc 2026.10.7__tar.gz

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,150 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyTrialmc
3
+ Version: 2026.10.7
4
+ Summary: Write Minecraft Java Edition mods in Python
5
+ Keywords: minecraft,mod,modloader,python
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: Topic :: Games/Entertainment
8
+ Requires-Python: >=3.9
9
+ Description-Content-Type: text/markdown
10
+
11
+ # pyTrialmc
12
+
13
+ Write Minecraft Java Edition mods in **Python**.
14
+
15
+ `pyTrialmc` is the Python library of the pyTrial loader. Your mod imports it, and
16
+ it ships the command line tool that creates, declares and packages mod projects.
17
+
18
+ | | |
19
+ | --- | --- |
20
+ | Release | 26 October 07 |
21
+ | Version | 2026.10.7 |
22
+ | Requires | Python 3.9 or newer |
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ pip install pyTrialmc
28
+ ```
29
+
30
+ ## Create a project
31
+
32
+ ```bash
33
+ cd my_mod
34
+ pytrialmc -init
35
+ ```
36
+
37
+ `-init` writes the project skeleton in the current directory:
38
+
39
+ ```
40
+ pytrial.mod.json mod metadata read by the loader
41
+ main.py the mod entry file
42
+ src/ one file per element
43
+ └── example_block.py
44
+ ```
45
+
46
+ `main.py` is the entry file the loader executes:
47
+
48
+ ```python
49
+ from pyTrialmc import *
50
+
51
+ mod = modsrc()
52
+
53
+ # Declare one element per line, every element is implemented by src/<name>.py.
54
+ # Any kind from ELEMENT_KINDS is available as mod.add.<kind>(name, **options).
55
+ # mod.add.block("example_block")
56
+ # mod.add.item("example_ingot", stack_size=64)
57
+
58
+ mod.main()
59
+ modmain()
60
+ ```
61
+
62
+ ## Declare elements
63
+
64
+ `mod.add` carries one method per element kind, named after the kind. The first
65
+ argument is the element name, and it also names the implementation file in `src/`.
66
+
67
+ ```python
68
+ mod.add.block("ruby_block", hardness=4.0)
69
+ mod.add.item("ruby_ingot", stack_size=64)
70
+ mod.add.creative_tab("ruby_tab", title="Ruby", icon="ruby_mod:ruby_ingot")
71
+ mod.add.command("ruby", description="Ruby command", aliases=["rb"])
72
+ ```
73
+
74
+ The catalog covers 37 kinds:
75
+
76
+ | | Kinds |
77
+ | --- | --- |
78
+ | | advancement, armor, armor_trim, attribute, banner_pattern, biome, block, command, creative_tab, custom_element, damage_type, dimension, enchantment, feature, fluid, function, gui, game_rule |
79
+ | | item, item_extension, key_binding, living_entity, loot_table, overlay, painting, particle, plant, potion_effect, potion_item, procedure, projectile, recipe, special_entity, structure, tool, villager_profession, villager_trade |
80
+
81
+ `block`, `item`, `creative_tab` and `command` are active: the loader bridges them
82
+ into the game today. Every other kind is declared and reported by the loader as
83
+ not supported yet, so declaring one never fails; the Python side stays unchanged
84
+ when its bridge lands.
85
+
86
+ ### Element files
87
+
88
+ One element per file, file name equal to the declared name, so
89
+ `mod.add.block("ruby_block")` is implemented by `src/ruby_block.py`:
90
+
91
+ ```python
92
+ OPTIONS = {"hardness": 2.5, "light": 4}
93
+
94
+
95
+ def configure(element):
96
+ element.options["resistance"] = 9.0
97
+ ```
98
+
99
+ Option priority: arguments passed to `mod.add.*` > `configure()` > `OPTIONS`.
100
+
101
+ ## Lifecycle hooks
102
+
103
+ The loader may also call these, if your entry file defines them:
104
+
105
+ ```python
106
+ def on_declare(ctx): ... # registration phase
107
+ def on_load(ctx): ... # everything is registered
108
+ def on_event(event_id, payload): ...# game or loader events
109
+ def on_hook(hook_id, args): ... # bytecode hook callbacks
110
+ def on_unload(): ... # the mod is going away
111
+ ```
112
+
113
+ `ctx` carries `mod_id`, `version`, `minecraft_version`, `root`, `config_dir`
114
+ and `cache_dir`.
115
+
116
+ ## Package the mod
117
+
118
+ ```bash
119
+ pytrialmc -b:1.21.8
120
+ ```
121
+
122
+ That writes `dist/<modId>-<modVersion>.pytm`. Copy the package into
123
+ `<gameDir>/pytrial/mods/`.
124
+
125
+ ## Command line
126
+
127
+ ```
128
+ pytrialmc -init create a mod project here (main.py + src/)
129
+ pytrialmc -b:<game version> build a loadable jar
130
+ pytrialmc -debug print debug output
131
+ ```
132
+
133
+ Environment: `PYTRIALMC_DEBUG=1`, `PYTRIALMC_COLOR=1`, `NO_COLOR=1`.
134
+
135
+ ## The loader
136
+
137
+ The Python library only declares content. The loader and the launcher are the
138
+ Java side of pyTrial: an independent loader that starts Minecraft itself through a
139
+ Java agent, without Forge or Fabric, then runs each mod in a Python process and
140
+ registers the declared content into the game.
141
+
142
+ ```bash
143
+ java -jar pytrial-bootstrap.jar install # install into the game directory
144
+ java -jar pytrial-bootstrap.jar launch # start the game with pyTrial
145
+ java -jar pytrial-bootstrap.jar mods # list the discovered Python mods
146
+ ```
147
+
148
+ Installing creates `<gameDir>/pytrial/` with `loader/`, `mods/`, `config/`,
149
+ `cache/` and launch scripts. Architecture and the mod format are documented in
150
+ `docs/`.
@@ -0,0 +1,140 @@
1
+ # pyTrialmc
2
+
3
+ Write Minecraft Java Edition mods in **Python**.
4
+
5
+ `pyTrialmc` is the Python library of the pyTrial loader. Your mod imports it, and
6
+ it ships the command line tool that creates, declares and packages mod projects.
7
+
8
+ | | |
9
+ | --- | --- |
10
+ | Release | 26 October 07 |
11
+ | Version | 2026.10.7 |
12
+ | Requires | Python 3.9 or newer |
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ pip install pyTrialmc
18
+ ```
19
+
20
+ ## Create a project
21
+
22
+ ```bash
23
+ cd my_mod
24
+ pytrialmc -init
25
+ ```
26
+
27
+ `-init` writes the project skeleton in the current directory:
28
+
29
+ ```
30
+ pytrial.mod.json mod metadata read by the loader
31
+ main.py the mod entry file
32
+ src/ one file per element
33
+ └── example_block.py
34
+ ```
35
+
36
+ `main.py` is the entry file the loader executes:
37
+
38
+ ```python
39
+ from pyTrialmc import *
40
+
41
+ mod = modsrc()
42
+
43
+ # Declare one element per line, every element is implemented by src/<name>.py.
44
+ # Any kind from ELEMENT_KINDS is available as mod.add.<kind>(name, **options).
45
+ # mod.add.block("example_block")
46
+ # mod.add.item("example_ingot", stack_size=64)
47
+
48
+ mod.main()
49
+ modmain()
50
+ ```
51
+
52
+ ## Declare elements
53
+
54
+ `mod.add` carries one method per element kind, named after the kind. The first
55
+ argument is the element name, and it also names the implementation file in `src/`.
56
+
57
+ ```python
58
+ mod.add.block("ruby_block", hardness=4.0)
59
+ mod.add.item("ruby_ingot", stack_size=64)
60
+ mod.add.creative_tab("ruby_tab", title="Ruby", icon="ruby_mod:ruby_ingot")
61
+ mod.add.command("ruby", description="Ruby command", aliases=["rb"])
62
+ ```
63
+
64
+ The catalog covers 37 kinds:
65
+
66
+ | | Kinds |
67
+ | --- | --- |
68
+ | | advancement, armor, armor_trim, attribute, banner_pattern, biome, block, command, creative_tab, custom_element, damage_type, dimension, enchantment, feature, fluid, function, gui, game_rule |
69
+ | | item, item_extension, key_binding, living_entity, loot_table, overlay, painting, particle, plant, potion_effect, potion_item, procedure, projectile, recipe, special_entity, structure, tool, villager_profession, villager_trade |
70
+
71
+ `block`, `item`, `creative_tab` and `command` are active: the loader bridges them
72
+ into the game today. Every other kind is declared and reported by the loader as
73
+ not supported yet, so declaring one never fails; the Python side stays unchanged
74
+ when its bridge lands.
75
+
76
+ ### Element files
77
+
78
+ One element per file, file name equal to the declared name, so
79
+ `mod.add.block("ruby_block")` is implemented by `src/ruby_block.py`:
80
+
81
+ ```python
82
+ OPTIONS = {"hardness": 2.5, "light": 4}
83
+
84
+
85
+ def configure(element):
86
+ element.options["resistance"] = 9.0
87
+ ```
88
+
89
+ Option priority: arguments passed to `mod.add.*` > `configure()` > `OPTIONS`.
90
+
91
+ ## Lifecycle hooks
92
+
93
+ The loader may also call these, if your entry file defines them:
94
+
95
+ ```python
96
+ def on_declare(ctx): ... # registration phase
97
+ def on_load(ctx): ... # everything is registered
98
+ def on_event(event_id, payload): ...# game or loader events
99
+ def on_hook(hook_id, args): ... # bytecode hook callbacks
100
+ def on_unload(): ... # the mod is going away
101
+ ```
102
+
103
+ `ctx` carries `mod_id`, `version`, `minecraft_version`, `root`, `config_dir`
104
+ and `cache_dir`.
105
+
106
+ ## Package the mod
107
+
108
+ ```bash
109
+ pytrialmc -b:1.21.8
110
+ ```
111
+
112
+ That writes `dist/<modId>-<modVersion>.pytm`. Copy the package into
113
+ `<gameDir>/pytrial/mods/`.
114
+
115
+ ## Command line
116
+
117
+ ```
118
+ pytrialmc -init create a mod project here (main.py + src/)
119
+ pytrialmc -b:<game version> build a loadable jar
120
+ pytrialmc -debug print debug output
121
+ ```
122
+
123
+ Environment: `PYTRIALMC_DEBUG=1`, `PYTRIALMC_COLOR=1`, `NO_COLOR=1`.
124
+
125
+ ## The loader
126
+
127
+ The Python library only declares content. The loader and the launcher are the
128
+ Java side of pyTrial: an independent loader that starts Minecraft itself through a
129
+ Java agent, without Forge or Fabric, then runs each mod in a Python process and
130
+ registers the declared content into the game.
131
+
132
+ ```bash
133
+ java -jar pytrial-bootstrap.jar install # install into the game directory
134
+ java -jar pytrial-bootstrap.jar launch # start the game with pyTrial
135
+ java -jar pytrial-bootstrap.jar mods # list the discovered Python mods
136
+ ```
137
+
138
+ Installing creates `<gameDir>/pytrial/` with `loader/`, `mods/`, `config/`,
139
+ `cache/` and launch scripts. Architecture and the mod format are documented in
140
+ `docs/`.
@@ -0,0 +1,73 @@
1
+ from .console import console, is_debug, set_debug
2
+ from .elements import ACTIVE_KINDS, ELEMENT_KINDS, ELEMENT_LABELS
3
+ from .runtime import (
4
+ close_gui,
5
+ error,
6
+ log,
7
+ off,
8
+ on,
9
+ open_gui,
10
+ register,
11
+ register_advancement,
12
+ register_biome,
13
+ register_block,
14
+ register_block_entity,
15
+ register_command,
16
+ register_creative_tab,
17
+ register_dimension,
18
+ register_enchantment,
19
+ register_entity,
20
+ register_feature,
21
+ register_gui,
22
+ register_item,
23
+ register_loot_table,
24
+ register_menu,
25
+ register_particle,
26
+ register_potion_effect,
27
+ register_recipe,
28
+ register_sound,
29
+ warn,
30
+ )
31
+ from .source import Element, ModSource, declared_elements, modmain, modsrc
32
+
33
+ __version__ = "2026.10.7"
34
+
35
+ __all__ = [
36
+ "console",
37
+ "is_debug",
38
+ "set_debug",
39
+ "log",
40
+ "warn",
41
+ "error",
42
+ "on",
43
+ "off",
44
+ "register",
45
+ "register_block",
46
+ "register_item",
47
+ "register_command",
48
+ "register_block_entity",
49
+ "register_entity",
50
+ "register_menu",
51
+ "register_creative_tab",
52
+ "register_sound",
53
+ "register_particle",
54
+ "register_recipe",
55
+ "register_advancement",
56
+ "register_loot_table",
57
+ "register_biome",
58
+ "register_dimension",
59
+ "register_feature",
60
+ "register_enchantment",
61
+ "register_potion_effect",
62
+ "register_gui",
63
+ "open_gui",
64
+ "close_gui",
65
+ "modsrc",
66
+ "modmain",
67
+ "ModSource",
68
+ "Element",
69
+ "declared_elements",
70
+ "ELEMENT_KINDS",
71
+ "ELEMENT_LABELS",
72
+ "ACTIVE_KINDS",
73
+ ]
@@ -0,0 +1,106 @@
1
+ import io
2
+ import json
3
+ import zipfile
4
+ from datetime import datetime, timezone
5
+ from pathlib import Path
6
+
7
+ from . import __version__
8
+ from . import crypto
9
+ from .color import GREEN, RED, paint
10
+ from .console import console
11
+
12
+ METADATA_FILE = "pytrial.mod.json"
13
+ BUILD_INFO_FILE = "pytrial.build.json"
14
+ OUTPUT_DIR = "dist"
15
+ PACKAGE_SUFFIX = ".pytm"
16
+
17
+ EXCLUDED_DIRS = {
18
+ "dist",
19
+ "build",
20
+ "__pycache__",
21
+ ".git",
22
+ ".venv",
23
+ ".idea",
24
+ ".vscode",
25
+ "venv",
26
+ "env",
27
+ }
28
+ EXCLUDED_SUFFIXES = {".pyc", ".pyo"}
29
+
30
+
31
+ def collect_files(root):
32
+ files = []
33
+ skipped = []
34
+ for path in sorted(root.rglob("*")):
35
+ if not path.is_file():
36
+ continue
37
+ relative = path.relative_to(root)
38
+ if any(part in EXCLUDED_DIRS for part in relative.parts[:-1]):
39
+ skipped.append(relative.as_posix())
40
+ continue
41
+ if path.suffix in EXCLUDED_SUFFIXES:
42
+ skipped.append(relative.as_posix())
43
+ continue
44
+ if relative.name == BUILD_INFO_FILE:
45
+ skipped.append(relative.as_posix())
46
+ continue
47
+ files.append(path)
48
+
49
+ console.debug("included entries: " + str([path.relative_to(root).as_posix() for path in files]))
50
+ if skipped:
51
+ console.debug("skipped entries: " + str(skipped))
52
+ return files
53
+
54
+
55
+ def build_package(root, game_version, output_dir=None):
56
+ game_version = (game_version or "").strip()
57
+ if not game_version:
58
+ print(paint("Missing game version, usage: pytrialmc -b:<game version>", RED))
59
+ return 1
60
+
61
+ metadata_path = root / METADATA_FILE
62
+ if not metadata_path.is_file():
63
+ print(paint("No " + METADATA_FILE + " in the current directory, run: pytrialmc -init", RED))
64
+ return 1
65
+
66
+ console.debug("build root: " + str(root))
67
+ console.debug("game version: " + game_version)
68
+
69
+ try:
70
+ metadata = json.loads(metadata_path.read_text(encoding="utf-8"))
71
+ except (json.JSONDecodeError, UnicodeDecodeError) as error:
72
+ print(paint(METADATA_FILE + " is not valid JSON: " + str(error), RED))
73
+ return 1
74
+
75
+ mod_id = metadata.get("id") or root.name
76
+ mod_version = metadata.get("version") or "1.0.0"
77
+ console.debug("mod id: " + str(mod_id) + ", mod version: " + str(mod_version))
78
+
79
+ target_dir = Path(output_dir) if output_dir else root / OUTPUT_DIR
80
+ target_dir.mkdir(parents=True, exist_ok=True)
81
+ package_path = target_dir / (mod_id + "-" + mod_version + PACKAGE_SUFFIX)
82
+
83
+ build_info = {
84
+ "builder": "pyTrialmc",
85
+ "builderVersion": __version__,
86
+ "gameVersion": game_version,
87
+ "modId": mod_id,
88
+ "modVersion": mod_version,
89
+ "builtAt": datetime.now(timezone.utc).isoformat(),
90
+ }
91
+
92
+ files = collect_files(root)
93
+ buffer = io.BytesIO()
94
+ with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as archive:
95
+ archive.writestr(BUILD_INFO_FILE, json.dumps(build_info, ensure_ascii=False, indent=2) + "\n")
96
+ for path in files:
97
+ archive.write(path, path.relative_to(root).as_posix())
98
+ package_path.write_bytes(crypto.encrypt(buffer.getvalue()))
99
+
100
+ console.debug("package written: " + str(package_path))
101
+
102
+ print(paint("Built " + str(package_path), GREEN))
103
+ print(" game version: " + game_version)
104
+ print(" entries: " + str(len(files) + 1))
105
+ print(" copy it into <gameDir>/pytrial/mods/")
106
+ return 0
@@ -0,0 +1,66 @@
1
+ import sys
2
+ from pathlib import Path
3
+
4
+ from . import __version__
5
+ from .build import build_package
6
+ from .color import BOLD, CYAN, DIM, RED, paint
7
+ from .console import console, is_debug, set_debug
8
+ from .project import init_project
9
+
10
+ DEBUG_FLAG = "-debug"
11
+
12
+
13
+ def usage_text():
14
+ lines = [
15
+ paint("pyTrialmc " + __version__, BOLD, CYAN),
16
+ "",
17
+ paint("Usage:", BOLD),
18
+ " pytrialmc -init " + paint("create a mod project here (main.py + src/)", DIM),
19
+ " pytrialmc -b:<game version> " + paint("build a loadable jar", DIM),
20
+ " pytrialmc -debug " + paint("print debug output", DIM),
21
+ "",
22
+ paint("Examples:", BOLD),
23
+ " pytrialmc -init",
24
+ " pytrialmc -b:1.21.8",
25
+ " pytrialmc -b:1.21.8 -debug",
26
+ "",
27
+ paint("Environment:", BOLD),
28
+ " PYTRIALMC_DEBUG=1 " + paint("same as -debug", DIM),
29
+ " PYTRIALMC_COLOR=1 " + paint("force colors, 0 to disable", DIM),
30
+ " NO_COLOR=1 " + paint("disable colors", DIM),
31
+ ]
32
+ return "\n".join(lines)
33
+
34
+
35
+ def main(argv=None):
36
+ args = list(sys.argv[1:] if argv is None else argv)
37
+
38
+ if DEBUG_FLAG in args:
39
+ args.remove(DEBUG_FLAG)
40
+ set_debug(True)
41
+
42
+ console.debug("argv: " + repr(args))
43
+ console.debug("python: " + sys.version.split()[0])
44
+ console.debug("cwd: " + str(Path.cwd()))
45
+ console.debug("colors: " + str(paint("", BOLD) != ""))
46
+
47
+ if not args or args[0] in ("-h", "--help", "help"):
48
+ print(usage_text())
49
+ return 0
50
+
51
+ command = args[0]
52
+ console.debug("command: " + command)
53
+
54
+ if command == "-init":
55
+ return init_project(Path.cwd())
56
+
57
+ if command.startswith("-b:"):
58
+ return build_package(Path.cwd(), command[len("-b:"):])
59
+
60
+ print(paint("Unknown option: " + command, RED))
61
+ print(usage_text())
62
+ return 1
63
+
64
+
65
+ if __name__ == "__main__":
66
+ sys.exit(main())
@@ -0,0 +1,34 @@
1
+ import os
2
+ import sys
3
+
4
+ RESET = "\033[0m"
5
+ BOLD = "\033[1m"
6
+ DIM = "\033[2m"
7
+ RED = "\033[31m"
8
+ GREEN = "\033[32m"
9
+ YELLOW = "\033[33m"
10
+ BLUE = "\033[34m"
11
+ MAGENTA = "\033[35m"
12
+ CYAN = "\033[36m"
13
+ GREY = "\033[90m"
14
+
15
+ _DISABLED = {"0", "false", "off", "never", "no"}
16
+ _ENABLED = {"1", "true", "on", "always", "force", "yes"}
17
+
18
+
19
+ def enabled(stream=None):
20
+ if os.environ.get("NO_COLOR"):
21
+ return False
22
+ override = os.environ.get("PYTRIALMC_COLOR", "").strip().lower()
23
+ if override in _DISABLED:
24
+ return False
25
+ if override in _ENABLED:
26
+ return True
27
+ target = sys.stdout if stream is None else stream
28
+ return bool(getattr(target, "isatty", lambda: False)())
29
+
30
+
31
+ def paint(text, *styles, stream=None):
32
+ if not styles or not enabled(stream):
33
+ return str(text)
34
+ return "".join(styles) + str(text) + RESET
@@ -0,0 +1,42 @@
1
+ import os
2
+
3
+ from . import runtime
4
+ from .color import CYAN, GREY, paint
5
+
6
+ _TRUTHY = {"1", "true", "on", "yes", "always"}
7
+
8
+ _debug = os.environ.get("PYTRIALMC_DEBUG", "").strip().lower() in _TRUTHY
9
+
10
+
11
+ def set_debug(value):
12
+ global _debug
13
+ _debug = bool(value)
14
+ return _debug
15
+
16
+
17
+ def is_debug():
18
+ return _debug
19
+
20
+
21
+ def _join(args):
22
+ return " ".join(str(arg) for arg in args)
23
+
24
+
25
+ def _write(text, color):
26
+ if runtime.is_bound():
27
+ runtime.log(text)
28
+ else:
29
+ print(paint(text, color))
30
+
31
+
32
+ class _Console:
33
+
34
+ def log(self, *args):
35
+ _write(_join(args), CYAN)
36
+
37
+ def debug(self, *args):
38
+ if _debug:
39
+ _write("[debug] " + _join(args), GREY)
40
+
41
+
42
+ console = _Console()
@@ -0,0 +1,42 @@
1
+ import hashlib
2
+ import os
3
+
4
+ MAGIC = b"PYTM"
5
+ VERSION = 1
6
+ SALT_LENGTH = 16
7
+ CHECKSUM_LENGTH = 32
8
+ HEADER_LENGTH = len(MAGIC) + 1 + SALT_LENGTH + CHECKSUM_LENGTH
9
+ KEY = hashlib.sha256(b"pyTrial mod package v1").digest()
10
+
11
+
12
+ def _keystream(salt, length):
13
+ stream = bytearray()
14
+ counter = 0
15
+ while len(stream) < length:
16
+ stream += hashlib.sha256(KEY + salt + counter.to_bytes(4, "little")).digest()
17
+ counter += 1
18
+ return bytes(stream[:length])
19
+
20
+
21
+ def _xor(data, stream):
22
+ return bytes(left ^ right for left, right in zip(data, stream))
23
+
24
+
25
+ def encrypt(data):
26
+ salt = os.urandom(SALT_LENGTH)
27
+ body = _xor(data, _keystream(salt, len(data)))
28
+ return MAGIC + bytes([VERSION]) + salt + hashlib.sha256(data).digest() + body
29
+
30
+
31
+ def decrypt(data):
32
+ if len(data) <= HEADER_LENGTH or data[: len(MAGIC)] != MAGIC:
33
+ raise ValueError("not a pyTrial package")
34
+ if data[len(MAGIC)] != VERSION:
35
+ raise ValueError("unsupported package version " + str(data[len(MAGIC)]))
36
+ start = len(MAGIC) + 1
37
+ salt = data[start : start + SALT_LENGTH]
38
+ checksum = data[start + SALT_LENGTH : HEADER_LENGTH]
39
+ plain = _xor(data[HEADER_LENGTH:], _keystream(salt, len(data) - HEADER_LENGTH))
40
+ if hashlib.sha256(plain).digest() != checksum:
41
+ raise ValueError("package checksum mismatch")
42
+ return plain