27047e8071
Выгрузили из ремейка Rustex всё, на чём будем работать, и переписали план под реальные цифры. Ассеты: - tools/extract-rustex-assets.py распаковывает res/ и game.jar и декодирует однобайтовый XOR (ключ по сигнатуре: PNG ^0x89, OGG ^0x4F, JSON ^0x7B). Итог: assets/rustex, 1.65 ГБ, 19 тыс. файлов. В репозиторий не кладём. - Не прочитались 553 .obj, 106 .rrs, 113 .nbt: у них другой шифр, нужен разбор кода (REA). Таблицы (в репозитории): - items.csv: 218 предметов с уроном, дальностью, темпом, прочностью, добычей за удар, материалом, иконкой и моделью; - sounds.csv: 505 звуковых событий с файлами; - item-texts.json: 543 названия и описания предметов. План (PLAN.md): - Сбор теперь по балансу ремейка: камень берёт 50 дерева и 35 руды, топор 85 дерева, каменная кирка 80 руды. Дерево 750 топором рубится за 9 ударов, с меткой за 6-7 - ровно те 7-10 ударов, которых ждали. - Исследования - версия после 2020: два пути. Стол (15/30/60/120, нужен сам предмет, превращается в чертёж) и тех-дерево верстака (125/250/500, ветки с предками, разблокировка личная). Утверждение "тех-дерево бесплатно" было ошибкой community-вики. - Лозы больше нет: она из джунглей, а джунглей нет. Выкинута из HP-таблицы, цен, таймеров гниения и правил шкафа. - Новые разделы 1.10 (мусорка на дороге: серая бочка - скрап, синяя - компоненты, красная - нефть и топливо, плюс ящики с едой) и 1.11 (дороги: сетка 3-4 полос плюс кольцо, сегменты - точки спавна мусорки). - Первый источник скрапа - мусорка, лутбоксы отложены: вопрос о прогрессии крафта снят. - Узлы: руда - своя 3D-модель с AABB, деревья - пока блоки мира.
265 lines
12 KiB
Markdown
265 lines
12 KiB
Markdown
# RustNotMC
|
|
|
|
A survival game built on a Minecraft 1.12.2 client as the rendering platform,
|
|
with its own server, its own protocol and its own accounts.
|
|
|
|
Подробности по оригиналу (Rust), наши решения и план работ: [PLAN.md](PLAN.md).
|
|
|
|
## Repositories
|
|
|
|
| Repository | License | What lives there |
|
|
|---|---|---|
|
|
| `rsx-client` | MIT | Loader, mod API, client hooks, deobfuscation tools |
|
|
| `rsx-protocol` | MIT | Wire format, packet schemas, shared vocabulary |
|
|
| `rsx-plugin-api` | MIT | Interfaces for server-side content |
|
|
| `rsx-server-core` | proprietary | Tick loop, world, entities, scaling |
|
|
| `rsx-auth` | proprietary | The server's own accounts, used when there is no launcher |
|
|
|
|
## Target
|
|
|
|
Base platform is Minecraft 1.12.2, forked into our own source tree so the game
|
|
is moddable from the start rather than patched from outside.
|
|
|
|
- game fork: Java 17, compiled by `game-fork\scripts\prepare-game.ps1`
|
|
- loader and mod API: Java 17
|
|
- mods: Java only
|
|
|
|
Capacity target is 200 players on 4 cores and 8 GB of RAM, scaling to 500 on
|
|
larger hardware.
|
|
|
|
## The fork
|
|
|
|
The client is not wrapped around the game jar. `game-fork` rebuilds it from the
|
|
verified vanilla jar into readable source, and the loader is an ordinary part
|
|
of that game rather than an agent injected into someone else's process.
|
|
|
|
```
|
|
vanilla client-1.12.2.jar verified Mojang signature
|
|
-> mcp-1.12.2-srg.zip 3344 classes, 20817 members
|
|
-> merge with MCP CSVs 9991 fields, 18715 methods renamed
|
|
-> SpecialSource 3310 classes in MCP names
|
|
-> FernFlower 2050 source files
|
|
-> hand patches + rules 114 patches, one regex rule
|
|
-> javac 3268 classes, clean
|
|
```
|
|
|
|
`prepare-game.ps1` runs the whole thing and is idempotent. Hand fixes live in
|
|
`game-fork\patches\decompiler-fixes.txt` and are re-applied on every run, so
|
|
regenerating the tree never loses them.
|
|
|
|
Compiling clean proves nothing. Decompilation quietly breaks code that still
|
|
compiles, still has the right methods and still runs: an entity that compares
|
|
itself with itself never leaves the ground, a list indexed by entity id
|
|
removes the wrong player. Every patch here was checked against the original
|
|
bytecode with `javap`, and `tools\javap_diff.py` is the tool that makes that
|
|
comparison usable. The self-comparison finder and the remove-by-index finder
|
|
both came out of this class of bug.
|
|
|
|
The fork holds Mojang-derived code and is **not published**. What ships in the
|
|
open repository is the loader, the mod API and the build tools.
|
|
|
|
## The server
|
|
|
|
The server owns the world. The client draws it and believes nothing.
|
|
|
|
### The wire
|
|
|
|
The protocol is ours, not the vanilla one. A vanilla 1.12.2 client and our
|
|
server cannot talk to each other at all, and that is the point: the game is
|
|
ours, so the wire is ours, and every packet in it is one we wrote down.
|
|
|
|
The client's own game code is not rewritten. The client reads our packets and
|
|
translates them into the game's, and the game does what it already knows how to
|
|
do: build a world, spawn entities, draw them. The transport is ours; the
|
|
renderer is the game's. The other direction is the same: the game says "the
|
|
player moved" and "the player broke a block", and those two become our packets.
|
|
|
|
### The world
|
|
|
|
A chunk is sixteen by sixteen columns, two hundred and fifty six high, one byte
|
|
per block. Generation is a pure function of the seed and the coordinates: no
|
|
shared state, no `java.util.Random` carried between calls, no dependence on the
|
|
order chunks are asked for. A chunk may be generated months after its
|
|
neighbour, on another machine, and has to agree with it exactly, because a save
|
|
file can only record what changed and not the terrain underneath it.
|
|
|
|
Chunks are written one file per chunk, to a temporary name and moved into
|
|
place, with a version and a checksum. A chunk that is half written when the
|
|
power goes is a chunk the server refuses to load, rather than one that loads
|
|
with half a world in it.
|
|
|
|
### Who gets in
|
|
|
|
The launcher is where accounts live: it has the passwords, the roles, the bans
|
|
and the friends. The game server has none of that and does not want a second
|
|
copy, so it asks the launcher whether a player is one of ours, and gets the ban
|
|
list in the same answer.
|
|
|
|
This is the part Yggdrasil does in vanilla, done with our own launcher. The
|
|
launcher already logs the player in and already hands the game a name, a uuid
|
|
and a token; the game hands that token to the server, and the server asks the
|
|
launcher about it. The token is used once and dropped.
|
|
|
|
The server's own accounts exist for a server of our own and for development.
|
|
When a launcher is configured they are shut, because a server with two doors
|
|
has no doors.
|
|
|
|
```
|
|
client game server launcher
|
|
| Hello | |
|
|
|------------------------->| |
|
|
| HelloAck | |
|
|
|<-------------------------| |
|
|
| LauncherAuth | |
|
|
|------------------------->|--- POST /auth/validate ->|
|
|
| |<-- {valid, reason} -----|
|
|
| AuthAck | |
|
|
|<-------------------------| |
|
|
| Login / LoginAck | |
|
|
|==========================>| |
|
|
| JoinWorld / WorldJoin | |
|
|
|<==========================| |
|
|
| ChunkData ... | |
|
|
|<==========================| |
|
|
```
|
|
|
|
## Modding
|
|
|
|
Mods are jars with a descriptor at `META-INF/rsxmod.properties`:
|
|
|
|
```properties
|
|
id=rsxm-demo
|
|
name=RustNotMC Demo
|
|
version=1.1.0
|
|
env=CLIENT
|
|
entrypoint=net.rustnotmc.demo.DemoMod
|
|
access=hud,commands,files
|
|
depends.core=>=1.0
|
|
```
|
|
|
|
`env` is `CLIENT`, `SERVER` or `BOTH`. The loader runs on the client today;
|
|
the field exists so a mod that targets both sides does not need a format
|
|
change when the loader moves to the server.
|
|
|
|
Each mod gets its own class loader. Load order comes from the dependency graph,
|
|
not from filenames. Shutdown runs in reverse.
|
|
|
|
### What a mod may touch
|
|
|
|
A client mod changes what the client shows. It never decides what is true about
|
|
the world: the server owns the world, the inventory, the player and the
|
|
network. Capabilities are only granted when something actually checks them.
|
|
|
|
| Capability | Grants | Checked |
|
|
|---|---|---|
|
|
| `CORE` | lifecycle, logging, own mod directory | always |
|
|
| `HUD` | draw overlays through the game's font | granted from descriptor |
|
|
| `INPUT` | read keyboard and mouse state | granted from descriptor |
|
|
| `COMMANDS` | register commands the player can type | granted from descriptor |
|
|
| `FILES` | a directory of its own under `run/mods/<id>` | granted from descriptor |
|
|
| `REFLECTION` | reach into game internals | never automatic, operator consent |
|
|
|
|
`WORLD`, `PLAYER`, `NETWORK`, `TEXT`, `INPUT` and `COMMAND` were removed: they
|
|
were advertised but not enforced, so a descriptor could claim them and receive
|
|
nothing. A capability that cannot be granted is worse than an absent one.
|
|
|
|
Commands carry a scope. `CLIENT` commands run on the client and are never sent
|
|
anywhere. `SERVER` and `BOTH` commands are registered but deliberately not
|
|
executed on the client, because the server is what has to see them and decide.
|
|
|
|
The client API contains no game types at all, which is what lets the loader
|
|
build and test with the game closed: the SPI is implemented on the game side
|
|
and the API never depends on it. `tick()` advances on the client's own tick,
|
|
not on frames, so a mod counts the same twenty steps a second at 60 fps and at
|
|
240.
|
|
|
|
## Building
|
|
|
|
```powershell
|
|
rsx-client\scripts\build.ps1 # dist\rsx-loader.jar
|
|
rsx-client\scripts\test.ps1 # self test, 128 checks
|
|
game-fork\scripts\prepare-game.ps1 # rebuild the fork from the vanilla jar
|
|
game-fork\scripts\compile-game.ps1 # compile only, fast iteration
|
|
rsx-client\scripts\run-game.ps1 # launch the client
|
|
|
|
rsx-protocol\scripts\build.ps1 # dist\rsx-protocol.jar
|
|
rsx-protocol\scripts\test.ps1 # 22 checks
|
|
rsx-server-core\scripts\build.ps1 # dist\rsx-server-core.jar
|
|
rsx-server-core\scripts\test.ps1 # 49 checks
|
|
rsx-server-core\scripts\run-server.ps1
|
|
```
|
|
|
|
Only a JDK is required. No Gradle, no network once the mappings are cached.
|
|
|
|
### Running the server
|
|
|
|
```
|
|
run-server.ps1 --port 25565 --world world --seed 0
|
|
run-server.ps1 --launcher-api https://launcher.example/api
|
|
```
|
|
|
|
`--launcher-api` points the server at the launcher's API. With it, the
|
|
launcher decides who gets in and the server's own accounts are shut.
|
|
`--allow-local-accounts` opens them again on purpose.
|
|
|
|
### Assets
|
|
|
|
The client will not run without a real asset tree, and the launcher on this
|
|
machine keeps assets the modern way: `indexes/1.12.json` pointing at
|
|
`objects/<aa>/<sha1>`. A 1.12.2 client reads neither. It wants
|
|
`<assetsDir>/minecraft/<path>`.
|
|
|
|
Pointing `--assetsDir` straight at the object store does not look broken. Text
|
|
and textures come from the jar, so the game looks complete, but every sound has
|
|
been an asset since 1.7.3 and none of them are on disk: the sound engine
|
|
starts, `play()` is called, nothing is found, and paulscode says nothing.
|
|
`tools\materialize_assets.py` lays the tree out and `run-game.ps1` calls it on
|
|
every launch, so this cannot be forgotten again.
|
|
|
|
### The base from the remake
|
|
|
|
The item models, textures, sounds and the whole item database are taken from
|
|
the Rustex remake as a temporary base. `tools\extract-rustex-assets.py` unpacks
|
|
them into `assets\rustex` (1.65 GB, not in the repository) and decodes the
|
|
one-byte XOR the remake uses. What we derived from that and keep in the
|
|
repository:
|
|
|
|
| File | What |
|
|
|---|---|
|
|
| `rsx-protocol\src\main\resources\items\items.csv` | 218 items: damage, range, tempo, durability, harvest per hit, material, icon, model |
|
|
| `rsx-protocol\src\main\resources\items\sounds.csv` | 505 sound events with their files |
|
|
| `rsx-protocol\src\main\resources\items\item-texts.json` | 543 item titles and descriptions |
|
|
|
|
These numbers are the balance we build on, and they are the ones the official
|
|
wiki contradicts (the wiki describes original Rust, the remake is faster). The
|
|
item icons come from the atlas of the remake and are placeholders: **replace
|
|
them with our own.**
|
|
|
|
## Tools
|
|
|
|
| Script | Purpose |
|
|
|---|---|
|
|
| `tools\extract-rustex-assets.py` | unpacks the assets of the Rustex remake and decodes them: every file is XOR-ed with one byte, the key comes from the format signature (PNG `^0x89`, OGG `^0x4F`, JSON `^0x7B`) |
|
|
| `tools\extract-items.py` | turns `models/stacks.json` of the remake into `rsx-protocol\...\items\items.csv`: 218 items with damage, range, tempo, durability, harvest per hit, material, icon, model |
|
|
| `tools\extract-sounds.py` | the sound registry of the remake into `sounds.csv`: 505 events with their files |
|
|
| `rsx-client\tools\merge_srg_mcp.py` | vanilla SRG plus MCP CSVs into one obf to MCP SRG |
|
|
| `rsx-client\tools\apply_patches.py` | re-applies the hand fixes, fails loudly if one misses |
|
|
| `rsx-client\tools\add_patch.py` | appends a reviewed patch to the fix file |
|
|
| `rsx-client\tools\fix_stale_locals.py` | FernFlower's stale local references, scoped per method |
|
|
| `rsx-client\tools\fix_generic_casts.py` | inserts the cast where a generic loses its type parameter |
|
|
| `rsx-client\tools\fix_decompiler_rules.py` | systematic rewrites that would be hundreds of patches |
|
|
| `rsx-client\tools\show_errors.py` | javac errors next to the source line that caused them |
|
|
| `rsx-client\tools\struct_diff.py` | class-by-class comparison against the reference, plus orphan analysis |
|
|
| `rsx-client\tools\javap_diff.py` | instruction-level comparison, normalising constant pool and offsets |
|
|
| `rsx-client\tools\find_self_comparisons.py` | finds `x == x`, the decompiler's signature bug |
|
|
| `rsx-client\tools\find_remove_index_bugs.py` | finds `List<Integer>.remove(int)` where an object was meant |
|
|
| `rsx-client\tools\find_callers.py` | who calls a field or method, via the constant pool |
|
|
| `rsx-client\tools\materialize_assets.py` | lays the asset tree out from the index and object store |
|
|
| `rsx-client\tools\ogg_duration.py` | real length of an ogg, to tell a short file from a cut-off one |
|
|
| `rsx-client\tools\stage_run.py` | writes the runtime classpath |
|
|
| `rsx-client\tools\fetch_missing.py` | pulls natives into the local run directory |
|
|
|
|
## Legal
|
|
|
|
Minecraft is a trademark of Mojang Studios. This project is not affiliated with
|
|
or endorsed by Mojang or Microsoft. Game assets are not redistributed here. |