#!/usr/bin/env python3 """Catalogue everything the Rustex remake has, so we know what we can use. The remake is a Rust-like game built on a Minecraft 1.12.2 client, and it carries a lot of things we do not have yet: the models of the monuments, the three dimensional models of the guns and of the building pieces, the icons of the items, the sounds of the animals, the player skins. This script walks the unpacked assets and writes two files: assets/rustex/CATALOG.md for a person to read assets/rustex/catalog.json for a program to read Usage: catalog-remake.py [assets dir] """ import csv import json import re import sys from collections import Counter, defaultdict from pathlib import Path ROOT = Path(r"G:\RustNotMC") ASSETS = Path(sys.argv[1]) if len(sys.argv) > 1 else ROOT / "assets" / "rustex" LEGACY = ASSETS / "legacy" / "minecraft" NEXTGEN = ASSETS / "nextgen" / "models" OUT_MD = ASSETS / "CATALOG.md" OUT_JSON = ASSETS / "catalog.json" def group_of(path: Path, base: Path): try: rel = path.relative_to(base) except ValueError: return "" parts = rel.parts return parts[0] if len(parts) > 1 else "" def scan_models(): """The three dimensional models of the remake.""" out = {} # The nextgen models: one folder per object, with the textures beside it. nextgen = {} if NEXTGEN.is_dir(): for folder in sorted(NEXTGEN.iterdir()): if not folder.is_dir(): continue files = [f for f in folder.rglob("*") if f.is_file()] nextgen[folder.name] = { "textures": [str(f.relative_to(folder)).replace("\\", "/") for f in files], "bytes": sum(f.stat().st_size for f in files), } out["nextgen"] = nextgen # The legacy models of the remake itself: the guns, the building pieces and # the furniture. They are in the remake's own format and are still locked, # but their names say what they are. obj = defaultdict(list) models = LEGACY / "models" if models.is_dir(): for path in sorted(models.rglob("*.obj")): rel = path.relative_to(models) parts = rel.parts # models/item/build/doorway/doorway_hqm0.obj -> the group is "build" kind = parts[1] if len(parts) > 3 else (parts[0] if len(parts) > 1 else "other") obj[kind].append("/".join(parts)) out["obj"] = {k: sorted(v) for k, v in sorted(obj.items())} return out def scan_item_textures(): """The pictures of the items in the style of the vanilla game.""" out = {} textures = LEGACY / "textures" for folder in ("items", "guns", "monuments", "remake", "terrain", "models"): path = textures / folder if path.is_dir(): files = list(path.rglob("*.png")) if files: out[folder] = len(files) return out def scan_icons(): """The icons of the items: one small picture per item.""" misc = LEGACY / "textures" / "models" / "misc" if not misc.is_dir(): return [] return sorted(p.stem for p in misc.glob("*.png")) def scan_entities(): """The creatures and the player.""" out = {} entity = LEGACY / "textures" / "entity" if entity.is_dir(): for folder in sorted(entity.iterdir()): if not folder.is_dir(): continue files = [f for f in folder.rglob("*.png")] if files: out[folder.name] = sorted(f.name for f in files) return out def scan_blocks(): """The blocks the remake adds to the vanilla ones.""" out = {} models = LEGACY / "models" / "block" if models.is_dir(): names = sorted(p.stem for p in models.glob("*.json")) out["block_models"] = len(names) out["block_model_names"] = names states = LEGACY / "blockstates" if states.is_dir(): out["blockstates"] = len(list(states.glob("*.json"))) textures = LEGACY / "textures" / "blocks" if textures.is_dir(): out["block_textures"] = len(list(textures.glob("*.png"))) return out def scan_sounds(): """The sound events of the remake.""" path = ROOT / "rsx-protocol" / "src" / "main" / "resources" / "items" / "sounds.csv" if not path.is_file(): return {} groups = Counter() with path.open(encoding="utf-8") as handle: for row in csv.DictReader(handle): ident = row["id"] if ident.startswith("rust."): groups[ident.split(".")[1]] += 1 else: groups["vanilla"] += 1 return dict(groups) def scan_items(): """The item database of the remake.""" path = ROOT / "rsx-protocol" / "src" / "main" / "resources" / "items" / "items.csv" if not path.is_file(): return [] with path.open(encoding="utf-8") as handle: return list(csv.DictReader(handle)) def main(): models = scan_models() icons = scan_icons() entities = scan_entities() blocks = scan_blocks() sounds = scan_sounds() items = scan_items() by_category = Counter(item.get("category", "") for item in items) with_stats = sum(1 for item in items if item.get("damage")) tools = [item["rid"] for item in items if item.get("category") == "tools"] data = { "items": {"count": len(items), "by_category": dict(by_category), "with_combat_stats": with_stats, "tools": tools}, "models": {"nextgen": len(models["nextgen"]), "nextgen_names": sorted(models["nextgen"]), "obj_groups": {k: len(v) for k, v in models["obj"].items()}, "obj_total": sum(len(v) for v in models["obj"].values())}, "icons": {"count": len(icons), "names": icons}, "entities": {k: len(v) for k, v in entities.items()}, "blocks": {k: v for k, v in blocks.items() if not k.endswith("_names")}, "sounds": sounds, } OUT_JSON.write_text(json.dumps(data, ensure_ascii=False, indent=1), encoding="utf-8") lines = [] lines.append("# Каталог объектов ремейка Rustex\n") lines.append("Собрано скриптом `tools/catalog-remake.py` по распакованным ассетам.\n") lines.append("Это то, что у ремейка есть и чего у нас пока нет.\n") lines.append("\n## Предметы\n") lines.append("Всего: **%d**, с боевыми числами: %d.\n" % (len(items), with_stats)) lines.append("| Категория | Сколько |") lines.append("|---|---|") for name, count in sorted(by_category.items(), key=lambda kv: -kv[1]): lines.append("| %s | %d |" % (name or "-", count)) lines.append("\n## Модели\n") lines.append("### Nextgen (объекты целиком, своя геометрия): %d\n" % len(models["nextgen"])) lines.append("```") lines.append(", ".join(sorted(models["nextgen"]))) lines.append("```") lines.append("\n### Модели ремейка (.obj, свой формат, пока зашифрованы): %d\n" % sum(len(v) for v in models["obj"].values())) lines.append("| Группа | Сколько | Примеры |") lines.append("|---|---|---|") for group, names in models["obj"].items(): sample = ", ".join(n.split("/")[-1].replace(".obj", "") for n in names[:5]) lines.append("| %s | %d | %s |" % (group, len(names), sample)) lines.append("\n### Текстуры в стиле ванильного клиента\n") lines.append("| Папка | Файлов |") lines.append("|---|---|") for folder, count in sorted(scan_item_textures().items()): lines.append("| %s | %d |" % (folder, count)) lines.append("\n## Иконки предметов: %d\n" % len(icons)) lines.append("```") lines.append(", ".join(icons)) lines.append("```") lines.append("\n## Сущности и игрок\n") lines.append("| Папка | Текстур |") lines.append("|---|---|") for name, files in sorted(entities.items()): lines.append("| %s | %d |" % (name, len(files))) lines.append("\nСкины игрока: `textures/entity/skins` (9 файлов 256x256: чёрный, белый, " "жёлтый, по три) и `skins_blink` (то же, но с моргающими глазами). " "Модель тела — ванильная, от ремейка только текстуры.") lines.append("\n## Блоки\n") lines.append("- моделей блоков: %s" % blocks.get("block_models", 0)) lines.append("- blockstates: %s" % blocks.get("blockstates", 0)) lines.append("- текстур блоков: %s" % blocks.get("block_textures", 0)) lines.append("\n## Звуки\n") lines.append("| Группа | Событий |") lines.append("|---|---|") for name, count in sorted(sounds.items(), key=lambda kv: -kv[1]): lines.append("| %s | %d |" % (name, count)) OUT_MD.write_text("\n".join(lines) + "\n", encoding="utf-8") print("каталог: %s" % OUT_MD) print("данные: %s" % OUT_JSON) print("предметов: %d, моделей nextgen: %d, .obj: %d, иконок: %d, звуков: %d" % (len(items), len(models["nextgen"]), sum(len(v) for v in models["obj"].values()), len(icons), sum(sounds.values()))) if __name__ == "__main__": main()