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