#!/usr/bin/env python3 """Renders a mod's Gradle/NeoForge project skeleton from a resolved JSON config. This script is intentionally "dumb": all version resolution and validation happens on the Nix side (lib/mkMod.nix). Given a flat JSON context and a directory of Jinja2 templates, it renders a fixed manifest of files plus one set of files per declared block into an output directory. Generated code always lives under a "generated" subpackage, kept separate from hand-written code (which lib/mkMod.nix symlinks in directly from the mod's own src/ directory) so regenerating never clobbers hand-written files. """ import argparse import json from pathlib import Path from jinja2 import Environment, FileSystemLoader from . import blockbuilder # (template file relative to --templates, output path relative to --out) # Output paths may reference {package_path}, filled in from the config # before rendering. STATIC_MANIFEST = [ ("build.gradle.jinja", "build.gradle"), ("settings.gradle.jinja", "settings.gradle"), ("gradle.properties.jinja", "gradle.properties"), ("gitignore.jinja", ".gitignore"), ("neoforge.mods.toml.jinja", "src/main/resources/META-INF/neoforge.mods.toml"), ] def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--config", required=True, type=Path, help="Path to the resolved mod config JSON") parser.add_argument("--templates", required=True, type=Path, help="Directory of .jinja templates for the target MC version") parser.add_argument("--out", required=True, type=Path, help="Output directory to render the project into") args = parser.parse_args() context = json.loads(args.config.read_text()) context["blocks"] = [blockbuilder.resolve_block(b) for b in context["blocks"]] context["blockPropertyImports"] = sorted({ imp for block in context["blocks"] for imp in block["propertyImports"] }) package_path = context["javaPackage"].replace(".", "/") env = Environment( loader=FileSystemLoader(str(args.templates)), keep_trailing_newline=True, ) def render(template_name: str, output_relpath: str, **extra) -> None: template = env.get_template(template_name) rendered = template.render(**context, **extra) output_path = args.out / output_relpath output_path.parent.mkdir(parents=True, exist_ok=True) output_path.write_text(rendered) for template_name, output_template in STATIC_MANIFEST: render(template_name, output_template.format(package_path=package_path)) # Main mod class: flat + concrete when there's no hand-written override, # otherwise an unannotated, abstract Base class under generated/. if context["customClassName"]: render("MainMod.java.jinja", f"src/main/java/{package_path}/generated/{context['className']}Base.java") else: render("MainMod.java.jinja", f"src/main/java/{package_path}/{context['className']}.java") # Client-only class: only generated when the mod actually declares one. if context["customClientClassName"]: render("ClientMod.java.jinja", f"src/main/java/{package_path}/generated/{context['clientClassName']}Base.java") # Block/block-entity registries: always generated (even empty), so the main # class can unconditionally call their register(modEventBus) helpers. render("Blocks.java.jinja", f"src/main/java/{package_path}/generated/blocks/{context['blocksClassName']}.java") render("BlockEntities.java.jinja", f"src/main/java/{package_path}/generated/blocks/{context['blockEntitiesClassName']}.java") for block in context["blocks"]: block_file_name = ("Base" + block["concreteClassName"]) if block["customClassName"] else block["concreteClassName"] render("Block.java.jinja", f"src/main/java/{package_path}/generated/blocks/{block_file_name}.java", block=block) if block["blockEntity"] is not None: be = block["blockEntity"] be_file_name = ("Base" + be["concreteClassName"]) if be["customClassName"] else be["concreteClassName"] render("BlockEntity.java.jinja", f"src/main/java/{package_path}/generated/blocks/{be_file_name}.java", block=block) # blockstates.json always references ":block/" regardless of # whether that model is generated below or hand-written (customModel). assets_dir = f"src/main/resources/assets/{context['modid']}" render("blockstates.json.jinja", f"{assets_dir}/blockstates/{block['path']}.json", block=block) if not block["customModel"]: render("BlockModel.json.jinja", f"{assets_dir}/models/block/{block['path']}.json", block=block) if block["item"]["enabled"] and not block["item"]["customModel"]: render("ItemModel.json.jinja", f"{assets_dir}/models/item/{block['path']}.json", block=block) if __name__ == "__main__": main()