Files
snowy/codegen/main.py
2026-08-12 07:09:10 -05:00

106 lines
4.8 KiB
Python

#!/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 "<modid>:block/<path>" 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()