241 lines
11 KiB
Markdown
241 lines
11 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.
|
|
|
|
## 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.
|
|
|
|
## Tools
|
|
|
|
| Script | Purpose |
|
|
|---|---|
|
|
| `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. |