Files
RustNotMC/tools/extract-items.py
T
sasheg dev 27047e8071 База из ремейка: ассеты, предметы, звуки и правки плана
Выгрузили из ремейка Rustex всё, на чём будем работать, и переписали план
под реальные цифры.

Ассеты:
- tools/extract-rustex-assets.py распаковывает res/ и game.jar и декодирует
  однобайтовый XOR (ключ по сигнатуре: PNG ^0x89, OGG ^0x4F, JSON ^0x7B).
  Итог: assets/rustex, 1.65 ГБ, 19 тыс. файлов. В репозиторий не кладём.
- Не прочитались 553 .obj, 106 .rrs, 113 .nbt: у них другой шифр, нужен
  разбор кода (REA).

Таблицы (в репозитории):
- items.csv: 218 предметов с уроном, дальностью, темпом, прочностью, добычей
  за удар, материалом, иконкой и моделью;
- sounds.csv: 505 звуковых событий с файлами;
- item-texts.json: 543 названия и описания предметов.

План (PLAN.md):
- Сбор теперь по балансу ремейка: камень берёт 50 дерева и 35 руды, топор 85
  дерева, каменная кирка 80 руды. Дерево 750 топором рубится за 9 ударов, с
  меткой за 6-7 - ровно те 7-10 ударов, которых ждали.
- Исследования - версия после 2020: два пути. Стол (15/30/60/120, нужен сам
  предмет, превращается в чертёж) и тех-дерево верстака (125/250/500, ветки с
  предками, разблокировка личная). Утверждение "тех-дерево бесплатно" было
  ошибкой community-вики.
- Лозы больше нет: она из джунглей, а джунглей нет. Выкинута из HP-таблицы,
  цен, таймеров гниения и правил шкафа.
- Новые разделы 1.10 (мусорка на дороге: серая бочка - скрап, синяя -
  компоненты, красная - нефть и топливо, плюс ящики с едой) и 1.11 (дороги:
  сетка 3-4 полос плюс кольцо, сегменты - точки спавна мусорки).
- Первый источник скрапа - мусорка, лутбоксы отложены: вопрос о прогрессии
  крафта снят.
- Узлы: руда - своя 3D-модель с AABB, деревья - пока блоки мира.
2026-10-21 19:31:51 +03:00

280 lines
9.1 KiB
Python

#!/usr/bin/env python3
"""Turn the item database of the Rustex remake into a table we can read.
The remake keeps its item definitions in
assets/rustex/legacy/minecraft/models/stacks.json . The file is not JSON
but the Minecraft "stacks" notation: a sequence of records
{id:"minecraft:iron_hoe",Count:1s,tag:{rid:"m92pistol",rdamage:25,...}}
The tag keys are the remake's own names, so the table keeps them. The
result is two files:
rsx-protocol/src/main/resources/items/items.csv one row per item
assets/rustex/data/item-texts.json the titles and the
descriptions out of
the language file
"""
import csv
import json
import re
import sys
from pathlib import Path
ROOT = Path(r"G:\RustNotMC")
STACKS = ROOT / "assets" / "rustex" / "legacy" / "minecraft" / "models" / "stacks.json"
LANG = ROOT / "assets" / "rustex" / "legacy" / "minecraft" / "rustex_lang" / "en_us.lang"
OUT_CSV = ROOT / "rsx-protocol" / "src" / "main" / "resources" / "items" / "items.csv"
OUT_TEXTS = ROOT / "rsx-protocol" / "src" / "main" / "resources" / "items" / "item-texts.json"
# The columns of the table, in the order they are written.
COLUMNS = [
"rid", "name", "category", "material", "nid",
"damage", "range", "cooldown", "reach",
"durability", "durability_max",
"wood", "ore", "skinning",
"ammo", "ammo_type", "weapon_id", "gun", "throw", "model", "icon",
"zoom_x", "zoom_y", "zoom_z", "zoom_count",
"heal", "fast_heal", "calories", "hydration", "radiation", "bleeding",
"consume_time", "food_cooldown", "eat_sound",
"armor_projectile", "armor_melee", "armor_bite", "armor_cold",
"armor_explosion", "armor_radiation", "armor_speed_reduction",
"armor_slot",
]
class Parser:
"""A reader for the Minecraft stacks notation."""
def __init__(self, text: str):
self.text = text
self.pos = 0
def skip(self):
while self.pos < len(self.text) and self.text[self.pos] in " \t\r\n":
self.pos += 1
def peek(self) -> str:
self.skip()
return self.text[self.pos] if self.pos < len(self.text) else ""
def read_value(self):
self.skip()
ch = self.text[self.pos]
if ch == "{":
return self.read_compound()
if ch == "[":
return self.read_list()
if ch == '"':
return self.read_string()
return self.read_scalar()
def read_compound(self):
self.pos += 1 # {
out = {}
while True:
self.skip()
ch = self.peek()
if ch == "}":
self.pos += 1
return out
if ch == ",":
self.pos += 1
continue
key = self.read_key()
self.skip()
if self.pos < len(self.text) and self.text[self.pos] == ":":
self.pos += 1
out[key] = self.read_value()
def read_list(self):
self.pos += 1 # [
out = []
while True:
self.skip()
if self.peek() == "]":
self.pos += 1
return out
out.append(self.read_value())
self.skip()
if self.peek() == ",":
self.pos += 1
def read_key(self):
start = self.pos
while self.text[self.pos] not in ':,}]':
self.pos += 1
return self.text[start:self.pos].strip()
def read_string(self):
self.pos += 1 # "
out = []
while self.text[self.pos] != '"':
if self.text[self.pos] == "\\":
self.pos += 1
out.append(self.text[self.pos])
self.pos += 1
self.pos += 1
return "".join(out)
def read_scalar(self):
start = self.pos
while self.pos < len(self.text) and self.text[self.pos] not in ',}]':
self.pos += 1
raw = self.text[start:self.pos].strip()
if raw.endswith("b"):
return raw[:-1] == "1"
if raw.endswith("s"):
return int(raw[:-1])
if raw.endswith("d") or raw.endswith("f"):
return float(raw[:-1])
try:
return int(raw)
except ValueError:
return raw
def flatten(tag: dict) -> dict:
"""The fields of a tag, with the names we use in the table."""
out = {}
def num(value):
if isinstance(value, bool):
return 1 if value else 0
if isinstance(value, (int, float)):
return value
return None
def put(key, value):
n = num(value)
if n is not None:
out[key] = n
put("damage", tag.get("rdamage"))
put("range", tag.get("rrange"))
put("cooldown", tag.get("rcooldown"))
put("reach", tag.get("rustreach"))
put("durability", tag.get("rdurability"))
put("durability_max", tag.get("rdurabilitymax"))
put("wood", tag.get("rwood"))
put("ore", tag.get("rore"))
put("skinning", tag.get("skinning"))
put("ammo", tag.get("rammo"))
put("ammo_type", tag.get("rammotype"))
put("weapon_id", tag.get("rweaponid"))
put("gun", tag.get("rustgun"))
put("throw", tag.get("rustthrow"))
put("zoom_x", tag.get("rustzoomx"))
put("zoom_y", tag.get("rustzoomy"))
put("zoom_z", tag.get("rustzoomz"))
put("zoom_count", tag.get("rustzoomcount"))
put("heal", tag.get("rheal"))
put("fast_heal", tag.get("rfastheal"))
put("calories", tag.get("rcalories"))
put("hydration", tag.get("rhydration"))
put("radiation", tag.get("rradiation"))
put("bleeding", tag.get("rbleeding"))
put("consume_time", tag.get("rconsumetime"))
put("food_cooldown", tag.get("rfoodcooldown"))
put("armor_projectile", tag.get("rarmorprojectile"))
put("armor_melee", tag.get("rarmormelee"))
put("armor_bite", tag.get("rarmorbite"))
put("armor_cold", tag.get("rarmorcold"))
put("armor_explosion", tag.get("rarmorexplosion"))
put("armor_radiation", tag.get("rarmorradiation"))
put("armor_speed_reduction", tag.get("rarmorspeedreduction"))
display = tag.get("display")
if isinstance(display, dict):
name = display.get("Name")
if isinstance(name, str):
out["name"] = re.sub(r"^.\S*", "", name).strip()
for slot, value in tag.items():
if slot.startswith("armorslot.") and value:
out["armor_slot"] = slot[len("armorslot."):]
return out
def parse_stacks(text: str):
parser = Parser(text)
items = []
while True:
parser.skip()
if parser.pos >= len(parser.text):
break
if parser.peek() != "{":
parser.pos += 1
continue
record = parser.read_compound()
tag = record.get("tag")
if not isinstance(tag, dict):
continue
rid = tag.get("rid")
if not isinstance(rid, str):
continue
row = {
"rid": rid,
"category": tag.get("rcategory", ""),
"material": tag.get("rmaterial", ""),
"nid": tag.get("nid", ""),
"model": tag.get("rmodel", ""),
"icon": tag.get("rusticon", ""),
}
row.update(flatten(tag))
items.append(row)
return items
def parse_lang(text: str):
"""The item titles and descriptions out of the language file."""
out = {}
for line in text.splitlines():
if "=" not in line:
continue
key, value = line.split("=", 1)
match = re.match(r"^rustex\.items\.([a-z]+)\.([a-z0-9_]+)\.(title|disc)$", key)
if not match:
continue
category, rid, kind = match.groups()
entry = out.setdefault(rid, {"category": category})
entry[kind] = value
return out
def main() -> int:
if not STACKS.is_file():
print("нет stacks.json:", STACKS)
return 1
items = parse_stacks(STACKS.read_text(encoding="utf-8", errors="replace"))
print("предметов:", len(items))
OUT_CSV.parent.mkdir(parents=True, exist_ok=True)
with OUT_CSV.open("w", encoding="utf-8", newline="") as handle:
writer = csv.DictWriter(handle, fieldnames=COLUMNS, extrasaction="ignore")
writer.writeheader()
for item in items:
writer.writerow({key: item.get(key, "") for key in COLUMNS})
print("таблица:", OUT_CSV)
if LANG.is_file():
texts = parse_lang(LANG.read_text(encoding="utf-8", errors="replace"))
OUT_TEXTS.parent.mkdir(parents=True, exist_ok=True)
OUT_TEXTS.write_text(json.dumps(texts, ensure_ascii=False, indent=1),
encoding="utf-8")
print("описаний:", len(texts), "->", OUT_TEXTS)
# What the table has to tell us about the weapons.
guns = [i for i in items if i.get("gun")]
print("огнестрела в таблице:", len(guns))
for gun in guns:
print(" %-22s id=%-3s магазин=%-3s модель=%s"
% (gun["rid"], gun.get("weapon_id", "?"), gun.get("ammo", "?"),
gun.get("model", "-")))
return 0
if __name__ == "__main__":
sys.exit(main())