Files
RustNotMC/tools/extract-items.py
T
sasheg dev 7505ddd84e Броня в семи местах и три зоны урона
- В реестр заведены 46 предметов одежды ремейка: слот, слой, какие части тела
  закрывает и сколько снимает из каждого вида вреда. Данные выгружены в items.csv
  (extract-items.py), в коде у каждого предмета armor(...).
- Семь мест: голова, лицо, грудь (2 слоя), ноги (2 слоя), руки, стопы, полный
  костюм. Пояс не делали - в ремейке его нет, есть только быстрая панель.
- Inventory сервера: семь мест по два слоя, вещи надеваются и снимаются.
- Damage: зона урона по точке попадания (голова верхние 0.4, ноги нижние 0.4),
  множители голова 2, тело 1, ноги 0.75; одежда снимает вред только там, где
  закрывает.
- Пакет Attack (цель, точка попадания, рука) и клиентский патч, который его
  шлёт. У игрока появилась жизнь.
- В items.csv добавлены колонки armor_slot, armor_layer, armor_zones.

Тесты: протокол 24 + 10, сервер 85. Форк собирается чисто, 3279 классов.
2026-10-21 22:43:52 +03:00

324 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 slots of clothing, as the remake names them, and our seven places:
# the head, the face, the chest, the legs, the hands, the feet and the whole
# body. The chest and the legs have two layers.
SLOT_NAMES = {
"helmet": "1", "hat": "2", "plate": "3", "jacket": "3",
"pants": "4", "kilt": "4", "gloves": "5", "boots": "6", "suit": "7",
}
LAYER_NAMES = {"plate": "0", "jacket": "1", "pants": "0", "kilt": "1"}
# 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 read_armor(records):
"""The clothing of every item: the slot, the layer and what it covers."""
out = {}
for rec in records:
m = re.search(r'rid:"([^"]+)"', rec)
s = re.search(r'armorslot\.([a-z_]+):1b', rec)
if not (m and s):
continue
slot = SLOT_NAMES.get(s.group(1))
if slot is None:
continue
layer = LAYER_NAMES.get(s.group(1), "0")
zones = 0
if "dorend.head:1b" in rec:
zones |= 1
if ("dorend.body:1b" in rec or "dorend.left_arm:1b" in rec
or "dorend.right_arm:1b" in rec):
zones |= 2
if "dorend.left_leg:1b" in rec or "dorend.right_leg:1b" in rec:
zones |= 4
out[m.group(1)] = (slot, layer, str(zones))
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))
# and the clothing: the slot it goes in, the layer and what it covers
armor = read_armor(STACKS.read_text(encoding="utf-8", errors="replace").split("\n"))
for item in items:
slot, layer, zones = armor.get(item["rid"], ("", "", ""))
item["armor_slot"] = slot
item["armor_layer"] = layer
item["armor_zones"] = zones
columns = COLUMNS + ["armor_layer", "armor_zones"]
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("с одеждой:", sum(1 for i in items if i.get("armor_slot")))
return 0
if __name__ == "__main__":
sys.exit(main())