From a6587d55ced6b37ae13e19a7d39b377ebf55fb40 Mon Sep 17 00:00:00 2001 From: sasheg dev <152582704+SashegDev@users.noreply.github.com> Date: Wed, 21 Oct 2026 21:02:46 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=BE=D0=BB=D1=83-=D0=BA=D0=B0=D1=81?= =?UTF-8?q?=D1=82=D0=BE=D0=BC=D0=BD=D1=8B=D0=B9=20=D0=B8=D0=B3=D1=80=D0=BE?= =?UTF-8?q?=D0=BA=20=D0=B8=20=D0=BA=D0=B0=D1=82=D0=B0=D0=BB=D0=BE=D0=B3=20?= =?UTF-8?q?=D0=BE=D0=B1=D1=8A=D0=B5=D0=BA=D1=82=D0=BE=D0=B2=20=D1=80=D0=B5?= =?UTF-8?q?=D0=BC=D0=B5=D0=B9=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Игрок: - 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 (декомпил). --- PLAN.md | 47 +++- game-fork/patches/decompiler-fixes.txt | 16 ++ .../rustnotmc/client/RsxMovementInput.java | 30 +++ .../net/rustnotmc/client/RsxPlayer.java | 40 +++ .../net/rustnotmc/client/RsxPlayerTuning.java | 108 ++++++++ tools/catalog-remake.py | 242 ++++++++++++++++++ 6 files changed, 481 insertions(+), 2 deletions(-) create mode 100644 game-fork/src-overlay/net/rustnotmc/client/RsxMovementInput.java create mode 100644 game-fork/src-overlay/net/rustnotmc/client/RsxPlayer.java create mode 100644 game-fork/src-overlay/net/rustnotmc/client/RsxPlayerTuning.java create mode 100644 tools/catalog-remake.py diff --git a/PLAN.md b/PLAN.md index 91acba4..6689205 100644 --- a/PLAN.md +++ b/PLAN.md @@ -753,6 +753,42 @@ ROLE_ELDER / ROLE_CREATOR`). (MTSDF, 2048×2048, тайлы 64×64) по именам `rusticon` из базы предметов. - **Пометка: заменить на свои** (см. п. 6). +### 2.8 Игрок: полу-кастомный объект + +Игрок — **ванильный, но с нашими числами движения**. Класс `RsxPlayer extends +EntityPlayerSP` (в `src-overlay`), рядом `RsxMovementInput extends +MovementInputFromOptions` и `RsxPlayerTuning` с числами. + +| что | ванильно | у нас | +|---|---|---| +| скорость подкрадывания | ~1.30 блока/с (множитель 0.3) | **1.5 блока/с** (множитель 0.34746) | +| высота прыжка | 1.25 блока (импульс 0.42) | **1.35 блока** (импульс 0.43713) | + +- Скорость прыжка считается по высоте: `v = 0.42 · √(h / 1.25)`. +- Подкрадывание: ванильный множитель 0.3 отменяется и ставится наш. +- **Правило пользователя:** если задать значение меньше, оно не принимается — + подкрадывание остаётся 1.5 блока/с, прыжок 1.5 блока. Значения по умолчанию + (1.5 и 1.35) через это правило не проходят. +- Переопределяется из командной строки: `-Drsx.sneakSpeed=1.6 -Drsx.jumpHeight=1.4`. +- Мобы прыгают как в ванилле: переопределение живёт в `RsxPlayer`, а не в + `EntityLivingBase`. +- Патчи в ванильное дерево — три строки (`PlayerControllerMP` создаёт + `RsxPlayer`, `Minecraft` создаёт `RsxMovementInput`), вся логика — в нашем коде. + +### 2.9 Декомпил ремейка лежит на диске + +`G:\src-decoded` (5207 файлов, строки расшифрованы), также `G:\src`, +`G:\builds\v31\src{,-decoded}` и `G:\decompiled-rustex`. Это **исходники клиента +ремейка** — читаемый Java. Значит, поведение клиента можно смотреть, а не +угадывать: конфиг оружия с отдачей (`recoilYawMin`, `recoilPitchMax`, +`ADSScale`, `movementPenalty`, `shotsUntilMax`, `aimconeCurve`), схема +viewmodel (`sway`, `bob`, `crouch`, `lower`, `freeLook`), формат данных — +Unity-подобные ключи (`m_Name`, `MonoBehaviour`), читаются через `ctkmb`. + +**Важно:** сами **значения** приходят с сервера (Unity-ассеты), в клиенте есть +только схема и значения по умолчанию. Урон, отдача и спрей огнестрела — серверные +данные; в клиенте только магазин, тип патрона, зум ADS, прочность и reach. + --- ## 3. Архитектура @@ -997,8 +1033,15 @@ rsx-client\scripts\run-game.ps1 # клиент восстанавливается по сигнатуре формата: PNG — `b[0] ^ 0x89`, OGG — `b[0] ^ 0x4F`, JSON — `b[0] ^ 0x7B`. Уже декодировано: `assets/rustex/` (1.65 ГБ, 19 тыс. файлов). Скрипты: `tools/extract-rustex-assets.py`, `tools/extract-items.py`, - `tools/extract-sounds.py`. **Осталось непрочитанным:** 553 `.obj`, 106 `.rrs`, - 113 `.nbt` — у них не однобайтовый шифр, нужен разбор кода. + `tools/extract-sounds.py`, `tools/catalog-remake.py`. + **Каталог всего, что есть у ремейка:** `assets/rustex/CATALOG.md` + + `catalog.json` (218 предметов, 91 объект nextgen, 553 модели `.obj` + (384 оружия, 128 постройки, 36 мебели, 4 руды), 133 иконки, 2004 звука, + текстуры монументов и животных, скины игрока в `assets/rustex/player/`). +- ⚠️ **Классы ремейка прочитать нельзя без взлома загрузчика.** Классы в `game.jar` + помечены `DE AD C0 DE` и зашифрованы своим форматом (не однобайтовым XOR, как + ассеты). **Но декомпил лежит на диске** — `G:\src-decoded` (п. 2.9): читать + поведение клиента можно, REA не нужен. - **Текстуры предметов — временные** (взяты из ремейка как заглушки, иконки из `assets/rustex/gamejar/assets/rustex/icons/atlas.png`). **ПОМЕТКА: заменить на свои.** - **База предметов** — `rsx-protocol/src/main/resources/items/items.csv` (218 шт, diff --git a/game-fork/patches/decompiler-fixes.txt b/game-fork/patches/decompiler-fixes.txt index 4797c20..dcb6f6c 100644 --- a/game-fork/patches/decompiler-fixes.txt +++ b/game-fork/patches/decompiler-fixes.txt @@ -1247,3 +1247,19 @@ import org.lwjgl.opengl.ContextCapabilities; } Entity entity = this.mc.getRenderViewEntity(); + +# Our player. The class is ours, the behaviour is the vanilla one, and the two +# numbers that differ from the vanilla live in RsxPlayerTuning. +=== FILE net/minecraft/client/multiplayer/PlayerControllerMP.java +--- BEFORE + return new EntityPlayerSP(this.mc, world, this.connection, statisticsmanager, recipebook); +--- AFTER + return new net.rustnotmc.client.RsxPlayer(this.mc, world, this.connection, statisticsmanager, recipebook); + +# The movement input of our player: the same input with our crouch in it. +=== FILE net/minecraft/client/Minecraft.java +--- BEFORE + this.player.movementInput = new MovementInputFromOptions(this.gameSettings); +--- AFTER + this.player.movementInput = new net.rustnotmc.client.RsxMovementInput(this.gameSettings); + diff --git a/game-fork/src-overlay/net/rustnotmc/client/RsxMovementInput.java b/game-fork/src-overlay/net/rustnotmc/client/RsxMovementInput.java new file mode 100644 index 0000000..dfe5836 --- /dev/null +++ b/game-fork/src-overlay/net/rustnotmc/client/RsxMovementInput.java @@ -0,0 +1,30 @@ +package net.rustnotmc.client; + +import net.minecraft.client.settings.GameSettings; +import net.minecraft.util.MovementInputFromOptions; + +/** + * The movement input with our crouch. + * + * The vanilla already slows the movement down by a third while the crouch key + * is down. We undo that and apply our own factor, so the crouch is a little + * faster than the vanilla one and the number itself lives in + * {@link RsxPlayerTuning}. + */ +public class RsxMovementInput extends MovementInputFromOptions { + + public RsxMovementInput(GameSettings settings) { + super(settings); + } + + @Override + public void updatePlayerMoveState() { + super.updatePlayerMoveState(); + if (!this.sneak) { + return; + } + float correction = RsxPlayerTuning.sneakCorrection(); + this.moveStrafe = (float) ((double) this.moveStrafe * (double) correction); + this.moveForward = (float) ((double) this.moveForward * (double) correction); + } +} diff --git a/game-fork/src-overlay/net/rustnotmc/client/RsxPlayer.java b/game-fork/src-overlay/net/rustnotmc/client/RsxPlayer.java new file mode 100644 index 0000000..3e0902b --- /dev/null +++ b/game-fork/src-overlay/net/rustnotmc/client/RsxPlayer.java @@ -0,0 +1,40 @@ +package net.rustnotmc.client; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.entity.EntityPlayerSP; +import net.minecraft.client.network.NetHandlerPlayClient; +import net.minecraft.stats.RecipeBook; +import net.minecraft.stats.StatisticsManager; +import net.minecraft.world.World; + +/** + * Our player. + * + * The vanilla player with two things changed: the crouch is a little faster + * and the jump is a little higher. Everything else is the vanilla behaviour, + * so the player still walks, swims, rides, fights and dies the way the game + * always did. + * + * The class exists so that the numbers of our movement live in our code and + * not in the patches of the vanilla tree: the patches only have to say "the + * player is ours", and everything else is here. + */ +public class RsxPlayer extends EntityPlayerSP { + + public RsxPlayer(Minecraft minecraft, World world, NetHandlerPlayClient connection, + StatisticsManager statistics, RecipeBook recipeBook) { + super(minecraft, world, connection, statistics, recipeBook); + this.movementInput = new RsxMovementInput(minecraft.gameSettings); + } + + /** + * How high the player jumps. + * + * The override lives here and not in EntityLivingBase, because the mobs + * have to keep jumping the way they always did. + */ + @Override + protected float getJumpUpwardsMotion() { + return RsxPlayerTuning.jumpMotion(); + } +} diff --git a/game-fork/src-overlay/net/rustnotmc/client/RsxPlayerTuning.java b/game-fork/src-overlay/net/rustnotmc/client/RsxPlayerTuning.java new file mode 100644 index 0000000..2430d10 --- /dev/null +++ b/game-fork/src-overlay/net/rustnotmc/client/RsxPlayerTuning.java @@ -0,0 +1,108 @@ +package net.rustnotmc.client; + +/** + * The numbers that make our player move the way our player moves. + * + * The player is the vanilla one with two things changed: the crouch is a + * little faster and the jump is a little higher. Both numbers live here so + * that the rest of the code has one place to point at, and so that a change + * is one line. + * + * They can be set from the command line: + * + * -Drsx.sneakSpeed=1.6 -Drsx.jumpHeight=1.4 + * + * A value that is set below the floor is not taken: the crouch stays at 1.5 + * blocks per second and the jump at 1.5 blocks. The defaults are the ones we + * want, and they are not put through the floor. + */ +public final class RsxPlayerTuning { + + /** How fast the player walks, in blocks per second. The vanilla. */ + public static final float WALK_SPEED = 4.317F; + + /** How fast the player crouches, in blocks per second. */ + public static final float SNEAK_SPEED = 1.5F; + + /** How much the vanilla crouch slows the movement down. */ + public static final float VANILLA_SNEAK_FACTOR = 0.3F; + + /** How high the player jumps, in blocks. The vanilla jumps 1.25. */ + public static final float JUMP_HEIGHT = 1.35F; + + /** The crouch never goes below this, in blocks per second. */ + public static final float SNEAK_SPEED_FLOOR = 1.5F; + + /** The jump never goes below this, in blocks. */ + public static final float JUMP_HEIGHT_FLOOR = 1.5F; + + /** The jump the vanilla has, in blocks. Used to scale the jump speed. */ + private static final float VANILLA_JUMP_HEIGHT = 1.25F; + + /** The speed the vanilla jumps with. */ + private static final float VANILLA_JUMP_MOTION = 0.42F; + + private RsxPlayerTuning() { + } + + /** The crouch speed: the setting, or the default. */ + public static float sneakSpeed() { + return setting("rsx.sneakSpeed", SNEAK_SPEED, SNEAK_SPEED_FLOOR); + } + + /** The jump height: the setting, or the default. */ + public static float jumpHeight() { + return setting("rsx.jumpHeight", JUMP_HEIGHT, JUMP_HEIGHT_FLOOR); + } + + /** The factor the crouch slows the movement down by. */ + public static float sneakFactor() { + return sneakSpeed() / WALK_SPEED; + } + + /** + * How much faster our crouch is than the vanilla one. + * + * The vanilla already multiplied the movement by a third. We undo that + * and put ours in its place. + */ + public static float sneakCorrection() { + return sneakFactor() / VANILLA_SNEAK_FACTOR; + } + + /** The upward speed a jump of our height needs. */ + public static float jumpMotion() { + return jumpMotion(jumpHeight()); + } + + /** + * The upward speed a jump of that height needs. + * + * The vanilla jumps with 0.42 and that carries it 1.25 blocks. The height + * grows with the square of the speed, so the speed for a height is the + * vanilla one times the square root of the ratio. + */ + public static float jumpMotion(float height) { + if (height <= 0.0F) { + return VANILLA_JUMP_MOTION; + } + return VANILLA_JUMP_MOTION * (float) Math.sqrt(height / VANILLA_JUMP_HEIGHT); + } + + private static float setting(String name, float fallback, float floor) { + String value = System.getProperty(name); + if (value == null || value.trim().isEmpty()) { + return fallback; + } + float parsed; + try { + parsed = Float.parseFloat(value.trim()); + } catch (NumberFormatException ignored) { + return fallback; + } + if (parsed != parsed) { + return fallback; + } + return parsed < floor ? floor : parsed; + } +} diff --git a/tools/catalog-remake.py b/tools/catalog-remake.py new file mode 100644 index 0000000..03b2970 --- /dev/null +++ b/tools/catalog-remake.py @@ -0,0 +1,242 @@ +#!/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()