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.
- pytrialmc-2026.10.7/PKG-INFO +150 -0
- pytrialmc-2026.10.7/README.md +140 -0
- pytrialmc-2026.10.7/pyTrialmc/__init__.py +73 -0
- pytrialmc-2026.10.7/pyTrialmc/build.py +106 -0
- pytrialmc-2026.10.7/pyTrialmc/cli.py +66 -0
- pytrialmc-2026.10.7/pyTrialmc/color.py +34 -0
- pytrialmc-2026.10.7/pyTrialmc/console.py +42 -0
- pytrialmc-2026.10.7/pyTrialmc/crypto.py +42 -0
- pytrialmc-2026.10.7/pyTrialmc/elements.py +135 -0
- pytrialmc-2026.10.7/pyTrialmc/project.py +97 -0
- pytrialmc-2026.10.7/pyTrialmc/runtime.py +295 -0
- pytrialmc-2026.10.7/pyTrialmc/source.py +182 -0
- pytrialmc-2026.10.7/pyTrialmc.egg-info/PKG-INFO +150 -0
- pytrialmc-2026.10.7/pyTrialmc.egg-info/SOURCES.txt +17 -0
- pytrialmc-2026.10.7/pyTrialmc.egg-info/dependency_links.txt +1 -0
- pytrialmc-2026.10.7/pyTrialmc.egg-info/entry_points.txt +2 -0
- pytrialmc-2026.10.7/pyTrialmc.egg-info/top_level.txt +1 -0
- pytrialmc-2026.10.7/pyproject.toml +24 -0
- pytrialmc-2026.10.7/setup.cfg +4 -0
|
@@ -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
|