Полу-кастомный игрок и каталог объектов ремейка

Игрок:
- 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 (декомпил).
This commit is contained in:
sasheg dev
2026-10-21 21:02:46 +03:00
parent c472bd4a8c
commit a6587d55ce
6 changed files with 481 additions and 2 deletions
+45 -2
View File
@@ -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 шт,
+16
View File
@@ -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);
@@ -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);
}
}
@@ -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();
}
}
@@ -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;
}
}
+242
View File
@@ -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()