Files
RustNotMC/tools/catalog-remake.py
sasheg dev a6587d55ce Полу-кастомный игрок и каталог объектов ремейка
Игрок:
- RsxPlayer extends EntityPlayerSP: наш игрок с нашими числами движения.
- RsxMovementInput extends MovementInputFromOptions: отменяет ванильный
  множитель подкрадывания 0.3 и ставит наш.
- RsxPlayerTuning: подкрадывание 1.5 блока/с (было ~1.3), прыжок 1.35 блока
  (было 1.25). Прыжок считается по высоте: v = 0.42 * sqrt(h / 1.25) = 0.43713.
  Правило пользователя: значение меньше заданного не принимается, остаётся 1.5.
  Переопределяется из командной строки: -Drsx.sneakSpeed, -Drsx.jumpHeight.
- Мобы прыгают как в ванилле: переопределение в RsxPlayer, а не в
  EntityLivingBase.
- Три строки патчей в ванильное дерево: PlayerControllerMP создаёт RsxPlayer,
  Minecraft создаёт RsxMovementInput. Вся логика в нашем коде.
- Форк собирается чисто, 3278 классов.

Каталог:
- tools/catalog-remake.py собирает assets/rustex/CATALOG.md и catalog.json.
- Что нашли у ремейка: 218 предметов (46 одежды, 44 ресурса, 33 оружия, 31
  медицина, 21 инструмент, 17 построек, 11 боеприпасов, 11 разных, 4 ловушки),
  91 объект nextgen (бочки, ящики, двери, верстаки, печи, руда, модели
  монументов), 553 модели .obj (384 оружия с перезарядкой, 128 деталей постройки,
  36 мебели, 4 руды), 133 иконки предметов, 2004 звука, текстуры монументов и
  животных, скины игрока (9 файлов 256x256 плюс 9 с моргающими глазами) в
  assets/rustex/player/.
- Скины игрока - текстуры ремейка, модель тела ванильная.

Декомпил ремейка лежит на диске: G:\src-decoded (5207 файлов, строки
расшифрованы). Поведение клиента можно читать, а не угадывать: конфиг оружия с
отдачей, схема viewmodel, формат данных. Значения же приходят с сервера, в
клиенте только схема и значения по умолчанию.

План дополнен разделами 2.8 (игрок) и 2.9 (декомпил).
2026-10-21 21:02:46 +03:00

243 lines
9.3 KiB
Python

#!/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()