yog-cli 0.39.0

Yog build tool — `yog build` compiles a mod and packages it as .yog.
Documentation
"""The game's ids as constants for mod code: yog-api's `vanilla` module
(`yog_api::vanilla::items::REDSTONE`), so mods don't spell ids in strings.
Blocks, items and entities come from ids.txt; effects, enchantments,
particles, damage types, dimensions, sounds and tags from
the client jars of every supported version (Loom's cache):

    python3 gen.py ids.txt && python3 gen_api.py ../../../yog-api/src/vanilla.rs
"""
import collections, os, re, sys, zipfile

VERSIONS = ["1.20.1", "1.21.1"]
ALL = set(VERSIONS)

found = collections.defaultdict(lambda: collections.defaultdict(set))  # kind -> path -> versions

here = os.path.dirname(os.path.abspath(__file__))
for line in open(os.path.join(here, "ids.txt")):
    if line.startswith("#") or not line.strip():
        continue
    kind, path, versions = line.split()
    if kind in ("block", "item", "entity"):
        found[kind][path] = ALL if versions == "*" else set(versions.split(","))

SOUND = re.compile(rb"^[a-z0-9_]+(\.[a-z0-9_]+)+$")
for v in VERSIONS:
    z = zipfile.ZipFile(os.path.expanduser(f"~/.gradle/caches/fabric-loom/{v}/minecraft-client.jar"))
    names = z.namelist()
    lang = __import__("json").loads(z.read("assets/minecraft/lang/en_us.json"))
    for k in lang:
        m = re.fullmatch(r"(effect|enchantment)\.minecraft\.([a-z0-9_]+)", k)
        if m:
            found[m[1]][m[2]].add(v)
    for n in names:
        m = re.fullmatch(r"assets/minecraft/particles/([a-z0-9_]+)\.json", n)
        if m:
            found["particle"][m[1]].add(v)
        m = re.fullmatch(r"data/minecraft/damage_type/([a-z0-9_]+)\.json", n)
        if m:
            found["damage_type"][m[1]].add(v)
        m = re.fullmatch(r"data/minecraft/dimension_type/([a-z0-9_]+)\.json", n)
        if m:
            found["dimension"][m[1]].add(v)
    for n in names:
        m = re.fullmatch(r"data/minecraft/tags/(blocks?|items?|entity_types?)/([a-z0-9_/]+)\.json", n)
        if m:
            kind = {"block": "block_tag", "blocks": "block_tag", "item": "item_tag", "items": "item_tag"}.get(m[1], "entity_tag")
            found[kind][m[2]].add(v)
    # Sound events: the string constants of the class that registers them.
    for n in names:
        if not n.endswith(".class"):
            continue
        data = z.read(n)
        if b"entity.firework_rocket.launch" not in data or b"block.glass.break" not in data:
            continue
        i = 10
        # Walk the constant pool's UTF-8 entries.
        count = int.from_bytes(data[8:10], "big")
        idx = 1
        while idx < count:
            tag = data[i]
            if tag == 1:
                ln = int.from_bytes(data[i + 1:i + 3], "big")
                s = data[i + 3:i + 3 + ln]
                if SOUND.match(s) and b"." in s:
                    found["sound"][s.decode()].add(v)
                i += 3 + ln
            elif tag in (3, 4):
                i += 5
            elif tag in (5, 6):
                i += 9
                idx += 1
            elif tag in (7, 8, 16, 19, 20):
                i += 3
            elif tag in (9, 10, 11, 12, 17, 18):
                i += 5
            elif tag == 15:
                i += 4
            else:
                break
            idx += 1

TITLES = {
    "block": ("blocks", "Blocks."),
    "item": ("items", "Items."),
    "entity": ("entities", "Entity types."),
    "effect": ("effects", "Mob effects."),
    "enchantment": ("enchantments", "Enchantments."),
    "particle": ("particles", "Particle types."),
    "damage_type": ("damage_types", "Damage types."),
    "dimension": ("dimensions", "Dimensions (and their types)."),
    "sound": ("sounds", "Sound events."),
    "block_tag": ("block_tags", "Block tags (`tag` paths)."),
    "item_tag": ("item_tags", "Item tags."),
    "entity_tag": ("entity_tags", "Entity type tags."),
}
KEYWORDS = {"as", "break", "const", "continue", "crate", "else", "enum", "extern", "false", "fn", "for", "if", "impl", "in",
            "let", "loop", "match", "mod", "move", "mut", "pub", "ref", "return", "self", "static", "struct", "super", "trait",
            "true", "type", "unsafe", "use", "where", "while", "async", "await", "dyn"}

out = ["//! The game's ids as constants (`yog_api::vanilla::items::REDSTONE`), so",
       "//! mod code names them instead of spelling them. Generated by",
       "//! `yog-cli/src/vanilla/gen_api.py` from the supported versions",
       f"//! ({', '.join(VERSIONS)}); one missing from a version says so.",
       "//! Don't edit — rerun the script.",
       ""]
for kind, (module, doc) in TITLES.items():
    out.append(f"/// {doc}")
    out.append(f"pub mod {module} {{")
    used = set()
    for path in sorted(found[kind]):
        ident = re.sub(r"[^A-Za-z0-9]", "_", path).upper()
        if ident[0].isdigit():
            ident = "_" + ident
        if ident.lower() in KEYWORDS:
            ident += "_"
        while ident in used:
            ident += "_"
        used.add(ident)
        vs = found[kind][path]
        if vs != ALL:
            out.append(f"    /// Only in {', '.join(sorted(vs))}.")
        out.append(f'    pub const {ident}: &str = "minecraft:{path}";')
    out.append("}")
    out.append("")
open(sys.argv[1], "w").write("\n".join(out))
print({k: len(v) for k, v in found.items()})