flaremc 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.
flare/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ from .compiler import _flatten_and, _eval_to_bool_score, _compile_relational
2
+ from .context import namespace, export, tick, push_context, runcommand, files, temp_obj, constant_obj, vars_obj, \
3
+ constants, _flare_assign, _flare_print, dbg
4
+ from .control_flow import _flare_if, _flare_while, _flare_for
5
+ from .types import NBTType, byte, boolean, short, long, double
6
+ from .variables import score, nbt, fixed, ref, getscore, nbtbyte, nbtbool, nbtshort, nbtint, nbtlong, nbtfloat, \
7
+ nbtdouble, nbtstr, nbtlist, nbtdict, nbtbytearray, nbtintarray, nbtlongarray
8
+
9
+ __all__ = ["namespace", "export", "tick", "score", "nbt", "fixed", "ref", "getscore", "_flare_print", "dbg", "nbtbyte", "nbtbool",
10
+ "nbtshort", "nbtint", "nbtlong", "nbtfloat", "nbtdouble", "nbtstr", "nbtlist", "nbtdict", "nbtbytearray",
11
+ "nbtintarray", "nbtlongarray"]
flare/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
flare/cli.py ADDED
@@ -0,0 +1,311 @@
1
+ import argparse
2
+ import ast
3
+ import json
4
+ import os
5
+ import shutil
6
+ import sys
7
+ import time
8
+ from pathlib import Path
9
+
10
+ from watchdog.events import FileSystemEventHandler
11
+ from watchdog.observers import Observer
12
+
13
+ from flare import context
14
+ from flare.preprocessor import FlareTransformer, preprocess_minecraft_commands
15
+
16
+
17
+ def init_project(path: str):
18
+ p = Path(path)
19
+ p.mkdir(parents=True, exist_ok=True)
20
+ json_path = p / "flare.json"
21
+
22
+ if json_path.exists():
23
+ print(f"Project already initialized at {p.absolute()}")
24
+ return
25
+
26
+ print("Initializing Flare project...")
27
+ try:
28
+ namespace = input("Namespace [flare]: ").strip() or "flare"
29
+ pack_format = input("Pack format [15]: ").strip() or "15"
30
+ description = input("Description [A Flare datapack]: ").strip() or "A Flare datapack"
31
+ except (KeyboardInterrupt, EOFError):
32
+ print("\nInitialization cancelled.")
33
+ return
34
+
35
+ config = {"namespace": namespace, "pack_format": int(pack_format), "description": description, "build_dir": "dist"}
36
+
37
+ with open(json_path, "w") as f:
38
+ json.dump(config, f, indent=4)
39
+ print(f"Created {json_path.absolute()}")
40
+
41
+
42
+ def build_datapack(file_path: str):
43
+ p = Path(file_path).parent
44
+ json_path = p / "flare.json"
45
+
46
+ if json_path.exists():
47
+ with open(json_path, "r") as f:
48
+ config = json.load(f)
49
+ else:
50
+ config = {"namespace": "flare", "pack_format": 15, "description": "A Flare datapack", "build_dir": "dist"}
51
+
52
+ namespace = config.get("namespace", "flare")
53
+ build_dir = Path(config.get("build_dir", "dist"))
54
+ if not build_dir.is_absolute():
55
+ build_dir = p / build_dir
56
+
57
+ context.reset_context()
58
+ context._current_namespace = namespace
59
+
60
+ print(f"Compiling {file_path}...")
61
+
62
+ old_modules = set(sys.modules.keys())
63
+
64
+ try:
65
+ abs_path = os.path.abspath(file_path)
66
+ sys.path.insert(0, os.path.dirname(abs_path))
67
+
68
+ with open(abs_path, "r") as f:
69
+ source = f.read()
70
+
71
+ source = preprocess_minecraft_commands(source)
72
+
73
+ tree = ast.parse(source, abs_path)
74
+ transformer = FlareTransformer()
75
+ tree = transformer.visit(tree)
76
+ ast.fix_missing_locations(tree)
77
+
78
+ global_env = {"__name__": "__main__", "__file__": abs_path}
79
+ exec("from flare import _flare_assign, _flare_if, _flare_while, _flare_for, runcommand", global_env)
80
+
81
+ exec(compile(tree, abs_path, "exec"), global_env)
82
+ sys.path.pop(0)
83
+ except Exception as e:
84
+ print(f"Build failed: {e}")
85
+ import traceback
86
+ traceback.print_exc()
87
+ return False, set(), None
88
+
89
+ new_modules = set(sys.modules.keys()) - old_modules
90
+ watch_files = {os.path.abspath(file_path)}
91
+ for mod_name in new_modules:
92
+ mod = sys.modules.get(mod_name)
93
+ if mod and getattr(mod, "__file__", None):
94
+ mod_file = os.path.abspath(mod.__file__)
95
+ if (mod_file.endswith(".fl") or mod_file.endswith(
96
+ ".py")) and "site-packages" not in mod_file and "lib/python" not in mod_file:
97
+ watch_files.add(mod_file)
98
+
99
+ if build_dir.exists():
100
+ shutil.rmtree(build_dir)
101
+
102
+ build_dir.mkdir(parents=True, exist_ok=True)
103
+
104
+ with open(build_dir / "pack.mcmeta", "w") as f:
105
+ json.dump({"pack": {"pack_format": config.get("pack_format", 15),
106
+ "description": config.get("description", "A Flare datapack")}}, f, indent=4)
107
+
108
+ tags = {"tick": [], "load": []}
109
+
110
+ load_key = f"{context._current_namespace}:load"
111
+ if "main" in context.files:
112
+ if load_key not in context.files:
113
+ context.files[load_key] = []
114
+ context.files[load_key].extend(context.files.pop("main"))
115
+
116
+ for filename, lines in context.files.items():
117
+ if not lines and filename != "main":
118
+ continue
119
+
120
+ if filename.endswith(":tick"):
121
+ tags["tick"].append(filename)
122
+ elif filename.endswith(":load"):
123
+ tags["load"].append(filename)
124
+
125
+ if ":" in filename:
126
+ ns, name = filename.split(":", 1)
127
+ file_p = build_dir / "data" / ns / "functions" / f"{name}.mcfunction"
128
+ else:
129
+ file_p = build_dir / "data" / context._current_namespace / "functions" / f"{filename}.mcfunction"
130
+
131
+ file_p.parent.mkdir(parents=True, exist_ok=True)
132
+ with open(file_p, "w") as f:
133
+ for line in lines:
134
+ f.write(f"{line}\n")
135
+
136
+ tag_dir = build_dir / "data" / "minecraft" / "tags" / "functions"
137
+ for tag_name, tag_funcs in tags.items():
138
+ if tag_funcs:
139
+ tag_dir.mkdir(parents=True, exist_ok=True)
140
+ tag_path = tag_dir / f"{tag_name}.json"
141
+ with open(tag_path, "w") as f:
142
+ json.dump({"values": tag_funcs}, f, indent=4)
143
+
144
+ print(f"Successfully built datapack to {build_dir.absolute()}")
145
+ return True, watch_files, build_dir
146
+
147
+
148
+ class WatcherHandler(FileSystemEventHandler):
149
+ def __init__(self, cli_args, watch_files):
150
+ self.cli_args = cli_args
151
+ self.watch_files = watch_files
152
+ self.rebuild_pending = False
153
+
154
+ def on_modified(self, event):
155
+ if not event.is_directory and event.src_path in self.watch_files:
156
+ self.rebuild_pending = True
157
+
158
+
159
+ def get_tags(build_dir: Path, tag_type: str, tag_name: str) -> list[str]:
160
+ tag_path = build_dir / "data" / "minecraft" / "tags" / tag_type / f"{tag_name}.json"
161
+ if tag_path.exists():
162
+ try:
163
+ with open(tag_path, "r") as f:
164
+ data = json.load(f)
165
+ return data.get("values", [])
166
+ except Exception:
167
+ pass
168
+ return []
169
+
170
+
171
+ def run_emulator(build_dir: Path):
172
+ try:
173
+ import mcemu
174
+ except ImportError:
175
+ print("mcemu is not installed. Run `pip install mcemu` to use the --run feature.")
176
+ return None
177
+ print("\n--- Starting mcemu ---")
178
+ try:
179
+ import mcemu.commands
180
+ except ImportError:
181
+ pass
182
+
183
+ emu = mcemu.Emulator()
184
+ emu.load_datapack(str(build_dir.absolute()))
185
+
186
+ return emu
187
+
188
+
189
+ def main():
190
+ parser = argparse.ArgumentParser(description="Flare CLI Datapack Compiler")
191
+ parser.add_argument("target", nargs="?", default=".",
192
+ help="File to build or directory to init. Use 'init' to initialize in current directory.")
193
+ parser.add_argument("--watch", action="store_true", help="Watch for file changes and rebuild")
194
+ parser.add_argument("--run", nargs="?", const="-1", default=None,
195
+ help="Run the compiled datapack in mcemu. Optionally specify a timeout in seconds.")
196
+
197
+ args = parser.parse_args()
198
+
199
+ is_init = args.target == "init"
200
+ if is_init:
201
+ init_project(".")
202
+ if not args.watch and not args.run:
203
+ return
204
+
205
+ if os.path.isdir(args.target):
206
+ file_path = os.path.join(args.target, "main.fl")
207
+ if not os.path.exists(file_path) and os.path.exists(os.path.join(args.target, "main.py")):
208
+ file_path = os.path.join(args.target, "main.py")
209
+ else:
210
+ if args.target.endswith(".fl") or args.target.endswith(".py"):
211
+ file_path = args.target
212
+ else:
213
+ file_path = f"{args.target}.fl"
214
+ if not os.path.exists(file_path) and os.path.exists(f"{args.target}.py"):
215
+ file_path = f"{args.target}.py"
216
+
217
+ if not os.path.exists(file_path) and not is_init:
218
+ print(f"Error: Target file {file_path} not found.")
219
+ return
220
+
221
+ success, watch_files, build_dir = build_datapack(file_path)
222
+
223
+ emu_thread = None
224
+ running = True
225
+
226
+ def run_loop(emu, timeout):
227
+ try:
228
+ start_time = time.time()
229
+ while running:
230
+ if timeout is not None and timeout >= 0:
231
+ if time.time() - start_time >= timeout:
232
+ break
233
+ if emu:
234
+ emu.tick()
235
+ time.sleep(0.05)
236
+ except KeyboardInterrupt:
237
+ pass
238
+
239
+ import threading
240
+ if success and args.run is not None:
241
+ try:
242
+ timeout = float(args.run) if args.run != "-1" else None
243
+ except ValueError:
244
+ timeout = None
245
+ emu = run_emulator(build_dir)
246
+ if emu:
247
+ emu_thread = threading.Thread(target=run_loop, args=(emu, timeout))
248
+ emu_thread.daemon = True
249
+ emu_thread.start()
250
+
251
+ if args.watch:
252
+ print(f"Watching for changes in {len(watch_files)} files...")
253
+ handler = WatcherHandler(args, watch_files)
254
+ observer = Observer()
255
+
256
+ watch_dirs = set(os.path.dirname(f) for f in watch_files)
257
+ for d in watch_dirs:
258
+ observer.schedule(handler, d, recursive=False)
259
+
260
+ observer.start()
261
+
262
+ try:
263
+ while True:
264
+ time.sleep(0.5)
265
+ if handler.rebuild_pending:
266
+ print("\nChange detected. Rebuilding...")
267
+ handler.rebuild_pending = False
268
+
269
+ if emu_thread:
270
+ running = False
271
+ emu_thread.join()
272
+
273
+ success, new_watch_files, build_dir = build_datapack(file_path)
274
+
275
+ if success and new_watch_files != watch_files:
276
+ observer.unschedule_all()
277
+ watch_files = new_watch_files
278
+ handler.watch_files = watch_files
279
+ watch_dirs = set(os.path.dirname(f) for f in watch_files)
280
+ for d in watch_dirs:
281
+ observer.schedule(handler, d, recursive=False)
282
+
283
+ if success and args.run is not None:
284
+ running = True
285
+ try:
286
+ timeout = float(args.run) if args.run != "-1" else None
287
+ except ValueError:
288
+ timeout = None
289
+ emu = run_emulator(build_dir)
290
+ if emu:
291
+ emu_thread = threading.Thread(target=run_loop, args=(emu, timeout))
292
+ emu_thread.daemon = True
293
+ emu_thread.start()
294
+
295
+ except KeyboardInterrupt:
296
+ observer.stop()
297
+ running = False
298
+ print("\nStopped watching.")
299
+ observer.join()
300
+ else:
301
+ if emu_thread:
302
+ try:
303
+ while emu_thread.is_alive():
304
+ time.sleep(0.1)
305
+ except KeyboardInterrupt:
306
+ running = False
307
+ print("\nStopped emulator.")
308
+
309
+
310
+ if __name__ == "__main__":
311
+ main()
flare/compiler.py ADDED
@@ -0,0 +1,102 @@
1
+ from . import context as ctx
2
+ from .context import runcommand, temp_obj
3
+ from .types import NBTType
4
+ from .variables import score, nbt, BinaryOp, UnaryOp, getscore
5
+
6
+
7
+ def _compile_relational(node, invert=False):
8
+ op_map = {"eq": ("if", "="), "ne": ("unless", "="), "lt": ("if", "<"), "le": ("if", "<="), "gt": ("if", ">"),
9
+ "ge": ("if", ">=")}
10
+ if node.op not in op_map:
11
+ raise ValueError(f"Not a relational op: {node.op}")
12
+
13
+ keyword, mcop = op_map[node.op]
14
+ if invert:
15
+ keyword = "unless" if keyword == "if" else "if"
16
+
17
+ left, right = node.left, node.right
18
+
19
+ if not isinstance(left, score):
20
+ if isinstance(left, (int, float)):
21
+ left = getscore(left)
22
+ else:
23
+ t = score(addr=f"!c{ctx._temp_id} {temp_obj}")
24
+ ctx._temp_id += 1
25
+ if isinstance(left, (BinaryOp, UnaryOp)):
26
+ left._eval_into(t)
27
+ else:
28
+ t.__iset__(left)
29
+ left = t
30
+
31
+ if not isinstance(right, score):
32
+ if isinstance(right, (int, float)):
33
+ right = getscore(right, left.multiplier)
34
+ else:
35
+ t = score(addr=f"!c{ctx._temp_id} {temp_obj}", multiplier=left.multiplier)
36
+ ctx._temp_id += 1
37
+ if isinstance(right, (BinaryOp, UnaryOp)):
38
+ right._eval_into(t)
39
+ else:
40
+ t.__iset__(right)
41
+ right = t
42
+
43
+ if left.multiplier != right.multiplier:
44
+ t = score(addr=f"!c{ctx._temp_id} {temp_obj}", multiplier=left.multiplier)
45
+ ctx._temp_id += 1
46
+ t.__iset__(right)
47
+ right = t
48
+
49
+ return f"{keyword} score {left.addr} {mcop} {right.addr}"
50
+
51
+
52
+ def _eval_to_bool_score(node):
53
+ dest = score(addr=f"!b{ctx._temp_id} {temp_obj}")
54
+ ctx._temp_id += 1
55
+ runcommand(f"scoreboard players set {dest.addr} 0")
56
+
57
+ if isinstance(node, BinaryOp) and node.op == "or":
58
+ left_conds = _flatten_and(node.left)
59
+ runcommand(f"execute {' '.join(left_conds)} run scoreboard players set {dest.addr} 1")
60
+ right_conds = _flatten_and(node.right)
61
+ runcommand(
62
+ f"execute if score {dest.addr} matches 0 {' '.join(right_conds)} run scoreboard players set {dest.addr} 1")
63
+ return dest
64
+
65
+ if isinstance(node, UnaryOp) and node.op == "not":
66
+ sub_dest = _eval_to_bool_score(node.operand)
67
+ runcommand(f"execute if score {sub_dest.addr} matches 0 run scoreboard players set {dest.addr} 1")
68
+ return dest
69
+
70
+ if isinstance(node, (BinaryOp, UnaryOp)):
71
+ t = score(addr=f"!b{ctx._temp_id} {temp_obj}")
72
+ ctx._temp_id += 1
73
+ node._eval_into(t)
74
+ runcommand(f"execute unless score {t.addr} matches 0 run scoreboard players set {dest.addr} 1")
75
+ return dest
76
+
77
+ if node:
78
+ runcommand(f"scoreboard players set {dest.addr} 1")
79
+ return dest
80
+
81
+
82
+ def _flatten_and(node, invert=False):
83
+ if not isinstance(node, (BinaryOp, UnaryOp)):
84
+ if isinstance(node, nbt) and (node.is_sequence() or node.type == NBTType.String):
85
+ node = BinaryOp(node.length(), 0, "ne")
86
+ else:
87
+ node = BinaryOp(node, 0, "ne")
88
+ if isinstance(node, UnaryOp) and node.op == "neg":
89
+ node = BinaryOp(node, 0, "ne")
90
+ if isinstance(node, UnaryOp) and node.op == "not":
91
+ return _flatten_and(node.operand, not invert)
92
+ if isinstance(node, BinaryOp):
93
+ if node.op == "and" and not invert:
94
+ return _flatten_and(node.left, invert) + _flatten_and(node.right, invert)
95
+ if node.op == "or" and invert:
96
+ return _flatten_and(node.left, invert) + _flatten_and(node.right, invert)
97
+ if node.op in ("eq", "ne", "lt", "le", "gt", "ge"):
98
+ return [_compile_relational(node, invert)]
99
+
100
+ dest = _eval_to_bool_score(node)
101
+ keyword = "unless" if invert else "if"
102
+ return [f"{keyword} score {dest.addr} matches 1"]
flare/context.py ADDED
@@ -0,0 +1,191 @@
1
+ import json
2
+
3
+ files = {"main": []}
4
+ current_file = "main"
5
+ _current_namespace = "flare"
6
+ functions = {}
7
+ constants = {}
8
+ constant_obj = "__flare__constant__"
9
+ vars_obj = "__flare__vars__"
10
+ temp_obj = "__flare__temp__"
11
+ _temp_id = 0
12
+ _func_id = 0
13
+ _objective_offset = 0
14
+ _constant_offset = 0
15
+
16
+
17
+ def reset_context():
18
+ global files, current_file, _current_namespace, functions, constants, _temp_id, _func_id, _objective_offset, _constant_offset
19
+ files = {"main": []}
20
+ current_file = "main"
21
+ _current_namespace = "flare"
22
+ functions = {}
23
+ constants = {}
24
+ _temp_id = 0
25
+ _func_id = 0
26
+ _objective_offset = 0
27
+ _constant_offset = 0
28
+
29
+
30
+ def ensure_objective(obj: str):
31
+ global _objective_offset, _constant_offset
32
+ if not obj:
33
+ return
34
+
35
+ load_file = f"{_current_namespace}:load"
36
+ if load_file not in files:
37
+ files[load_file] = []
38
+
39
+ cmd = f"scoreboard objectives add {obj} dummy"
40
+ if cmd not in files[load_file]:
41
+ files[load_file].insert(_objective_offset, cmd)
42
+ _objective_offset += 1
43
+ _constant_offset += 1
44
+
45
+
46
+ def ensure_constant(name: str, obj: str, val: int):
47
+ global _constant_offset
48
+ ensure_objective(obj)
49
+
50
+ load_file = f"{_current_namespace}:load"
51
+ cmd = f"scoreboard players set {name} {obj} {val}"
52
+ if cmd not in files[load_file]:
53
+ files[load_file].insert(_constant_offset, cmd)
54
+ _constant_offset += 1
55
+
56
+
57
+ class _ContextManager:
58
+ def __init__(self, new_file: str):
59
+ self.new_file = new_file
60
+ self.old_file = None
61
+
62
+ def __enter__(self):
63
+ global current_file
64
+ self.old_file = current_file
65
+ current_file = self.new_file
66
+ if self.new_file not in files:
67
+ files[self.new_file] = []
68
+
69
+ def __exit__(self, exc_type, exc_val, exc_tb):
70
+ global current_file
71
+ current_file = self.old_file
72
+
73
+
74
+ def push_context(name: str):
75
+ return _ContextManager(name)
76
+
77
+
78
+ def namespace(name: str):
79
+ global _current_namespace
80
+ _current_namespace = name
81
+
82
+
83
+ def __float_prec(x: float) -> int:
84
+ return len(str(x).split(".")[-1])
85
+
86
+
87
+ def runcommand(command: str):
88
+ files[current_file].append(command)
89
+
90
+ import builtins
91
+
92
+ def dbg(*args):
93
+ processed_str = " ".join(str(arg) for arg in args)
94
+ builtins.print(processed_str)
95
+ _flare_print(processed_str)
96
+
97
+ def _flare_print(*args):
98
+ from .variables import score, nbt # to avoid circular import
99
+ components = []
100
+ for i, arg in enumerate(args):
101
+ if i > 0:
102
+ components.append({"text": " "})
103
+
104
+ if isinstance(arg, score):
105
+ if getattr(arg, 'multiplier', 1.0) != 1.0:
106
+ scale_str = f"{arg.multiplier:.15f}".rstrip("0")
107
+ if scale_str.endswith("."):
108
+ scale_str += "0"
109
+ runcommand(
110
+ f"execute store result storage flare:temp __flare_debug_{i} double {scale_str} run scoreboard players get {arg.addr}")
111
+ components.append({"nbt": f"__flare_debug_{i}", "storage": "flare:temp"})
112
+ else:
113
+ name, obj = arg.addr.split(" ", 1)
114
+ components.append({"score": {"name": name, "objective": obj}})
115
+ elif isinstance(arg, nbt):
116
+ nbt_comp = {"nbt": arg.path or "{}"}
117
+ if arg.path == "":
118
+ nbt_comp["nbt"] = "{}"
119
+
120
+ if arg.target_type == "storage":
121
+ nbt_comp["storage"] = arg.target
122
+ elif arg.target_type == "entity":
123
+ nbt_comp["entity"] = arg.target
124
+ elif arg.target_type == "block":
125
+ nbt_comp["block"] = arg.target
126
+
127
+ if arg.path == "":
128
+ nbt_comp["nbt"] = "{}"
129
+
130
+ components.append(nbt_comp)
131
+ else:
132
+ components.append({"text": str(arg)})
133
+
134
+ if len(components) == 1:
135
+ comp = components[0]
136
+ if "text" in comp and len(comp) == 1:
137
+ cmd_text = json.dumps(comp["text"])
138
+ else:
139
+ cmd_text = json.dumps(comp)
140
+ else:
141
+ cmd_text = json.dumps(components)
142
+
143
+ runcommand(f"tellraw @a {cmd_text}")
144
+
145
+
146
+ def export(func=None, *, append=False):
147
+ if func is None:
148
+ def wrapper(f):
149
+ return export(f, append=append)
150
+
151
+ return wrapper
152
+
153
+ func_name = f"{_current_namespace}:{func.__name__}"
154
+ if func_name in files and not append:
155
+ raise ValueError(f"Function {func_name} already exists. Use @export(append=True) to append.")
156
+
157
+ with push_context(func_name):
158
+ func()
159
+
160
+ return func
161
+
162
+
163
+ def _flare_assign(var_name, value, local_env, global_env):
164
+ if var_name in local_env:
165
+ target = local_env[var_name]
166
+ elif var_name in global_env:
167
+ target = global_env[var_name]
168
+ else:
169
+ target = None
170
+
171
+ if target is not None and hasattr(target, "__iset__"):
172
+ target.__iset__(value)
173
+ return target
174
+
175
+ if target is None and hasattr(value, "__icopy__"):
176
+ return value.__icopy__(varid=f"{_current_namespace}_{var_name}")
177
+
178
+ return value
179
+
180
+
181
+ def tick(func=None):
182
+ if func is None:
183
+ def wrapper(f):
184
+ return tick(f)
185
+
186
+ return wrapper
187
+
188
+ func_name = f"{_current_namespace}:tick"
189
+ with push_context(func_name):
190
+ func()
191
+ return func