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 классов.
324 lines
11 KiB
Python
324 lines
11 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 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())
|