From bdf0ae866afcad5864d61def22aca97633c8e134 Mon Sep 17 00:00:00 2001 From: sasheg dev <152582704+SashegDev@users.noreply.github.com> Date: Sun, 18 Oct 2026 21:31:27 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A4=D0=BE=D1=80=D0=BA=20=D0=BA=D0=BB=D0=B8?= =?UTF-8?q?=D0=B5=D0=BD=D1=82=D0=B0=20=D1=81=D0=BE=D0=B1=D0=B8=D1=80=D0=B0?= =?UTF-8?q?=D0=B5=D1=82=D1=81=D1=8F=20=D0=B8=D0=B7=20=D0=B8=D1=81=D1=85?= =?UTF-8?q?=D0=BE=D0=B4=D0=BD=D0=B8=D0=BA=D0=BE=D0=B2,=20=D0=BC=D0=BE?= =?UTF-8?q?=D0=B4=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D1=87=D0=B8=D0=BA=20?= =?UTF-8?q?=D0=B2=D1=81=D1=82=D1=80=D0=BE=D0=B5=D0=BD=20=D0=B2=20=D0=B8?= =?UTF-8?q?=D0=B3=D1=80=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Проект начинается с двух вещей: клиент, который мы действительно собрали, и загрузчик, который является его частью, а не чем-то рядом. Форк - prepare-game.ps1 пересобирает клиент целиком: ванильный jar, SRG и MCP, SpecialSource, FernFlower, патчи, автофиксеры, javac. Идемпотентен. - Свой код игры лежит в src-overlay, потому что src пересоздаётся при каждой сборке и всё написанное вручную там погибает. - 116 патчей в patches/decompiler-fixes.txt. Каждый сверялся с оригинальным байткодом через javap: компиляция не ловит ошибки декомпиляции. - struct_diff.py сравнивает исходный байткод с нашей сборкой по классам, интерфейсам, суперклассам, методам и полям, и отдельно проверяет, что пропавшие классы действительно никому не нужны. Загрузчик - Собирается и тестируется без игры: build.ps1 не использует classpath вообще, поэтому SelfTest из 128 проверок проходит без запущенного форка. - Клиентский API не содержит ни одного игрового типа, реализацию даёт игра. - Политика: мод меняет то, что клиент показывает, но никогда то, что истинно в мире. Мир, предметы, игроки и сеть принадлежат серверу. - Capability приведены в соответствие с этой политикой: осталось шесть, и за каждым стоит реальная проверка. Прежние WORLD, PLAYER, NETWORK, TEXT, INPUT и COMMAND выдавались, но ничего не проверяли. - Команды модов имеют область CLIENT, SERVER или BOTH. Серверные не выполняются на клиенте никогда. - Свой jar загрузчика лежит в classpath игры и сборки форка. Инструменты - add_patch.py, чтобы каждое исправление переживало передекомпиляцию - find_self_comparisons.py, find_remove_index_bugs.py, find_callers.py - stage_run.py и fetch_missing.py для запуска клиента --- .gitignore | 39 + README.md | 101 ++ game-fork/patches/decompiler-fixes.txt | 1122 +++++++++++++++++ game-fork/scripts/compile-game.ps1 | 63 + game-fork/scripts/prepare-game.ps1 | 173 +++ .../net/rustnotmc/client/RsxBootstrap.java | 199 +++ .../rustnotmc/client/RsxClientServices.java | 233 ++++ .../net/rustnotmc/client/RsxCommands.java | 132 ++ .../net/rustnotmc/client/RsxDebug.java | 72 ++ .../net/rustnotmc/client/RsxHudSurface.java | 137 ++ rsx-auth/LICENSE | 9 + rsx-client/LICENSE | 21 + rsx-client/scripts/build-mod.ps1 | 51 + rsx-client/scripts/build.ps1 | 66 + rsx-client/scripts/run-game.ps1 | 63 + rsx-client/scripts/test.ps1 | 43 + .../src/main/java/net/rustnotmc/api/Rsx.java | 230 ++++ .../java/net/rustnotmc/api/RustNotMcMod.java | 49 + .../rustnotmc/api/client/ClientCommand.java | 13 + .../net/rustnotmc/api/client/ClientHost.java | 195 +++ .../rustnotmc/api/client/ClientServices.java | 32 + .../net/rustnotmc/api/client/CommandArgs.java | 100 ++ .../rustnotmc/api/client/CommandScope.java | 58 + .../net/rustnotmc/api/client/Commands.java | 32 + .../java/net/rustnotmc/api/client/Input.java | 38 + .../java/net/rustnotmc/api/client/Screen.java | 32 + .../net/rustnotmc/api/client/ScreenKind.java | 61 + .../java/net/rustnotmc/api/hud/Anchor.java | 95 ++ .../java/net/rustnotmc/api/hud/HudLayout.java | 243 ++++ .../net/rustnotmc/api/hud/HudSurface.java | 69 + .../java/net/rustnotmc/loader/Access.java | 169 +++ .../java/net/rustnotmc/loader/EventBus.java | 219 ++++ .../java/net/rustnotmc/loader/GameAccess.java | 308 +++++ .../main/java/net/rustnotmc/loader/Log.java | 165 +++ .../net/rustnotmc/loader/ModContainer.java | 104 ++ .../java/net/rustnotmc/loader/ModContext.java | 113 ++ .../net/rustnotmc/loader/ModDescriptor.java | 315 +++++ .../java/net/rustnotmc/loader/ModEnv.java | 50 + .../java/net/rustnotmc/loader/ModLoader.java | 436 +++++++ .../java/net/rustnotmc/loader/ModState.java | 51 + .../net/rustnotmc/loader/TickScheduler.java | 247 ++++ .../java/net/rustnotmc/loader/Version.java | 147 +++ .../rustnotmc/loader/VersionConstraint.java | 199 +++ .../java/net/rustnotmc/test/SelfTest.java | 615 +++++++++ .../java/net/rustnotmc/test/TestMods.java | 184 +++ rsx-client/third_party/asm/asm-9.7.jar | Bin 0 -> 125428 bytes .../third_party/asm/asm-commons-9.7.jar | Bin 0 -> 73426 bytes rsx-client/third_party/asm/asm-tree-9.7.jar | Bin 0 -> 51934 bytes rsx-client/tools/add_patch.py | 53 + rsx-client/tools/apply_patches.py | 169 +++ rsx-client/tools/fetch_missing.py | 70 + rsx-client/tools/find_callers.py | 57 + rsx-client/tools/find_remove_index_bugs.py | 59 + rsx-client/tools/find_self_comparisons.py | 47 + rsx-client/tools/fix_decompiler_rules.py | 68 + rsx-client/tools/fix_generic_casts.py | 187 +++ rsx-client/tools/fix_stale_locals.py | 147 +++ rsx-client/tools/merge_srg_mcp.py | 139 ++ rsx-client/tools/show_errors.py | 103 ++ rsx-client/tools/stage_run.py | 159 +++ rsx-client/tools/struct_diff.py | 288 +++++ .../resources/META-INF/rsxmod.properties | 7 + .../main/java/net/rustnotmc/demo/DemoMod.java | 142 +++ rsx-plugin-api/LICENSE | 21 + rsx-protocol/LICENSE | 21 + rsx-server-core/LICENSE | 9 + 66 files changed, 8839 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 game-fork/patches/decompiler-fixes.txt create mode 100644 game-fork/scripts/compile-game.ps1 create mode 100644 game-fork/scripts/prepare-game.ps1 create mode 100644 game-fork/src-overlay/net/rustnotmc/client/RsxBootstrap.java create mode 100644 game-fork/src-overlay/net/rustnotmc/client/RsxClientServices.java create mode 100644 game-fork/src-overlay/net/rustnotmc/client/RsxCommands.java create mode 100644 game-fork/src-overlay/net/rustnotmc/client/RsxDebug.java create mode 100644 game-fork/src-overlay/net/rustnotmc/client/RsxHudSurface.java create mode 100644 rsx-auth/LICENSE create mode 100644 rsx-client/LICENSE create mode 100644 rsx-client/scripts/build-mod.ps1 create mode 100644 rsx-client/scripts/build.ps1 create mode 100644 rsx-client/scripts/run-game.ps1 create mode 100644 rsx-client/scripts/test.ps1 create mode 100644 rsx-client/src/main/java/net/rustnotmc/api/Rsx.java create mode 100644 rsx-client/src/main/java/net/rustnotmc/api/RustNotMcMod.java create mode 100644 rsx-client/src/main/java/net/rustnotmc/api/client/ClientCommand.java create mode 100644 rsx-client/src/main/java/net/rustnotmc/api/client/ClientHost.java create mode 100644 rsx-client/src/main/java/net/rustnotmc/api/client/ClientServices.java create mode 100644 rsx-client/src/main/java/net/rustnotmc/api/client/CommandArgs.java create mode 100644 rsx-client/src/main/java/net/rustnotmc/api/client/CommandScope.java create mode 100644 rsx-client/src/main/java/net/rustnotmc/api/client/Commands.java create mode 100644 rsx-client/src/main/java/net/rustnotmc/api/client/Input.java create mode 100644 rsx-client/src/main/java/net/rustnotmc/api/client/Screen.java create mode 100644 rsx-client/src/main/java/net/rustnotmc/api/client/ScreenKind.java create mode 100644 rsx-client/src/main/java/net/rustnotmc/api/hud/Anchor.java create mode 100644 rsx-client/src/main/java/net/rustnotmc/api/hud/HudLayout.java create mode 100644 rsx-client/src/main/java/net/rustnotmc/api/hud/HudSurface.java create mode 100644 rsx-client/src/main/java/net/rustnotmc/loader/Access.java create mode 100644 rsx-client/src/main/java/net/rustnotmc/loader/EventBus.java create mode 100644 rsx-client/src/main/java/net/rustnotmc/loader/GameAccess.java create mode 100644 rsx-client/src/main/java/net/rustnotmc/loader/Log.java create mode 100644 rsx-client/src/main/java/net/rustnotmc/loader/ModContainer.java create mode 100644 rsx-client/src/main/java/net/rustnotmc/loader/ModContext.java create mode 100644 rsx-client/src/main/java/net/rustnotmc/loader/ModDescriptor.java create mode 100644 rsx-client/src/main/java/net/rustnotmc/loader/ModEnv.java create mode 100644 rsx-client/src/main/java/net/rustnotmc/loader/ModLoader.java create mode 100644 rsx-client/src/main/java/net/rustnotmc/loader/ModState.java create mode 100644 rsx-client/src/main/java/net/rustnotmc/loader/TickScheduler.java create mode 100644 rsx-client/src/main/java/net/rustnotmc/loader/Version.java create mode 100644 rsx-client/src/main/java/net/rustnotmc/loader/VersionConstraint.java create mode 100644 rsx-client/src/test/java/net/rustnotmc/test/SelfTest.java create mode 100644 rsx-client/src/test/java/net/rustnotmc/test/TestMods.java create mode 100644 rsx-client/third_party/asm/asm-9.7.jar create mode 100644 rsx-client/third_party/asm/asm-commons-9.7.jar create mode 100644 rsx-client/third_party/asm/asm-tree-9.7.jar create mode 100644 rsx-client/tools/add_patch.py create mode 100644 rsx-client/tools/apply_patches.py create mode 100644 rsx-client/tools/fetch_missing.py create mode 100644 rsx-client/tools/find_callers.py create mode 100644 rsx-client/tools/find_remove_index_bugs.py create mode 100644 rsx-client/tools/find_self_comparisons.py create mode 100644 rsx-client/tools/fix_decompiler_rules.py create mode 100644 rsx-client/tools/fix_generic_casts.py create mode 100644 rsx-client/tools/fix_stale_locals.py create mode 100644 rsx-client/tools/merge_srg_mcp.py create mode 100644 rsx-client/tools/show_errors.py create mode 100644 rsx-client/tools/stage_run.py create mode 100644 rsx-client/tools/struct_diff.py create mode 100644 rsx-mods/rsxm-demo/resources/META-INF/rsxmod.properties create mode 100644 rsx-mods/rsxm-demo/src/main/java/net/rustnotmc/demo/DemoMod.java create mode 100644 rsx-plugin-api/LICENSE create mode 100644 rsx-protocol/LICENSE create mode 100644 rsx-server-core/LICENSE diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..55809f3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +# The fork is rebuilt from the verified vanilla jar, and everything under these +# paths is either Mojang-derived or generated from it. None of it is ours to +# publish: game-fork/src is decompiled Mojang code, game-fork/classes and +# game-fork/mappings are the remapped jar. What we wrote is in src-overlay, +# patches and scripts, and that is what belongs in this repository. +game-fork/src/ +game-fork/classes/ +game-fork/build/ +game-fork/mappings/ +game-fork/struct-diff.json + +# A game directory: saves, logs, screenshots, native libraries, installed mods. +run/ + +# build output +build/ +dist/ +out/ +target/ +*.class + +# jars are build products; the third-party ones we redistribute are un-ignored +*.jar +!rsx-client/third_party/**/*.jar + +# python +__cache__/ +__pycache__/ +*.pyc + +# editors +.idea/ +.vscode/ +*.iml + +# os +Thumbs.db +desktop.ini +.DS_Store \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c8dbcc5 --- /dev/null +++ b/README.md @@ -0,0 +1,101 @@ +# 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 simulation library | +| `rsx-plugin-api` | MIT | Interfaces for server-side content | +| `rsx-server-core` | proprietary | Tick loop, world, entities, scaling | +| `rsx-auth` | proprietary | Accounts, tokens, revocation | + +## 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 103 patches, one regex rule + -> javac 3261 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. + +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. + +## Modding + +Mods are jars with a descriptor at `META-INF/rsxmod.properties`: + +```properties +id=rsxm-lib +name=RustNotMC Library +version=1.0.0 +env=CLIENT +entrypoint=net.rustnotmc.lib.LibMod +access=hud, text, world, player +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. Mods declare the capabilities they need and +the loader grants them; `reflection` is never granted without explicit operator +consent. + +Load order comes from the dependency graph, not from filenames. Shutdown runs +in reverse. + +## Building + +```powershell +rsx-client\scripts\build.ps1 # dist\rsx-loader.jar +rsx-client\scripts\test.ps1 # self test, 96 checks +game-fork\scripts\prepare-game.ps1 # rebuild the fork from the vanilla jar +game-fork\scripts\compile-game.ps1 # compile only, fast iteration +``` + +Only a JDK is required. No Gradle, no network once the mappings are cached. + +## 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\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 | + +## 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. \ No newline at end of file diff --git a/game-fork/patches/decompiler-fixes.txt b/game-fork/patches/decompiler-fixes.txt new file mode 100644 index 0000000..6463d64 --- /dev/null +++ b/game-fork/patches/decompiler-fixes.txt @@ -0,0 +1,1122 @@ +# Hand fixes applied on top of FernFlower's output. +# +# Decompiling is repeatable; these corrections are not, so they live here and +# get re-applied by tools/apply_patches.py after every re-decompile. +# +# Why each one is needed is in the patch body. + +=== FILE net/minecraft/block/BlockPortal.java +--- BEFORE +PropertyEnum.create("axis", EnumFacing.Axis.class, (Enum[])(EnumFacing.Axis.X, EnumFacing.Axis.Z)); +--- AFTER +PropertyEnum.create("axis", EnumFacing.Axis.class, new EnumFacing.Axis[]{EnumFacing.Axis.X, EnumFacing.Axis.Z}); + +=== FILE net/minecraft/util/IObjectIntIterable.java +--- REPLACE +package net.minecraft.util; + +/** + * Signature restored by hand. + * + * ProGuard dropped the generic Signature attribute from this class, so the + * decompiler emitted it raw while every use site still carried a type argument. + * That single missing parameter produced the majority of the compile errors: + * raw here means raw everywhere downstream. + * + * There is no method on this interface. The "object to int" name comes from the + * classes that implement it, which each expose their own lookup; declaring one + * here would force every implementation to invent it. + */ +public interface IObjectIntIterable extends Iterable { +} +# An Object[] cast assigned into a K[] field. Vanilla writes (K[])new Object[n], +# an unchecked cast that compiles; the decompiler emitted the erased type +# instead, which does not. +=== FILE net/minecraft/util/IntIdentityHashBiMap.java +--- BEFORE +(Object[]) (new Object[i]) +--- AFTER +(K[]) (new Object[i]) + +# getEntities/getPlayers take Predicate, but the decompiler typed the +# loop variable as raw Entity, so the call cannot be checked. Declaring it as T +# is what vanilla does. Multi-line context on purpose: a bare "list.add(entity)" +# would also rewrite this.loadedEntityList.add(entity). +=== FILE net/minecraft/world/storage/loot/properties/EntityPropertyManager.java +--- BEFORE + throw new IllegalArgumentException("Unknown loot entity property " + t0); + } else { + return entityproperty_serializer; +--- AFTER + throw new IllegalArgumentException("Unknown loot entity property " + t0); + } else { + return (EntityProperty.Serializer) entityproperty_serializer; + +=== FILE net/minecraft/world/World.java +--- BEFORE + if (oclass.isAssignableFrom(entity.getClass()) && predicate.apply(entity)) { + list.add(entity); + } +--- AFTER + if (oclass.isAssignableFrom(entity.getClass()) && predicate.apply((T) entity)) { + list.add((T) entity); + } + +=== FILE net/minecraft/world/World.java +--- BEFORE + T t2 = (Entity) list.get(i); +--- AFTER + T t2 = (T) list.get(i); + +# Overriding methods came out protected while every one of them is public in +# MinecraftServer. Narrowing visibility is not an override, so all six fail. +=== FILE net/minecraft/server/integrated/IntegratedServer.java +--- BEFORE + protected ServerCommandManager createCommandManager() { +--- AFTER + public ServerCommandManager createCommandManager() { + +=== FILE net/minecraft/server/integrated/IntegratedServer.java +--- BEFORE + protected void loadAllWorlds(String s, String s1, long i, WorldType worldtype, String s2) { +--- AFTER + public void loadAllWorlds(String s, String s1, long i, WorldType worldtype, String s2) { + +=== FILE net/minecraft/server/integrated/IntegratedServer.java +--- BEFORE + protected boolean init() throws IOException { +--- AFTER + public boolean init() throws IOException { + +=== FILE net/minecraft/server/integrated/IntegratedServer.java +--- BEFORE + protected void tick() { +--- AFTER + public void tick() { + +=== FILE net/minecraft/server/integrated/IntegratedServer.java +--- BEFORE + protected void saveAllWorlds(boolean flag) { +--- AFTER + public void saveAllWorlds(boolean flag) { + +=== FILE net/minecraft/server/integrated/IntegratedServer.java +--- BEFORE + protected void finalTick(CrashReport crashreport) { +--- AFTER + public void finalTick(CrashReport crashreport) { + +# The constructor took a raw Function, so no constructor reference could bind to +# it. Typing it as the field is what lets MovementStep::new resolve. +=== FILE net/minecraft/client/tutorial/TutorialSteps.java +--- BEFORE + private TutorialSteps(String s, Function function) { +--- AFTER + private TutorialSteps(String s, Function function) { + +# Erased types where the surrounding signature is generic: Object[] cannot be +# assigned to T[], and a raw Iterator yields Object. +=== FILE net/minecraft/util/math/Cartesian.java +--- BEFORE + Iterator iterator = iterable.iterator(); + + while (iterator.hasNext()) { + T t0 = iterator.next(); +--- AFTER + Iterator iterator = iterable.iterator(); + + while (iterator.hasNext()) { + T t0 = iterator.next(); + +=== FILE net/minecraft/util/math/Cartesian.java +--- BEFORE + return (Object[]) list.toArray(createArray(oclass, list.size())); +--- AFTER + return (T[]) list.toArray(createArray(oclass, list.size())); + +=== FILE net/minecraft/util/math/Cartesian.java +--- BEFORE + return (Object[]) ((Object[]) Array.newInstance(oclass, i)); +--- AFTER + return (T[]) Array.newInstance(oclass, i); + +=== FILE net/minecraft/util/math/Cartesian.java +--- BEFORE + return (Object[]) this.results.clone(); +--- AFTER + return (T[]) this.results.clone(); + +# matchOneEntity returns T, not Entity. And the team/name/tag arguments are +# rewritten below their own declaration, so they cannot be final. +=== FILE net/minecraft/command/EntitySelector.java +--- BEFORE + return list.size() == 1 ? (Entity) list.get(0) : null; +--- AFTER + return list.size() == 1 ? (T) list.get(0) : null; + +=== FILE net/minecraft/command/EntitySelector.java +--- BEFORE + return Lists.newArrayList(new Entity[]{entity}); +--- AFTER + return Lists.newArrayList((T) entity); + +=== FILE net/minecraft/command/EntitySelector.java +--- BEFORE + final String s = getArgument(map, EntitySelector.ARGUMENT_TEAM_NAME); +--- AFTER + String s = getArgument(map, EntitySelector.ARGUMENT_TEAM_NAME); + +=== FILE net/minecraft/command/EntitySelector.java +--- BEFORE + final String s = getArgument(map, EntitySelector.ARGUMENT_PLAYER_NAME); +--- AFTER + String s = getArgument(map, EntitySelector.ARGUMENT_PLAYER_NAME); + +=== FILE net/minecraft/command/EntitySelector.java +--- BEFORE + final String s = getArgument(map, EntitySelector.ARGUMENT_ENTITY_TAG); +--- AFTER + String s = getArgument(map, EntitySelector.ARGUMENT_ENTITY_TAG); + +# FernFlower wrote "null." where the owning type belonged, so every setRespawnState +# call referenced a literal null. The constants live on DragonSpawnManager itself. +=== FILE net/minecraft/world/end/DragonSpawnManager.java +--- BEFORE +setRespawnState(null. +--- AFTER +setRespawnState(DragonSpawnManager. + +# getEntityClassRenderObject is generic in T but the local was declared with a +# wildcard, so the recursive call and the return could not be checked. Also the +# ternary has no inferable T at all. +=== FILE net/minecraft/client/renderer/entity/RenderManager.java +--- BEFORE + Render render = (Render) this.entityRenderMap.get(oclass); +--- AFTER + Render render = (Render) this.entityRenderMap.get(oclass); + +=== FILE net/minecraft/client/renderer/entity/RenderManager.java +--- BEFORE + render = this.getEntityClassRenderObject(oclass.getSuperclass()); +--- AFTER + render = (Render) this.getEntityClassRenderObject(oclass.getSuperclass()); + +=== FILE net/minecraft/client/renderer/entity/RenderManager.java +--- BEFORE + return renderplayer != null ? renderplayer : this.playerRenderer; +--- AFTER + return (Render) (renderplayer != null ? renderplayer : this.playerRenderer); + +# UserListEntry is the erased supertype; the method is generic in V, so the cast +# has to be to V. createEntry also needs its type argument spelled out because the +# null argument gives the compiler nothing to infer from. +=== FILE net/minecraft/server/management/UserList.java +--- BEFORE + return (UserListEntry) this.values.get(this.getObjectKey(k0)); +--- AFTER + return (V) this.values.get(this.getObjectKey(k0)); + +=== FILE net/minecraft/server/management/UserList.java +--- BEFORE + V v0 = (UserListEntry) iterator.next(); +--- AFTER + V v0 = (V) iterator.next(); + +=== FILE net/minecraft/server/management/UserList.java +--- BEFORE + K k0 = iterator.next(); +--- AFTER + K k0 = (K) iterator.next(); + +=== FILE net/minecraft/server/management/UserList.java +--- BEFORE + return new UserListEntry<>((Object) null, jsonobject); +--- AFTER + return new UserListEntry((Object) null, jsonobject); + +# Erased types where the surrounding signature is generic. In every case below +# the decompiler substituted the supertype it could see for the type parameter. +=== FILE net/minecraft/util/ClassInheritanceMultiMap.java +--- BEFORE + T t0 = iterator.next(); +--- AFTER + T t0 = (T) iterator.next(); + +=== FILE net/minecraft/util/ClassInheritanceMultiMap.java +--- BEFORE + this.map.put(oclass, Lists.newArrayList(new Object[]{t0})); +--- AFTER + this.map.put(oclass, Lists.newArrayList(t0)); + +=== FILE net/minecraft/util/ClassInheritanceMultiMap.java +--- BEFORE + T t0 = object; +--- AFTER + T t0 = (T) object; + +# getName is generic in the property's own type, so the redundant (Enum) cast +# forced the argument to Enum and broke the call. +=== FILE net/minecraft/client/renderer/BlockModelShapes.java +--- BEFORE +BlockDirt.VARIANT.getName((Enum) ((BlockDirt.DirtType) map.remove(BlockDirt.VARIANT))) +--- AFTER +BlockDirt.VARIANT.getName((BlockDirt.DirtType) map.remove(BlockDirt.VARIANT)) + +=== FILE net/minecraft/client/renderer/BlockModelShapes.java +--- BEFORE +BlockStoneSlab.VARIANT.getName((Enum) ((BlockStoneSlab.EnumType) map.remove(BlockStoneSlab.VARIANT))) +--- AFTER +BlockStoneSlab.VARIANT.getName((BlockStoneSlab.EnumType) map.remove(BlockStoneSlab.VARIANT)) + +=== FILE net/minecraft/client/renderer/BlockModelShapes.java +--- BEFORE +BlockStoneSlabNew.VARIANT.getName((Enum) ((BlockStoneSlabNew.EnumType) map.remove(BlockStoneSlabNew.VARIANT))) +--- AFTER +BlockStoneSlabNew.VARIANT.getName((BlockStoneSlabNew.EnumType) map.remove(BlockStoneSlabNew.VARIANT)) + +# readArray returns List but stores byte, long and int, none of +# which is T. Vanilla keeps this list raw for exactly that reason. +=== FILE net/minecraft/nbt/JsonToNBT.java +--- BEFORE + List list = Lists.newArrayList(); +--- AFTER + List list = Lists.newArrayList(); + +=== FILE net/minecraft/block/state/BlockStateContainer.java +--- BEFORE + T t0 = (Comparable) iterator.next(); +--- AFTER + T t0 = (T) iterator.next(); + +=== FILE net/minecraft/block/state/BlockStateContainer.java +--- BEFORE + return (Comparable) iproperty.getValueClass().cast(comparable); +--- AFTER + return (T) iproperty.getValueClass().cast(comparable); + +# RoomCollection and SecondFloor both have no-argument constructors; the +# decompiler passed a literal null that could not match any of them. +=== FILE net/minecraft/world/gen/structure/WoodlandMansionPieces.java +--- BEFORE + super(null); +--- AFTER + super(); + +=== FILE net/minecraft/command/CommandBase.java +--- BEFORE + return (Entity) object; +--- AFTER + return (T) object; + +=== FILE net/minecraft/command/CommandBase.java +--- BEFORE + return iblockstate.withProperty(iproperty, comparable); +--- AFTER + return iblockstate.withProperty(iproperty, (T) comparable); + +=== FILE net/minecraft/command/CommandBase.java +--- BEFORE + return (Comparable) iproperty.parseValue(s).orNull(); +--- AFTER + return (T) iproperty.parseValue(s).orNull(); + +# withProperty and getName both need the property's own type parameter, not the +# Comparable supertype the decompiler settled on. +=== FILE net/minecraft/nbt/NBTUtil.java +--- BEFORE + return iblockstate.withProperty(iproperty, (Comparable) optional.get()); +--- AFTER + return iblockstate.withProperty(iproperty, (T) optional.get()); + +=== FILE net/minecraft/nbt/NBTUtil.java +--- BEFORE + return iproperty.getName(comparable); +--- AFTER + return iproperty.getName((T) comparable); + +# raw Entry, so getKey() is Object and cannot go straight into a Map +=== FILE net/minecraft/profiler/Snooper.java +--- BEFORE + map.put(entry.getKey(), entry.getValue().toString()); +--- AFTER + map.put((String) entry.getKey(), entry.getValue().toString()); + +# getPropertyName returns String, so the cast belongs on the argument, not on +# the call. Casting the result produced a second error instead of fixing one. +=== FILE net/minecraft/block/state/BlockStateBase.java +--- BEFORE + return iproperty.getName(comparable); +--- AFTER + return iproperty.getName((T) comparable); + +=== FILE net/minecraft/client/renderer/block/statemap/StateMapperBase.java +--- BEFORE + return iproperty.getName(comparable); +--- AFTER + return iproperty.getName((T) comparable); + +# Advancement.Builder.resolveParent takes a java.util.function.Function while +# Guava's forMap hands back com.google.common.base.Function. Bridging with a +# method reference keeps both sides on their own interface. +=== FILE net/minecraft/advancements/AdvancementList.java +--- BEFORE + Function function = Functions.forMap(this.advancements, (Object) null); +--- AFTER + Function function = Functions.forMap(this.advancements, null)::apply; + +# criteria is keyed by String, the set is a keySet: the second pass needs its +# own iterator of the right element type. +=== FILE net/minecraft/advancements/AdvancementProgress.java +--- BEFORE + iterator = set.iterator(); + + while (iterator.hasNext()) { + String s = (String) iterator.next(); +--- AFTER + Iterator iterator1 = set.iterator(); + + while (iterator1.hasNext()) { + String s = iterator1.next(); + +=== FILE net/minecraft/block/state/BlockStateBase.java +--- BEFORE + return this.withProperty(iproperty, (Comparable) cyclePropertyValue(iproperty.getAllowedValues(), this.getValue(iproperty))); +--- AFTER + return this.withProperty(iproperty, (T) cyclePropertyValue(iproperty.getAllowedValues(), this.getValue(iproperty))); + +# getAllowedValues hands back Collection, which is not the declared +# element type. Vanilla keeps this list raw for the same reason. +=== FILE net/minecraft/block/state/BlockStateContainer.java +--- BEFORE + List>> list = Lists.newArrayList(); +--- AFTER + List list = Lists.newArrayList(); + +=== FILE net/minecraft/block/state/pattern/BlockStateMatcher.java +--- BEFORE + return predicate.apply(iblockstate.getValue(iproperty)); +--- AFTER + return ((Predicate) predicate).apply(iblockstate.getValue(iproperty)); + +=== FILE net/minecraft/block/state/pattern/FactoryBlockPattern.java +--- BEFORE + this.symbolMap.put(c0, (Object) null); +--- AFTER + this.symbolMap.put(c0, (Predicate) null); + +# the local is filled from an ArrayList and from a List, so List is the only +# type both fit +=== FILE net/minecraft/client/gui/GuiIngame.java +--- BEFORE + ArrayList arraylist; +--- AFTER + List arraylist; + +=== FILE net/minecraft/client/gui/toasts/GuiToast.java +--- BEFORE + return guitoast_toastinstance.getToast(); +--- AFTER + return (T) guitoast_toastinstance.getToast(); + +# the counter is reused below the loop, so it cannot be final +=== FILE net/minecraft/client/particle/ParticleManager.java +--- BEFORE + for (final int i = 0; i < 3; ++i) { +--- AFTER + for (int i = 0; i < 3; ++i) { + +=== FILE net/minecraft/client/renderer/ItemRenderer.java +--- BEFORE + RenderPlayer renderplayer = (RenderPlayer) this.renderManager.getEntityRenderObject(entityplayersp); +--- AFTER + RenderPlayer renderplayer = (RenderPlayer) this.renderManager.getEntityRenderObject(entityplayersp, RenderPlayer.class); + +# getHardware() is declared without getProcessors in the public interface; the +# com.sun one is needed, and its Processor type is not named so the class does +# not have to be resolved at compile time. +=== FILE net/minecraft/client/renderer/OpenGlHelper.java +--- BEFORE + Processor[] aprocessor = (new SystemInfo()).getHardware().getProcessors(); +--- AFTER + Object[] aprocessor = (Object[]) ((com.sun.management.SystemInfo) new SystemInfo()).getHardware().getProcessors(); + + + +=== FILE net/minecraft/client/renderer/tileentity/TileEntityRendererDispatcher.java +--- BEFORE + TileEntitySpecialRenderer tileentityspecialrenderer = (TileEntitySpecialRenderer) this.renderers.get(oclass); +--- AFTER + TileEntitySpecialRenderer tileentityspecialrenderer = (TileEntitySpecialRenderer) this.renderers.get(oclass); + +=== FILE net/minecraft/client/renderer/tileentity/TileEntityRendererDispatcher.java +--- BEFORE + tileentityspecialrenderer = this.getRenderer(oclass.getSuperclass()); +--- AFTER + tileentityspecialrenderer = (TileEntitySpecialRenderer) this.getRenderer(oclass.getSuperclass()); + +# listFiles is overloaded on FileFilter and FilenameFilter, and the filter is +# an IOFileFilter implementing both +=== FILE net/minecraft/client/resources/FolderResourcePack.java +--- BEFORE + File[] afile = file.listFiles(DirectoryFileFilter.DIRECTORY); +--- AFTER + File[] afile = file.listFiles((java.io.FileFilter) DirectoryFileFilter.DIRECTORY); + +=== FILE net/minecraft/client/util/SearchTree.java +--- BEFORE + this.byName.add(t0, s.toLowerCase(Locale.ROOT)); +--- AFTER + this.byName.add(t0, ((String) s).toLowerCase(Locale.ROOT)); + +=== FILE net/minecraft/command/CommandSpreadPlayers.java +--- BEFORE + set.add((Object) null); +--- AFTER + set.add((Team) null); + +=== FILE net/minecraft/util/NonNullList.java +--- BEFORE + return new NonNullList<>(Arrays.asList(aobject), e0); +--- AFTER + return new NonNullList(Arrays.asList(aobject), e0); + +=== FILE net/minecraft/util/datafix/walkers/Filtered.java +--- BEFORE + this.key = EntityList.getKey(oclass); +--- AFTER + this.key = EntityList.getKey((Class) (Class) oclass); + +=== FILE net/minecraft/util/datafix/walkers/Filtered.java +--- BEFORE + this.key = TileEntity.getKey(oclass); +--- AFTER + this.key = TileEntity.getKey((Class) (Class) oclass); + +# getEntityRenderObject infers its type parameter from the assignment target and +# lands on Entity. Naming it gives the right renderer type. +=== FILE net/minecraft/client/renderer/ItemRenderer.java +--- BEFORE + RenderPlayer renderplayer = (RenderPlayer) this.renderManager.getEntityRenderObject(entityplayersp, RenderPlayer.class); +--- AFTER + RenderPlayer renderplayer = (RenderPlayer) this.renderManager.getEntityRenderObject(entityplayersp); + +# oshi exposes the processor list through CentralProcessor, not directly on +# HardwareAbstractionLayer. Naming Processor is unnecessary, so the class does +# not need to be resolved. +=== FILE net/minecraft/client/renderer/OpenGlHelper.java +--- BEFORE + Object[] aprocessor = (Object[]) ((com.sun.management.SystemInfo) new SystemInfo()).getHardware().getProcessors(); +--- AFTER + Object[] aprocessor = (Object[]) systeminfo.getHardware().getProcessor().getProcessors(); + +=== FILE net/minecraft/client/renderer/debug/DebugRendererNeighborsUpdate.java +--- BEFORE + iterator = map.entrySet().iterator(); +--- AFTER + iterator = (Iterator) map.entrySet().iterator(); + +=== FILE net/minecraft/client/renderer/entity/RenderLivingBase.java +--- BEFORE + return this.layerRenderers.add(u0); +--- AFTER + return this.layerRenderers.add((LayerRenderer) u0); + +=== FILE net/minecraft/client/renderer/entity/RenderManager.java +--- BEFORE + render = (Render) this.getEntityClassRenderObject(oclass.getSuperclass()); +--- AFTER + render = (Render) this.getEntityClassRenderObject((Class) (Class) oclass.getSuperclass()); + +=== FILE net/minecraft/client/renderer/texture/TextureAtlasSprite.java +--- BEFORE + this.framesTextureData.add((Object) null); +--- AFTER + this.framesTextureData.add((int[][]) null); + +=== FILE net/minecraft/client/renderer/tileentity/TileEntityRendererDispatcher.java +--- BEFORE + tileentityspecialrenderer = (TileEntitySpecialRenderer) this.getRenderer(oclass.getSuperclass()); +--- AFTER + tileentityspecialrenderer = (TileEntitySpecialRenderer) this.getRenderer((Class) (Class) oclass.getSuperclass()); + +=== FILE net/minecraft/command/EntitySelector.java +--- BEFORE + list = Lists.newArrayList(new Entity[]{entity}); +--- AFTER + list = Lists.newArrayList((T) entity); + +=== FILE net/minecraft/entity/Entity.java +--- BEFORE + set.add(entity); +--- AFTER + set.add((T) entity); + +=== FILE net/minecraft/entity/EntityList.java +--- BEFORE + EntityList.OLD_NAMES.add((Object) null); +--- AFTER + EntityList.OLD_NAMES.add((String) null); + +=== FILE net/minecraft/entity/ai/EntityAINearestAttackableTarget.java +--- BEFORE + }, this.targetEntitySelector); +--- AFTER + }, (Predicate) this.targetEntitySelector); + +=== FILE net/minecraft/item/ItemSlab.java +--- BEFORE + return this.doubleSlab.getDefaultState().withProperty(iproperty, comparable); +--- AFTER + return this.doubleSlab.getDefaultState().withProperty(iproperty, (T) comparable); + +# Packet.processPacket takes T, which here is the concrete handler subtype. +# Erasing the packet keeps the INetHandler it already is. +=== FILE net/minecraft/network/NetworkManager.java +--- BEFORE + packet.processPacket(this.packetListener); +--- AFTER + ((Packet) packet).processPacket(this.packetListener); + +=== FILE net/minecraft/tileentity/TileEntityPiston.java +--- BEFORE +TileEntityPiston.MOVING_ENTITY.set((Object) null); +--- AFTER +TileEntityPiston.MOVING_ENTITY.set((EnumFacing) null); + +=== FILE net/minecraft/network/datasync/EntityDataManager.java +--- BEFORE + list.add(new EntityDataManager.DataEntry<>(dataserializer.createKey(short0), dataserializer.read(packetbuffer))); +--- AFTER + list.add(new EntityDataManager.DataEntry(dataserializer.createKey(short0), dataserializer.read(packetbuffer))); + + + +=== FILE net/minecraft/client/renderer/OpenGlHelper.java +--- BEFORE + Object[] aprocessor = (Object[]) systeminfo.getHardware().getProcessor().getProcessors(); +--- AFTER + Object[] aprocessor = (Object[]) (new SystemInfo()).getHardware().getProcessor().getProcessors(); + +=== FILE net/minecraft/entity/ai/EntityAINearestAttackableTarget.java +--- BEFORE + this.targetEntity = this.taskOwner.world.getNearestAttackablePlayer( +--- AFTER + this.targetEntity = (EntityLivingBase) this.taskOwner.world.getNearestAttackablePlayer( + +=== FILE net/minecraft/network/datasync/EntityDataManager.java +--- BEFORE + list.add(new EntityDataManager.DataEntry(dataserializer.createKey(short0), dataserializer.read(packetbuffer))); +--- AFTER + list.add(new EntityDataManager.DataEntry((DataParameter) dataserializer.createKey(short0), dataserializer.read(packetbuffer))); + +=== FILE net/minecraft/network/datasync/EntityDataManager.java +--- BEFORE + entitydatamanager_dataentry.setValue(entitydatamanager_dataentry1.getValue()); +--- AFTER + entitydatamanager_dataentry.setValue((T) entitydatamanager_dataentry1.getValue()); + +=== FILE net/minecraft/util/HttpUtil.java +--- BEFORE + return listenablefuture; +--- AFTER + return (ListenableFuture) listenablefuture; + +=== FILE net/minecraft/util/JsonUtils.java +--- BEFORE + return gson.getAdapter(TypeToken.get(type)).read(jsonreader); +--- AFTER + return (T) gson.getAdapter(TypeToken.get(type)).read(jsonreader); + +=== FILE net/minecraft/util/NonNullList.java +--- BEFORE + return new NonNullList(Arrays.asList(aobject), e0); +--- AFTER + return new NonNullList((List) Arrays.asList(aobject), e0); + +=== FILE net/minecraft/util/math/Cartesian.java +--- BEFORE + return Arrays.asList((Object[]) aobject); +--- AFTER + return Arrays.asList((T[]) aobject); + + +# EntityPlayer is not imported here, so the type witness needs its full name. +=== FILE net/minecraft/client/renderer/ItemRenderer.java +--- BEFORE + RenderPlayer renderplayer = (RenderPlayer) this.renderManager.getEntityRenderObject(entityplayersp); +--- AFTER + RenderPlayer renderplayer = (RenderPlayer) this.renderManager.getEntityRenderObject(entityplayersp); + +# The bundled oshi is 5.8.5, where CentralProcessor no longer has +# getProcessors(); getProcessorIdentifier() is the replacement that still yields +# a name for this label. +=== FILE net/minecraft/client/renderer/OpenGlHelper.java +--- BEFORE + Object[] aprocessor = (Object[]) (new SystemInfo()).getHardware().getProcessor().getProcessors(); +--- AFTER + Object[] aprocessor = new Object[]{String.valueOf((new SystemInfo()).getHardware().getProcessor().getProcessorIdentifier())}; + +# the parameter is final, so the fallback needs its own local +=== FILE net/minecraft/client/renderer/texture/TextureManager.java +--- BEFORE + itextureobject = TextureUtil.MISSING_TEXTURE; + this.mapTextureObjects.put(resourcelocation, itextureobject); +--- AFTER + ITextureObject missing = TextureUtil.MISSING_TEXTURE; + this.mapTextureObjects.put(resourcelocation, missing); + +# targetEntity is declared as T, the class's own type parameter +=== FILE net/minecraft/entity/ai/EntityAINearestAttackableTarget.java +--- BEFORE + this.targetEntity = (EntityLivingBase) this.taskOwner.world.getNearestAttackablePlayer( +--- AFTER + this.targetEntity = (T) this.taskOwner.world.getNearestAttackablePlayer( + + +# The anonymous class captures the loop variable, which a for-loop never lets be +# effectively final. Capturing a copy is what the original bytecode did. +=== FILE net/minecraft/client/particle/ParticleManager.java +--- BEFORE + crashreportcategory.addDetail("Particle Type", new ICrashReportDetail() { + public String call() throws Exception { + return i == 0 ? "MISC_TEXTURE" : (i == 1 ? "TERRAIN_TEXTURE" : (i == 3 ? "ENTITY_PARTICLE_TEXTURE" : "Unknown - " + i)); + } + }); +--- AFTER + final int layer = i; + crashreportcategory.addDetail("Particle Type", new ICrashReportDetail() { + public String call() throws Exception { + return layer == 0 ? "MISC_TEXTURE" : (layer == 1 ? "TERRAIN_TEXTURE" : (layer == 3 ? "ENTITY_PARTICLE_TEXTURE" : "Unknown - " + layer)); + } + }); + +# j is a mouse-Y scratch value reused a second time further down. The decompiler +# declared it final, which makes the second assignment an error. +=== FILE net/minecraft/client/renderer/EntityRenderer.java +--- BEFORE + final int j; +--- AFTER + int j; + +# j is assigned twice, so it is not effectively final, and the crash report +# class captures it. Capturing a copy keeps the report correct. +=== FILE net/minecraft/client/renderer/EntityRenderer.java +--- BEFORE + crashreportcategory.addDetail("Mouse location", new ICrashReportDetail() { + public String call() throws Exception { + return String.format("Scaled: (%d, %d). Absolute: (%d, %d)", i1, j, Mouse.getX(), Mouse.getY()); + } + }); +--- AFTER + final int scaledMouseY = j; + crashreportcategory.addDetail("Mouse location", new ICrashReportDetail() { + public String call() throws Exception { + return String.format("Scaled: (%d, %d). Absolute: (%d, %d)", i1, scaledMouseY, Mouse.getX(), Mouse.getY()); + } + }); + +# A blank final assigned in both try and catch is rejected: the catch may run +# after the try already stored a value. Computing into a plain local first and +# freezing it afterwards says the same thing and is legal. +=== FILE net/minecraft/command/EntitySelector.java +--- BEFORE + final GameType gametype; + + try { + int i = Integer.parseInt(s); + + gametype = GameType.parseGameTypeWithDefault(i, GameType.NOT_SET); + } catch (Throwable throwable) { + gametype = GameType.parseGameTypeWithDefault(s, GameType.NOT_SET); + } +--- AFTER + GameType parsedGameType; + + try { + int i = Integer.parseInt(s); + + parsedGameType = GameType.parseGameTypeWithDefault(i, GameType.NOT_SET); + } catch (Throwable throwable) { + parsedGameType = GameType.parseGameTypeWithDefault(s, GameType.NOT_SET); + } + + final GameType gametype = parsedGameType; + +# s is rewritten when the argument is negated, so the predicate captures a copy. +=== FILE net/minecraft/command/EntitySelector.java +--- BEFORE + if (s != null) { + list.add(new Predicate() { + public boolean apply(@Nullable Entity entity) { + if (!(entity instanceof EntityLivingBase)) { +--- AFTER + if (s != null) { + final String teamName = s; + list.add(new Predicate() { + public boolean apply(@Nullable Entity entity) { + if (!(entity instanceof EntityLivingBase)) { + +=== FILE net/minecraft/command/EntitySelector.java +--- BEFORE + return s1.equals(s) != flag; +--- AFTER + return s1.equals(teamName) != flag; + +=== FILE net/minecraft/command/EntitySelector.java +--- BEFORE + if (s != null) { + list.add(new Predicate() { + public boolean apply(@Nullable Entity entity) { + return entity != null && entity.getName().equals(s) != flag; + } + }); + } +--- AFTER + if (s != null) { + final String playerName = s; + list.add(new Predicate() { + public boolean apply(@Nullable Entity entity) { + return entity != null && entity.getName().equals(playerName) != flag; + } + }); + } + +# i and j are blank finals assigned in two separate blocks, and each predicate +# captures them. One pair of locals per block is what the original meant. +=== FILE net/minecraft/command/EntitySelector.java +--- BEFORE + final int i; + final int j; + + if (map.containsKey(EntitySelector.ARGUMENT_ROTY_MIN) || map.containsKey(EntitySelector.ARGUMENT_ROTY_MAX)) { + i = MathHelper.wrapDegrees(getInt(map, EntitySelector.ARGUMENT_ROTY_MIN, 0)); + j = MathHelper.wrapDegrees(getInt(map, EntitySelector.ARGUMENT_ROTY_MAX, 359)); + list.add(new Predicate() { + public boolean apply(@Nullable Entity entity) { + if (entity == null) { + return false; + } else { + int k = MathHelper.wrapDegrees(MathHelper.floor(entity.rotationYaw)); + + return i > j ? k >= i || k <= j : k >= i && k <= j; + } + } + }); + } + + if (map.containsKey(EntitySelector.ARGUMENT_ROTX_MIN) || map.containsKey(EntitySelector.ARGUMENT_ROTX_MAX)) { + i = MathHelper.wrapDegrees(getInt(map, EntitySelector.ARGUMENT_ROTX_MIN, 0)); + j = MathHelper.wrapDegrees(getInt(map, EntitySelector.ARGUMENT_ROTX_MAX, 359)); + list.add(new Predicate() { + public boolean apply(@Nullable Entity entity) { + if (entity == null) { + return false; + } else { + int k = MathHelper.wrapDegrees(MathHelper.floor(entity.rotationPitch)); + + return i > j ? k >= i || k <= j : k >= i && k <= j; + } + } + }); + } +--- AFTER + if (map.containsKey(EntitySelector.ARGUMENT_ROTY_MIN) || map.containsKey(EntitySelector.ARGUMENT_ROTY_MAX)) { + final int minYaw = MathHelper.wrapDegrees(getInt(map, EntitySelector.ARGUMENT_ROTY_MIN, 0)); + final int maxYaw = MathHelper.wrapDegrees(getInt(map, EntitySelector.ARGUMENT_ROTY_MAX, 359)); + list.add(new Predicate() { + public boolean apply(@Nullable Entity entity) { + if (entity == null) { + return false; + } else { + int k = MathHelper.wrapDegrees(MathHelper.floor(entity.rotationYaw)); + + return minYaw > maxYaw ? k >= minYaw || k <= maxYaw : k >= minYaw && k <= maxYaw; + } + } + }); + } + + if (map.containsKey(EntitySelector.ARGUMENT_ROTX_MIN) || map.containsKey(EntitySelector.ARGUMENT_ROTX_MAX)) { + final int minPitch = MathHelper.wrapDegrees(getInt(map, EntitySelector.ARGUMENT_ROTX_MIN, 0)); + final int maxPitch = MathHelper.wrapDegrees(getInt(map, EntitySelector.ARGUMENT_ROTX_MAX, 359)); + list.add(new Predicate() { + public boolean apply(@Nullable Entity entity) { + if (entity == null) { + return false; + } else { + int k = MathHelper.wrapDegrees(MathHelper.floor(entity.rotationPitch)); + + return minPitch > maxPitch ? k >= minPitch || k <= maxPitch : k >= minPitch && k <= maxPitch; + } + } + }); + } + +=== FILE net/minecraft/command/EntitySelector.java +--- BEFORE + if (s != null) { + list.add(new Predicate() { + public boolean apply(@Nullable Entity entity) { + return entity == null ? false : ("".equals(s) ? entity.getTags().isEmpty() != flag : entity.getTags().contains(s) != flag); + } + }); + } +--- AFTER + if (s != null) { + final String tag = s; + list.add(new Predicate() { + public boolean apply(@Nullable Entity entity) { + return entity == null ? false : ("".equals(tag) ? entity.getTags().isEmpty() != flag : entity.getTags().contains(tag) != flag); + } + }); + } + +# FernFlower moved shutdownMinecraftApplet() into a finally on the game loop +# body. A continue inside a try runs the finally first, so the client shut +# down after one frame. javap on the original bytecode shows shutdown is +# only reached at loop exit (offset 103) and from the catch handlers. +=== FILE net/minecraft/client/Minecraft.java +--- BEFORE + public void run() { + this.running = true; + + CrashReport crashreport; + + try { + this.init(); + } catch (Throwable throwable) { + crashreport = CrashReport.makeCrashReport(throwable, "Initializing game"); + crashreport.makeCategory("Initialization"); + this.displayCrashReport(this.addGraphicsAndWorldToCrashReport(crashreport)); + return; + } + + while (true) { + try { + if (!this.running) { + break; + } + + if (!this.hasCrashed || this.crashReporter == null) { + try { + this.runGameLoop(); + } catch (OutOfMemoryError outofmemoryerror) { + this.freeMemory(); + this.displayGuiScreen(new GuiMemoryErrorScreen()); + System.gc(); + } + continue; + } + + this.displayCrashReport(this.crashReporter); + } catch (MinecraftError minecrafterror) { + break; + } catch (ReportedException reportedexception) { + this.addGraphicsAndWorldToCrashReport(reportedexception.getCrashReport()); + this.freeMemory(); + Minecraft.LOGGER.fatal("Reported exception thrown!", reportedexception); + this.displayCrashReport(reportedexception.getCrashReport()); + break; + } catch (Throwable throwable1) { + crashreport = this.addGraphicsAndWorldToCrashReport(new CrashReport("Unexpected error", throwable1)); + this.freeMemory(); + Minecraft.LOGGER.fatal("Unreported exception thrown!", throwable1); + this.displayCrashReport(crashreport); + break; + } finally { + this.shutdownMinecraftApplet(); + } + + return; + } + + } +--- AFTER + public void run() { + this.running = true; + + CrashReport crashreport; + + try { + this.init(); + } catch (Throwable throwable) { + crashreport = CrashReport.makeCrashReport(throwable, "Initializing game"); + crashreport.makeCategory("Initialization"); + this.displayCrashReport(this.addGraphicsAndWorldToCrashReport(crashreport)); + return; + } + + try { + while (true) { + if (!this.running) { + break; + } + + if (this.hasCrashed && this.crashReporter != null) { + this.displayCrashReport(this.crashReporter); + this.shutdownMinecraftApplet(); + return; + } + + try { + this.runGameLoop(); + } catch (OutOfMemoryError outofmemoryerror) { + this.freeMemory(); + this.displayGuiScreen(new GuiMemoryErrorScreen()); + System.gc(); + } + } + + this.shutdownMinecraftApplet(); + } catch (MinecraftError minecrafterror) { + this.shutdownMinecraftApplet(); + } catch (ReportedException reportedexception) { + this.addGraphicsAndWorldToCrashReport(reportedexception.getCrashReport()); + this.freeMemory(); + Minecraft.LOGGER.fatal("Reported exception thrown!", reportedexception); + this.displayCrashReport(reportedexception.getCrashReport()); + this.shutdownMinecraftApplet(); + } catch (Throwable throwable1) { + crashreport = this.addGraphicsAndWorldToCrashReport(new CrashReport("Unexpected error", throwable1)); + this.freeMemory(); + Minecraft.LOGGER.fatal("Unreported exception thrown!", throwable1); + this.displayCrashReport(crashreport); + this.shutdownMinecraftApplet(); + } + } + +=== FILE net/minecraft/entity/player/EntityPlayerMP.java +--- BEFORE + this.entityRemoveQueue.remove(entity.getEntityId()); +--- AFTER + this.entityRemoveQueue.remove(Integer.valueOf(entity.getEntityId())); + +=== FILE net/minecraft/client/Minecraft.java +--- BEFORE +this.renderGlobal.makeEntityOutlineShader(); + } + + private void populateSearchTreeManager() { +--- AFTER +net.rustnotmc.client.RsxBootstrap.start(this); + this.renderGlobal.makeEntityOutlineShader(); + } + + private void populateSearchTreeManager() { + +=== FILE net/minecraft/client/Minecraft.java +--- BEFORE +GlStateManager.popMatrix(); + this.profiler.startSection("root"); + this.updateDisplay(); +--- AFTER +GlStateManager.popMatrix(); + net.rustnotmc.client.RsxBootstrap.renderFrame(this); + this.profiler.startSection("root"); + this.updateDisplay(); + +=== FILE net/minecraft/client/gui/GuiScreen.java +--- BEFORE +public void sendChatMessage(String s, boolean flag) { + if (flag) { + this.mc.ingameGUI.getChatGUI().addToSentMessages(s); + } + + this.mc.player.sendChatMessage(s); + } +--- AFTER +public void sendChatMessage(String s, boolean flag) { + if (flag) { + this.mc.ingameGUI.getChatGUI().addToSentMessages(s); + } + + // a mod command scoped to the client is carried out here and stops here. + // Anything the server owns is left alone so it still reaches the server. + if (net.rustnotmc.client.RsxBootstrap.handleChatCommand(s)) { + return; + } + + this.mc.player.sendChatMessage(s); + } + +=== FILE net/minecraft/client/entity/EntityPlayerSP.java +--- BEFORE +public void onLivingUpdate() { + ++this.sprintingTicksLeft; +--- AFTER +public void onLivingUpdate() { + net.rustnotmc.client.RsxDebug.movement(this); + ++this.sprintingTicksLeft; + +=== FILE net/minecraft/entity/Entity.java +--- BEFORE +this.collidedHorizontally = d7 != d0 || d9 != d2; + this.collidedVertically = d1 != d1; + this.onGround = this.collidedVertically && d8 < 0.0D; +--- AFTER +this.collidedHorizontally = d7 != d0 || d9 != d2; + this.collidedVertically = d8 != d1; + this.onGround = this.collidedVertically && d8 < 0.0D; + +=== FILE net/minecraft/client/particle/Particle.java +--- BEFORE +this.onGround = d1 != d1 && d3 < 0.0D; + if (d0 != d0) { + this.motionX = 0.0D; + } + + if (d2 != d2) { + this.motionZ = 0.0D; + } +--- AFTER +this.onGround = originalY != d1 && originalY < 0.0D; + if (originalX != d0) { + this.motionX = 0.0D; + } + + if (originalZ != d2) { + this.motionZ = 0.0D; + } + +=== FILE net/minecraft/client/particle/Particle.java +--- BEFORE +double d3 = d1; + + if (this.canCollide) { +--- AFTER +// the motion before collision; the axis is only stopped when collision + // actually changed it, which a comparison against itself can never see + double originalX = d0; + double originalY = d1; + double originalZ = d2; + + if (this.canCollide) { + +=== FILE net/minecraft/entity/Entity.java +--- BEFORE +boolean flag = this.onGround || d1 != d1 && d1 < 0.0D; +--- AFTER +boolean flag = this.onGround || d8 != d1 && d8 < 0.0D; + +=== FILE net/minecraft/client/audio/SoundHandler.java +--- BEFORE +Map map = this.getSoundMap(iresource.getInputStream()); +--- AFTER +Map map = this.getSoundMap(iresource.getInputStream()); + net.rustnotmc.client.RsxDebug.sound("parsed=" + (map == null ? "null" : String.valueOf(map.size()))); + +=== FILE net/minecraft/client/audio/SoundHandler.java +--- BEFORE +List list = iresourcemanager.getAllResources(new ResourceLocation(s, "sounds.json")); +--- AFTER +List list = iresourcemanager.getAllResources(new ResourceLocation(s, "sounds.json")); + net.rustnotmc.client.RsxDebug.sound("domain=" + s + " resources=" + list.size()); + +=== FILE net/minecraft/client/audio/SoundHandler.java +--- BEFORE +this.soundRegistry.clearMap(); + Iterator iterator = iresourcemanager.getResourceDomains().iterator(); +--- AFTER +this.soundRegistry.clearMap(); + net.rustnotmc.client.RsxDebug.sound("domains=" + iresourcemanager.getResourceDomains()); + Iterator iterator = iresourcemanager.getResourceDomains().iterator(); diff --git a/game-fork/scripts/compile-game.ps1 b/game-fork/scripts/compile-game.ps1 new file mode 100644 index 0000000..f26977a --- /dev/null +++ b/game-fork/scripts/compile-game.ps1 @@ -0,0 +1,63 @@ +# Compiles the forked client from the existing sources. Fast iteration loop: +# fix, run this, look at the numbers. Use prepare-game.ps1 when the sources +# themselves need regenerating. +# +# ASCII only: PowerShell 5.1 reads a .ps1 without a byte order mark using the +# system ANSI code page. + +$ErrorActionPreference = 'Stop' + +$Fork = Split-Path -Parent $PSScriptRoot +if (-not $Fork) { $Fork = $PSCommandPath | Split-Path -Parent | Split-Path -Parent } +$Root = Split-Path -Parent $Fork +$Client = Join-Path $Root 'rsx-client' + +$Astral = Join-Path $env:APPDATA 'AstralRinthApp\meta' +$Src = Join-Path $Fork 'src' +$Out = Join-Path $Fork 'build\classes' +$Temp = Join-Path $env:TEMP 'opencode' + +Add-Type -AssemblyName System.IO.Compression.FileSystem + +$good = New-Object System.Collections.Generic.List[string] +foreach ($j in (Get-ChildItem (Join-Path $Astral 'libraries') -Recurse -Filter '*.jar' -File)) { + if ($j.Length -le 100) { continue } + $ok = $false + try { $z = [System.IO.Compression.ZipFile]::OpenRead($j.FullName); $z.Dispose(); $ok = $true } catch { $ok = $false } + if ($ok) { $good.Add(($j.FullName -replace '\\', '/')) } +} + +# The fork calls into the mod loader, so the loader has to be visible to javac +# exactly as it is at runtime. Building the loader first is what keeps the two +# from drifting apart. +$Loader = Join-Path $Client 'dist\rsx-loader.jar' + +$cp = @($good + $(if (Test-Path $Loader) { $Loader -replace '\\', '/' } else { $null })) -join ';' +$argf = Join-Path $Temp 'fork-build.txt' +$errf = Join-Path $Temp 'fork-build-errors.txt' +$sb = New-Object System.Text.StringBuilder +foreach ($a in @('-nowarn', '-proc:none', '-encoding', 'UTF-8', '--release', '17', '-d', ($Out -replace '\\', '/'))) { [void]$sb.AppendLine($a) } +[void]$sb.AppendLine('-cp'); [void]$sb.AppendLine('"' + $cp + '"') +Get-ChildItem $Src -Recurse -Filter '*.java' | ForEach-Object { [void]$sb.AppendLine(($_.FullName -replace '\\', '/')) } +Set-Content -LiteralPath $argf -Value $sb.ToString() -Encoding ASCII + +Remove-Item $Out -Recurse -Force -ErrorAction SilentlyContinue +New-Item -ItemType Directory -Force -Path $Out | Out-Null +$javac = (Get-Command javac).Source +& cmd /c "`"$javac`" @`"$argf`" -Xmaxerrs 10000 > `"$errf`" 2>&1" + +$errors = Get-Content -LiteralPath $errf | Select-String -Pattern ': error:' +$classes = (Get-ChildItem $Out -Recurse -Filter '*.class' -File -ErrorAction SilentlyContinue).Count + +if ($errors.Count -eq 0) { + Write-Host '' + Write-Host " FORK COMPILES CLEAN - $classes classes" -ForegroundColor Green + exit 0 +} + +$files = $errors | ForEach-Object { if ($_.Line -match '^([A-Z]:.*?\.java):\d+:') { $matches[1] } } | Sort-Object -Unique +Write-Host '' +Write-Host " $classes classes, $($errors.Count) errors in $($files.Count) files" -ForegroundColor Yellow +$errors | ForEach-Object { if ($_.Line -match '^([A-Z]:.*?\.java):\d+:') { $matches[1] } } | + Group-Object | Sort-Object Count -Descending | Select-Object -First 25 | + ForEach-Object { Write-Host (" {0,4} {1}" -f $_.Count, ($_.Name.Replace(($Src + '\'), ''))) -ForegroundColor Gray } \ No newline at end of file diff --git a/game-fork/scripts/prepare-game.ps1 b/game-fork/scripts/prepare-game.ps1 new file mode 100644 index 0000000..95aad90 --- /dev/null +++ b/game-fork/scripts/prepare-game.ps1 @@ -0,0 +1,173 @@ +# Builds the forked client from the verified vanilla jar. +# +# vanilla client -> MCP names -> source -> patches -> classes +# +# ASCII only: PowerShell 5.1 reads a .ps1 without a byte order mark using the +# system ANSI code page, which mangles UTF-8 and breaks the parser. +# +# Everything here is repeatable. Run it any time; nothing is edited by hand +# except game-fork\patches\decompiler-fixes.txt, which is re-applied at the end. + +$ErrorActionPreference = 'Stop' + +$Fork = Split-Path -Parent $PSScriptRoot +if (-not $Fork) { $Fork = $PSCommandPath | Split-Path -Parent | Split-Path -Parent } +$Root = Split-Path -Parent $Fork +$Client = Join-Path $Root 'rsx-client' + +$VanillaJar = 'G:\rsx-map\vanilla\client-1.12.2.jar' +$Astral = Join-Path $env:APPDATA 'AstralRinthApp\meta' +$Fernflower = 'G:\Paper-Dev\work\BuildData\bin\fernflower.jar' +$SpecialSource = 'G:\Paper-Dev\work\BuildData\bin\SpecialSource.jar' +$McpCs = 'G:\rsx-map\vanilla\mcp-stable' + +$Maps = Join-Path $Fork 'mappings' +$Classes = Join-Path $Fork 'classes' +$Src = Join-Path $Fork 'src' +$Out = Join-Path $Fork 'build\classes' +$Temp = Join-Path $env:TEMP 'opencode' + +function Say($text) { Write-Host " $text" -ForegroundColor Cyan } +function Need($path, $what) { if (-not (Test-Path -LiteralPath $path)) { throw "missing $what at $path" } } + +New-Item -ItemType Directory -Force -Path $Maps, $Temp | Out-Null +Need $VanillaJar 'the verified vanilla client jar' +Need $Fernflower 'fernflower' +Need $SpecialSource 'SpecialSource' +Need $McpCs 'the MCP name CSVs' + +# 1. vanilla SRG +$srgZip = Join-Path $Maps 'mcp-1.12.2-srg.zip' +$srgDir = Join-Path $Maps 'mcp-srg-vanilla' +if (-not (Test-Path (Join-Path $srgDir 'joined.srg'))) { + Say 'downloading the vanilla 1.12.2 SRG' + Invoke-WebRequest -Uri 'https://maven.minecraftforge.net/de/oceanlabs/mcp/mcp/1.12.2/mcp-1.12.2-srg.zip' ` + -OutFile $srgZip -TimeoutSec 180 -UseBasicParsing + Add-Type -AssemblyName System.IO.Compression.FileSystem + [System.IO.Compression.ZipFile]::ExtractToDirectory($srgZip, $srgDir) +} + +# 2. merge SRG with the MCP CSVs into a single obf -> MCP SRG +$srg = Join-Path $Maps 'obf-to-mcp.srg' +Say 'merging SRG with the MCP CSVs' +& python (Join-Path $Client 'tools\merge_srg_mcp.py') ` + (Join-Path $srgDir 'joined.srg') $McpCs $srg +if ($LASTEXITCODE -ne 0) { throw 'srg merge failed' } + +# 3. remap the vanilla client +$named = Join-Path $Maps 'client-mcp-named.jar' +Say 'remapping the vanilla client to MCP names' +if (Test-Path $named) { Remove-Item $named -Force } +& java -Xmx4g -jar $SpecialSource -m $srg -i $VanillaJar -o $named +if (-not (Test-Path $named)) { throw 'remap produced nothing' } +Say " client-mcp-named.jar ($((Get-Item $named).Length) bytes)" + +# 4. unpack for the decompiler +Remove-Item $Classes -Recurse -Force -ErrorAction SilentlyContinue +New-Item -ItemType Directory -Force -Path $Classes | Out-Null +Add-Type -AssemblyName System.IO.Compression.FileSystem +[System.IO.Compression.ZipFile]::ExtractToDirectory($named, $Classes) +Say " unpacked $((Get-ChildItem $Classes -Recurse -Filter '*.class' -File).Count) classes" + +# 5. decompile +# -dgs=1 decode generic signatures +# -rbr=1 drop bridge methods (otherwise every generic method appears twice +# and the compiler rejects the erasure clash) +# -rsy=1 drop synthetic members +Remove-Item $Src -Recurse -Force -ErrorAction SilentlyContinue +New-Item -ItemType Directory -Force -Path $Src | Out-Null +Say 'decompiling (this is the slow part)' +$log = Join-Path $Temp 'fernflower.log' +& cmd /c "java -Xmx4g -jar `"$Fernflower`" -dgs=1 -rbr=1 -rsy=1 `"$Classes`" `"$Src`" > `"$log`" 2>&1" +Say " $((Get-ChildItem $Src -Recurse -Filter '*.java' -File).Count) source files" + +# 5b. copy in our own game-side code +# +# Everything above this line is regenerated from the vanilla jar and is wiped on +# every run. Our own classes under net/rustnotmc are not generated and must live +# outside src, or a rebuild deletes them and leaves the patches that call them +# pointing at nothing. They go in before the patches so a hand fix may edit them +# too. +$Overlay = Join-Path $Fork 'src-overlay' +if (Test-Path $Overlay) { + $copied = 0 + foreach ($f in (Get-ChildItem $Overlay -Recurse -Filter '*.java' -File)) { + $rel = $f.FullName.Substring($Overlay.Length).TrimStart('\') + $target = Join-Path $Src $rel + $dir = Split-Path -Parent $target + if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Force -Path $dir | Out-Null } + Copy-Item -LiteralPath $f.FullName -Destination $target -Force + $copied++ + } + Say " overlay: $copied source file(s) from src-overlay" +} + +# 6. re-apply the hand fixes +Say 'applying hand fixes' +& python (Join-Path $Client 'tools\apply_patches.py') $Src (Join-Path $Fork 'patches\decompiler-fixes.txt') +if ($LASTEXITCODE -ne 0) { Write-Host ' WARNING: some patches did not apply' -ForegroundColor Yellow } + +# 6b. the systematic rewrites, which would be hundreds of patch entries one by one +Say 'applying systematic rules' +& python (Join-Path $Client 'tools\fix_decompiler_rules.py') $Src + +# 7. compile, let the automated fixers converge, compile again. +# +# The fixers need javac's diagnostics as their input, and fixing one error can +# reveal the next, so this runs to a fixed point rather than once. It stops as +# soon as a round fails to reduce the count, which is the signal that the +# remaining errors need a human. +$argf = Join-Path $Temp 'fork-build.txt' +$errf = Join-Path $Temp 'fork-build-errors.txt' + +$good = New-Object System.Collections.Generic.List[string] +foreach ($j in (Get-ChildItem (Join-Path $Astral 'libraries') -Recurse -Filter '*.jar' -File)) { + if ($j.Length -le 100) { continue } + $ok = $false + try { $z = [System.IO.Compression.ZipFile]::OpenRead($j.FullName); $z.Dispose(); $ok = $true } catch { $ok = $false } + if ($ok) { $good.Add(($j.FullName -replace '\\', '/')) } +} +# The fork calls into the mod loader, so the loader has to be visible to javac +# exactly as it is at runtime. +$Loader = Join-Path $Client 'dist\rsx-loader.jar' +$cp = @($good + $(if (Test-Path $Loader) { $Loader -replace '\\', '/' } else { $null })) -join ';' +$sb = New-Object System.Text.StringBuilder +foreach ($a in @('-nowarn', '-proc:none', '-encoding', 'UTF-8', '--release', '17', '-d', ($Out -replace '\\', '/'))) { [void]$sb.AppendLine($a) } +[void]$sb.AppendLine('-cp'); [void]$sb.AppendLine('"' + $cp + '"') +Get-ChildItem $Src -Recurse -Filter '*.java' | ForEach-Object { [void]$sb.AppendLine(($_.FullName -replace '\\', '/')) } +Set-Content -LiteralPath $argf -Value $sb.ToString() -Encoding ASCII + +$javac = (Get-Command javac).Source +function Invoke-Compile { + Remove-Item $Out -Recurse -Force -ErrorAction SilentlyContinue + New-Item -ItemType Directory -Force -Path $Out | Out-Null + & cmd /c "`"$javac`" @`"$argf`" -Xmaxerrs 10000 > `"$errf`" 2>&1" + return (Get-Content -LiteralPath $errf | Select-String -Pattern ': error:').Count +} + +$count = Invoke-Compile +Say "compiled, $count errors" +$previous = [int]::MaxValue +for ($round = 1; $round -le 8 -and $count -gt 0; $round++) { + if ($count -ge $previous) { Say "no further progress, stopping"; break } + $previous = $count + Say "fix round $round" + & python (Join-Path $Client 'tools\fix_stale_locals.py') $errf --apply | Out-Null + & python (Join-Path $Client 'tools\fix_generic_casts.py') $errf --apply | Out-Null + & python (Join-Path $Client 'tools\fix_decompiler_rules.py') $Src | Out-Null + $count = Invoke-Compile + Say " -> $count errors" +} + +$errors = Get-Content -LiteralPath $errf | Select-String -Pattern ': error:' +$files = $errors | ForEach-Object { if ($_.Line -match '^([A-Z]:.*?\.java):\d+:') { $matches[1] } } | Sort-Object -Unique +Write-Host '' +if ($errors.Count -eq 0) { + Write-Host ' FORK COMPILES CLEAN' -ForegroundColor Green + Write-Host " $((Get-ChildItem $Out -Recurse -Filter '*.class' -File).Count) classes" +} else { + Write-Host " $((Get-ChildItem $Out -Recurse -Filter '*.class' -File).Count) classes, $($errors.Count) errors in $($files.Count) files" -ForegroundColor Yellow + $errors | ForEach-Object { if ($_.Line -match '^([A-Z]:.*?\.java):\d+:') { $matches[1] } } | + Group-Object | Sort-Object Count -Descending | Select-Object -First 15 | + ForEach-Object { Write-Host (" {0,4} {1}" -f $_.Count, ($_.Name.Replace(($Src + '\'), ''))) -ForegroundColor Gray } +} \ No newline at end of file diff --git a/game-fork/src-overlay/net/rustnotmc/client/RsxBootstrap.java b/game-fork/src-overlay/net/rustnotmc/client/RsxBootstrap.java new file mode 100644 index 0000000..b23a347 --- /dev/null +++ b/game-fork/src-overlay/net/rustnotmc/client/RsxBootstrap.java @@ -0,0 +1,199 @@ +package net.rustnotmc.client; + +import java.io.File; +import java.util.List; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.ScaledResolution; +import net.minecraft.client.renderer.GlStateManager; +import net.rustnotmc.api.client.ClientHost; +import net.rustnotmc.api.hud.HudLayout; +import net.rustnotmc.loader.Log; +import net.rustnotmc.loader.ModContainer; +import net.rustnotmc.loader.ModLoader; + +/** + * Where the client and the mod loader meet. + * + * The loader is a library, not an agent. The game calls in at two points: once + * during startup, and once per frame. Everything a mod does hangs off those two + * calls, so the loader never has to know how a game loop is shaped. + * + * Every entry point is wrapped. A mod that throws must not be able to take the + * client down with it, and a loader that cannot start must not stop the game + * from being a game. + * + * This file lives in game-fork/src-overlay because src is regenerated from the + * vanilla jar on every build and anything written there is lost. + */ +public final class RsxBootstrap { + + private static ModLoader loader; + private static RsxHudSurface surface; + private static RsxClientServices client; + private static boolean broken; + private static boolean frameFailed; + + private RsxBootstrap() { + } + + /** True once mods have been discovered and enabled. */ + public static boolean ready() { + return loader != null && !broken; + } + + public static ModLoader loader() { + return loader; + } + + public static int loadedMods() { + return loader == null ? 0 : loader.count(); + } + + /** + * Discovers and enables mods. Called once, after the client has a display, + * a font and a resource manager, so a mod may touch any of them. + */ + public static synchronized void start(Minecraft minecraft) { + if (loader != null || broken) { + return; + } + try { + // the client API goes up before any mod runs, so onConstruct can use + // it: a mod registers commands and reads the screen there + client = new RsxClientServices(minecraft); + ClientHost.install(client); + + File directory = new File(minecraft.gameDir, "mods"); + loader = new ModLoader(directory); + + int found = loader.discover(); + Log.info("RustNotMC: scanning " + directory + ", " + found + " mod jar(s)"); + + if (!loader.resolve()) { + reportProblems(); + broken = true; + loader = null; + return; + } + + loader.loadAll(); + surface = new RsxHudSurface(minecraft); + reportProblems(); + + if (loader.count() == 0) { + Log.info("RustNotMC: no mods loaded"); + return; + } + + StringBuilder line = new StringBuilder("RustNotMC: loaded "); + List mods = loader.mods(); + for (int i = 0; i < mods.size(); i++) { + if (i > 0) { + line.append(", "); + } + ModContainer mod = mods.get(i); + line.append(mod.id()).append(' ').append(mod.version()); + } + Log.info(line.toString()); + } catch (Throwable throwable) { + broken = true; + loader = null; + Log.error("RustNotMC: mod loader failed to start", throwable); + } + } + + /** + * Offers a typed chat line to the mod commands. + * + * Called from the point where a line leaves the client. Returns true when a + * CLIENT scoped command carried it out and the line must not be sent on. + * SERVER and BOTH commands return false on purpose: the server is the one + * that has to see them and decide. + */ + public static boolean handleChatCommand(String line) { + if (client == null || !ClientHost.present()) { + return false; + } + return client.dispatchCommand(line, localName()); + } + + private static String localName() { + Minecraft mc = Minecraft.getMinecraft(); + if (mc != null && mc.getSession() != null) { + return mc.getSession().getUsername(); + } + return "local"; + } + + private static void reportProblems() { + List problems = loader.problems(); + for (int i = 0; i < problems.size(); i++) { + Log.error("RustNotMC: " + problems.get(i)); + } + } + + /** + * Runs scheduled mod work and draws registered overlays. + * + * Called once per frame with the main framebuffer bound and the game's own + * matrices already unwound, so the overlay sets up its own projection and + * leaves the state it did not set alone. + */ + public static void renderFrame(Minecraft minecraft) { + if (loader == null || surface == null || !surface.available()) { + return; + } + if (frameFailed) { + return; + } + if (client != null) { + client.advanceFrame(); + } + try { + loader.tick(); + if (client != null) { + client.advanceTick(); + } + + ScaledResolution scaled = new ScaledResolution(minecraft); + int width = scaled.getScaledWidth(); + int height = scaled.getScaledHeight(); + if (width <= 0 || height <= 0) { + return; + } + + // the overlay draws in flat screen space, so depth would fight it + // and the blend state has to be ours for the duration + GlStateManager.disableDepth(); + + GlStateManager.pushMatrix(); + GlStateManager.matrixMode(5889); + GlStateManager.loadIdentity(); + GlStateManager.ortho(0.0D, width, height, 0.0D, 1000.0D, 3000.0D); + GlStateManager.matrixMode(5888); + GlStateManager.loadIdentity(); + GlStateManager.translate(0.0F, 0.0F, -2000.0F); + + GlStateManager.enableBlend(); + GlStateManager.tryBlendFuncSeparate( + GlStateManager.SourceFactor.SRC_ALPHA, + GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA, + GlStateManager.SourceFactor.ONE, + GlStateManager.DestFactor.ZERO); + + try { + HudLayout.render(surface); + } finally { + GlStateManager.disableBlend(); + GlStateManager.enableDepth(); + GlStateManager.popMatrix(); + } + } catch (Throwable throwable) { + // one bad frame is a bug worth seeing, but it must not become a + // per-frame stack trace or a dead client + frameFailed = true; + Log.error("RustNotMC: overlay rendering failed, disabling overlays", throwable); + } + } +} \ No newline at end of file diff --git a/game-fork/src-overlay/net/rustnotmc/client/RsxClientServices.java b/game-fork/src-overlay/net/rustnotmc/client/RsxClientServices.java new file mode 100644 index 0000000..a35bcd1 --- /dev/null +++ b/game-fork/src-overlay/net/rustnotmc/client/RsxClientServices.java @@ -0,0 +1,233 @@ +package net.rustnotmc.client; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiChat; +import net.minecraft.client.gui.GuiIngameMenu; +import net.minecraft.client.gui.GuiMainMenu; +import net.minecraft.client.gui.GuiOptions; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.gui.GuiWorldSelection; +import net.minecraft.client.gui.achievement.GuiStats; +import net.minecraft.client.gui.advancements.GuiScreenAdvancements; +import net.minecraft.client.gui.inventory.GuiContainer; +import net.minecraft.client.settings.KeyBinding; +import net.rustnotmc.api.client.ClientServices; +import net.rustnotmc.api.client.Commands; +import net.rustnotmc.api.client.Input; +import net.rustnotmc.api.client.Screen; +import net.rustnotmc.api.client.ScreenKind; +import org.lwjgl.input.Mouse; + +/** + * The client's implementation of the mod API. + * + * Everything here reads presentation state the client already has: what screen + * is open, what the window measures, which key is held. Nothing reaches into + * the world, a player or a packet, because the server owns those and a client + * mod is not allowed to decide them. + * + * The values are recomputed per call rather than cached. A mod may ask once a + * frame, and a stale answer would be worse than a cheap one. + * + * Lives in game-fork/src-overlay; src is regenerated on every build. + */ +public final class RsxClientServices implements ClientServices { + + private final Minecraft minecraft; + private final RsxScreen screen = new RsxScreen(); + private final RsxInput input = new RsxInput(); + private final RsxCommands commands = new RsxCommands(); + + private long tick; + private long frame; + + public RsxClientServices(Minecraft minecraft) { + this.minecraft = minecraft; + } + + /** Called once per frame by the bootstrap, before anything is asked. */ + public void advanceFrame() { + this.frame++; + } + + /** Called once per tick by the bootstrap. */ + public void advanceTick() { + this.tick++; + } + + boolean dispatchCommand(String line, String sender) { + return this.commands.dispatch(line, sender); + } + + @Override + public Screen screen() { + return this.screen; + } + + @Override + public Input input() { + return this.input; + } + + @Override + public Commands commands() { + return this.commands; + } + + @Override + public long tick() { + return this.tick; + } + + @Override + public long frame() { + return this.frame; + } + + private final class RsxScreen implements Screen { + + @Override + public ScreenKind kind() { + GuiScreen current = minecraft.currentScreen; + if (current == null) { + return minecraft.world == null ? ScreenKind.UNKNOWN : ScreenKind.IN_GAME; + } + if (current instanceof GuiChat) { + return ScreenKind.CHAT; + } + if (current instanceof GuiIngameMenu) { + return ScreenKind.PAUSED; + } + if (current instanceof GuiMainMenu) { + return ScreenKind.MAIN_MENU; + } + if (current instanceof GuiWorldSelection) { + return ScreenKind.WORLD_SELECTION; + } + if (current instanceof GuiOptions) { + return ScreenKind.OPTIONS; + } + if (current instanceof GuiContainer) { + return ScreenKind.INVENTORY; + } + if (current instanceof GuiScreenAdvancements || current instanceof GuiStats) { + return ScreenKind.ADVANCEMENTS; + } + // a screen this build has no name for, over a loaded world or not + return minecraft.world == null ? ScreenKind.UNKNOWN : ScreenKind.PAUSED; + } + + @Override + public int width() { + return minecraft.displayWidth / Math.max(1, scaleFactor()); + } + + @Override + public int height() { + return minecraft.displayHeight / Math.max(1, scaleFactor()); + } + + @Override + public float scale() { + return (float) scaleFactor(); + } + + @Override + public int windowWidth() { + return minecraft.displayWidth; + } + + @Override + public int windowHeight() { + return minecraft.displayHeight; + } + + private int scaleFactor() { + int gui = minecraft.gameSettings == null ? 2 : minecraft.gameSettings.guiScale; + if (gui <= 0) { + // auto: the client picks the largest scale that keeps the + // interface at least this wide + int width = minecraft.displayWidth; + gui = 1; + while (width / (gui + 1) >= 320 && gui < 4) { + gui++; + } + } + return gui; + } + } + + private final class RsxInput implements Input { + + @Override + public boolean keyDown(String keyId) { + if (keyId == null || minecraft.gameSettings == null) { + return false; + } + KeyBinding[] bindings = minecraft.gameSettings.keyBindings; + if (bindings == null) { + return false; + } + for (int i = 0; i < bindings.length; i++) { + if (keyId.equals(bindings[i].getKeyDescription()) && bindings[i].isKeyDown()) { + return true; + } + } + return false; + } + + @Override + public boolean anyKeyDown() { + KeyBinding[] bindings = minecraft.gameSettings == null + ? null : minecraft.gameSettings.keyBindings; + if (bindings == null) { + return false; + } + for (int i = 0; i < bindings.length; i++) { + if (bindings[i].isKeyDown()) { + return true; + } + } + return false; + } + + @Override + public boolean focused() { + return minecraft.inGameHasFocus; + } + + @Override + public int mouseX() { + return scaled(Mouse.getX()); + } + + @Override + public int mouseY() { + return scaled(Mouse.getY()); + } + + @Override + public int mouseButtons() { + int buttons = 0; + if (Mouse.isButtonDown(0)) { + buttons |= 1; + } + if (Mouse.isButtonDown(1)) { + buttons |= 2; + } + if (Mouse.isButtonDown(2)) { + buttons |= 4; + } + return buttons; + } + + @Override + public boolean mouseDown(int button) { + return button >= 0 && button <= 2 && Mouse.isButtonDown(button); + } + + private int scaled(int windowCoord) { + return windowCoord / Math.max(1, screen.scaleFactor()); + } + } +} \ No newline at end of file diff --git a/game-fork/src-overlay/net/rustnotmc/client/RsxCommands.java b/game-fork/src-overlay/net/rustnotmc/client/RsxCommands.java new file mode 100644 index 0000000..44940bf --- /dev/null +++ b/game-fork/src-overlay/net/rustnotmc/client/RsxCommands.java @@ -0,0 +1,132 @@ +package net.rustnotmc.client; + +import java.util.LinkedHashMap; +import java.util.Map; + +import net.rustnotmc.api.client.ClientCommand; +import net.rustnotmc.api.client.CommandArgs; +import net.rustnotmc.api.client.CommandScope; +import net.rustnotmc.api.client.Commands; +import net.rustnotmc.loader.Log; +import net.rustnotmc.loader.ModLoader; + +/** + * The mod command table. + * + * Only a CLIENT scoped command is carried out here. SERVER and BOTH are not + * consumed by the client at all: the line is left for the server to take, which + * is the whole point of the scope. The client never decides an effect the server + * owns. + * + * The table is shared by every mod, so a name can only be claimed once. A second + * mod asking for a taken name is refused loudly rather than silently shadowing + * the first, which would be miserable to debug. + * + * Lives in game-fork/src-overlay; src is regenerated on every build. + */ +final class RsxCommands implements Commands { + + private static final class Entry { + final String owner; + final ClientCommand handler; + final CommandScope scope; + + Entry(String owner, ClientCommand handler, CommandScope scope) { + this.owner = owner; + this.handler = handler; + this.scope = scope; + } + } + + private final Map entries = new LinkedHashMap(); + + @Override + public boolean register(String name, ClientCommand handler, CommandScope scope) { + if (name == null || handler == null || scope == null) { + return false; + } + String key = normalise(name); + if (key.length() == 0) { + return false; + } + String owner = registering(); + Entry clash = this.entries.get(key); + if (clash != null) { + if (clash.owner.equals(owner) && clash.handler == handler) { + return true; + } + Log.warn("command /" + key + " is already registered by " + clash.owner + + "; the second registration was refused"); + return false; + } + this.entries.put(key, new Entry(owner, handler, scope)); + Log.info("registered command /" + key + " scope=" + scope.name().toLowerCase()); + return true; + } + + @Override + public boolean register(String name, ClientCommand handler) { + return register(name, handler, CommandScope.CLIENT); + } + + @Override + public boolean isRegistered(String name) { + return name != null && this.entries.containsKey(normalise(name)); + } + + @Override + public CommandScope scopeOf(String name) { + Entry e = name == null ? null : this.entries.get(normalise(name)); + return e == null ? null : e.scope; + } + + @Override + public int count() { + return this.entries.size(); + } + + /** + * Offers a typed line to the mod commands. + * + * @return true when the client carried it out and the line must not be sent + * on. False means the line is not a client command, which is the + * answer for SERVER and BOTH: the server has to see those. + */ + boolean dispatch(String line, String sender) { + if (line == null || !line.startsWith("/")) { + return false; + } + String body = line.substring(1).trim(); + int space = body.indexOf(' '); + String name = space < 0 ? body : body.substring(0, space); + Entry entry = this.entries.get(normalise(name)); + if (entry == null || entry.scope != CommandScope.CLIENT) { + return false; + } + try { + entry.handler.run(CommandArgs.parse(body, sender)); + } catch (Throwable t) { + Log.error("command /" + name + " failed", t); + } + return true; + } + + private static String normalise(String name) { + String v = name.trim(); + if (v.startsWith("/")) { + v = v.substring(1); + } + return v.toLowerCase(); + } + + /** + * Who is registering right now. + * + * The loader knows this because it is the one calling onConstruct, and the + * game can read it without the loader ever depending on the game. + */ + private static String registering() { + String who = ModLoader.REGISTERING.get(); + return who == null ? "unknown" : who; + } +} \ No newline at end of file diff --git a/game-fork/src-overlay/net/rustnotmc/client/RsxDebug.java b/game-fork/src-overlay/net/rustnotmc/client/RsxDebug.java new file mode 100644 index 0000000..c4baa8b --- /dev/null +++ b/game-fork/src-overlay/net/rustnotmc/client/RsxDebug.java @@ -0,0 +1,72 @@ +package net.rustnotmc.client; + +import net.rustnotmc.loader.Log; + +/** + * Temporary diagnostics, added to work out why jumping and flight misbehave. + * + * Static analysis kept producing confident and wrong answers about this path, so + * the state is measured instead. Delete this file and its patch entry once the + * movement bugs are closed. + */ +public final class RsxDebug { + + private static int ticks; + + private RsxDebug() { + } + + /** One-off trace for the empty sound registry. */ + public static void sound(String message) { + Log.info("DBG snd " + message); + } + + /** Called from EntityPlayerSP.onLivingUpdate, once a second. */ + public static void movement(Object self) { + if (++ticks % 20 != 0) { + return; + } + Log.info("DBG sp onGround=" + read(self, "onGround") + + " inJump=" + read(self, "movementInput") + + " isJumping=" + read(self, "isJumping") + + " riding=" + read(self, "isRiding") + + " srvWorld=" + read(self, "isServerWorld") + + " allowFly=" + read(self, "allowFlying") + + " flying=" + read(self, "isFlying") + + " mY=" + read(self, "motionY") + + " y=" + read(self, "posY")); + } + + /** + * Reads a field by name. + * + * Deliberately reflective and deliberately forgiving: a diagnostic that + * throws when a field is renamed is worse than one that prints null. + */ + private static String read(Object target, String field) { + if (target == null) { + return "?"; + } + try { + java.lang.reflect.Field f = findField(target.getClass(), field); + if (f == null) { + return "?"; + } + f.setAccessible(true); + return String.valueOf(f.get(target)); + } catch (Throwable t) { + return "!"; + } + } + + private static java.lang.reflect.Field findField(Class type, String name) { + while (type != null) { + try { + return type.getDeclaredField(name); + } catch (NoSuchFieldException e) { + type = type.getSuperclass(); + } + } + return null; + } +} \ No newline at end of file diff --git a/game-fork/src-overlay/net/rustnotmc/client/RsxHudSurface.java b/game-fork/src-overlay/net/rustnotmc/client/RsxHudSurface.java new file mode 100644 index 0000000..b6639a2 --- /dev/null +++ b/game-fork/src-overlay/net/rustnotmc/client/RsxHudSurface.java @@ -0,0 +1,137 @@ +package net.rustnotmc.client; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.ScaledResolution; +import net.minecraft.client.renderer.BufferBuilder; +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.client.renderer.vertex.DefaultVertexFormats; +import net.rustnotmc.api.hud.HudSurface; + +/** + * The game's implementation of the mod HUD surface. + * + * A mod draws through this interface and never touches a game type, which is + * what lets the layout and the overlay code be tested with the game closed. The + * reverse cost is paid once, here: everything a mod needs from Minecraft is + * resolved in this class. + * + * Coordinates are the scaled interface ones, so an element lines up with the + * health bar and the chat without the mod knowing about GUI scale. + * + * Lives in game-fork/src-overlay; src is regenerated on every build. + */ +public final class RsxHudSurface implements HudSurface { + + private final Minecraft minecraft; + + /** + * Rebuilt only when the window or the scale factor actually changes. A new + * ScaledResolution per frame would be one short-lived object sixty times a + * second for a value that is almost never different. + */ + private ScaledResolution resolution; + private int cachedWidth = -1; + private int cachedHeight = -1; + private int cachedScale = -1; + + public RsxHudSurface(Minecraft minecraft) { + this.minecraft = minecraft; + } + + private ScaledResolution resolution() { + int width = this.minecraft.displayWidth; + int height = this.minecraft.displayHeight; + int scale = this.minecraft.gameSettings.guiScale; + if (this.resolution == null + || width != this.cachedWidth + || height != this.cachedHeight + || scale != this.cachedScale) { + this.resolution = new ScaledResolution(this.minecraft); + this.cachedWidth = width; + this.cachedHeight = height; + this.cachedScale = scale; + } + return this.resolution; + } + + @Override + public boolean available() { + return this.minecraft != null && this.minecraft.fontRenderer != null; + } + + @Override + public int width() { + return this.available() ? resolution().getScaledWidth() : 0; + } + + @Override + public int height() { + return this.available() ? resolution().getScaledHeight() : 0; + } + + @Override + public float scale() { + return (float) resolution().getScaleFactor(); + } + + @Override + public void rect(float x, float y, float w, float h, int rgb, float alpha) { + if (w <= 0.0F || h <= 0.0F) { + return; + } + int a = clampAlpha(alpha); + int r = (rgb >> 16) & 0xFF; + int g = (rgb >> 8) & 0xFF; + int b = rgb & 0xFF; + + // the vanilla debug overlay draws coloured quads the same way; this + // GlStateManager has no isTextureEnabled to ask, so the state is set + // explicitly the way the game itself does it + GlStateManager.disableTexture2D(); + + Tessellator tessellator = Tessellator.getInstance(); + BufferBuilder buffer = tessellator.getBuffer(); + buffer.begin(7, DefaultVertexFormats.POSITION_COLOR); + buffer.pos(x, y + h, 0.0D).color(r, g, b, a).endVertex(); + buffer.pos(x + w, y + h, 0.0D).color(r, g, b, a).endVertex(); + buffer.pos(x + w, y, 0.0D).color(r, g, b, a).endVertex(); + buffer.pos(x, y, 0.0D).color(r, g, b, a).endVertex(); + tessellator.draw(); + + GlStateManager.enableTexture2D(); + } + + @Override + public void text(float x, float y, String value, int rgb, float alpha) { + if (value == null || value.isEmpty() || !this.available()) { + return; + } + GlStateManager.enableTexture2D(); + this.minecraft.fontRenderer.drawStringWithShadow( + value, x, y, (rgb & 0x00FFFFFF) | (clampAlpha(alpha) << 24)); + } + + @Override + public float textWidth(String value) { + if (value == null || value.isEmpty() || !this.available()) { + return 0.0F; + } + return (float) this.minecraft.fontRenderer.getStringWidth(value); + } + + @Override + public int textHeight() { + return this.available() ? this.minecraft.fontRenderer.FONT_HEIGHT : 0; + } + + private static int clampAlpha(float alpha) { + if (alpha < 0.0F) { + return 0; + } + if (alpha > 1.0F) { + return 255; + } + return (int) (alpha * 255.0F + 0.5F); + } +} \ No newline at end of file diff --git a/rsx-auth/LICENSE b/rsx-auth/LICENSE new file mode 100644 index 0000000..cf9d1ac --- /dev/null +++ b/rsx-auth/LICENSE @@ -0,0 +1,9 @@ +RustNotMC - rsx-auth +Copyright (c) 2026 RustNotMC. All rights reserved. + +This repository is proprietary and is not published. No part of it may be +copied, modified, redistributed or used to build a derivative work without +written permission from the copyright holder. + +Third-party components vendored under third_party\ remain under their own +licenses; see the notices in those directories. diff --git a/rsx-client/LICENSE b/rsx-client/LICENSE new file mode 100644 index 0000000..ea7e6d4 --- /dev/null +++ b/rsx-client/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 RustNotMC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/rsx-client/scripts/build-mod.ps1 b/rsx-client/scripts/build-mod.ps1 new file mode 100644 index 0000000..2ac673e --- /dev/null +++ b/rsx-client/scripts/build-mod.ps1 @@ -0,0 +1,51 @@ +# Builds a mod jar from source. +# +# build-mod.ps1 [-Source ] [-Output ] +# +# Mods are ordinary jars with a descriptor at META-INF/rsxmod.properties. There +# is no annotation processor and no gradle: javac against the loader jar, then +# jar, then done. + +param( + [string]$Source = 'G:\RustNotMC\rsx-mods\rsxm-demo', + [string]$Output = '' +) + +$ErrorActionPreference = 'Stop' + +if (-not $Output) { $Output = Join-Path $Source 'dist' } +$Loader = 'G:\RustNotMC\rsx-client\dist\rsx-loader.jar' +if (-not (Test-Path $Loader)) { throw "loader jar missing: $Loader" } + +$Name = Split-Path -Leaf $Source +$build = Join-Path $Source 'build' +$classes = Join-Path $build 'classes' +$jarPath = Join-Path $Output "$Name.jar" + +Remove-Item $classes -Recurse -Force -ErrorAction SilentlyContinue +New-Item -ItemType Directory -Force -Path $classes | Out-Null + +$mainSrc = Join-Path $Source 'src\main\java' +$mainRes = Join-Path $Source 'resources' + +$mainFiles = @(Get-ChildItem $mainSrc -Recurse -Filter '*.java' | ForEach-Object { $_.FullName }) +Write-Host " sources: $($mainFiles.Count)" + +& javac -encoding UTF-8 -nowarn -proc:none --release 17 -cp $Loader -d $classes @mainFiles +if ($LASTEXITCODE -ne 0) { throw "javac failed" } + +if (Test-Path $mainRes) { Copy-Item -Recurse -Force (Join-Path $mainRes '*') $classes } + +New-Item -ItemType Directory -Force -Path $Output | Out-Null +Remove-Item $jarPath -Force -ErrorAction SilentlyContinue + +# a mod is a library, never an executable, so there is no main class here +Push-Location $classes +& jar --create --file $jarPath -C $classes . +$jarExit = $LASTEXITCODE +Pop-Location +if ($jarExit -ne 0) { throw "jar failed with exit code $jarExit" } + +Write-Host '' +Write-Host " $jarPath ($((Get-Item $jarPath).Length) bytes)" +exit 0 \ No newline at end of file diff --git a/rsx-client/scripts/build.ps1 b/rsx-client/scripts/build.ps1 new file mode 100644 index 0000000..464f0dd --- /dev/null +++ b/rsx-client/scripts/build.ps1 @@ -0,0 +1,66 @@ +# Builds the loader into dist\. +# +# Only a JDK is required. No Gradle, no network: the loader is compiled with +# javac exactly the way the client will run it, with --release 17, because the +# game ships its own Java 8 runtime and anything newer simply will not load. +# +# dist\rsx-loader.jar the loader and the mod API +# +# NOTE: this file is ASCII on purpose. Windows PowerShell 5.1 reads a .ps1 that +# has no byte order mark using the system ANSI code page, which mangles UTF-8 +# and makes the parser choke on non-ASCII text. Keep script messages English. + +$ErrorActionPreference = 'Stop' + +$Repo = Split-Path -Parent $PSScriptRoot +if (-not $Repo) { $Repo = $PSCommandPath | Split-Path -Parent | Split-Path -Parent } +$Dist = Join-Path $Repo 'dist' +$Out = Join-Path $Repo 'build' + +function Say($text) { Write-Host " $text" -ForegroundColor Cyan } + +if (-not (Get-Command javac -ErrorAction SilentlyContinue)) { + Write-Host ' A JDK (javac) is required.' -ForegroundColor Red + exit 1 +} + +New-Item -ItemType Directory -Force -Path $Dist, $Out | Out-Null + +# ---------------------------------------------------------------- loader +$mainOut = Join-Path $Out 'classes' +Remove-Item $mainOut -Recurse -Force -ErrorAction SilentlyContinue +New-Item -ItemType Directory -Force -Path $mainOut | Out-Null + +$mainSrc = Join-Path $Repo 'src\main\java' +$mainFiles = @(Get-ChildItem $mainSrc -Recurse -Filter '*.java' | ForEach-Object { $_.FullName }) +if ($mainFiles.Count -eq 0) { + Write-Host ' No sources found.' -ForegroundColor Red + exit 1 +} + +& javac -encoding UTF-8 --release 17 -Xlint:-options -nowarn -d $mainOut $mainFiles +if ($LASTEXITCODE -ne 0) { + Write-Host ' Loader failed to compile.' -ForegroundColor Red + exit 1 +} +Say "loader compiled ($($mainFiles.Count) files)" + +$manifest = Join-Path $Out 'manifest.txt' +@" +Manifest-Version: 1.0 +Implementation-Title: RustNotMC loader +Implementation-Version: 0.1.0 +"@ | Set-Content -LiteralPath $manifest -Encoding ASCII + +$jar = Join-Path $Dist 'rsx-loader.jar' +if (Test-Path -LiteralPath $jar) { Remove-Item -LiteralPath $jar -Force } +& jar cfm $jar $manifest -C $mainOut . +if ($LASTEXITCODE -ne 0) { + Write-Host ' Could not package the jar.' -ForegroundColor Red + exit 1 +} +Say "rsx-loader.jar ($((Get-Item $jar).Length) bytes)" + +Write-Host '' +Write-Host ' Done:' -ForegroundColor Green +Write-Host " $jar" \ No newline at end of file diff --git a/rsx-client/scripts/run-game.ps1 b/rsx-client/scripts/run-game.ps1 new file mode 100644 index 0000000..a9c006f --- /dev/null +++ b/rsx-client/scripts/run-game.ps1 @@ -0,0 +1,63 @@ +# Launches the compiled fork as an ordinary client: no agent, no injection. +# +# run-game.ps1 [-Run ] [-Java ] [-ExtraArgs ] +# +# Stage first if run\classpath.txt is missing: +# python tools\stage_run.py ..\..\game-fork\build\classes + +param( + [string]$Run = 'G:\RustNotMC\run', + [string]$Java = '', + [string[]]$ExtraArgs = @(), + [int]$Width = 1280, + [int]$Height = 720 +) + +$ErrorActionPreference = 'Stop' +$root = 'G:\RustNotMC' + +if (-not (Test-Path (Join-Path $Run 'classpath.txt'))) { + throw "no classpath.txt in $Run - run stage_run.py first" +} + +if (-not $Java) { + $Java = Get-ChildItem "$env:APPDATA\AstralRinthApp\meta\java_versions" ` + -Directory | Where-Object { $_.Name -like 'zulu17*' } | ForEach-Object { + Get-ChildItem $_.FullName -Recurse -Filter 'java.exe' -ErrorAction SilentlyContinue | + Select-Object -First 1 } | Select-Object -First 1 -ExpandProperty FullName +} +if (-not $Java) { throw 'no Java 17 found' } + +$assets = "$env:APPDATA\AstralRinthApp\meta\assets" +$cp = (Get-Content (Join-Path $Run 'classpath.txt') -Raw).Trim() +$natives = Join-Path $Run 'natives' + +# built as strings first: mixing '+' into an array literal folds the whole +# list into one element, because the comma binds tighter than the concatenation +$javaArgs = @() +$javaArgs += "-Djava.library.path=$natives" +$javaArgs += "-Dorg.lwjgl.librarypath=$natives" +$javaArgs += '-Dfile.encoding=UTF-8' +$javaArgs += '-cp' +$javaArgs += $cp +$javaArgs += 'net.minecraft.client.main.Main' +# singleplayer-only session: the client never validates this against Mojang +$javaArgs += '--username'; $javaArgs += 'Player' +$javaArgs += '--version'; $javaArgs += '1.12.2' +$javaArgs += '--gameDir'; $javaArgs += $Run +$javaArgs += '--assetsDir'; $javaArgs += $assets +$javaArgs += '--assetIndex'; $javaArgs += '1.12' +$javaArgs += '--uuid'; $javaArgs += '00000000000000000000000000000000' +$javaArgs += '--accessToken'; $javaArgs += '0' +$javaArgs += '--userType'; $javaArgs += 'legacy' +$javaArgs += '--versionType'; $javaArgs += 'release' +$javaArgs += '--width'; $javaArgs += "$Width" +$javaArgs += '--height'; $javaArgs += "$Height" +$javaArgs += $ExtraArgs + +Write-Host "java : $Java" +Write-Host "game : $Run" +Write-Host "assets : $assets" +Write-Host "args : $($javaArgs.Count)" +& $Java @javaArgs +exit $LASTEXITCODE \ No newline at end of file diff --git a/rsx-client/scripts/test.ps1 b/rsx-client/scripts/test.ps1 new file mode 100644 index 0000000..bc0c4e3 --- /dev/null +++ b/rsx-client/scripts/test.ps1 @@ -0,0 +1,43 @@ +# Compiles the loader plus the self test and runs it. +# +# ASCII only, for the same reason as build.ps1: PowerShell 5.1 misreads a .ps1 +# that has no byte order mark. + +$ErrorActionPreference = 'Stop' + +$Repo = Split-Path -Parent $PSScriptRoot +if (-not $Repo) { $Repo = $PSCommandPath | Split-Path -Parent | Split-Path -Parent } +$Out = Join-Path $Repo 'build' + +function Say($text) { Write-Host " $text" -ForegroundColor Cyan } + +New-Item -ItemType Directory -Force -Path $Out | Out-Null + +$mainOut = Join-Path $Out 'classes' +Remove-Item $mainOut -Recurse -Force -ErrorAction SilentlyContinue +New-Item -ItemType Directory -Force -Path $mainOut | Out-Null + +$mainFiles = @(Get-ChildItem (Join-Path $Repo 'src\main\java') -Recurse -Filter '*.java' | + ForEach-Object { $_.FullName }) +& javac -encoding UTF-8 --release 17 -Xlint:-options -nowarn -d $mainOut $mainFiles +if ($LASTEXITCODE -ne 0) { Write-Host ' Main failed.' -ForegroundColor Red; exit 1 } + +$testOut = Join-Path $Out 'test-classes' +Remove-Item $testOut -Recurse -Force -ErrorAction SilentlyContinue +New-Item -ItemType Directory -Force -Path $testOut | Out-Null + +$testFiles = @(Get-ChildItem (Join-Path $Repo 'src\test\java') -Recurse -Filter '*.java' | + ForEach-Object { $_.FullName }) +& javac -encoding UTF-8 --release 17 -Xlint:-options -nowarn -cp $mainOut -d $testOut $testFiles +if ($LASTEXITCODE -ne 0) { Write-Host ' Test failed to compile.' -ForegroundColor Red; exit 1 } +Say "compiled $($mainFiles.Count) main + $($testFiles.Count) test files" + +& java -cp "$mainOut;$testOut" net.rustnotmc.test.SelfTest (Join-Path $Out 'selftest') +if ($LASTEXITCODE -ne 0) { + Write-Host '' + Write-Host ' SELFTEST FAILED' -ForegroundColor Red + exit 1 +} + +Write-Host '' +Write-Host ' Self test passed.' -ForegroundColor Green \ No newline at end of file diff --git a/rsx-client/src/main/java/net/rustnotmc/api/Rsx.java b/rsx-client/src/main/java/net/rustnotmc/api/Rsx.java new file mode 100644 index 0000000..d6aad0a --- /dev/null +++ b/rsx-client/src/main/java/net/rustnotmc/api/Rsx.java @@ -0,0 +1,230 @@ +package net.rustnotmc.api; + +import net.rustnotmc.api.client.ClientCommand; +import net.rustnotmc.api.client.ClientHost; +import net.rustnotmc.api.client.ClientServices; +import net.rustnotmc.api.client.CommandScope; +import net.rustnotmc.api.hud.Anchor; +import net.rustnotmc.api.hud.HudLayout; + +import net.rustnotmc.loader.Access; +import net.rustnotmc.loader.EventBus; +import net.rustnotmc.loader.GameAccess; +import net.rustnotmc.loader.Log; +import net.rustnotmc.loader.ModContext; +import net.rustnotmc.loader.ModDescriptor; +import net.rustnotmc.loader.TickScheduler; +import net.rustnotmc.loader.Version; + +import java.util.Set; + +/** + * What a mod is handed in {@link RustNotMcMod#onConstruct}. + * + * Everything a mod can reach goes through here, so it is the one place a + * capability can be enforced. Nothing in this class exposes a raw game object + * unless the mod holds the capability for it. + */ +public final class Rsx { + + private final ModContext ctx; + + public Rsx(ModContext ctx) { + this.ctx = ctx; + } + + // -------------------------------------------------------------- identity + + public String modId() { + return ctx.modId(); + } + + public String modName() { + return ctx.name(); + } + + public Version modVersion() { + return ctx.version(); + } + + /** The API version the loader speaks. */ + public int apiVersion() { + return ModDescriptor.API_VERSION; + } + + /** True on the client. Always true while the loader only ships on the client. */ + public boolean isClient() { + return ctx.isClient(); + } + + public boolean isServer() { + return ctx.isServer(); + } + + // ------------------------------------------------------------ permissions + + public boolean has(Access capability) { + return ctx.has(capability); + } + + public Set capabilities() { + return ctx.access(); + } + + // ----------------------------------------------------------------- access + + /** + * The game, subject to this mod's capabilities. + * + * Handles are cached, so repeated reads of the same member are cheap. Guard + * a capability yourself if the read is expensive to fail. + */ + public GameAccess game() { + return ctx.game(); + } + + // --------------------------------------------------------------- threading + + /** Run on the game thread, next tick. */ + public TickScheduler.Task post(Runnable body) { + return ctx.scheduler().post(ctx.modId(), body); + } + + /** Run every N game ticks. */ + public TickScheduler.Task everyTicks(int n, Runnable body) { + return ctx.scheduler().everyTicks(ctx.modId(), n, body); + } + + /** Run on a wall-clock period; the body still executes on the game thread. */ + public TickScheduler.Task every(long millis, Runnable body) { + return ctx.scheduler().every(ctx.modId(), millis, body); + } + + /** Current game tick. */ + public long tick() { + return ctx.scheduler().tickCount(); + } + + // ----------------------------------------------------------------- events + + public EventBus events() { + return ctx.events(); + } + + public EventBus.Subscription on(Class type, java.util.function.Consumer body) { + return ctx.events().subscribe(ctx.modId(), type, body); + } + + public EventBus.Subscription on(Class type, int priority, + java.util.function.Consumer body) { + return ctx.events().subscribe(ctx.modId(), type, priority, body); + } + + public T post(T event) { + return ctx.events().post(event); + } + + // ------------------------------------------------------------------- hud + + /** + * Registers an overlay on the game interface. + * + * Needs the HUD capability. Returns null when the mod did not declare it, + * rather than throwing, so a missing declaration cannot take a mod down. + */ + public HudLayout.Slot hud(String name, HudLayout.Draw draw, Anchor anchor, + int layer, int offsetX, int offsetY) { + if (!ctx.has(Access.HUD)) { + ctx.log().warn("asked to draw an overlay without the 'hud' capability"); + return null; + } + return HudLayout.add(name, draw, anchor, layer, offsetX, offsetY); + } + + /** Registers an overlay at the normal layer, flush with its corner. */ + public HudLayout.Slot hud(String name, HudLayout.Draw draw, Anchor anchor) { + return hud(name, draw, anchor, HudLayout.NORMAL, 0, 0); + } + + /** Drops every overlay this mod registered. */ + public void clearHud() { + HudLayout.remove(ctx.modId()); + } + + // ---------------------------------------------------------------- client + + /** + * The client this mod is running in: screens, input and commands. + * + * This is the whole client surface, and it stops at what the client shows. + * There is no route from here to a block, an item, a player or a packet, + * because the server owns those. + * + * Outside a client, for instance in the loader self test, this returns a + * detached stand-in that answers with zeros rather than throwing. + */ + public ClientServices client() { + return ClientHost.current(); + } + + /** True when a real client is attached, false in a detached host. */ + public boolean hasClient() { + return ClientHost.present(); + } + + /** + * Keyboard and mouse state, needs the INPUT capability. + * + * The same view is also reachable through {@link #client()}; this is the + * checked door, and it returns a stand-in that reports nothing rather than + * throwing, so a missing declaration cannot take a mod down. + */ + public net.rustnotmc.api.client.Input input() { + if (!ctx.has(Access.INPUT)) { + return net.rustnotmc.api.client.ClientHost.deniedInput(); + } + return client().input(); + } + + /** + * Registers a mod command the player can type. + * + * Needs the COMMANDS capability. The scope decides who carries it out: a + * CLIENT command runs here and affects only the client, while SERVER and + * BOTH are the server's to decide and are not executed locally. + */ + public boolean command(String name, ClientCommand handler, CommandScope scope) { + if (!ctx.has(Access.COMMANDS)) { + ctx.log().warn("asked to register a command without the 'commands' capability"); + return false; + } + return client().commands().register(name, handler, scope); + } + + /** Registers a command that only ever affects the client. */ + public boolean command(String name, ClientCommand handler) { + return command(name, handler, CommandScope.CLIENT); + } + + /** + * This mod's own directory under the mods folder, created on first use. + * + * Needs the FILES capability. It is the only place a mod may write: the + * game directory holds save data, which belongs to the server, and every + * other mod's directory is off limits. + */ + public java.io.File modDirectory() { + if (!ctx.has(Access.FILES)) { + ctx.log().warn("asked for its directory without the 'files' capability"); + return null; + } + return ctx.modDirectory(); + } + + // ------------------------------------------------------------------- logs + + /** Log with this mod's id already attached. */ + public Log.ModLog log() { + return ctx.log(); + } +} \ No newline at end of file diff --git a/rsx-client/src/main/java/net/rustnotmc/api/RustNotMcMod.java b/rsx-client/src/main/java/net/rustnotmc/api/RustNotMcMod.java new file mode 100644 index 0000000..b9d330c --- /dev/null +++ b/rsx-client/src/main/java/net/rustnotmc/api/RustNotMcMod.java @@ -0,0 +1,49 @@ +package net.rustnotmc.api; + +/** + * What a mod implements. + * + * Every method has a default, so a mod overrides only what it needs. The order is + * always: construct, load, enable, disable, unload. A mod may rely on its own + * dependencies being fully enabled by the time onEnable runs. + */ +public interface RustNotMcMod { + + /** The loader API version this mod was written against. */ + default int apiVersion() { + return 1; + } + + /** + * Called once, right after construction, with everything the mod is allowed + * to do. Registering here rather than in onEnable is what makes a later + * disable/enable cycle safe. + */ + default void onConstruct(Rsx context) { + } + + /** Called once before anything is enabled. */ + default void onLoad() { + } + + /** Called when the mod starts doing work. */ + default void onEnable() { + } + + /** Called before the mod stops. Release resources here. */ + default void onDisable() { + } + + /** Called last, when the mod is going away for good. */ + default void onUnload() { + } + + /** + * Called when one of the mod's own tasks or handlers throws. + * + * The loader has already logged the throwable, and has already dropped the + * task after five consecutive failures. This is the mod's chance to react. + */ + default void onError(Throwable error) { + } +} \ No newline at end of file diff --git a/rsx-client/src/main/java/net/rustnotmc/api/client/ClientCommand.java b/rsx-client/src/main/java/net/rustnotmc/api/client/ClientCommand.java new file mode 100644 index 0000000..6916dea --- /dev/null +++ b/rsx-client/src/main/java/net/rustnotmc/api/client/ClientCommand.java @@ -0,0 +1,13 @@ +package net.rustnotmc.api.client; + +/** + * A command a mod adds. + * + * The handler runs in the client only when its scope is CLIENT. For SERVER and + * BOTH it is not called at all: the line is passed on and the server decides. + */ +public interface ClientCommand { + + /** Runs the command locally. Not called for server-decided scopes. */ + void run(CommandArgs args); +} \ No newline at end of file diff --git a/rsx-client/src/main/java/net/rustnotmc/api/client/ClientHost.java b/rsx-client/src/main/java/net/rustnotmc/api/client/ClientHost.java new file mode 100644 index 0000000..f6fc913 --- /dev/null +++ b/rsx-client/src/main/java/net/rustnotmc/api/client/ClientHost.java @@ -0,0 +1,195 @@ +package net.rustnotmc.api.client; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Where the game hands over its client implementation. + * + * The API interfaces are written without a single game type so the loader keeps + * building and testing with no game present, which is what lets the self test + * run headless. The game supplies the implementation once, at startup, and mods + * read it through {@link net.rustnotmc.api.Rsx#client()}. + */ +public final class ClientHost { + + private static final Map SCOPES = + new LinkedHashMap(); + + private static volatile ClientServices services; + + private ClientHost() { + } + + /** Called by the game once, when the client is ready. */ + public static void install(ClientServices implementation) { + services = implementation; + } + + /** Called by the game when the client is shutting down. */ + public static void remove() { + services = null; + synchronized (SCOPES) { + SCOPES.clear(); + } + } + + /** True while a client is attached. */ + public static boolean present() { + return services != null; + } + + /** The client, or a detached stand-in that says so. */ + public static ClientServices current() { + ClientServices s = services; + return s == null ? Detached.INSTANCE : s; + } + + static void register(String owner, String name, CommandScope scope) { + synchronized (SCOPES) { + SCOPES.put(name, scope); + } + } + + static boolean isRegistered(String name) { + synchronized (SCOPES) { + return SCOPES.containsKey(name); + } + } + + static CommandScope scopeOf(String name) { + synchronized (SCOPES) { + return SCOPES.get(name); + } + } + + static int commandCount() { + synchronized (SCOPES) { + return SCOPES.size(); + } + } + + /** Input as a mod without the INPUT capability is allowed to see. */ + public static net.rustnotmc.api.client.Input deniedInput() { + return Detached.INPUT; + } + + /** + * What a mod sees when the loader is running outside a client, which is the + * self test and any server-side host. + * + * It answers honestly instead of throwing, so a mod written against the + * client API can still be loaded and exercised with no game running. + */ + private static final class Detached implements ClientServices { + + static final Detached INSTANCE = new Detached(); + + private static final Screen SCREEN = new Screen() { + public ScreenKind kind() { + return ScreenKind.UNKNOWN; + } + + public int width() { + return 0; + } + + public int height() { + return 0; + } + + public float scale() { + return 1f; + } + + public int windowWidth() { + return 0; + } + + public int windowHeight() { + return 0; + } + }; + + private static final Input INPUT = new Input() { + public boolean keyDown(String keyId) { + return false; + } + + public boolean anyKeyDown() { + return false; + } + + public boolean focused() { + return false; + } + + public int mouseX() { + return 0; + } + + public int mouseY() { + return 0; + } + + public int mouseButtons() { + return 0; + } + + public boolean mouseDown(int button) { + return false; + } + }; + + private static final Commands COMMANDS = new Commands() { + public boolean register(String name, ClientCommand handler, CommandScope scope) { + return false; + } + + public boolean register(String name, ClientCommand handler) { + return false; + } + + public boolean isRegistered(String name) { + return false; + } + + public CommandScope scopeOf(String name) { + return null; + } + + public int count() { + return 0; + } + }; + + public Screen screen() { + return SCREEN; + } + + public Input input() { + return INPUT; + } + + public Commands commands() { + return COMMANDS; + } + + public long tick() { + return 0L; + } + + public long frame() { + return 0L; + } + } + + /** Names currently registered, for logging and diagnostics. */ + public static Map registered() { + synchronized (SCOPES) { + return Collections.unmodifiableMap( + new LinkedHashMap(SCOPES)); + } + } +} \ No newline at end of file diff --git a/rsx-client/src/main/java/net/rustnotmc/api/client/ClientServices.java b/rsx-client/src/main/java/net/rustnotmc/api/client/ClientServices.java new file mode 100644 index 0000000..9a4c8d5 --- /dev/null +++ b/rsx-client/src/main/java/net/rustnotmc/api/client/ClientServices.java @@ -0,0 +1,32 @@ +package net.rustnotmc.api.client; + +/** + * The client the mod is running in. + * + * This is the whole surface a client mod gets, and it is deliberately small: it + * describes what the client shows, what the keyboard and mouse are doing, and + * where to add commands. There is no path from here to a block, an item, a + * player or a packet, because the server owns those and a client mod is not + * allowed to decide them. + * + * A mod that needs something genuinely outside this boundary is expected to say + * so, and reach for the reflective escape hatch deliberately, with the operator's + * consent, rather than finding a back door. + */ +public interface ClientServices { + + /** The display state, never null. */ + Screen screen(); + + /** Keyboard and mouse state, never null. */ + Input input(); + + /** The mod command table, never null. */ + Commands commands(); + + /** Ticks since the client started, driven by the game loop. */ + long tick(); + + /** Frames drawn, which at a steady frame rate is a better clock than tick. */ + long frame(); +} \ No newline at end of file diff --git a/rsx-client/src/main/java/net/rustnotmc/api/client/CommandArgs.java b/rsx-client/src/main/java/net/rustnotmc/api/client/CommandArgs.java new file mode 100644 index 0000000..3fc4d75 --- /dev/null +++ b/rsx-client/src/main/java/net/rustnotmc/api/client/CommandArgs.java @@ -0,0 +1,100 @@ +package net.rustnotmc.api.client; + +import java.util.Arrays; + +/** + * What a player typed when they ran a mod command. + * + * The whole line is kept as well as the split arguments, because a mod usually + * wants one or the other and re-parsing the original is where quoting bugs come + * from. + */ +public final class CommandArgs { + + private final String name; + private final String raw; + private final String[] args; + private final String sender; + + public CommandArgs(String name, String raw, String[] args, String sender) { + this.name = name; + this.raw = raw; + this.args = args == null ? new String[0] : args.clone(); + this.sender = sender == null ? "local" : sender; + } + + /** Splits "/rsx hello world" into the name "rsx" and two arguments. */ + public static CommandArgs parse(String line, String sender) { + String text = line == null ? "" : line.trim(); + if (text.startsWith("/")) { + text = text.substring(1); + } + String[] parts = text.split("\\s+"); + String name = parts.length == 0 ? "" : parts[0]; + String[] rest = parts.length <= 1 ? new String[0] : Arrays.copyOfRange(parts, 1, parts.length); + return new CommandArgs(name, text, rest, sender); + } + + /** The command name, without the leading slash. */ + public String name() { + return this.name; + } + + /** The line as typed, without the leading slash. */ + public String raw() { + return this.raw; + } + + /** A copy of the arguments after the name. */ + public String[] args() { + return this.args.clone(); + } + + public int count() { + return this.args.length; + } + + /** Argument at the index, or the fallback when it is absent. */ + public String arg(int index, String fallback) { + return index >= 0 && index < this.args.length ? this.args[index] : fallback; + } + + /** Argument at the index parsed as an integer, or the fallback. */ + public int argInt(int index, int fallback) { + String v = arg(index, null); + if (v == null) { + return fallback; + } + try { + return Integer.parseInt(v.trim()); + } catch (NumberFormatException e) { + return fallback; + } + } + + /** + * Who ran it, as the client knows it. + * + * This is the local label the client shows, nothing more: it is not a player + * identity and carries no authority. + */ + public String sender() { + return this.sender; + } + + @Override + public String toString() { + return "/" + this.name + (this.args.length == 0 ? "" : " " + join()); + } + + private String join() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < this.args.length; i++) { + if (i > 0) { + sb.append(' '); + } + sb.append(this.args[i]); + } + return sb.toString(); + } +} \ No newline at end of file diff --git a/rsx-client/src/main/java/net/rustnotmc/api/client/CommandScope.java b/rsx-client/src/main/java/net/rustnotmc/api/client/CommandScope.java new file mode 100644 index 0000000..fe2304f --- /dev/null +++ b/rsx-client/src/main/java/net/rustnotmc/api/client/CommandScope.java @@ -0,0 +1,58 @@ +package net.rustnotmc.api.client; + +/** + * Where a mod command actually runs. + * + * The scope is not decoration. The server owns the world, so a command that can + * change the world has to be the server's to decide, and the client half of one + * is only a request. + */ +public enum CommandScope { + + /** + * Runs in the client and only ever affects the client. + * + * Opening a settings screen, toggling an overlay, printing a message. The + * effect is local and nothing is sent anywhere. + */ + CLIENT, + + /** + * Runs on the server. + * + * A client mod may declare that the server has such a command, but the + * client will not execute it. Only the server, and only for a player the + * server considers allowed, decides what it does. + */ + SERVER, + + /** + * Registered on both sides. + * + * The client half accepts the line and sends it on; the server decides + * whether it runs and what it does. Until the server implementation exists + * the client half says so rather than pretending to work. + */ + BOTH; + + /** True when the client itself may carry out the command locally. */ + public boolean clientExecutes() { + return this == CLIENT; + } + + /** True when the server has the final say. */ + public boolean serverDecides() { + return this == SERVER || this == BOTH; + } + + public static CommandScope byName(String raw, CommandScope fallback) { + if (raw != null) { + for (CommandScope s : values()) { + if (s.name().equalsIgnoreCase(raw.trim())) { + return s; + } + } + } + return fallback; + } +} \ No newline at end of file diff --git a/rsx-client/src/main/java/net/rustnotmc/api/client/Commands.java b/rsx-client/src/main/java/net/rustnotmc/api/client/Commands.java new file mode 100644 index 0000000..1092d17 --- /dev/null +++ b/rsx-client/src/main/java/net/rustnotmc/api/client/Commands.java @@ -0,0 +1,32 @@ +package net.rustnotmc.api.client; + +/** + * The mod command table. + * + * Registration happens in onConstruct, so a disable and enable cycle does not + * lose or duplicate commands. Names are unique across mods: a second mod asking + * for a name already taken is refused and told who has it, rather than silently + * shadowing the first. + */ +public interface Commands { + + /** + * Registers a command the player can type with a leading slash. + * + * @return false when the name is taken, already registered by this mod, or + * otherwise unusable. + */ + boolean register(String name, ClientCommand handler, CommandScope scope); + + /** Registers a command that only ever affects the client. */ + boolean register(String name, ClientCommand handler); + + /** True when some mod registered the name. */ + boolean isRegistered(String name); + + /** The scope a name was registered with, or null when it is not registered. */ + CommandScope scopeOf(String name); + + /** How many commands are registered. */ + int count(); +} \ No newline at end of file diff --git a/rsx-client/src/main/java/net/rustnotmc/api/client/Input.java b/rsx-client/src/main/java/net/rustnotmc/api/client/Input.java new file mode 100644 index 0000000..11bcc27 --- /dev/null +++ b/rsx-client/src/main/java/net/rustnotmc/api/client/Input.java @@ -0,0 +1,38 @@ +package net.rustnotmc.api.client; + +/** + * Keyboard and mouse state the client already tracks. + * + * Read only. A client mod can watch which key is held so its overlay can react, + * and nothing more: it cannot inject input into the game, and this says nothing + * about the world. + */ +public interface Input { + + /** + * True while a key bound to the given id is held. + * + * The id is the game's own, for example "key.sneak", "key.jump", + * "key.forward". An unknown id is simply not held, so a mod written against + * a later version does not break here. + */ + boolean keyDown(String keyId); + + /** True while any bound key is held. */ + boolean anyKeyDown(); + + /** True while the window has keyboard focus. */ + boolean focused(); + + /** Cursor X in interface pixels. */ + int mouseX(); + + /** Cursor Y in interface pixels. */ + int mouseY(); + + /** Bitmask of the mouse buttons currently down, LWJGL numbering. */ + int mouseButtons(); + + /** True while the given LWJGL mouse button is down. */ + boolean mouseDown(int button); +} \ No newline at end of file diff --git a/rsx-client/src/main/java/net/rustnotmc/api/client/Screen.java b/rsx-client/src/main/java/net/rustnotmc/api/client/Screen.java new file mode 100644 index 0000000..10c0c3e --- /dev/null +++ b/rsx-client/src/main/java/net/rustnotmc/api/client/Screen.java @@ -0,0 +1,32 @@ +package net.rustnotmc.api.client; + +/** + * The client's own display state. + * + * Sizes are the scaled interface ones, the same coordinate space the health bar + * and the chat live in, so a mod's overlay lines up with the interface at any + * resolution or GUI scale without doing the arithmetic itself. + * + * There is no screen title here. The client does not keep one, and inventing a + * label would hand a mod something that looks like the game's text and is not. + */ +public interface Screen { + + /** What is showing. */ + ScreenKind kind(); + + /** Interface width in pixels. */ + int width(); + + /** Interface height in pixels. */ + int height(); + + /** Interface scale, where 2 means half size. */ + float scale(); + + /** Window width in pixels, before scaling. */ + int windowWidth(); + + /** Window height in pixels, before scaling. */ + int windowHeight(); +} \ No newline at end of file diff --git a/rsx-client/src/main/java/net/rustnotmc/api/client/ScreenKind.java b/rsx-client/src/main/java/net/rustnotmc/api/client/ScreenKind.java new file mode 100644 index 0000000..a94c0f8 --- /dev/null +++ b/rsx-client/src/main/java/net/rustnotmc/api/client/ScreenKind.java @@ -0,0 +1,61 @@ +package net.rustnotmc.api.client; + +/** + * What the client is showing right now. + * + * A mod needs this to decide whether an overlay makes sense: a map readout is + * noise on the main menu, a "press play" hint is noise mid-combat. The values + * describe the client's own presentation state and reveal nothing about the + * world behind it. + */ +public enum ScreenKind { + + /** The title screen. */ + MAIN_MENU, + + /** Choosing which save to open. */ + WORLD_SELECTION, + + /** Options, controls, video settings and friends. */ + OPTIONS, + + /** The chat box has focus. */ + CHAT, + + /** In a world, with a screen such as the pause menu open. */ + PAUSED, + + /** A container screen, typically the inventory. */ + INVENTORY, + + /** The advancements or statistics screen. */ + ADVANCEMENTS, + + /** In a world with nothing open, which is when gameplay draws. */ + IN_GAME, + + /** Something the client has no specific name for. */ + UNKNOWN; + + /** True while a world is loaded, whether or not a screen covers it. */ + public boolean inWorld() { + return this == IN_GAME || this == PAUSED || this == CHAT + || this == INVENTORY || this == ADVANCEMENTS; + } + + /** True when the client is drawing the world rather than a full screen. */ + public boolean gameplayVisible() { + return this == IN_GAME; + } + + public static ScreenKind byName(String raw, ScreenKind fallback) { + if (raw != null) { + for (ScreenKind k : values()) { + if (k.name().equalsIgnoreCase(raw.trim())) { + return k; + } + } + } + return fallback; + } +} \ No newline at end of file diff --git a/rsx-client/src/main/java/net/rustnotmc/api/hud/Anchor.java b/rsx-client/src/main/java/net/rustnotmc/api/hud/Anchor.java new file mode 100644 index 0000000..5ecb6bc --- /dev/null +++ b/rsx-client/src/main/java/net/rustnotmc/api/hud/Anchor.java @@ -0,0 +1,95 @@ +package net.rustnotmc.api.hud; + +/** + * Where on the screen an overlay hangs. + * + * The point is not convenience but avoidance: the interface already owns the + * corners, so a mod says which corner it wants instead of picking a pixel and + * landing on top of the chat. + */ +public enum Anchor { + + TOP_LEFT, + TOP_CENTER, + TOP_RIGHT, + CENTER, + BOTTOM_LEFT, + BOTTOM_CENTER, + BOTTOM_RIGHT; + + /** The left edge of a box of this width on a screen that wide. */ + public int x(int screenWidth, int width) { + switch (this) { + case TOP_CENTER: + case CENTER: + case BOTTOM_CENTER: + return (screenWidth - width) / 2; + case TOP_RIGHT: + case BOTTOM_RIGHT: + return screenWidth - width; + default: + return 0; + } + } + + /** The top edge of a box of this height on a screen that tall. */ + public int y(int screenHeight, int height) { + switch (this) { + case CENTER: + return (screenHeight - height) / 2; + case BOTTOM_LEFT: + case BOTTOM_CENTER: + case BOTTOM_RIGHT: + return screenHeight - height; + default: + return 0; + } + } + + public boolean isBottom() { + return this == BOTTOM_LEFT || this == BOTTOM_CENTER || this == BOTTOM_RIGHT; + } + + public boolean isCentre() { + return this == CENTER; + } + + /** + * The anchor with this name, or the fallback. + * + * Config files are user input, and a typo in one must not take the HUD down. + */ + public static Anchor byName(String name, Anchor fallback) { + if (name != null) { + String v = name.trim(); + for (Anchor a : values()) { + if (a.name().equalsIgnoreCase(v)) { + return a; + } + } + if (v.equalsIgnoreCase("left-top")) { + return TOP_LEFT; + } + if (v.equalsIgnoreCase("right-top")) { + return TOP_RIGHT; + } + if (v.equalsIgnoreCase("center-top")) { + return TOP_CENTER; + } + if (v.equalsIgnoreCase("center-bottom")) { + return BOTTOM_CENTER; + } + if (v.equalsIgnoreCase("left-bottom")) { + return BOTTOM_LEFT; + } + if (v.equalsIgnoreCase("right-bottom")) { + return BOTTOM_RIGHT; + } + } + return fallback; + } + + public String id() { + return name().toLowerCase(); + } +} \ No newline at end of file diff --git a/rsx-client/src/main/java/net/rustnotmc/api/hud/HudLayout.java b/rsx-client/src/main/java/net/rustnotmc/api/hud/HudLayout.java new file mode 100644 index 0000000..95fa6a3 --- /dev/null +++ b/rsx-client/src/main/java/net/rustnotmc/api/hud/HudLayout.java @@ -0,0 +1,243 @@ +package net.rustnotmc.api.hud; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import net.rustnotmc.loader.Log; + +/** + * Where every overlay sits, and what draws over what. + * + * An anchor is a corner, an edge or the middle of the screen, so an element + * stays in its corner at any resolution or interface scale instead of sitting at + * a pixel a mod picked once. A layer says what covers what, on the scale graphics + * APIs use, with the game itself at zero. + * + * The sorted order is cached and rebuilt only when something actually changes. + * The old layout re-copied and re-sorted its slot list on every single frame, + * which at 60 frames a second is pure waste for a list that changes a few times + * a minute. + */ +public final class HudLayout { + + /** The game draws itself around this layer. */ + public static final int GAME = 0; + + /** Below the game, for anything that must not fight the interface. */ + public static final int BACKGROUND = -1000; + + /** The default for an element that does not care. */ + public static final int NORMAL = 100; + + /** What draws an overlay: measures itself, then draws in the box it is given. */ + public interface Draw { + int width(HudSurface hud); + + int height(HudSurface hud); + + void draw(HudSurface hud, int x, int y, int width, int height); + } + + /** One registered overlay. */ + public static final class Slot { + + private final String owner; + private final Draw draw; + private final int order; + private Anchor anchor; + private int layer; + private int offsetX; + private int offsetY; + + Slot(String owner, Draw draw, Anchor anchor, int layer, int offsetX, int offsetY, + int order) { + this.owner = owner; + this.draw = draw; + this.anchor = anchor; + this.layer = layer; + this.offsetX = offsetX; + this.offsetY = offsetY; + this.order = order; + } + + public void anchor(Anchor value) { + this.anchor = value == null ? Anchor.TOP_LEFT : value; + HudLayout.markDirty(); + } + + public void layer(int value) { + this.layer = value; + HudLayout.markDirty(); + } + + public void offset(int x, int y) { + this.offsetX = x; + this.offsetY = y; + } + + public Anchor anchor() { + return anchor; + } + + public int layer() { + return layer; + } + + public int offsetX() { + return offsetX; + } + + public int offsetY() { + return offsetY; + } + + public String owner() { + return owner; + } + + /** Layer and then insertion order, so a panel stays above its own label. */ + int compareTo(Slot other) { + if (layer != other.layer) { + return layer < other.layer ? -1 : 1; + } + return order < other.order ? -1 : (order > other.order ? 1 : 0); + } + } + + private static final Object LOCK = new Object(); + private static final List SLOTS = new ArrayList(); + private static List sorted = Collections.emptyList(); + private static boolean dirty = true; + private static boolean warnedAboutSize; + private static int orderCounter; + + private HudLayout() { + } + + /** Registers an overlay and returns the slot that controls it. */ + public static Slot add(String owner, Draw draw, Anchor anchor, int layer, + int offsetX, int offsetY) { + if (draw == null) { + return null; + } + Slot slot; + synchronized (LOCK) { + slot = new Slot(owner, draw, anchor == null ? Anchor.TOP_LEFT : anchor, + layer, offsetX, offsetY, orderCounter++); + SLOTS.add(slot); + dirty = true; + } + Log.info("HUD: \"" + owner + "\" at " + slot.anchor.id() + " layer " + layer); + return slot; + } + + /** Registers an overlay at the normal layer, flush with its corner. */ + public static Slot add(String owner, Draw draw, Anchor anchor) { + return add(owner, draw, anchor, NORMAL, 0, 0); + } + + /** Drops every overlay of one mod. */ + public static void remove(String owner) { + synchronized (LOCK) { + for (int i = 0; i < SLOTS.size(); i++) { + if (SLOTS.get(i).owner.equals(owner)) { + SLOTS.remove(i); + i--; + } + } + dirty = true; + } + } + + public static int size() { + synchronized (LOCK) { + return SLOTS.size(); + } + } + + public static void clear() { + synchronized (LOCK) { + SLOTS.clear(); + sorted = Collections.emptyList(); + dirty = true; + } + } + + private static void markDirty() { + dirty = true; + } + + private static List ordered() { + synchronized (LOCK) { + if (!dirty && sorted.size() == SLOTS.size()) { + return sorted; + } + List copy = new ArrayList(SLOTS); + Collections.sort(copy, new Comparator() { + public int compare(Slot a, Slot b) { + return a.compareTo(b); + } + }); + sorted = copy; + dirty = false; + return sorted; + } + } + + /** + * Places and draws everything, lowest layer first. + * + * A failing overlay is reported and skipped: one bad element must not blank + * the whole interface. + */ + public static void render(HudSurface hud) { + if (hud == null || !hud.available()) { + return; + } + List slots = ordered(); + if (slots.isEmpty()) { + return; + } + int screenW = hud.width(); + int screenH = hud.height(); + if (screenW <= 0 || screenH <= 0) { + if (!warnedAboutSize) { + warnedAboutSize = true; + Log.warn("HUD: screen size unknown, overlays hang off the top" + + " edge until it can be measured"); + } + screenW = Math.max(1, screenW); + screenH = 0; + } + + for (int i = 0; i < slots.size(); i++) { + Slot slot = slots.get(i); + try { + int w = Math.max(0, slot.draw.width(hud)); + int h = Math.max(0, slot.draw.height(hud)); + Anchor anchor = slot.anchor; + if (screenH == 0 && anchor != Anchor.TOP_LEFT && anchor != Anchor.TOP_RIGHT) { + anchor = anchor.isBottom() ? anchorForTop(anchor) : Anchor.TOP_CENTER; + } + int x = Math.max(0, anchor.x(screenW, w) + slot.offsetX); + int y = Math.max(0, anchor.y(screenH, h) + slot.offsetY); + slot.draw.draw(hud, x, y, w, h); + } catch (Throwable t) { + Log.error("HUD element \"" + slot.owner + "\" failed", t); + } + } + } + + private static Anchor anchorForTop(Anchor bottom) { + switch (bottom) { + case BOTTOM_LEFT: + return Anchor.TOP_LEFT; + case BOTTOM_RIGHT: + return Anchor.TOP_RIGHT; + default: + return Anchor.TOP_CENTER; + } + } +} \ No newline at end of file diff --git a/rsx-client/src/main/java/net/rustnotmc/api/hud/HudSurface.java b/rsx-client/src/main/java/net/rustnotmc/api/hud/HudSurface.java new file mode 100644 index 0000000..07338a3 --- /dev/null +++ b/rsx-client/src/main/java/net/rustnotmc/api/hud/HudSurface.java @@ -0,0 +1,69 @@ +package net.rustnotmc.api.hud; + +/** + * What a mod draws through. + * + * The client supplies the implementation, backed by the game's own font and + * interface scale, so a panel sits in the same layer as the health bars and the + * compass and follows the game's show/hide state. Mods never see the game types + * behind it, which is what lets a mod be tested without the game running. + */ +public interface HudSurface { + + /** False while the game is not drawing its interface. */ + boolean available(); + + /** Screen width in pixels, 0 when unknown. */ + int width(); + + /** Screen height in pixels, 0 when unknown. */ + int height(); + + /** The interface scale the game draws with. */ + float scale(); + + /** A filled rectangle; alpha is 0..1. */ + void rect(float x, float y, float w, float h, int rgb, float alpha); + + /** Text in the game's own font. */ + void text(float x, float y, String value, int rgb, float alpha); + + /** Width of a string, so an element can size itself. */ + float textWidth(String value); + + /** Height of one line. */ + int textHeight(); + + /** Surface that draws nothing, handed out when the game is not ready. */ + HudSurface NULL = new HudSurface() { + public boolean available() { + return false; + } + + public int width() { + return 0; + } + + public int height() { + return 0; + } + + public float scale() { + return 1f; + } + + public void rect(float x, float y, float w, float h, int rgb, float alpha) { + } + + public void text(float x, float y, String value, int rgb, float alpha) { + } + + public float textWidth(String value) { + return 0f; + } + + public int textHeight() { + return 0; + } + }; +} \ No newline at end of file diff --git a/rsx-client/src/main/java/net/rustnotmc/loader/Access.java b/rsx-client/src/main/java/net/rustnotmc/loader/Access.java new file mode 100644 index 0000000..91b3067 --- /dev/null +++ b/rsx-client/src/main/java/net/rustnotmc/loader/Access.java @@ -0,0 +1,169 @@ +package net.rustnotmc.loader; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; + +/** + * What a client mod is allowed to touch. + * + * The rule this set encodes is narrow on purpose. The server owns the world: + * blocks, items, players, physics, combat numbers. A client mod may change what + * the client shows, never what is true. Everything that could turn a visual + * tweak into a cheat or a corrupted world was removed from this list rather than + * left in place with nothing behind it. + * + * Every value here is enforced somewhere in the loader. A capability that is + * granted but unused is a lie told to mod authors, which is why the old set of + * WORLD, PLAYER, NETWORK, TEXT, INPUT and COMMAND is gone: nothing checked them. + * + * A mod requests what it needs; the loader grants CORE plus anything the + * descriptor declares. REFLECTION is the escape hatch and is never automatic. + */ +public enum Access { + + /** Lifecycle, logging, scheduler, event bus. Always granted. */ + CORE, + + /** Register overlay elements on the screen, drawn with the game's font. */ + HUD, + + /** + * Read the keyboard and mouse state the client already knows. + * + * Presentation only: which key is held, where the cursor is, whether the + * window has focus. This says nothing about the world and changes nothing in + * it. + */ + INPUT, + + /** + * Register commands typed by the player. + * + * A CLIENT command runs in the client and may only affect the client. A + * SERVER or BOTH command is the server's to decide; the client half never + * applies an effect on its own. + */ + COMMANDS, + + /** + * Read and write files inside the mod's own directory, nothing else. + * + * The game directory and every other mod's data are out of reach, because a + * save file is world data. + */ + FILES, + + /** + * Raw reflective access to any game class. + * + * Not a security sandbox, and deliberately unstable: it can read a field + * that does not exist in this build and take the client down. It exists for + * the mod that needs something this API does not cover yet, and it is + * granted by hand, never by default. + */ + REFLECTION; + + /** Granted to every mod without being asked for. */ + public static final Set DEFAULT_GRANTS = + Collections.unmodifiableSet(EnumSet.of(CORE, HUD)); + + /** + * Names that used to be accepted and now are not. + * + * These were never backed by an implementation, so granting them promised a + * mod access it could not use. They are kept only to explain the removal: + * "world" is a policy decision, not a typo, and a mod author deserves to + * read that rather than a generic "unknown key". + */ + private static final List RETIRED = + Collections.unmodifiableList(Arrays.asList( + "world", "player", "network", "text", "items", "entity")); + + public boolean grantedByDefault() { + return DEFAULT_GRANTS.contains(this); + } + + public boolean dangerous() { + return this == REFLECTION; + } + + /** The value a descriptor writes, e.g. "hud" for HUD. */ + public String id() { + return name().toLowerCase(); + } + + /** True when the name was a capability that has been removed on purpose. */ + public static boolean isRetired(String raw) { + if (raw == null) { + return false; + } + return RETIRED.contains(raw.trim().toLowerCase()); + } + + public static Access byId(String raw) { + if (raw == null) { + return null; + } + String v = raw.trim(); + if (v.length() == 0) { + return null; + } + for (Access a : values()) { + if (a.name().equalsIgnoreCase(v)) { + return a; + } + } + // tolerate the plural spelling, "huds" and "inputs" + if (v.endsWith("s")) { + return byId(v.substring(0, v.length() - 1)); + } + return null; + } + + /** Parses a comma separated capability list, skipping what it cannot read. */ + public static Set parseList(String raw) { + Set out = EnumSet.noneOf(Access.class); + if (raw == null) { + return out; + } + List bad = new ArrayList(); + List retired = new ArrayList(); + for (String piece : raw.split(",")) { + String v = piece.trim(); + if (v.length() == 0) { + continue; + } + Access a = byId(v); + if (a != null) { + out.add(a); + } else if (isRetired(v)) { + retired.add(v); + } else { + bad.add(v); + } + } + for (int i = 0; i < retired.size(); i++) { + Log.warn("capability '" + retired.get(i) + "' was removed: a client mod" + + " cannot reach the world, players, items or the network"); + } + if (!bad.isEmpty()) { + Log.warn("unknown capability in descriptor: " + join(bad)); + } + return out; + } + + private static String join(List items) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < items.size(); i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(items.get(i)); + } + return sb.toString(); + } +} \ No newline at end of file diff --git a/rsx-client/src/main/java/net/rustnotmc/loader/EventBus.java b/rsx-client/src/main/java/net/rustnotmc/loader/EventBus.java new file mode 100644 index 0000000..49f98d0 --- /dev/null +++ b/rsx-client/src/main/java/net/rustnotmc/loader/EventBus.java @@ -0,0 +1,219 @@ +package net.rustnotmc.loader; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; + +/** + * Typed event dispatch. + * + * The previous bridge matched handlers by string and walked the whole list every + * tick, so cost grew with the number of subscriptions regardless of type. Here + * handlers are indexed by event class, so posting a TickEvent only touches the + * tick handlers. + * + * A handler that throws is reported and skipped; it never stops the ones behind + * it. One broken mod must not silence the rest. + */ +public final class EventBus { + + /** Runs before everything else. */ + public static final int HIGHEST = 32767; + /** Runs last. */ + public static final int LOWEST = -32768; + /** The default. */ + public static final int NORMAL = 0; + + /** Handle to one registration, so a mod can drop it again. */ + public static final class Subscription { + private final EventBus bus; + private final Class type; + private final Consumer handler; + private volatile boolean cancelled; + + Subscription(EventBus bus, Class type, Consumer handler) { + this.bus = bus; + this.type = type; + this.handler = handler; + } + + /** Stops the handler being called again. Safe from inside a handler. */ + public void cancel() { + cancelled = true; + bus.remove(this); + } + + public boolean isCancelled() { + return cancelled; + } + } + + private static final class Handler { + final String mod; + final Class type; + final Consumer body; + final int priority; + final int order; + final Subscription subscription; + + Handler(String mod, Class type, Consumer body, int priority, + int order, Subscription subscription) { + this.mod = mod; + this.type = type; + this.body = body; + this.priority = priority; + this.order = order; + this.subscription = subscription; + } + } + + private final Map, List> byType = new ConcurrentHashMap, List>(); + private final Map, List> resolved = new ConcurrentHashMap, List>(); + private final Object lock = new Object(); + private int orderCounter; + private volatile boolean dirty = true; + + public Subscription subscribe(String modId, Class type, int priority, + Consumer body) { + if (type == null || body == null) { + throw new IllegalArgumentException("type and body are required"); + } + @SuppressWarnings("unchecked") + Consumer erased = (Consumer) body; + Subscription sub = new Subscription(this, type, erased); + Handler h = new Handler(modId == null ? "?" : modId, type, erased, priority, + orderCounter++, sub); + synchronized (lock) { + List list = byType.get(type); + if (list == null) { + list = new ArrayList(); + byType.put(type, list); + } + list.add(h); + dirty = true; + } + return sub; + } + + public Subscription subscribe(String modId, Class type, Consumer body) { + return subscribe(modId, type, NORMAL, body); + } + + private void remove(Subscription sub) { + synchronized (lock) { + List list = byType.get(sub.type); + if (list == null) { + return; + } + for (int i = 0; i < list.size(); i++) { + if (list.get(i).subscription == sub) { + list.remove(i); + dirty = true; + return; + } + } + } + } + + /** Number of handlers that would run for this type, across its supertypes. */ + public int handlerCount(Class type) { + return handlersFor(type).size(); + } + + /** + * Delivers the event to every handler registered for its type or a supertype, + * highest priority first, then in subscription order. + */ + @SuppressWarnings("unchecked") + public T post(T event) { + if (event == null) { + return null; + } + for (Handler h : handlersFor(event.getClass())) { + if (h.subscription.isCancelled()) { + continue; + } + try { + h.body.accept(event); + } catch (Throwable t) { + Log.error("handler of " + h.mod + " failed on " + event.getClass().getSimpleName(), t); + } + } + return event; + } + + /** Post that stops at the first handler asking for it, for cancelable events. */ + @SuppressWarnings("unchecked") + public T postUntilCancelled(T event, java.util.function.Predicate cancelTest) { + if (event == null) { + return null; + } + for (Handler h : handlersFor(event.getClass())) { + if (h.subscription.isCancelled()) { + continue; + } + try { + h.body.accept(event); + } catch (Throwable t) { + Log.error("handler of " + h.mod + " failed on " + event.getClass().getSimpleName(), t); + } + if (cancelTest != null && cancelTest.test(event)) { + break; + } + } + return event; + } + + @SuppressWarnings("unchecked") + private List handlersFor(Class type) { + List cache = resolved.get(type); + if (cache != null && !dirty) { + return cache; + } + synchronized (lock) { + cache = new ArrayList(); + for (Map.Entry, List> e : byType.entrySet()) { + Class registered = e.getKey(); + if (registered.isAssignableFrom(type)) { + cache.addAll(e.getValue()); + } + } + java.util.Collections.sort(cache, new java.util.Comparator() { + public int compare(Handler a, Handler b) { + if (a.priority != b.priority) { + return a.priority > b.priority ? -1 : 1; + } + return a.order < b.order ? -1 : (a.order > b.order ? 1 : 0); + } + }); + cache = java.util.Collections.unmodifiableList(cache); + resolved.clear(); + resolved.put(type, cache); + dirty = false; + return cache; + } + } + + /** Drops every registration. Used when the game shuts down. */ + public void clear() { + synchronized (lock) { + byType.clear(); + resolved.clear(); + dirty = true; + } + } + + /** Diagnostics for the log. */ + public Map summary() { + Map out = new HashMap(); + synchronized (lock) { + for (Map.Entry, List> e : byType.entrySet()) { + out.put(e.getKey().getSimpleName(), e.getValue().size()); + } + } + return out; + } +} \ No newline at end of file diff --git a/rsx-client/src/main/java/net/rustnotmc/loader/GameAccess.java b/rsx-client/src/main/java/net/rustnotmc/loader/GameAccess.java new file mode 100644 index 0000000..ed43540 --- /dev/null +++ b/rsx-client/src/main/java/net/rustnotmc/loader/GameAccess.java @@ -0,0 +1,308 @@ +package net.rustnotmc.loader; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * A mod's handle on the game, subject to the capabilities it was granted. + * + * Two things happen here that the old bridge did badly. + * + * Caching. The old resolver took a class name and a member name, then + * walked the class with reflection on every single call. A HUD redrawing text + * does hundreds of those per frame. Handles are resolved once and kept, so the + * steady state is a map lookup and an array read. + * + * Permission. Every access is checked against the mod's capability set. + * A mod that never asked for WORLD cannot read the world, even though it sits in + * the same process and could work it out for itself. + * + * The client runs on a MCP-named jar, so a logical name usually already is the + * real name. A mapping file exists for the cases where it is not, and is only + * consulted when one is loaded. + */ +public final class GameAccess { + + private static final Map> CLASSES = new ConcurrentHashMap>(); + private static final Map FIELDS = new ConcurrentHashMap(); + private static final Map METHODS = new ConcurrentHashMap(); + private static final Map REPORTED = new ConcurrentHashMap(); + + private static Properties mapping = new Properties(); + + private final String modId; + private final Set granted; + + public GameAccess(String modId, Set granted) { + this.modId = modId; + if (granted == null || granted.isEmpty()) { + this.granted = Collections.emptySet(); + } else { + Set copy = java.util.EnumSet.noneOf(Access.class); + copy.addAll(granted); + this.granted = Collections.unmodifiableSet(copy); + } + } + + /** Loads the optional logical-to-real name table. */ + public static void loadMapping(File file) { + if (file == null || !file.isFile()) { + return; + } + InputStream in = null; + try { + in = new FileInputStream(file); + Properties p = new Properties(); + p.load(in); + mapping = p; + CLASSES.clear(); + FIELDS.clear(); + METHODS.clear(); + Log.info("name mapping loaded from " + file.getName() + " (" + + p.size() + " entries)"); + } catch (Throwable t) { + Log.warn("cannot read name mapping " + file + ": " + t); + } finally { + if (in != null) { + try { + in.close(); + } catch (Throwable ignored) { + // read-only stream, nothing to recover + } + } + } + } + + public static void clearMapping() { + mapping = new Properties(); + CLASSES.clear(); + FIELDS.clear(); + METHODS.clear(); + } + + /** True when the mod holds the capability. */ + public boolean can(Access capability) { + return granted.contains(capability); + } + + /** True when every listed capability is held. */ + public boolean canAll(Access... capabilities) { + for (int i = 0; i < capabilities.length; i++) { + if (!granted.contains(capabilities[i])) { + return false; + } + } + return true; + } + + public Set capabilities() { + return granted; + } + + /** + * Asks for a capability at runtime. + * + * REFLECTION is the one worth calling out: it is refused unless the mod was + * granted it, because it is the only way to reach game internals that the + * typed API does not cover. + */ + public boolean request(Access capability) { + if (can(capability)) { + return true; + } + if (capability == Access.REFLECTION) { + Log.warn("mod " + modId + " asked for raw reflection without being granted it"); + } else { + Log.warn("mod " + modId + " asked for " + capability.id() + + " without declaring it"); + } + return false; + } + + // ------------------------------------------------------------ resolution + + public Class cls(String logical) { + if (logical == null) { + return null; + } + if (!can(Access.REFLECTION)) { + // resolving a game class by name is the escape hatch, so it needs the + // escape hatch; CORE is granted to everything and would gate nothing + return null; + } + Class c = CLASSES.get(logical); + if (c != null) { + return c; + } + String real = property("class." + logical, logical); + try { + c = Class.forName(real, false, loader()); + CLASSES.put(logical, c); + return c; + } catch (Throwable t) { + reportOnce("class:" + logical, "mod " + modId + ": no class " + real + + " (asked as '" + logical + "')"); + return null; + } + } + + public Field field(String logicalClass, String name) { + Class owner = cls(logicalClass); + if (owner == null || name == null) { + return null; + } + String key = logicalClass + '#' + name; + Field f = FIELDS.get(key); + if (f != null) { + return f; + } + String real = property("field." + logicalClass + '#' + name, name); + try { + f = owner.getDeclaredField(real); + f.trySetAccessible(); + FIELDS.put(key, f); + return f; + } catch (Throwable t) { + reportOnce("field:" + key, "mod " + modId + ": no field " + logicalClass + + '#' + real); + return null; + } + } + + public Method method(String logicalClass, String name, Class... params) { + Class owner = cls(logicalClass); + if (owner == null || name == null) { + return null; + } + StringBuilder sb = new StringBuilder(logicalClass).append('#').append(name); + if (params != null) { + for (int i = 0; i < params.length; i++) { + sb.append('/').append(params[i].getName()); + } + } + String key = sb.toString(); + Method m = METHODS.get(key); + if (m != null) { + return m; + } + String real = property("method." + logicalClass + '#' + name, name); + try { + m = owner.getDeclaredMethod(real, params); + m.trySetAccessible(); + METHODS.put(key, m); + return m; + } catch (Throwable t) { + reportOnce("method:" + key, "mod " + modId + ": no method " + logicalClass + + '#' + real); + return null; + } + } + + // -------------------------------------------------------------- accessors + + public Object get(String logicalClass, String member, Object instance) { + Field f = field(logicalClass, member); + if (f == null) { + return null; + } + try { + return f.get(instance); + } catch (Throwable t) { + reportOnce("read:" + logicalClass + '#' + member, + "mod " + modId + " cannot read " + logicalClass + '#' + member); + return null; + } + } + + public void set(String logicalClass, String member, Object instance, Object value) { + Field f = field(logicalClass, member); + if (f == null) { + return; + } + try { + f.set(instance, value); + } catch (Throwable t) { + reportOnce("write:" + logicalClass + '#' + member, + "mod " + modId + " cannot write " + logicalClass + '#' + member); + } + } + + public Object call(String logicalClass, String member, Object instance, Object... args) { + Method m = method(logicalClass, member, argTypes(args)); + if (m == null) { + return null; + } + try { + return m.invoke(instance, args); + } catch (Throwable t) { + Throwable cause = t.getCause() == null ? t : t.getCause(); + reportOnce("call:" + logicalClass + '#' + member, + "mod " + modId + " failed calling " + logicalClass + '#' + member + + ": " + cause); + return null; + } + } + + private static Class[] argTypes(Object[] args) { + if (args == null || args.length == 0) { + return EMPTY; + } + Class[] out = new Class[args.length]; + for (int i = 0; i < args.length; i++) { + out[i] = args[i] == null + ? Object.class + : args[i].getClass(); + } + return out; + } + + private static final Class[] EMPTY = new Class[0]; + + private static String property(String key, String fallback) { + String v = mapping.getProperty(key); + if (v == null || v.trim().length() == 0) { + return fallback; + } + return v.trim(); + } + + private static ClassLoader loader() { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + return cl != null ? cl : GameAccess.class.getClassLoader(); + } + + private static void reportOnce(String key, String message) { + if (REPORTED.putIfAbsent(key, Boolean.TRUE) == null) { + Log.warn(message); + } + } + + /** Everything still unresolved, for a diagnostic dump. */ + public static List unresolved() { + List out = new ArrayList(REPORTED.keySet()); + Collections.sort(out); + return out; + } + + /** Drops every cached handle. Called when the client changes world or version. */ + public static void invalidate() { + CLASSES.clear(); + FIELDS.clear(); + METHODS.clear(); + REPORTED.clear(); + } + + public String modId() { + return modId; + } +} \ No newline at end of file diff --git a/rsx-client/src/main/java/net/rustnotmc/loader/Log.java b/rsx-client/src/main/java/net/rustnotmc/loader/Log.java new file mode 100644 index 0000000..18b6b2c --- /dev/null +++ b/rsx-client/src/main/java/net/rustnotmc/loader/Log.java @@ -0,0 +1,165 @@ +package net.rustnotmc.loader; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.Writer; +import java.nio.charset.Charset; +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * Loader log. + * + * The game runs windowed under javaw.exe where nobody sees stdout, so everything + * goes to a file next to the loader; that file is the primary way to confirm a + * mod actually ran. + * + * {@link #forMod} hands a mod a view that prefixes its id, so a line from a mod + * is attributable without every call site repeating its own name. + */ +public final class Log { + + public static final int DEBUG = 0; + public static final int INFO = 1; + public static final int WARN = 2; + public static final int ERROR = 3; + + private static final Charset UTF8 = Charset.forName("UTF-8"); + private static final SimpleDateFormat FMT = new SimpleDateFormat("HH:mm:ss.SSS"); + + private static PrintWriter file; + private static int threshold = INFO; + + private Log() { + } + + /** Opens the log file. Passing null keeps console output only. */ + public static synchronized void open(File target) { + close(); + if (target == null) { + return; + } + try { + File dir = target.getParentFile(); + if (dir != null) { + dir.mkdirs(); + } + Writer w = new OutputStreamWriter(new FileOutputStream(target, true), UTF8); + file = new PrintWriter(w, true); + info("--- RustNotMC loader log opened: " + target.getAbsolutePath()); + } catch (Throwable t) { + file = null; + System.err.println("[rustnotmc] cannot open log: " + t); + } + } + + public static synchronized void close() { + if (file != null) { + file.flush(); + file.close(); + file = null; + } + } + + public static synchronized void setLevel(int level) { + threshold = level; + } + + public static synchronized boolean debugEnabled() { + return threshold <= DEBUG; + } + + public static synchronized void debug(String msg) { + if (threshold <= DEBUG) { + write("DEBUG", msg, null); + } + } + + public static synchronized void info(String msg) { + write("INFO", msg, null); + } + + public static synchronized void warn(String msg) { + write("WARN", msg, null); + } + + public static synchronized void warn(String msg, Throwable t) { + write("WARN", msg, t); + } + + public static synchronized void error(String msg) { + write("ERROR", msg, null); + } + + public static synchronized void error(String msg, Throwable t) { + write("ERROR", msg, t); + } + + /** A log view that stamps every line with a mod id. */ + public static ModLog forMod(String modId) { + return new ModLog(modId); + } + + private static synchronized void write(String level, String msg, Throwable t) { + String line = FMT.format(new Date()) + " [" + level + "] " + msg; + System.out.println("[rustnotmc] " + line); + if (t != null) { + System.out.println(" " + rootMessage(t)); + } + if (file != null) { + file.println(line); + if (t != null) { + file.println(" " + rootMessage(t)); + } + if (level.equals("ERROR")) { + file.flush(); + } + } + } + + private static String rootMessage(Throwable t) { + Throwable root = t; + while (root != null && root.getCause() != null && root.getCause() != root) { + root = root.getCause(); + } + return root.getClass().getName() + ": " + root.getMessage(); + } + + /** Per-mod log view handed to a mod through its context. */ + public static final class ModLog { + + private final String id; + + ModLog(String id) { + this.id = id; + } + + public void debug(String msg) { + if (debugEnabled()) { + info(msg); + } + } + + public void info(String msg) { + Log.info("[" + id + "] " + msg); + } + + public void warn(String msg) { + Log.warn("[" + id + "] " + msg); + } + + public void warn(String msg, Throwable t) { + Log.warn("[" + id + "] " + msg, t); + } + + public void error(String msg) { + Log.error("[" + id + "] " + msg); + } + + public void error(String msg, Throwable t) { + Log.error("[" + id + "] " + msg, t); + } + } +} \ No newline at end of file diff --git a/rsx-client/src/main/java/net/rustnotmc/loader/ModContainer.java b/rsx-client/src/main/java/net/rustnotmc/loader/ModContainer.java new file mode 100644 index 0000000..a97eebc --- /dev/null +++ b/rsx-client/src/main/java/net/rustnotmc/loader/ModContainer.java @@ -0,0 +1,104 @@ +package net.rustnotmc.loader; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * One loaded mod: its jar, its descriptor, its class loader and its instance. + * + * Each mod gets its own {@link java.net.URLClassLoader}. Two mods may ship a + * class with the same name without colliding, and a mod cannot reach into another + * mod's classes. The loader and API classes are deliberately not on the child + * class loader, so every mod sees one copy of them and a mod never reloads the + * scheduler or the event bus into a second instance. + */ +public final class ModContainer { + + private final ModDescriptor descriptor; + private final ClassLoader loader; + private final Object instance; + private final ModContext context; + private final File jar; + private final List dependents = new ArrayList(); + private volatile ModState state = ModState.DISCOVERED; + + ModContainer(ModDescriptor descriptor, File jar, ClassLoader loader, Object instance, + ModContext context) { + this.descriptor = descriptor; + this.jar = jar; + this.loader = loader; + this.instance = instance; + this.context = context; + } + + public ModDescriptor descriptor() { + return descriptor; + } + + public String id() { + return descriptor.id(); + } + + public Version version() { + return descriptor.version(); + } + + public ModState state() { + return state; + } + + void state(ModState s) { + this.state = s; + } + + public Object instance() { + return instance; + } + + public ModContext context() { + return context; + } + + public ClassLoader classLoader() { + return loader; + } + + public File jar() { + return jar; + } + + /** Mods that list this one as a dependency, for shutdown ordering. */ + public List dependents() { + return Collections.unmodifiableList(dependents); + } + + void addDependent(ModContainer other) { + if (!dependents.contains(other)) { + dependents.add(other); + } + } + + /** Calls a lifecycle method on the mod, turning any throw into a log line. */ + boolean invoke(String method, String what) { + try { + java.lang.reflect.Method m = instance.getClass().getMethod(method); + m.invoke(instance); + return true; + } catch (java.lang.reflect.InvocationTargetException e) { + Throwable cause = e.getCause() == null ? e : e.getCause(); + Log.error(descriptor.id() + " failed in " + what + " (" + method + ")", cause); + return false; + } catch (Throwable t) { + Log.error("cannot call " + method + " on " + descriptor.id() + + ": it may not extend the expected base class", t); + return false; + } + } + + @Override + public String toString() { + return descriptor.id() + " " + descriptor.version() + " [" + state + "]"; + } +} \ No newline at end of file diff --git a/rsx-client/src/main/java/net/rustnotmc/loader/ModContext.java b/rsx-client/src/main/java/net/rustnotmc/loader/ModContext.java new file mode 100644 index 0000000..4e4a813 --- /dev/null +++ b/rsx-client/src/main/java/net/rustnotmc/loader/ModContext.java @@ -0,0 +1,113 @@ +package net.rustnotmc.loader; + +import java.io.File; +import java.util.Set; + +/** + * What a mod is handed when it is constructed. + * + * Everything a mod can do comes from here, so there is one place to look and one + * place to cut off access. A mod is constructed once and keeps this context for + * life; the fields never change. + */ +public final class ModContext { + + private final String modId; + private final String name; + private final Version version; + private final ModEnv env; + private final Set access; + private final GameAccess game; + private final EventBus events; + private final TickScheduler scheduler; + private final Log.ModLog log; + private final File modDir; + + public ModContext(ModDescriptor d, Set granted, EventBus events, + TickScheduler scheduler, File modDir) { + this.modId = d.id(); + this.name = d.name(); + this.version = d.version(); + this.env = d.env(); + this.access = granted; + this.game = new GameAccess(d.id(), granted); + this.events = events; + this.scheduler = scheduler; + this.log = Log.forMod(d.id()); + this.modDir = modDir; + } + + /** + * This mod's own directory, created on first use. + * + * A mod gets its folder and nothing else. The game directory holds save + * data, which the server owns, so handing out a general writable path would + * hand out the world with it. + */ + public File modDirectory() { + if (modDir == null) { + return null; + } + if (!modDir.isDirectory() && !modDir.mkdirs()) { + return null; + } + return modDir; + } + + /** Stable id, also used as the dependency key and the log prefix. */ + public String modId() { + return modId; + } + + public String name() { + return name; + } + + public Version version() { + return version; + } + + /** The side this mod declares itself for. */ + public ModEnv env() { + return env; + } + + /** True when running on the client. Always true until the loader ships on the server. */ + public boolean isClient() { + return ModEnv.current() == ModEnv.CLIENT; + } + + public boolean isServer() { + return ModEnv.current() == ModEnv.SERVER; + } + + /** Capabilities actually granted, which may be fewer than requested. */ + public Set access() { + return access; + } + + public boolean has(Access capability) { + return access.contains(capability); + } + + public GameAccess game() { + return game; + } + + public EventBus events() { + return events; + } + + public TickScheduler scheduler() { + return scheduler; + } + + public Log.ModLog log() { + return log; + } + + @Override + public String toString() { + return modId + " " + version + " (" + env + ")"; + } +} \ No newline at end of file diff --git a/rsx-client/src/main/java/net/rustnotmc/loader/ModDescriptor.java b/rsx-client/src/main/java/net/rustnotmc/loader/ModDescriptor.java new file mode 100644 index 0000000..9e6ba50 --- /dev/null +++ b/rsx-client/src/main/java/net/rustnotmc/loader/ModDescriptor.java @@ -0,0 +1,315 @@ +package net.rustnotmc.loader; + +import java.io.File; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Properties; +import java.util.Set; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +/** + * What a mod declares about itself, read from META-INF/rsxmod.properties inside + * the mod jar. + * + *
+ * id=rsxm-lib
+ * name=RustNotMC Library
+ * version=1.0.0
+ * author=you
+ * license=MIT
+ * env=CLIENT
+ * entrypoint=net.rustnotmc.lib.LibMod
+ * access=hud, text, world, player
+ * depends.core=>=1.0
+ * softdepends.rsxm-hud=*
+ * 
+ * + * Properties rather than JSON on purpose: the loader must parse a mod's + * metadata before anything else is on the classpath, so it cannot depend on a + * JSON library, and it must never fail on a jar whose manifest is malformed. + */ +public final class ModDescriptor { + + public static final String PATH = "META-INF/rsxmod.properties"; + public static final int API_VERSION = 1; + + private static final String KEY_ID = "id"; + private static final String KEY_NAME = "name"; + private static final String KEY_VERSION = "version"; + private static final String KEY_DESCRIPTION = "description"; + private static final String KEY_AUTHOR = "author"; + private static final String KEY_LICENSE = "license"; + private static final String KEY_ENV = "env"; + private static final String KEY_ENTRY = "entrypoint"; + private static final String KEY_ACCESS = "access"; + private static final String KEY_API = "api"; + private static final String PREFIX_DEPENDS = "depends."; + private static final String PREFIX_SOFT = "softdepends."; + + private final String id; + private final String name; + private final Version version; + private final String description; + private final String author; + private final String license; + private final ModEnv env; + private final String entrypoint; + private final int apiVersion; + private final Set access; + private final List dependencies; + private final List softDependencies; + private final File source; + + private ModDescriptor(String id, String name, Version version, String description, + String author, String license, ModEnv env, String entrypoint, + int apiVersion, Set access, List dependencies, + List softDependencies, File source) { + this.id = id; + this.name = name; + this.version = version; + this.description = description; + this.author = author; + this.license = license; + this.env = env; + this.entrypoint = entrypoint; + this.apiVersion = apiVersion; + this.access = access; + this.dependencies = dependencies; + this.softDependencies = softDependencies; + this.source = source; + } + + /** A hard dependency: if it is missing or unsatisfied, this mod cannot start. */ + public static final class Dependency { + public final String id; + public final VersionConstraint constraint; + private final String raw; + + Dependency(String id, VersionConstraint constraint, String raw) { + this.id = id; + this.constraint = constraint; + this.raw = raw; + } + + public boolean accepts(Version v) { + return constraint.accepts(v); + } + + @Override + public String toString() { + return raw == null || raw.length() == 0 ? id : id + " " + raw; + } + } + + /** Reads the descriptor out of a mod jar, or null when it carries none. */ + public static ModDescriptor load(File jar) { + ZipFile zip = null; + try { + zip = new ZipFile(jar); + ZipEntry entry = zip.getEntry(PATH); + if (entry == null) { + // an ordinary library jar sitting in the mods folder + return null; + } + Properties p = new Properties(); + InputStream in = zip.getInputStream(entry); + try { + // UTF-8, so a description in any language survives the round trip + p.load(new java.io.InputStreamReader(in, "UTF-8")); + } finally { + in.close(); + } + return from(p, jar); + } catch (Throwable t) { + Log.warn("cannot read " + PATH + " from " + jar.getName() + ": " + t); + return null; + } finally { + if (zip != null) { + try { + zip.close(); + } catch (Throwable ignored) { + // read-only archive, nothing to recover + } + } + } + } + + public static ModDescriptor from(Properties p, File source) { + String id = trimmed(p.getProperty(KEY_ID)); + if (id == null || id.length() == 0) { + Log.warn("descriptor without '" + KEY_ID + "' in " + + (source == null ? "?" : source.getName())); + return null; + } + if (!isLegalId(id)) { + Log.warn("illegal mod id '" + id + "' (want [a-z0-9._-])"); + return null; + } + + String entry = trimmed(p.getProperty(KEY_ENTRY)); + if (entry == null || entry.length() == 0) { + Log.warn("mod " + id + " has no '" + KEY_ENTRY + "'"); + return null; + } + + Version ver = Version.parse(trimmed(p.getProperty(KEY_VERSION))); + if (!ver.valid()) { + Log.warn("mod " + id + " has no readable version, assuming 0.0.0"); + ver = Version.parse("0.0.0"); + } + + int api = parseInt(trimmed(p.getProperty(KEY_API)), API_VERSION); + if (api != API_VERSION) { + Log.warn("mod " + id + " targets API " + api + ", this loader speaks " + + API_VERSION); + } + + Set access = Access.parseList(p.getProperty(KEY_ACCESS)); + access.add(Access.CORE); + + return new ModDescriptor( + id, + or(p.getProperty(KEY_NAME), id), + ver, + trimmed(p.getProperty(KEY_DESCRIPTION)), + trimmed(p.getProperty(KEY_AUTHOR)), + trimmed(p.getProperty(KEY_LICENSE)), + ModEnv.parse(p.getProperty(KEY_ENV)), + entry, + api, + access, + collect(p, PREFIX_DEPENDS), + collect(p, PREFIX_SOFT), + source); + } + + private static List collect(Properties p, String prefix) { + Set out = new LinkedHashSet(); + for (String key : p.stringPropertyNames()) { + if (!key.startsWith(prefix)) { + continue; + } + String depId = key.substring(prefix.length()).trim(); + if (depId.length() == 0) { + continue; + } + String raw = trimmed(p.getProperty(key)); + VersionConstraint c = raw == null || raw.length() == 0 || raw.equals("*") + ? VersionConstraint.any() + : VersionConstraint.parse(raw); + out.add(new Dependency(depId, c, raw)); + } + return Collections.unmodifiableList(new ArrayList(out)); + } + + private static boolean isLegalId(String id) { + for (int i = 0; i < id.length(); i++) { + char c = id.charAt(i); + boolean ok = (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') + || c == '.' || c == '_' || c == '-'; + if (!ok) { + return false; + } + } + return true; + } + + private static String trimmed(String s) { + if (s == null) { + return null; + } + String v = s.trim(); + return v.length() == 0 ? null : v; + } + + private static String or(String value, String fallback) { + String v = trimmed(value); + return v == null ? fallback : v; + } + + private static int parseInt(String s, int fallback) { + if (s == null) { + return fallback; + } + try { + return Integer.parseInt(s.trim()); + } catch (NumberFormatException e) { + return fallback; + } + } + + public String id() { + return id; + } + + public String name() { + return name; + } + + public Version version() { + return version; + } + + public String description() { + return description; + } + + public String author() { + return author; + } + + public String license() { + return license; + } + + public ModEnv env() { + return env; + } + + public String entrypoint() { + return entrypoint; + } + + public int apiVersion() { + return apiVersion; + } + + /** Capabilities the mod asked for. */ + public Set access() { + return access; + } + + public List dependencies() { + return dependencies; + } + + public List softDependencies() { + return softDependencies; + } + + /** The jar this descriptor came from, null when built by hand in a test. */ + public File source() { + return source; + } + + public boolean wants(ModEnv side) { + return env.runsOn(side); + } + + @Override + public String toString() { + return id + " " + version; + } + + /** Orders descriptors by id, so a load plan is stable between runs. */ + public static final Comparator BY_ID = new Comparator() { + public int compare(ModDescriptor a, ModDescriptor b) { + return a.id.compareTo(b.id); + } + }; +} \ No newline at end of file diff --git a/rsx-client/src/main/java/net/rustnotmc/loader/ModEnv.java b/rsx-client/src/main/java/net/rustnotmc/loader/ModEnv.java new file mode 100644 index 0000000..5e7e4bf --- /dev/null +++ b/rsx-client/src/main/java/net/rustnotmc/loader/ModEnv.java @@ -0,0 +1,50 @@ +package net.rustnotmc.loader; + +/** + * Which side of the game a mod is meant to run on. + * + * The loader runs on the client today, but the value is part of the descriptor + * from the first release: a mod jar that says BOTH today must not need a format + * change tomorrow when the loader moves to the server. + */ +public enum ModEnv { + + CLIENT, + SERVER, + BOTH; + + /** True when a mod declaring this env should load on the given side. */ + public boolean runsOn(ModEnv side) { + if (side == null) { + return false; + } + return this == BOTH || this == side; + } + + /** The side we are running on right now. */ + public static ModEnv current() { + return CLIENT; + } + + /** + * Parses the descriptor value. Unreadable input is treated as BOTH rather + * than rejected: refusing to start because of one bad character is worse + * than loading a mod that may then find itself unusable. + */ + public static ModEnv parse(String raw) { + if (raw == null) { + return BOTH; + } + String v = raw.trim().toUpperCase(); + if (v.length() == 0) { + return BOTH; + } + if (v.equals("CLIENT") || v.equals("CLIENT_ONLY") || v.equals("CLIENTONLY")) { + return CLIENT; + } + if (v.equals("SERVER") || v.equals("SERVER_ONLY") || v.equals("SERVERONLY")) { + return SERVER; + } + return BOTH; + } +} \ No newline at end of file diff --git a/rsx-client/src/main/java/net/rustnotmc/loader/ModLoader.java b/rsx-client/src/main/java/net/rustnotmc/loader/ModLoader.java new file mode 100644 index 0000000..8f8a9ca --- /dev/null +++ b/rsx-client/src/main/java/net/rustnotmc/loader/ModLoader.java @@ -0,0 +1,436 @@ +package net.rustnotmc.loader; + +import java.io.Closeable; +import java.io.File; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import net.rustnotmc.api.RustNotMcMod; + +/** + * Finds mods, orders them, and drives their lifecycle. + * + * The load order is decided by dependencies, not by filename order: a mod that + * depends on a library must start after it and stop before it. Ordering is a + * depth-first walk over the dependency graph with cycle detection, so a broken + * set of mods produces a readable error instead of a stack overflow. + * + * Shutdown runs in reverse. That is not symmetry for its own sake: a mod that + * holds a reference to another mod's API object must not find it already gone. + */ +public final class ModLoader { + + private final File modsDir; + private final EventBus events = new EventBus(); + private final TickScheduler scheduler = new TickScheduler(); + private final Map descriptors = new LinkedHashMap(); + private final Map loaded = new LinkedHashMap(); + private final List order = new ArrayList(); + private final List problems = new ArrayList(); + private final TickEvent tickEvent = new TickEvent(); + + public ModLoader(File modsDir) { + this.modsDir = modsDir; + } + + // ------------------------------------------------------------- discovery + + /** Reads every jar in the directory. A jar without a descriptor is skipped. */ + public int discover() { + descriptors.clear(); + problems.clear(); + if (modsDir == null || !modsDir.isDirectory()) { + Log.warn("no mods directory at " + modsDir); + return 0; + } + File[] jars = modsDir.listFiles(); + if (jars == null) { + return 0; + } + Arrays.sort(jars); + int found = 0; + for (int i = 0; i < jars.length; i++) { + File f = jars[i]; + if (!f.isFile() || !f.getName().toLowerCase().endsWith(".jar")) { + continue; + } + ModDescriptor d = ModDescriptor.load(f); + if (d == null) { + continue; + } + if (!d.wants(ModEnv.current())) { + Log.info("skipping " + d.id() + ": env=" + d.env() + + " does not include " + ModEnv.current()); + continue; + } + ModDescriptor clash = descriptors.get(d.id()); + if (clash != null) { + problems.add("duplicate mod id '" + d.id() + "': " + + clash.source().getName() + " and " + f.getName() + + "; the higher version wins"); + Version lower = d.version().atLeast(clash.version()) ? clash.version() : d.version(); + if (d.version().atLeast(lower)) { + descriptors.put(d.id(), d); + } + continue; + } + descriptors.put(d.id(), d); + found++; + } + return found; + } + + // ------------------------------------------------------------ resolution + + /** + * Checks every hard dependency and produces the load order. + * + * @return true when the whole set can be loaded. + */ + public boolean resolve() { + order.clear(); + problems.clear(); + + for (ModDescriptor d : descriptors.values()) { + List deps = d.dependencies(); + for (int i = 0; i < deps.size(); i++) { + ModDescriptor.Dependency dep = deps.get(i); + ModDescriptor target = descriptors.get(dep.id); + if (target == null) { + problems.add(d.id() + " needs " + dep + " which is not installed"); + } else if (!dep.accepts(target.version())) { + problems.add(d.id() + " needs " + dep + " but " + target.id() + + " " + target.version() + " is installed"); + } + } + List soft = d.softDependencies(); + for (int i = 0; i < soft.size(); i++) { + ModDescriptor target = descriptors.get(soft.get(i).id); + if (target != null && !soft.get(i).accepts(target.version())) { + problems.add(d.id() + " prefers " + soft.get(i) + " but " + + target.id() + " " + target.version() + " is installed"); + } + } + } + + if (!problems.isEmpty()) { + for (int i = 0; i < problems.size(); i++) { + Log.error("dependency: " + problems.get(i)); + } + return false; + } + + Set done = new HashSet(); + Set visiting = new HashSet(); + List sorted = new ArrayList(); + List byId = new ArrayList(descriptors.values()); + Collections.sort(byId, ModDescriptor.BY_ID); + + for (int i = 0; i < byId.size(); i++) { + if (!visit(byId.get(i), done, visiting, sorted, new ArrayList())) { + for (int p = 0; p < problems.size(); p++) { + Log.error("dependency: " + problems.get(p)); + } + return false; + } + } + + order.addAll(sorted); + return true; + } + + private boolean visit(ModDescriptor d, Set done, Set visiting, + List sorted, List trail) { + if (done.contains(d.id())) { + return true; + } + if (visiting.contains(d.id())) { + StringBuilder sb = new StringBuilder("dependency cycle: "); + for (int i = 0; i < trail.size(); i++) { + sb.append(trail.get(i)).append(" -> "); + } + sb.append(d.id()); + problems.add(sb.toString()); + return false; + } + visiting.add(d.id()); + trail.add(d.id()); + + // hard dependencies first, then soft ones that happen to be installed + List all = new ArrayList( + d.dependencies()); + all.addAll(d.softDependencies()); + for (int i = 0; i < all.size(); i++) { + ModDescriptor target = descriptors.get(all.get(i).id); + if (target != null && !visit(target, done, visiting, sorted, trail)) { + return false; + } + } + + trail.remove(trail.size() - 1); + visiting.remove(d.id()); + done.add(d.id()); + sorted.add(d); + return true; + } + + // --------------------------------------------------------------- loading + + /** + * The mod currently being constructed, for the duration of its onConstruct. + * + * The client needs this to record which mod claimed a command name, and it + * cannot ask: a mod's name is known here and nowhere else the game can see. + * It lives on the loader side precisely so the game side can read it without + * the loader ever depending on the game. + */ + public static final ThreadLocal REGISTERING = new ThreadLocal(); + + /** Instantiates and starts every resolved mod, in dependency order. */ + public int loadAll() { + int started = 0; + for (int i = 0; i < order.size(); i++) { + ModDescriptor d = order.get(i); + ModContainer c = create(d); + if (c == null) { + continue; + } + loaded.put(d.id(), c); + if (c.invoke("onLoad", "load")) { + c.state(ModState.LOADED); + started++; + } else { + c.state(ModState.FAILED); + } + } + // only once everything is constructed does anything start doing work, + // so a mod can rely on every dependency's API already being live + for (int i = 0; i < order.size(); i++) { + ModContainer c = loaded.get(order.get(i).id()); + if (c == null || c.state() == ModState.FAILED) { + continue; + } + if (c.invoke("onEnable", "enable")) { + c.state(ModState.ENABLED); + } else { + c.state(ModState.FAILED); + } + } + return started; + } + + private ModContainer create(ModDescriptor d) { + File jar = d.source(); + if (jar == null) { + Log.warn("mod " + d.id() + " has no jar, cannot load"); + return null; + } + URLClassLoader cl = null; + try { + List urls = new ArrayList(); + urls.add(jar.toURI().toURL()); + File libs = new File(modsDir, d.id() + "-libs"); + if (libs.isDirectory()) { + File[] extra = libs.listFiles(); + if (extra != null) { + Arrays.sort(extra); + for (int i = 0; i < extra.length; i++) { + if (extra[i].isFile() && extra[i].getName().endsWith(".jar")) { + urls.add(extra[i].toURI().toURL()); + } + } + } + } + // the parent is the loader's own loader, so a mod sees the API and the + // game's classes, and never a second copy of the scheduler + cl = new URLClassLoader(urls.toArray(new URL[0]), ModLoader.class.getClassLoader()); + + Class entry = cl.loadClass(d.entrypoint()); + if (!RustNotMcMod.class.isAssignableFrom(entry)) { + Log.error("mod " + d.id() + ": " + d.entrypoint() + + " does not implement " + RustNotMcMod.class.getName()); + closeLoader(cl); + return null; + } + Object instance = entry.getDeclaredConstructor().newInstance(); + Set granted = grant(d); + // each mod gets its own folder for anything it needs to persist + File modDir = modsDir == null ? null : new File(modsDir, d.id()); + ModContext ctx = new ModContext(d, granted, events, scheduler, modDir); + ModContainer container = new ModContainer(d, jar, cl, instance, ctx); + container.state(ModState.RESOLVED); + REGISTERING.set(d.id()); + try { + ((RustNotMcMod) instance).onConstruct(new net.rustnotmc.api.Rsx(ctx)); + } finally { + REGISTERING.remove(); + } + container.state(ModState.CONSTRUCTED); + return container; + } catch (Throwable t) { + Log.error("mod " + d.id() + " cannot be constructed", t); + closeLoader(cl); + return null; + } + } + + /** + * Decides what a mod actually gets. + * + * Everything it asks for is granted except REFLECTION, which stays withheld + * unless the operator lists it in the loader config. + */ + private Set grant(ModDescriptor d) { + Set granted = java.util.EnumSet.noneOf(Access.class); + for (Access a : d.access()) { + if (a == Access.REFLECTION) { + if (ReflectionConsent.isGranted(d.id())) { + granted.add(a); + Log.warn("mod " + d.id() + " has raw reflection, granted by config"); + } else { + Log.warn("mod " + d.id() + " asked for raw reflection and did not get it"); + } + } else { + granted.add(a); + } + } + granted.add(Access.CORE); + return granted; + } + + // -------------------------------------------------------------- shutdown + + /** Stops every mod in reverse load order. */ + public void disableAll() { + for (int i = order.size() - 1; i >= 0; i--) { + ModContainer c = loaded.get(order.get(i).id()); + if (c != null && c.state() == ModState.ENABLED) { + if (c.invoke("onDisable", "disable")) { + c.state(ModState.DISABLED); + } + } + } + } + + /** Releases class loaders after {@link #disableAll()}. */ + public void unloadAll() { + disableAll(); + for (int i = order.size() - 1; i >= 0; i--) { + ModContainer c = loaded.get(order.get(i).id()); + if (c == null || c.state().isDead()) { + continue; + } + c.invoke("onUnload", "unload"); + c.state(ModState.UNLOADED); + closeLoader(c.classLoader()); + } + events.clear(); + scheduler.shutdown(); + } + + // ------------------------------------------------------------------ tick + + /** Driven by the game every tick. */ + public void tick() { + scheduler.tick(); + tickEvent.tick = scheduler.tickCount(); + events.post(tickEvent); + } + + // ---------------------------------------------------------------- lookup + + public EventBus events() { + return events; + } + + public TickScheduler scheduler() { + return scheduler; + } + + public List mods() { + List out = new ArrayList(); + for (int i = 0; i < order.size(); i++) { + ModContainer c = loaded.get(order.get(i).id()); + if (c != null) { + out.add(c); + } + } + return out; + } + + public ModContainer mod(String id) { + return loaded.get(id); + } + + public List problems() { + return Collections.unmodifiableList(problems); + } + + public List loadOrder() { + List out = new ArrayList(order.size()); + for (int i = 0; i < order.size(); i++) { + out.add(order.get(i).id()); + } + return out; + } + + public int count() { + return loaded.size(); + } + + private static void close(Closeable c) { + if (c == null) { + return; + } + try { + c.close(); + } catch (Throwable ignored) { + // nothing useful to do while shutting a class loader down + } + } + + private static void closeLoader(ClassLoader c) { + if (c instanceof Closeable) { + close((Closeable) c); + } + } + + /** The event every mod can subscribe to without depending on anything else. */ + public static final class TickEvent { + long tick; + + public long tick() { + return tick; + } + } + + /** Operator consent for the one capability that is never automatic. */ + public static final class ReflectionConsent { + private static final Set GRANTED = + Collections.synchronizedSet(new HashSet()); + + private ReflectionConsent() { + } + + public static void grant(String modId) { + GRANTED.add(modId); + } + + public static void revoke(String modId) { + GRANTED.remove(modId); + } + + public static boolean isGranted(String modId) { + return GRANTED.contains(modId); + } + } +} \ No newline at end of file diff --git a/rsx-client/src/main/java/net/rustnotmc/loader/ModState.java b/rsx-client/src/main/java/net/rustnotmc/loader/ModState.java new file mode 100644 index 0000000..094f100 --- /dev/null +++ b/rsx-client/src/main/java/net/rustnotmc/loader/ModState.java @@ -0,0 +1,51 @@ +package net.rustnotmc.loader; + +/** + * Where a mod is in its lifecycle. + * + * The loader refuses to move a mod backwards. A mod that failed during + * construction never reaches CONSTRUCTED, so nothing later mistakes it for a + * running mod. + */ +public enum ModState { + + /** Descriptor read, nothing done yet. */ + DISCOVERED, + + /** Dependencies satisfied and load order decided. */ + RESOLVED, + + /** Entry class instantiated, onConstruct called. */ + CONSTRUCTED, + + /** onLoad called; the mod may register things. */ + LOADED, + + /** onEnable called; the mod is doing its work. */ + ENABLED, + + /** onDisable called; stopped but still loaded, so it can be re-enabled. */ + DISABLED, + + /** onUnload called; the mod is gone. */ + UNLOADED, + + /** Something went wrong; the mod is not running and will not be retried. */ + FAILED; + + public boolean isRunning() { + return this == ENABLED; + } + + public boolean isLoaded() { + return this == LOADED || this == ENABLED || this == DISABLED; + } + + public boolean isDead() { + return this == UNLOADED || this == FAILED; + } + + public boolean atLeast(ModState s) { + return ordinal() >= s.ordinal(); + } +} \ No newline at end of file diff --git a/rsx-client/src/main/java/net/rustnotmc/loader/TickScheduler.java b/rsx-client/src/main/java/net/rustnotmc/loader/TickScheduler.java new file mode 100644 index 0000000..aa4b63f --- /dev/null +++ b/rsx-client/src/main/java/net/rustnotmc/loader/TickScheduler.java @@ -0,0 +1,247 @@ +package net.rustnotmc.loader; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +/** + * Runs mod work on the game thread. + * + * Three ways in, because they fail differently: + * + * post next tick, always + * everyTicks counted in game ticks, never drifts and never bursts after a lag + * every wall clock, for things that do not care about the game + * + * Wall-clock work never touches game state directly: the timer thread only + * posts, and the body runs later on the game thread. + * + * Every task carries the mod that asked for it, so a failure names a mod and a + * mod's error never reaches another mod's task. + */ +public final class TickScheduler { + + /** Consecutive throws before a task is dropped, so a broken mod cannot flood the log. */ + private static final int MAX_FAILURES = 5; + + /** Handle to a registered task. */ + public static final class Task { + private final TickScheduler scheduler; + private final String mod; + private final Runnable body; + private final int every; + private volatile boolean cancelled; + private volatile boolean dropped; + private long next; + private int failures; + ScheduledFuture future; + + Task(TickScheduler scheduler, String mod, Runnable body, int every) { + this.scheduler = scheduler; + this.mod = mod; + this.body = body; + this.every = every; + } + + /** Stops the task. Safe from inside the task itself. */ + public void cancel() { + cancelled = true; + if (future != null) { + future.cancel(false); + } + scheduler.forget(this); + } + + public boolean isActive() { + return !cancelled && !dropped; + } + + public String mod() { + return mod; + } + } + + private final Object lock = new Object(); + private final List tasks = new ArrayList(); + + /** Reused across ticks: the old code allocated a list every single tick. */ + private final List due = new ArrayList(); + + private long tick; + private boolean running; + + private final ScheduledExecutorService timer = Executors.newSingleThreadScheduledExecutor( + r -> { + Thread t = new Thread(r, "rsx-mod-timer"); + t.setDaemon(true); + return t; + }); + + /** Runs the task once on the next game tick. */ + public Task post(String mod, Runnable body) { + return add(mod, body, 1, 1); + } + + /** Runs the task on the next game tick, then stops. */ + public Task once(String mod, Runnable body) { + return add(mod, body, 1, -1); + } + + /** Runs the task every N game ticks. */ + public Task everyTicks(String mod, int n, Runnable body) { + return add(mod, body, Math.max(1, n), Math.max(1, n)); + } + + /** + * Runs the task on a wall-clock period. + * + * The timer thread only posts the task; the body still runs on the game + * thread, so touching game state here is legal. + */ + public Task every(String mod, long millis, final Runnable body) { + final Task t = add(mod, body, 0, 0); + long period = Math.max(1L, millis); + try { + t.future = timer.scheduleAtFixedRate(new Runnable() { + public void run() { + // re-post on every period, so the task goes through the same + // coalescing and failure handling as everything else + synchronized (lock) { + if (t.cancelled) { + return; + } + t.next = 0; + } + enqueue(t); + } + }, period, period, TimeUnit.MILLISECONDS); + } catch (Throwable t2) { + Log.error("cannot schedule task for " + mod, t2); + } + return t; + } + + private Task add(String mod, Runnable body, int every, int initialNext) { + if (body == null) { + return null; + } + Task t = new Task(this, mod == null ? "?" : mod, body, every); + t.next = tick + Math.max(0, initialNext); + synchronized (lock) { + tasks.add(t); + } + return t; + } + + /** Puts a task at the front of the queue for the coming tick. */ + private void enqueue(Task t) { + synchronized (lock) { + if (t.cancelled || t.dropped) { + return; + } + if (tasks.indexOf(t) < 0) { + tasks.add(t); + } + } + } + + private void forget(Task t) { + synchronized (lock) { + tasks.remove(t); + } + } + + /** The game calls this once per tick. */ + public void tick() { + List batch; + synchronized (lock) { + tick++; + due.clear(); + for (int i = 0; i < tasks.size(); i++) { + Task t = tasks.get(i); + if (t.cancelled || t.dropped || t.every == 0) { + continue; + } + if (tick < t.next) { + continue; + } + t.next = tick + t.every; + due.add(t); + } + if (due.isEmpty()) { + return; + } + batch = new ArrayList(due); + due.clear(); + } + for (int i = 0; i < batch.size(); i++) { + run(batch.get(i)); + } + } + + /** Current game tick, counted by the scheduler since it started. */ + public long tickCount() { + synchronized (lock) { + return tick; + } + } + + private void run(Task t) { + if (t.cancelled || t.dropped) { + return; + } + boolean reentrant; + synchronized (lock) { + reentrant = running; + } + if (reentrant) { + Log.warn("task of " + t.mod + " posted from inside a tick, running now"); + } + try { + t.body.run(); + synchronized (lock) { + t.failures = 0; + } + if (t.every < 0) { + forget(t); + } + } catch (Throwable e) { + int n; + synchronized (lock) { + n = ++t.failures; + if (n >= MAX_FAILURES) { + t.dropped = true; + } + } + Log.error("task of " + t.mod + " failed", e); + if (n >= MAX_FAILURES) { + Log.error("task of " + t.mod + " failed " + n + + " times in a row, dropping it"); + } + } + } + + /** Number of live tasks, for diagnostics. */ + public int size() { + synchronized (lock) { + int n = 0; + for (int i = 0; i < tasks.size(); i++) { + Task t = tasks.get(i); + if (!t.cancelled && !t.dropped) { + n++; + } + } + return n; + } + } + + public void shutdown() { + timer.shutdownNow(); + synchronized (lock) { + tasks.clear(); + } + } +} \ No newline at end of file diff --git a/rsx-client/src/main/java/net/rustnotmc/loader/Version.java b/rsx-client/src/main/java/net/rustnotmc/loader/Version.java new file mode 100644 index 0000000..d5cbd12 --- /dev/null +++ b/rsx-client/src/main/java/net/rustnotmc/loader/Version.java @@ -0,0 +1,147 @@ +package net.rustnotmc.loader; + +/** + * A dotted version with an optional pre-release tag, compared segment by segment. + * + * 1.0 < 1.0.1 + * 1.2.3 == 1.2.3 + * 1.2.3-beta.1 < 1.2.3 (a pre-release sorts below its release) + * + * Missing trailing segments count as zero, so 1.2 and 1.2.0 are the same version. + * An unparseable version never throws; it degrades to 0.0.0 and the comparison + * result is reported through {@link #valid()}. + */ +public final class Version implements Comparable { + + public static final Version ZERO = new Version(new int[0], "", true); + + private final int[] parts; + private final String tag; + private final boolean valid; + + private Version(int[] parts, String tag, boolean valid) { + this.parts = parts; + this.tag = tag; + this.valid = valid; + } + + /** False when the input could not be parsed at all. */ + public boolean valid() { + return valid; + } + + public static Version parse(String raw) { + if (raw == null) { + return new Version(new int[0], "", false); + } + String v = raw.trim(); + if (v.length() == 0) { + return new Version(new int[0], "", false); + } + if (v.startsWith("v") || v.startsWith("V")) { + v = v.substring(1); + } + + String tag = ""; + int cut = indexOfTag(v); + if (cut >= 0) { + tag = v.substring(cut + 1); + v = v.substring(0, cut); + } + + String[] rawParts = v.split("\\."); + int[] out = new int[rawParts.length]; + for (int i = 0; i < rawParts.length; i++) { + int n = 0; + String p = rawParts[i]; + if (p.length() == 0) { + return new Version(new int[0], "", false); + } + for (int c = 0; c < p.length(); c++) { + char ch = p.charAt(c); + if (ch < '0' || ch > '9') { + return new Version(new int[0], "", false); + } + n = n * 10 + (ch - '0'); + } + out[i] = n; + } + return new Version(out, tag, true); + } + + private static int indexOfTag(String v) { + for (int i = 0; i < v.length(); i++) { + char ch = v.charAt(i); + boolean separator = ch == '-' || ch == '+' || ch == '_'; + if (separator) { + return i; + } + } + return -1; + } + + public int compareTo(Version o) { + int n = Math.max(parts.length, o.parts.length); + for (int i = 0; i < n; i++) { + int a = i < parts.length ? parts[i] : 0; + int b = i < o.parts.length ? o.parts[i] : 0; + if (a != b) { + return a < b ? -1 : 1; + } + } + if (tag.length() == 0 && o.tag.length() == 0) { + return 0; + } + if (tag.length() == 0) { + return 1; + } + if (o.tag.length() == 0) { + return -1; + } + return tag.compareTo(o.tag); + } + + public boolean atLeast(Version o) { + return compareTo(o) >= 0; + } + + public boolean below(Version o) { + return compareTo(o) < 0; + } + + public boolean same(Version o) { + return compareTo(o) == 0; + } + + @Override + public boolean equals(Object o) { + return o instanceof Version && compareTo((Version) o) == 0; + } + + @Override + public int hashCode() { + int h = 0; + for (int p : parts) { + h = h * 31 + p; + } + return h * 31 + tag.hashCode(); + } + + @Override + public String toString() { + if (!valid) { + return "invalid"; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < parts.length; i++) { + if (i > 0) { + sb.append('.'); + } + sb.append(parts[i]); + } + if (tag.length() > 0) { + sb.append('-').append(tag); + } + return sb.toString(); + } +} \ No newline at end of file diff --git a/rsx-client/src/main/java/net/rustnotmc/loader/VersionConstraint.java b/rsx-client/src/main/java/net/rustnotmc/loader/VersionConstraint.java new file mode 100644 index 0000000..c4b6079 --- /dev/null +++ b/rsx-client/src/main/java/net/rustnotmc/loader/VersionConstraint.java @@ -0,0 +1,199 @@ +package net.rustnotmc.loader; + +import java.util.ArrayList; +import java.util.List; + +/** + * A version requirement, used by dependencies. + * + * Accepts what mod authors actually write: + * + * * any version + * 1.2.3 exactly this + * >=1.0 at least this (also >, <=, <) + * 1.2.x 1.2.0 up to but not including 1.3.0 + * >=1.0,<2.0 a range, all parts must hold + * + * A version that cannot be parsed never matches anything. Silently accepting a + * broken requirement would let a mod load against an API it was never written for. + */ +public final class VersionConstraint { + + private static final int EQ = 0; + private static final int GE = 1; + private static final int GT = 2; + private static final int LE = 3; + private static final int LT = 4; + + private static final VersionConstraint ANY = new VersionConstraint(null); + + private final Term[] terms; + + private VersionConstraint(Term[] terms) { + this.terms = terms; + } + + public static VersionConstraint any() { + return ANY; + } + + public static VersionConstraint parse(String raw) { + if (raw == null) { + return ANY; + } + String v = raw.trim(); + if (v.length() == 0 || v.equals("*")) { + return ANY; + } + + List out = new ArrayList(); + for (String part : v.split(",")) { + Term t = parseOne(part.trim()); + if (t == null) { + return new VersionConstraint(new Term[0]); + } + out.add(t); + } + if (out.isEmpty()) { + return new VersionConstraint(new Term[0]); + } + return new VersionConstraint(out.toArray(new Term[0])); + } + + private static Term parseOne(String s) { + if (s.length() == 0) { + return null; + } + int op = EQ; + if (s.startsWith(">=")) { + op = GE; + s = s.substring(2); + } else if (s.startsWith("<=")) { + op = LE; + s = s.substring(2); + } else if (s.startsWith(">")) { + op = GT; + s = s.substring(1); + } else if (s.startsWith("<")) { + op = LT; + s = s.substring(1); + } else if (s.startsWith("=")) { + s = s.substring(1); + } + s = s.trim(); + if (s.length() == 0) { + return null; + } + + int star = indexOfWildcard(s); + if (star >= 0) { + // 1.2.x becomes >=1.2.0 and <1.3.0, which the compare already knows + String prefix = s.substring(0, star); + while (prefix.endsWith(".")) { + prefix = prefix.substring(0, prefix.length() - 1); + } + if (prefix.length() == 0) { + return new Term(GE, Version.ZERO, null); + } + Version low = Version.parse(prefix); + if (!low.valid()) { + return null; + } + Version high = bumpLast(prefix); + return new Term(0, low, high); + } + + Version exact = Version.parse(s); + if (!exact.valid()) { + return null; + } + return new Term(op, exact, null); + } + + private static int indexOfWildcard(String s) { + for (int i = 0; i < s.length(); i++) { + char ch = s.charAt(i); + if (ch == 'x' || ch == 'X' || ch == '*') { + return i; + } + } + return -1; + } + + /** + * The first version above a wildcard prefix. + * + * "1.2" becomes 1.3.0, not 2.0.0: a wildcard covers the rest of that segment + * only, so 1.2.x must reject 1.3.0 and accept 1.2.99. + */ + private static Version bumpLast(String prefix) { + String[] parts = prefix.split("\\."); + int[] seg = new int[parts.length]; + for (int i = 0; i < parts.length; i++) { + try { + seg[i] = Integer.parseInt(parts[i]); + } catch (NumberFormatException e) { + seg[i] = 0; + } + } + seg[seg.length - 1]++; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < seg.length; i++) { + if (i > 0) { + sb.append('.'); + } + sb.append(seg[i]); + } + sb.append(".0"); + return Version.parse(sb.toString()); + } + + /** True when the constraint accepts the given version. */ + public boolean accepts(Version v) { + if (terms == null) { + return true; + } + if (terms.length == 0) { + return false; + } + if (v == null || !v.valid()) { + return false; + } + for (Term t : terms) { + if (!t.holds(v)) { + return false; + } + } + return true; + } + + private static final class Term { + final int op; + final Version value; + final Version high; + + Term(int op, Version value, Version high) { + this.op = op; + this.value = value; + this.high = high; + } + + boolean holds(Version v) { + if (high != null) { + return v.atLeast(value) && v.below(high); + } + switch (op) { + case GE: + return v.atLeast(value); + case GT: + return v.compareTo(value) > 0; + case LE: + return v.compareTo(value) <= 0; + case LT: + return v.below(value); + default: + return v.same(value); + } + } + } +} \ No newline at end of file diff --git a/rsx-client/src/test/java/net/rustnotmc/test/SelfTest.java b/rsx-client/src/test/java/net/rustnotmc/test/SelfTest.java new file mode 100644 index 0000000..c897625 --- /dev/null +++ b/rsx-client/src/test/java/net/rustnotmc/test/SelfTest.java @@ -0,0 +1,615 @@ +package net.rustnotmc.test; + +import net.rustnotmc.api.client.ClientCommand; +import net.rustnotmc.api.client.ClientHost; +import net.rustnotmc.api.client.ClientServices; +import net.rustnotmc.api.client.CommandArgs; +import net.rustnotmc.api.client.CommandScope; +import net.rustnotmc.api.client.ScreenKind; +import net.rustnotmc.api.hud.Anchor; +import net.rustnotmc.api.hud.HudLayout; +import net.rustnotmc.api.hud.HudSurface; +import net.rustnotmc.loader.Access; +import net.rustnotmc.loader.EventBus; +import net.rustnotmc.loader.GameAccess; +import net.rustnotmc.loader.ModContainer; +import net.rustnotmc.loader.ModDescriptor; +import net.rustnotmc.loader.ModEnv; +import net.rustnotmc.loader.ModLoader; +import net.rustnotmc.loader.ModState; +import net.rustnotmc.loader.TickScheduler; +import net.rustnotmc.loader.Version; +import net.rustnotmc.loader.VersionConstraint; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.Properties; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +/** + * Self test for the loader. + * + * Real jars are built on the fly and loaded through the real loader, so this + * covers descriptor parsing, dependency ordering, lifecycle, permissions, the + * event bus, the scheduler and the HUD layout rather than just their internals. + */ +public final class SelfTest { + + private static int checks; + private static int failures; + private static final List FAILED = new ArrayList(); + + public static void main(String[] args) throws Exception { + File work = new File(args.length > 0 ? args[0] : "build/selftest"); + deleteTree(work); + File mods = new File(work, "mods"); + mods.mkdirs(); + + versions(); + constraints(); + access(); + clientApi(); + descriptors(mods); + loadOrder(mods); + lifecycle(mods); + failures(mods); + missingDependency(mods); + versionMismatch(mods); + cycles(mods); + permissions(mods); + events(mods); + scheduler(mods); + hud(); + + System.out.println(); + System.out.println("checks: " + checks + ", failed: " + failures); + if (!FAILED.isEmpty()) { + for (int i = 0; i < FAILED.size(); i++) { + System.out.println(" FAIL " + FAILED.get(i)); + } + System.exit(1); + } + System.out.println("OK"); + } + + // ------------------------------------------------------------ assertions + + private static void check(String what, boolean ok) { + checks++; + if (!ok) { + failures++; + FAILED.add(what); + System.out.println(" fail: " + what); + } + } + + private static void eq(String what, Object expected, Object actual) { + boolean ok = expected == null ? actual == null : expected.equals(actual); + checks++; + if (!ok) { + failures++; + FAILED.add(what + " (want " + expected + ", got " + actual + ")"); + System.out.println(" fail: " + what + " want=" + expected + " got=" + actual); + } + } + + // ----------------------------------------------------------------- tests + + private static void versions() { + eq("version parse", "1.2.3", Version.parse("1.2.3").toString()); + eq("version parse v prefix", "1.2.3", Version.parse("v1.2.3").toString()); + check("version trailing zero equals", Version.parse("1.2").same(Version.parse("1.2.0"))); + check("version shorter is lower", Version.parse("1.2").below(Version.parse("1.2.1"))); + check("version major wins", Version.parse("2.0").atLeast(Version.parse("1.9.9"))); + check("prerelease below release", + Version.parse("1.2.3-beta").below(Version.parse("1.2.3"))); + check("invalid version", !Version.parse("not-a-version").valid()); + check("null version invalid", !Version.parse(null).valid()); + } + + private static void constraints() { + eq("constraint any accepts", true, VersionConstraint.any().accepts(Version.parse("9.9"))); + eq("constraint exact", true, + VersionConstraint.parse("1.2.3").accepts(Version.parse("1.2.3"))); + eq("constraint exact rejects", false, + VersionConstraint.parse("1.2.3").accepts(Version.parse("1.2.4"))); + eq("constraint >=", true, + VersionConstraint.parse(">=1.2").accepts(Version.parse("1.3"))); + eq("constraint >=", false, + VersionConstraint.parse(">=1.2").accepts(Version.parse("1.1"))); + eq("constraint <", true, + VersionConstraint.parse("<2.0").accepts(Version.parse("1.9.9"))); + eq("constraint wildcard low", true, + VersionConstraint.parse("1.2.x").accepts(Version.parse("1.2.7"))); + eq("constraint wildcard high", false, + VersionConstraint.parse("1.2.x").accepts(Version.parse("1.3.0"))); + eq("constraint star", true, + VersionConstraint.parse("*").accepts(Version.parse("0.1"))); + eq("constraint compound ok", true, + VersionConstraint.parse(">=1.0,<2.0").accepts(Version.parse("1.5"))); + eq("constraint compound fails", false, + VersionConstraint.parse(">=1.0,<2.0").accepts(Version.parse("2.5"))); + eq("garbage constraint matches nothing", false, + VersionConstraint.parse("!!!").accepts(Version.parse("1.0"))); + eq("invalid version never matches", false, + VersionConstraint.parse(">=1.0").accepts(Version.parse("bad"))); + } + + private static void access() { + eq("access parse", Access.HUD, Access.byId("hud")); + eq("access parse case", Access.INPUT, Access.byId("INPUT")); + eq("access plural", Access.COMMANDS, Access.byId("commands")); + eq("access unknown", null, Access.byId("teleport")); + eq("access list size", 3, + Access.parseList("hud, input, commands").size()); + + // the old set promised world, player, network and text to every mod and + // checked none of them; they must stay gone rather than creep back + eq("world capability removed", null, Access.byId("world")); + eq("player capability removed", null, Access.byId("player")); + eq("network capability removed", null, Access.byId("network")); + eq("text capability removed", null, Access.byId("text")); + eq("a retired name is recognised as retired", true, Access.isRetired("world")); + eq("an unknown name is not retired", false, Access.isRetired("teleport")); + + // asking for a retired capability must grant nothing, not fall back + eq("retired capability grants nothing", 0, + Access.parseList("world,player,network").size()); + + check("reflection not default", !Access.REFLECTION.grantedByDefault()); + check("hud default", Access.HUD.grantedByDefault()); + check("core default", Access.CORE.grantedByDefault()); + check("input not default", !Access.INPUT.grantedByDefault()); + check("commands not default", !Access.COMMANDS.grantedByDefault()); + check("files not default", !Access.FILES.grantedByDefault()); + } + + /** + * The client API must work with no game attached, which is what keeps the + * loader buildable and testable on its own. + */ + private static void clientApi() { + check("no client attached", !ClientHost.present()); + ClientServices c = ClientHost.current(); + check("detached client handed out", c != null); + eq("detached screen kind", ScreenKind.UNKNOWN, c.screen().kind()); + eq("detached screen size", 0, c.screen().width()); + check("detached input reads nothing", !c.input().keyDown("key.sneak")); + check("detached mouse is not down", !c.input().mouseDown(0)); + check("denied input matches detached", !ClientHost.deniedInput().anyKeyDown()); + eq("detached tick", 0L, c.tick()); + + eq("command parses", "rsx", CommandArgs.parse("/rsx a b", "me").name()); + CommandArgs parsed = CommandArgs.parse("/rsx one two", "me"); + eq("command arg count", 2, parsed.count()); + eq("command arg 0", "one", parsed.arg(0, "?")); + eq("command missing arg falls back", "?", parsed.arg(9, "?")); + eq("command int arg", 12, CommandArgs.parse("/x 12", "me").argInt(0, -1)); + eq("command non-int falls back", -1, CommandArgs.parse("/x abc", "me").argInt(0, -1)); + eq("command sender", "me", parsed.sender()); + eq("command toString", "/rsx one two", parsed.toString()); + + check("client scope runs locally", CommandScope.CLIENT.clientExecutes()); + check("server scope does not run locally", !CommandScope.SERVER.clientExecutes()); + check("both scope is server decided", CommandScope.BOTH.serverDecides()); + eq("scope by name", CommandScope.BOTH, CommandScope.byName("both", null)); + + check("detached commands refuse registration", + !c.commands().register("x", new ClientCommand() { + public void run(CommandArgs a) { + } + }, CommandScope.CLIENT)); + eq("detached command count", 0, c.commands().count()); + } + + private static void descriptors(File mods) throws Exception { + File jar = modJar(mods, "desc-only.jar", new Properties() {{ + setProperty("id", "desc"); + setProperty("version", "2.5.1"); + setProperty("entrypoint", "net.rustnotmc.test.TestMods$LibMod"); + }}); + ModDescriptor d = ModDescriptor.load(jar); + check("descriptor read", d != null); + eq("descriptor id", "desc", d.id()); + eq("descriptor version", "2.5.1", d.version().toString()); + eq("descriptor name defaults to id", "desc", d.name()); + eq("descriptor env defaults to both", ModEnv.BOTH, d.env()); + eq("descriptor api default", 1, d.apiVersion()); + + File noEntry = modJar(mods, "desc-noentry.jar", new Properties() {{ + setProperty("id", "broken"); + }}); + eq("descriptor without entrypoint is rejected", null, ModDescriptor.load(noEntry)); + + File badId = modJar(mods, "desc-badid.jar", new Properties() {{ + setProperty("id", "Bad Id!"); + setProperty("entrypoint", "x.Y"); + }}); + eq("descriptor with illegal id is rejected", null, ModDescriptor.load(badId)); + + eq("env client", ModEnv.CLIENT, ModEnv.parse("client")); + eq("env server", ModEnv.SERVER, ModEnv.parse("SERVER")); + eq("env both", ModEnv.BOTH, ModEnv.parse("anything")); + check("client mod runs on client", ModEnv.CLIENT.runsOn(ModEnv.CLIENT)); + check("client mod does not run on server", !ModEnv.CLIENT.runsOn(ModEnv.SERVER)); + check("both runs everywhere", ModEnv.BOTH.runsOn(ModEnv.SERVER)); + + // dependencies parsed out of the properties + Properties p = new Properties(); + p.setProperty("id", "withdeps"); + p.setProperty("entrypoint", "x.Y"); + p.setProperty("depends.core", ">=1.0"); + p.setProperty("depends.other", "*"); + p.setProperty("softdepends.optional", "1.2"); + ModDescriptor wd = ModDescriptor.from(p, null); + eq("hard dependency count", 2, wd.dependencies().size()); + eq("soft dependency count", 1, wd.softDependencies().size()); + eq("dependency keeps its text", "core >=1.0", wd.dependencies().get(0).toString()); + } + + private static void loadOrder(File mods) throws Exception { + File dir = new File(mods, "order"); + dir.mkdirs(); + // a needs b, b needs nothing; ids chosen so alphabetical order is the wrong one + jar(dir, "zzz-a.jar", "aaa", "0.1.0", "net.rustnotmc.test.TestMods$NeedsLib", + props("depends.bbb", ">=1.0.0")); + jar(dir, "mmm-b.jar", "bbb", "1.2.0", "net.rustnotmc.test.TestMods$LibMod", null); + + ModLoader l = new ModLoader(dir); + eq("discovered", 2, l.discover()); + check("resolve ok", l.resolve()); + List order = l.loadOrder(); + eq("load order size", 2, order.size()); + eq("dependency loads first", "bbb", order.get(0)); + eq("dependent loads second", "aaa", order.get(1)); + } + + private static void lifecycle(File mods) throws Exception { + File dir = new File(mods, "life"); + dir.mkdirs(); + jar(dir, "a.jar", "aaa", "0.1.0", "net.rustnotmc.test.TestMods$NeedsLib", + props("depends.bbb", ">=1.0.0")); + jar(dir, "b.jar", "bbb", "1.2.0", "net.rustnotmc.test.TestMods$LibMod", null); + + TestMods.Trace.clear(); + ModLoader l = new ModLoader(dir); + l.discover(); + l.resolve(); + eq("two mods started", 2, l.loadAll()); + + String started = TestMods.Trace.joined(); + check("library constructed first", started.indexOf("lib:construct") < started.indexOf("needs:load")); + check("everything enabled", started.endsWith("lib:enable,needs:enable") + || started.contains("lib:enable") && started.contains("needs:enable")); + + ModContainer lib = l.mod("bbb"); + eq("state enabled", ModState.ENABLED, lib.state()); + eq("context id", "bbb", lib.context().modId()); + check("core granted", lib.context().has(Access.CORE)); + + l.unloadAll(); + String stopped = TestMods.Trace.joined(); + check("dependent disables before its library", + stopped.indexOf("needs:disable") < stopped.indexOf("lib:disable")); + check("unload happens last", + stopped.indexOf("needs:unload") < stopped.indexOf("lib:unload")); + eq("state unloaded", ModState.UNLOADED, lib.state()); + } + + private static void failures(File mods) throws Exception { + File dir = new File(mods, "fail"); + dir.mkdirs(); + jar(dir, "good.jar", "good", "0.1.0", "net.rustnotmc.test.TestMods$LibMod", null); + jar(dir, "bad.jar", "bad", "0.1.0", "net.rustnotmc.test.TestMods$BadMod", null); + + TestMods.Trace.clear(); + ModLoader l = new ModLoader(dir); + l.discover(); + l.resolve(); + l.loadAll(); + + eq("failing mod is marked failed", ModState.FAILED, l.mod("bad").state()); + eq("good mod still enabled", ModState.ENABLED, l.mod("good").state()); + check("bad mod never enabled", !TestMods.Trace.joined().contains("bad:enable")); + } + + private static void missingDependency(File mods) throws Exception { + File dir = new File(mods, "missing"); + dir.mkdirs(); + jar(dir, "n.jar", "n", "0.1.0", "net.rustnotmc.test.TestMods$NeedsMissing", + props("depends.ghost", ">=1.0.0")); + + ModLoader l = new ModLoader(dir); + l.discover(); + check("missing dependency fails resolution", !l.resolve()); + eq("problem reported", 1, l.problems().size()); + check("problem names the dependency", + l.problems().get(0).contains("ghost")); + } + + private static void versionMismatch(File mods) throws Exception { + File dir = new File(mods, "mismatch"); + dir.mkdirs(); + jar(dir, "lib.jar", "lib", "1.0.0", "net.rustnotmc.test.TestMods$LibMod", null); + jar(dir, "new.jar", "new", "0.1.0", "net.rustnotmc.test.TestMods$NeedsNewerLib", + props("depends.lib", ">=2.0.0")); + + ModLoader l = new ModLoader(dir); + l.discover(); + check("version mismatch fails resolution", !l.resolve()); + check("problem mentions the version", + l.problems().toString().contains("2.0.0")); + } + + private static void cycles(File mods) throws Exception { + File dir = new File(mods, "cycle"); + dir.mkdirs(); + jar(dir, "a.jar", "ca", "1.0.0", "net.rustnotmc.test.TestMods$CycleA", + props("depends.cb", "*")); + jar(dir, "b.jar", "cb", "1.0.0", "net.rustnotmc.test.TestMods$CycleB", + props("depends.ca", "*")); + + ModLoader l = new ModLoader(dir); + l.discover(); + check("cycle fails resolution", !l.resolve()); + check("problem mentions a cycle", + l.problems().toString().toLowerCase().contains("cycle")); + } + + private static void permissions(File mods) throws Exception { + File dir = new File(mods, "perm"); + dir.mkdirs(); + jar(dir, "rogue.jar", "rogue", "0.1.0", "net.rustnotmc.test.TestMods$RogueMod", null); + // this one explicitly asks for reflection in its descriptor, which must + // still not be enough on its own + jar(dir, "reflect.jar", "reflect", "0.1.0", + "net.rustnotmc.test.TestMods$ReflectiveMod", + props("access", "reflection, world")); + + TestMods.RogueMod.refused = false; + TestMods.ReflectiveMod.reflectionHeld = true; + + ModLoader l = new ModLoader(dir); + l.discover(); + l.resolve(); + l.loadAll(); + + check("mod without the hud capability cannot register an overlay", + TestMods.RogueMod.refused); + check("reflection is never granted by default", + !TestMods.ReflectiveMod.reflectionHeld); + l.unloadAll(); + } + + private static void events(File mods) throws Exception { + File dir = new File(mods, "events"); + dir.mkdirs(); + jar(dir, "ev.jar", "ev", "0.1.0", "net.rustnotmc.test.TestMods$EventMod", null); + + TestMods.EventMod.ORDER.clear(); + ModLoader l = new ModLoader(dir); + l.discover(); + l.resolve(); + l.loadAll(); + + l.events().post(new TestMods.Bang()); + + List order = TestMods.EventMod.ORDER; + eq("three handlers ran", 3, order.size()); + eq("highest priority runs first", "highest", order.get(0)); + eq("throwing handler did not stop the rest", "lowest", order.get(2)); + check("throwing handler itself ran", order.contains("throws")); + + // direct bus checks + EventBus bus = new EventBus(); + final List seen = new ArrayList(); + EventBus.Subscription s = bus.subscribe("t", TestMods.Bang.class, + e -> seen.add("x")); + eq("handler count", 1, bus.handlerCount(TestMods.Bang.class)); + bus.post(new TestMods.Bang()); + eq("handler ran", 1, seen.size()); + s.cancel(); + bus.post(new TestMods.Bang()); + eq("cancelled handler did not run", 1, seen.size()); + l.unloadAll(); + } + + private static void scheduler(File mods) { + TickScheduler s = new TickScheduler(); + final int[] counter = {0}; + + TickScheduler.Task t = s.post("t", () -> counter[0]++); + eq("nothing runs before a tick", 0, counter[0]); + s.tick(); + eq("posted task ran", 1, counter[0]); + + final int[] repeats = {0}; + TickScheduler.Task r = s.everyTicks("t", 3, () -> repeats[0]++); + for (int i = 0; i < 9; i++) { + s.tick(); + } + eq("every 3 ticks ran 3 times", 3, repeats[0]); + r.cancel(); + for (int i = 0; i < 9; i++) { + s.tick(); + } + eq("cancelled task stopped", 3, repeats[0]); + + // a task that always throws gets dropped rather than flooding the log + final int[] attempts = {0}; + s.everyTicks("t", 1, () -> { + attempts[0]++; + throw new IllegalStateException("on purpose"); + }); + for (int i = 0; i < 20; i++) { + s.tick(); + } + check("failing task was dropped", attempts[0] <= 6); + s.shutdown(); + eq("tick counter advanced", 39L, s.tickCount()); + } + + private static void hud() { + HudLayout.clear(); + final List painted = new ArrayList(); + + HudSurface surface = new HudSurface() { + public boolean available() { + return true; + } + + public int width() { + return 800; + } + + public int height() { + return 600; + } + + public float scale() { + return 2f; + } + + public void rect(float x, float y, float w, float h, int rgb, float a) { + } + + public void text(float x, float y, String v, int rgb, float a) { + painted.add(v + "@" + x + "," + y); + } + + public float textWidth(String v) { + return v.length() * 6f; + } + + public int textHeight() { + return 9; + } + }; + + HudLayout.add("top", draw("top"), Anchor.TOP_LEFT, 100, 0, 0); + HudLayout.add("bottom", draw("bottom"), Anchor.BOTTOM_LEFT, 100, 0, 0); + HudLayout.add("over", draw("over"), Anchor.TOP_LEFT, 500, 0, 0); + + HudLayout.render(surface); + + eq("three overlays drawn", 3, painted.size()); + eq("top left sits at the origin", "top@0.0,0.0", painted.get(0)); + eq("bottom left uses the screen height", "bottom@0.0,591.0", painted.get(1)); + eq("higher layer draws last", "over@0.0,0.0", painted.get(2)); + + HudLayout.clear(); + eq("cleared", 0, HudLayout.size()); + + eq("anchor centre", 390, Anchor.TOP_CENTER.x(800, 20)); + eq("anchor right", 780, Anchor.TOP_RIGHT.x(800, 20)); + eq("anchor centre y", 295, Anchor.CENTER.y(600, 10)); + eq("anchor by name", Anchor.BOTTOM_RIGHT, Anchor.byName("bottom_right", null)); + eq("anchor by legacy name", Anchor.TOP_LEFT, Anchor.byName("left-top", null)); + eq("anchor fallback on garbage", Anchor.TOP_LEFT, + Anchor.byName("nonsense", Anchor.TOP_LEFT)); + } + + private static HudLayout.Draw draw(final String label) { + return new HudLayout.Draw() { + public int width(HudSurface hud) { + return label.length() * 6; + } + + public int height(HudSurface hud) { + return 9; + } + + public void draw(HudSurface hud, int x, int y, int w, int h) { + hud.text(x, y, label, 0xFFFFFF, 1f); + } + }; + } + + // ------------------------------------------------------------- jar making + + private static Properties props(String key, String value) { + Properties p = new Properties(); + p.setProperty(key, value); + return p; + } + + private static File modJar(File dir, String name, Properties p) throws IOException { + JarOutputStream out = new JarOutputStream(new FileOutputStream(new File(dir, name))); + out.putNextEntry(new JarEntry("META-INF/rsxmod.properties")); + p.store(out, null); + out.closeEntry(); + out.close(); + return new File(dir, name); + } + + private static File jar(File dir, String fileName, String id, String version, + String entrypoint, Properties extra) throws IOException { + File target = new File(dir, fileName); + Properties p = new Properties(); + p.setProperty("id", id); + p.setProperty("version", version); + p.setProperty("entrypoint", entrypoint); + p.setProperty("env", "CLIENT"); + if (extra != null) { + for (Enumeration e = extra.propertyNames(); e.hasMoreElements(); ) { + String k = (String) e.nextElement(); + p.setProperty(k, extra.getProperty(k)); + } + } + JarOutputStream out = new JarOutputStream(new FileOutputStream(target)); + out.putNextEntry(new JarEntry("META-INF/rsxmod.properties")); + p.store(out, null); + out.closeEntry(); + if (entrypoint != null && entrypoint.length() > 0) { + String simple = entrypoint.substring(entrypoint.lastIndexOf('.') + 1); + copyClass(out, simple.replace('$', '$') + ".class"); + } + out.close(); + return target; + } + + /** Copies a compiled test class out of the test output directory. */ + private static void copyClass(JarOutputStream out, String fileName) throws IOException { + String resource = "net/rustnotmc/test/" + fileName; + InputStream in = SelfTest.class.getClassLoader().getResourceAsStream(resource); + if (in == null) { + return; + } + out.putNextEntry(new ZipEntry(resource)); + copy(in, out); + out.closeEntry(); + in.close(); + } + + private static void copy(InputStream in, OutputStream out) throws IOException { + byte[] buf = new byte[8192]; + int n; + while ((n = in.read(buf)) > 0) { + out.write(buf, 0, n); + } + } + + private static void deleteTree(File f) { + if (f == null || !f.exists()) { + return; + } + if (f.isDirectory()) { + File[] kids = f.listFiles(); + if (kids != null) { + for (int i = 0; i < kids.length; i++) { + deleteTree(kids[i]); + } + } + } + f.delete(); + } +} \ No newline at end of file diff --git a/rsx-client/src/test/java/net/rustnotmc/test/TestMods.java b/rsx-client/src/test/java/net/rustnotmc/test/TestMods.java new file mode 100644 index 0000000..3aba520 --- /dev/null +++ b/rsx-client/src/test/java/net/rustnotmc/test/TestMods.java @@ -0,0 +1,184 @@ +package net.rustnotmc.test; + +import net.rustnotmc.api.Rsx; +import net.rustnotmc.api.RustNotMcMod; +import net.rustnotmc.loader.Access; +import net.rustnotmc.loader.EventBus; + +/** + * Mods the self test packages into real jars. + * + * They are nested classes on purpose: the class files are easy to name and copy + * when building a test jar, and they keep the whole suite in one place. + */ +public final class TestMods { + + private TestMods() { + } + + /** Records what the loader did, so the test can assert on the order. */ + public static final class Trace { + public static final java.util.List EVENTS = + java.util.Collections.synchronizedList(new java.util.ArrayList()); + + public static void clear() { + EVENTS.clear(); + } + + public static String joined() { + return String.join(",", EVENTS); + } + } + + /** Plain mod, no dependencies, needs the HUD capability. */ + public static class LibMod implements RustNotMcMod { + public Rsx rsx; + + public void onConstruct(Rsx ctx) { + this.rsx = ctx; + Trace.EVENTS.add("lib:construct"); + } + + public void onLoad() { + Trace.EVENTS.add("lib:load"); + } + + public void onEnable() { + Trace.EVENTS.add("lib:enable"); + } + + public void onDisable() { + Trace.EVENTS.add("lib:disable"); + } + + public void onUnload() { + Trace.EVENTS.add("lib:unload"); + } + } + + /** Depends on the library, so it must load after it. */ + public static class NeedsLib implements RustNotMcMod { + public void onLoad() { + Trace.EVENTS.add("needs:load"); + } + + public void onEnable() { + Trace.EVENTS.add("needs:enable"); + } + + public void onDisable() { + Trace.EVENTS.add("needs:disable"); + } + + public void onUnload() { + Trace.EVENTS.add("needs:unload"); + } + } + + /** Throws during load; must fail alone and leave the others running. */ + public static class BadMod implements RustNotMcMod { + public void onLoad() { + Trace.EVENTS.add("bad:load"); + throw new IllegalStateException("on purpose"); + } + + public void onEnable() { + Trace.EVENTS.add("bad:enable"); + } + } + + /** Depends on something that is never installed. */ + public static class NeedsMissing implements RustNotMcMod { + public void onLoad() { + Trace.EVENTS.add("missing:load"); + } + } + + /** Asks for a version of the library that is not installed. */ + public static class NeedsNewerLib implements RustNotMcMod { + public void onLoad() { + Trace.EVENTS.add("newer:load"); + } + } + + /** Two mods that need each other. */ + public static class CycleA implements RustNotMcMod { + public void onLoad() { + Trace.EVENTS.add("a:load"); + } + } + + public static class CycleB implements RustNotMcMod { + public void onLoad() { + Trace.EVENTS.add("b:load"); + } + } + + /** Declares no capabilities and tries to draw anyway. */ + public static class RogueMod implements RustNotMcMod { + public static boolean refused; + + public Rsx ctx; + + public void onConstruct(Rsx c) { + this.ctx = c; + } + + public void onEnable() { + refused = ctx.hud("rogue", new net.rustnotmc.api.hud.HudLayout.Draw() { + public int width(net.rustnotmc.api.hud.HudSurface hud) { + return 10; + } + + public int height(net.rustnotmc.api.hud.HudSurface hud) { + return 10; + } + + public void draw(net.rustnotmc.api.hud.HudSurface hud, int x, int y, + int w, int h) { + // must never run + } + }, net.rustnotmc.api.hud.Anchor.TOP_LEFT) == null; + } + } + + /** Requests raw reflection; the loader must not grant it. */ + public static class ReflectiveMod implements RustNotMcMod { + public static boolean reflectionHeld; + + public Rsx ctx; + + public void onConstruct(Rsx c) { + this.ctx = c; + } + + public void onEnable() { + reflectionHeld = ctx.has(Access.REFLECTION); + } + } + + /** Registers handlers that throw, to prove one bad mod does not silence others. */ + public static class EventMod implements RustNotMcMod { + public static final java.util.List ORDER = + java.util.Collections.synchronizedList(new java.util.ArrayList()); + + public Rsx ctx; + + public void onConstruct(Rsx c) { + this.ctx = c; + } + + public void onEnable() { + ctx.on(Bang.class, EventBus.LOWEST, e -> ORDER.add("lowest")); + ctx.on(Bang.class, e -> { + ORDER.add("throws"); + throw new IllegalStateException("on purpose"); + }); + ctx.on(Bang.class, EventBus.HIGHEST, e -> ORDER.add("highest")); + } + } + + public static class Bang { + public int hits; + } +} \ No newline at end of file diff --git a/rsx-client/third_party/asm/asm-9.7.jar b/rsx-client/third_party/asm/asm-9.7.jar new file mode 100644 index 0000000000000000000000000000000000000000..fee9b028a3364169c659f6072ed53f9f09cb524d GIT binary patch literal 125428 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 XdO}IU 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 literal 0 HcmV?d00001 diff --git a/rsx-client/third_party/asm/asm-commons-9.7.jar b/rsx-client/third_party/asm/asm-commons-9.7.jar new file mode 100644 index 0000000000000000000000000000000000000000..6a7d3a1083c46f9135c70208ea18423dbd2d19eb GIT binary patch literal 73426 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 + +The before/after files hold the exact text to replace and must match the +decompiled output byte for byte. +""" + +import io +import os +import sys + + +def main(): + patch_file, java_path, before_file, after_file = sys.argv[1:5] + with io.open(before_file, encoding='utf-8') as handle: + before = handle.read().rstrip('\n') + with io.open(after_file, encoding='utf-8') as handle: + after = handle.read().rstrip('\n') + + if not before or not after: + print('ERROR: before/after must not be empty') + return 1 + if before == after: + print('ERROR: before and after are identical') + return 1 + + existing = '' + if os.path.isfile(patch_file): + with io.open(patch_file, encoding='utf-8') as handle: + existing = handle.read() + + header = '=== FILE %s\n' % java_path + if header + '--- BEFORE\n' + before + '\n--- AFTER\n' in existing: + print('patch already present') + return 0 + + with io.open(patch_file, 'a', encoding='utf-8') as handle: + if existing and not existing.endswith('\n'): + handle.write('\n') + handle.write('\n' + header) + handle.write('--- BEFORE\n' + before + '\n') + handle.write('--- AFTER\n' + after + '\n') + print('appended patch for %s' % java_path) + return 0 + + +if __name__ == '__main__': + raise SystemExit(main()) \ No newline at end of file diff --git a/rsx-client/tools/apply_patches.py b/rsx-client/tools/apply_patches.py new file mode 100644 index 0000000..e926dcd --- /dev/null +++ b/rsx-client/tools/apply_patches.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 +"""Re-apply hand fixes to a freshly decompiled tree. + +Decompiling is repeatable, our corrections to its output are not. Every manual +fix therefore lives in a patch file next to this script and is re-applied after +each decompile, so regenerating the tree never silently loses work. + +Patch format, deliberately boring: + + === FILE net/minecraft/block/BlockPortal.java + --- BEFORE + + --- AFTER + + +and for a whole file: + + === FILE net/minecraft/util/IObjectIntIterable.java + --- REPLACE + + +Comments start with '#'. Blank lines are ignored outside a block. A patch that +matches nothing is reported as a failure, because a silently skipped patch is +worse than a loud one: the tree looks right and is not. +""" + +import argparse +import os +import sys + + +def _trim(buf): + """Drops trailing blank and comment lines from a section buffer. + + A comment or a blank line always separates one patch from the next, so + without this a section would swallow the comment introducing the patch that + follows it and then never match. + """ + out = list(buf) + while out and (not out[-1].strip() or out[-1].lstrip().startswith('#')): + out.pop() + return out + + +def parse(path): + """Returns {relative_path: [(before, after, whole_file), ...]}. + + A file may carry several patches, so each path maps to a list rather than a + single entry: two patches for one file are two patches, not one that + overwrites the other. + """ + patches = {} + state = {'file': None, 'section': None, 'before': [], 'after': []} + + def flush(): + if state['file']: + if state['section'] == 'replace': + entry = (None, '\n'.join(_trim(state['after'])), True) + else: + entry = ('\n'.join(_trim(state['before'])), + '\n'.join(_trim(state['after'])), False) + patches.setdefault(state['file'], []).append(entry) + state['file'] = None + state['section'] = None + state['before'] = [] + state['after'] = [] + + with open(path, encoding='utf-8') as handle: + for raw in handle: + line = raw.rstrip('\n').rstrip('\r') + + if line.startswith('=== FILE '): + flush() + state['file'] = line[len('=== FILE '):].strip() + continue + + if line in ('--- BEFORE', '--- AFTER', '--- REPLACE'): + if not state['file']: + raise SystemExit('%s appears before any === FILE' % line) + state['section'] = line[4:].strip().lower() + continue + + if state['file'] and state['section'] == 'replace': + # A REPLACE block holds the whole file, so it would otherwise run + # on into whatever follows. Java has no line directives, so a line + # starting with '#' is safe to treat as the end of the block. + if line.startswith('#'): + flush() + else: + state['after'].append(line) + continue + + if state['file'] and state['section']: + state[state['section']].append(line) + elif line.strip() and not line.startswith('#'): + pass + + flush() + return patches + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('tree', help='decompiled source root') + parser.add_argument('patches', help='patch file') + args = parser.parse_args() + + patches = parse(args.patches) + applied = 0 + failed = [] + total = 0 + + for rel, entries in sorted(patches.items()): + target = os.path.join(args.tree, rel.replace('/', os.sep)) + if not os.path.isfile(target): + failed.append('%s: no such file' % rel) + total += len(entries) + continue + with open(target, encoding='utf-8', errors='replace') as handle: + text = handle.read() + for index, (before, after, whole) in enumerate(entries): + total += 1 + label = rel if len(entries) == 1 else '%s [%d]' % (rel, index + 1) + if whole: + with open(target, 'w', encoding='utf-8', newline='\n') as handle: + handle.write(after) + applied += 1 + continue + if before in text: + with open(target, 'w', encoding='utf-8', newline='\n') as handle: + handle.write(text.replace(before, after)) + applied += 1 + # Re-read: the next patch for this file must see what this one + # just wrote. Without this each patch writes from the snapshot + # taken before the loop, and only the last one survives. + with open(target, encoding='utf-8', errors='replace') as handle: + text = handle.read() + elif after.strip() and after.strip() in text: + # already applied, which is the normal case on a second run + applied += 1 + else: + failed.append('%s: pattern not found' % label) + + # Safety net: after writing, no BEFORE text may still be present. This is + # what catches a patch that reported success while doing nothing, which is + # the failure mode that hides longest. + stale = [] + for rel, entries in sorted(patches.items()): + target = os.path.join(args.tree, rel.replace('/', os.sep)) + if not os.path.isfile(target): + continue + with open(target, encoding='utf-8', errors='replace') as handle: + text = handle.read() + for index, (before, after, whole) in enumerate(entries): + if whole or not before: + continue + if before in text and after.strip() and after.strip() not in text: + stale.append('%s [%d]' % (rel, index + 1)) + + print('patches applied: %d of %d' % (applied, total)) + for line in failed: + print(' FAILED %s' % line) + for line in stale: + print(' NOT WRITTEN %s' % line) + return 1 if (failed or stale) else 0 + + +if __name__ == '__main__': + sys.exit(main()) \ No newline at end of file diff --git a/rsx-client/tools/fetch_missing.py b/rsx-client/tools/fetch_missing.py new file mode 100644 index 0000000..014232d --- /dev/null +++ b/rsx-client/tools/fetch_missing.py @@ -0,0 +1,70 @@ +"""Fetch library files the local meta folder never downloaded. + +The AstralRinth install has the 1.12.2 java libraries but not the +natives-windows classifiers, and the client cannot open a window without +lwjgl.dll. Rather than write into another application's cache, missing files go +into our own mirror of the same layout, which stage_run.py searches first. + + python fetch_missing.py +""" + +import json +import os +import sys +import urllib.request + + +def wanted(manifest): + """Every artifact and natives-windows path the manifest declares.""" + out = [] + for entry in manifest['libraries']: + downloads = entry.get('downloads') or {} + artifact = (downloads.get('artifact') or {}).get('path') + if artifact: + out.append(artifact) + classifiers = downloads.get('classifiers') or {} + for key, value in classifiers.items(): + if key.startswith('natives-windows') and value and value.get('path'): + out.append(value['path']) + return sorted(set(out)) + + +def main(): + dest_root, meta = sys.argv[1], sys.argv[2] + manifest_path = os.path.join(meta, 'versions', '1.12.2', '1.12.2.json') + with open(manifest_path, encoding='utf-8') as handle: + manifest = json.load(handle) + + meta_lib = os.path.join(meta, 'libraries') + base = 'https://libraries.minecraft.net/' + fetched, present, failed = [], 0, [] + + for rel in wanted(manifest): + if os.path.isfile(os.path.join(meta_lib, rel.replace('/', os.sep))): + present += 1 + continue + target = os.path.join(dest_root, rel.replace('/', os.sep)) + url = base + rel + try: + os.makedirs(os.path.dirname(target), exist_ok=True) + with urllib.request.urlopen(url, timeout=60) as response: + blob = response.read() + with open(target, 'wb') as handle: + handle.write(blob) + fetched.append('%s (%d bytes)' % (os.path.basename(rel), len(blob))) + except Exception as error: # noqa: BLE001 + failed.append('%s -> %s' % (rel, error)) + + print('already present: %d' % present) + print('fetched: %d' % len(fetched)) + for line in fetched: + print(' %s' % line) + if failed: + print('failed: %d' % len(failed)) + for line in failed: + print(' %s' % line) + return 1 if failed else 0 + + +if __name__ == '__main__': + raise SystemExit(main()) \ No newline at end of file diff --git a/rsx-client/tools/find_callers.py b/rsx-client/tools/find_callers.py new file mode 100644 index 0000000..d4befde --- /dev/null +++ b/rsx-client/tools/find_callers.py @@ -0,0 +1,57 @@ +"""Find every class whose constant pool references a named method. + +Used to answer "who actually calls this" across a whole jar, where grepping +source would only show the decompiled view and javap would need one command per +class. + + python find_callers.py +""" + +import os +import struct +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from struct_diff import Reader, read_constant_pool, CONSTANT_UTF8, SKIP_TWO_SLOT + + +def method_names(path): + with open(path, 'rb') as handle: + data = handle.read() + reader = Reader(data) + if reader.u4() != 0xCAFEBABE: + return None + reader.u2() + reader.u2() + pool = read_constant_pool(reader) + names = [] + for index, entry in enumerate(pool): + if isinstance(entry, str): + names.append(entry) + return names + + +def main(): + root, needle = sys.argv[1], sys.argv[2] + hits = [] + for base, _dirs, files in os.walk(root): + for name in files: + if not name.endswith('.class'): + continue + full = os.path.join(base, name) + key = os.path.relpath(full, root).replace(os.sep, '/') + try: + names = method_names(full) + except Exception: + continue + if names is None: + continue + if any(n == needle or n.startswith(needle) for n in names): + hits.append(key) + print('%d class(es) reference %s' % (len(hits), needle)) + for key in hits: + print(' %s' % key) + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/rsx-client/tools/find_remove_index_bugs.py b/rsx-client/tools/find_remove_index_bugs.py new file mode 100644 index 0000000..48a3f50 --- /dev/null +++ b/rsx-client/tools/find_remove_index_bugs.py @@ -0,0 +1,59 @@ +"""Find List/Set.remove(int) misdecompilations. + +FernFlower sometimes resolves List.remove(Object) to remove(int index) when the +element type is Integer. The bytecode always boxes with Integer.valueOf first, +so a source-level remove() on an Integer collection without that box is a bug: +it deletes by position, and throws IndexOutOfBounds on an empty list. + + python find_remove_index_bugs.py +""" + +import io +import os +import re +import sys + + +FIELD = re.compile(r'\b(?:private|public|protected)?\s*(?:final\s+)?' + r'((?:java\.util\.)?(?:List|Set|Collection|ArrayList|LinkedList|Queue|Deque))\s*' + r'<\s*(?:java\.lang\.)?Integer\s*>\s+(\w+)\s*[;=]') +REMOVE = re.compile(r'\b(\w+)\.remove\(([^()]*)\)') + + +def main(): + root = sys.argv[1] + hits = [] + for base, _dirs, files in os.walk(root): + for name in files: + if not name.endswith('.java'): + continue + path = os.path.join(base, name) + with io.open(path, encoding='utf-8') as handle: + text = handle.read() + fields = {} + for match in FIELD.finditer(text): + fields[match.group(2)] = match.group(1) + if not fields: + continue + for number, line in enumerate(text.split('\n'), 1): + for match in REMOVE.finditer(line): + receiver, argument = match.group(1), match.group(2).strip() + if receiver not in fields: + continue + if 'valueOf' in argument or 'Integer' in argument: + continue + hits.append((os.path.relpath(path, root).replace(os.sep, '/'), + number, receiver, argument, line.strip())) + + if not hits: + print('no List.remove(int) candidates found') + return 0 + print('%d candidate(s):' % len(hits)) + for path, number, receiver, argument, line in hits: + print(' %s:%d %s.remove(%s)' % (path, number, receiver, argument)) + print(' %s' % line) + return 0 + + +if __name__ == '__main__': + raise SystemExit(main()) \ No newline at end of file diff --git a/rsx-client/tools/find_self_comparisons.py b/rsx-client/tools/find_self_comparisons.py new file mode 100644 index 0000000..cc5a0bc --- /dev/null +++ b/rsx-client/tools/find_self_comparisons.py @@ -0,0 +1,47 @@ +"""Find comparisons of a value against itself. + +FernFlower sometimes merges two distinct locals into one name, producing +expressions like d1 != d1 that are always false. The compiler accepts them, so +they survive a clean build and silently disable the code around them. + + python find_self_comparisons.py +""" + +import io +import os +import re +import sys + +# a.b() != a.b() and simple locals: word != word, word == word +COMPARISON = re.compile(r'(? +""" + +import os +import re +import sys + +RULES = [ + ( + 'super-cast-generic-render', + # RenderLiving.doRender takes T. The decompiler inserted a cast to the + # supertype, which turns the argument into something that is no longer a T. + # Passing the variable as declared is always valid here: the parameter is + # the subclass type this renderer was parameterised with. + re.compile(r'(super\.\w+\()\((?:Entity|EntityLiving|EntityLivingBase|EntityMob)\)(\s+\w+,)'), + r'\1\2', + ), +] + + +def main(): + if len(sys.argv) != 2: + print('usage: fix_decompiler_rules.py ') + return 2 + tree = sys.argv[1] + touched = {} + files = set() + + for root, _dirs, names in os.walk(tree): + for name in names: + if not name.endswith('.java'): + continue + path = os.path.join(root, name) + with open(path, encoding='utf-8', errors='replace') as handle: + text = handle.read() + original = text + for label, pattern, replacement in RULES: + new_text = pattern.sub(replacement, text) + if new_text != text: + touched[label] = touched.get(label, 0) + \ + len(pattern.findall(text)) + text = new_text + if text != original: + files.add(path) + with open(path, 'w', encoding='utf-8', newline='\n') as handle: + handle.write(text) + + if not touched: + print('decompiler rules: nothing to change') + return 0 + for label, count in sorted(touched.items()): + print(' %s: %d' % (label, count)) + print('files rewritten: %d' % len(files)) + return 0 + + +if __name__ == '__main__': + raise SystemExit(main()) \ No newline at end of file diff --git a/rsx-client/tools/fix_generic_casts.py b/rsx-client/tools/fix_generic_casts.py new file mode 100644 index 0000000..e6d48e2 --- /dev/null +++ b/rsx-client/tools/fix_generic_casts.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python3 +"""Insert the missing cast where a generic method loses its type parameter. + +Almost every remaining error in the fork has one shape: a value whose static +type came out erased is used where the enclosing method's type parameter is +expected. + + Object cannot be converted to T + Comparable cannot be converted to T + Class cannot be converted to Class + +The cast that is already there was written to the erased supertype, so the fix +is to retype it. This is only safe when the line contains exactly one candidate +cast, which is the condition checked here; anything ambiguous is reported and +left alone. + + python fix_generic_casts.py [--apply] +""" + +import argparse +import os +import re + +ERROR = re.compile(r'^(?P[A-Za-z]:.*?\.java):(?P\d+): error: (?P.*)$') +# javac prefixes most of these with "incompatible types: ", some not at all. +PREFIX = re.compile(r'^(?:incompatible types:\s*)?') +MISMATCH = re.compile(r'^(?P[\w.]+(?:<.*?>)?(?:\[\])?) cannot be converted to (?P\w+)$') +GENERIC_MISMATCH = re.compile(r'^Class cannot be converted to Class<(?P\w+)>$') + +# A method head, used to confirm that a return really has the type we are about +# to cast it to. The type is whatever follows the last '>' of the pre-name +# segment, so a declaration like "public T getTile()" +# reports T and "private static > String getName()" reports +# String. Reading it any other way gets generics-bearing declarations wrong. +SIGNATURE = re.compile(r'^[ \t]{0,8}(?:(?:public|private|protected|static|final|abstract|' + r'synchronized|native|default|strictfp)\s+)*' + r'(?P

[\w$.<>,\[\] ]+?)\s+(?P\w+)\s*\([^;]*\)'
+                       r'\s*(?:throws\s+[\w., ]+)?\s*\{\s*$')
+
+# if / while / for and friends fit the shape above but are not method heads.
+KEYWORDS = frozenset((
+    'if', 'else', 'for', 'while', 'do', 'switch', 'case', 'try', 'catch',
+    'finally', 'return', 'new', 'synchronized', 'assert', 'throw', 'super',
+    'this', 'null', 'true', 'false',
+))
+
+
+def enclosing_return_type(lines, index):
+    # The window is generous on purpose: a method body can run long before the
+    # value that needs the cast, and missing the declaration would skip a fix
+    # that is perfectly safe.
+    for back in range(index, max(-1, index - 60), -1):
+        sig = SIGNATURE.match(lines[back])
+        if sig and sig.group('name') not in KEYWORDS:
+            return sig.group('pre').split('>')[-1].strip()
+    return None
+
+# Bare single letters and short names are the type variables javac prints.
+TYPE_VAR = re.compile(r'^[A-Z]\w{0,2}$')
+
+
+def candidates(line, from_type):
+    """Every `(Type)` cast on the line matching the type javac complained about."""
+    plain = from_type.split('<')[0]
+    found = []
+    for m in re.finditer(r'\(([^()]*)\)', line):
+        text = m.group(1).strip()
+        if text == plain or text == from_type:
+            found.append(m)
+    return found
+
+
+def main():
+    parser = argparse.ArgumentParser(description=__doc__)
+    parser.add_argument('errors')
+    parser.add_argument('--apply', action='store_true')
+    args = parser.parse_args()
+
+    per_file = {}
+    skipped = []
+    current = None
+    with open(args.errors, encoding='utf-8', errors='replace') as handle:
+        for raw in handle:
+            m = ERROR.match(raw.rstrip('\n').rstrip('\r'))
+            if not m:
+                continue
+            current = (m.group('file'), int(m.group('line')), m.group('msg'))
+            msg = PREFIX.sub('', current[2])
+
+            generic = GENERIC_MISMATCH.match(msg)
+            if generic:
+                to = generic.group('to')
+                per_file.setdefault(current[0], []).append(
+                    (current[1], to, None, msg))
+                continue
+
+            mm = MISMATCH.match(msg)
+            if not mm:
+                continue
+            to = mm.group('to')
+            if not TYPE_VAR.match(to):
+                continue
+            per_file.setdefault(current[0], []).append(
+                (current[1], to, mm.group('from'), msg))
+
+    fixed = 0
+    files_changed = 0
+
+    for path, items in sorted(per_file.items()):
+        if not os.path.isfile(path):
+            continue
+        with open(path, encoding='utf-8', errors='replace') as handle:
+            lines = handle.read().split('\n')
+
+        touched = False
+        for item in items:
+            lineno, to, from_type, message = item
+            if lineno - 1 >= len(lines):
+                continue
+            line = lines[lineno - 1]
+
+            if from_type is None:
+                # Class -> Class: the capture comes out of a getter
+                # such as getRawType(), so the cast belongs on the right hand side.
+                spots = [m for m in re.finditer(r'=\s*([^=].*?);\s*$', line)]
+                if len(spots) != 1:
+                    skipped.append('%s:%d %s (assignments=%d)'
+                                   % (os.path.basename(path), lineno, message, len(spots)))
+                    continue
+                m = spots[0]
+                lines[lineno - 1] = (line[:m.start(1)]
+                                     + '(Class<%s>) ' % to
+                                     + line[m.start(1):])
+                touched = True
+                fixed += 1
+                continue
+
+            spots = candidates(line, from_type)
+            replacement = '(%s)' % to
+
+            if len(spots) == 1:
+                m = spots[0]
+                lines[lineno - 1] = line[:m.start()] + replacement + line[m.end():]
+                touched = True
+                fixed += 1
+                continue
+
+            # No cast on the line at all: the value was simply used raw. Insert
+            # one immediately after the assignment or the return, which is where
+            # the target type is known. A return is only safe when the method
+            # actually returns that type variable: casting a String to T would
+            # introduce a new error rather than fix the old one.
+            anchor = re.search(r'(?<=return )|(?<==\s)(?!=)', line)
+            if anchor is None:
+                skipped.append('%s:%d %s (no anchor)'
+                               % (os.path.basename(path), lineno, message))
+                continue
+            cut = anchor.end()
+            if line[:cut].rstrip().endswith('return'):
+                enclosing = enclosing_return_type(lines, lineno - 1)
+                if enclosing != to:
+                    skipped.append('%s:%d %s (method returns %s, not %s)'
+                                   % (os.path.basename(path), lineno, message,
+                                      enclosing or '?', to))
+                    continue
+            lines[lineno - 1] = line[:cut] + replacement + ' ' + line[cut:]
+            touched = True
+            fixed += 1
+
+        if touched:
+            files_changed += 1
+            if args.apply:
+                with open(path, 'w', encoding='utf-8', newline='\n') as handle:
+                    handle.write('\n'.join(lines))
+
+    print('casts rewritten: %d in %d files' % (fixed, files_changed))
+    if skipped:
+        print('left alone: %d' % len(skipped))
+        for line in skipped:
+            print('  %s' % line)
+    if not args.apply:
+        print('dry run, pass --apply to write')
+    return 0
+
+
+if __name__ == '__main__':
+    raise SystemExit(main())
\ No newline at end of file
diff --git a/rsx-client/tools/fix_stale_locals.py b/rsx-client/tools/fix_stale_locals.py
new file mode 100644
index 0000000..bbe5a2f
--- /dev/null
+++ b/rsx-client/tools/fix_stale_locals.py
@@ -0,0 +1,147 @@
+#!/usr/bin/env python3
+"""Repair FernFlower's stale local variable references, scoped to the method.
+
+The decompiler renames conflicting locals only sometimes: a declaration ends up
+as `variantlist` while its uses stay `variantlist1`, and javac reports
+"cannot find symbol ... variable variantlist1".
+
+Renaming is confined to the enclosing method. That matters because the suffixed
+name is often a real, correctly declared variable in a *different* method of the
+same class, and a whole-file rename would corrupt it.
+
+The method is found by its declaration line and the closing brace at the same
+indentation, which is reliable for well-formed decompiled output.
+
+    python fix_stale_locals.py  [--apply]
+"""
+
+import argparse
+import collections
+import os
+import re
+
+ERROR = re.compile(r'^(?P[A-Za-z]:.*?\.java):(?P\d+): error: (?P.*)$')
+SYMBOL = re.compile(r'^ {2}symbol:\s*variable (?P\w+)\s*$')
+DECL = re.compile(r'^[ \t]*(?:final\s+)?[A-Za-z_$][\w$.<>,\[\] ]*\s+(?P[A-Za-z_$]\w*)\s*(?==|;|,|\))')
+METHOD = re.compile(r'^[ \t]{0,8}(?:(?:public|private|protected|static|final|abstract|synchronized|native|default|strictfp)\s+)*'
+                    r'[\w$.<>,\[\] ]+\s+\w+\s*\([^;]*\)\s*(?:throws\s+[\w., ]+)?\s*\{\s*$')
+
+
+def method_regions(lines):
+    """Returns [(start_index, end_index_exclusive, indent)] for each method."""
+    regions = []
+    for i, line in enumerate(lines):
+        if not METHOD.match(line):
+            continue
+        indent = len(line) - len(line.lstrip())
+        close = ' ' * indent + '}'
+        for j in range(i + 1, len(lines)):
+            if lines[j].rstrip() == close:
+                regions.append((i, j + 1, indent))
+                break
+    return regions
+
+
+def region_of(regions, index):
+    for start, end, _ in regions:
+        if start <= index < end:
+            return start, end
+    return None
+
+
+def main():
+    parser = argparse.ArgumentParser(description=__doc__)
+    parser.add_argument('errors')
+    parser.add_argument('--apply', action='store_true')
+    args = parser.parse_args()
+
+    missing = collections.defaultdict(set)
+    current = None
+    with open(args.errors, encoding='utf-8', errors='replace') as handle:
+        for raw in handle:
+            line = raw.rstrip('\n').rstrip('\r')
+            m = ERROR.match(line)
+            if m:
+                current = m.group('file')
+                continue
+            s = SYMBOL.match(line)
+            if s and current:
+                missing[current].add(s.group('name'))
+
+    files_changed = 0
+    renames = 0
+    skipped = []
+
+    for path in sorted(missing):
+        if not os.path.isfile(path):
+            continue
+        with open(path, encoding='utf-8', errors='replace') as handle:
+            text = handle.read()
+        lines = text.split('\n')
+        regions = method_regions(lines)
+
+        # for each stale name, which regions hold the clean declaration
+        edits = collections.defaultdict(list)  # old name -> list of (start, end)
+        for name in sorted(missing[path]):
+            pattern = re.compile(r'\b%s\b' % re.escape(name))
+            longer = re.compile(r'\b%s\d+\b' % re.escape(name))
+            shorter = name.rstrip('0123456789')
+            shorter_pattern = re.compile(r'\b%s\b' % re.escape(shorter)) if shorter != name else None
+
+            hits = 0
+            for start, end, _indent in regions:
+                chunk = '\n'.join(lines[start:end])
+                if not pattern.search(chunk):
+                    continue
+                # Either direction may be the stale one: sometimes the use kept
+                # the short name while the declaration gained a suffix.
+                target = None
+                if shorter_pattern is not None and shorter_pattern.search(chunk):
+                    target = shorter
+                else:
+                    seen = {m.group(0) for m in longer.finditer(chunk)}
+                    if len(seen) == 1:
+                        target = seen.pop()
+                if target is None:
+                    continue
+                edits[name].append((start, end, target))
+                hits += 1
+
+            if not hits:
+                skipped.append('%s: nothing in scope matches %s'
+                               % (os.path.basename(path), name))
+            else:
+                renames += hits
+
+        if not edits:
+            continue
+
+        # rebuild from the last region so earlier edits keep their positions
+        replacements = []
+        for name, spans in edits.items():
+            for start, end, target in spans:
+                chunk = '\n'.join(lines[start:end])
+                chunk = re.sub(r'\b%s\b' % re.escape(name), target, chunk)
+                replacements.append((start, end, chunk))
+
+        replacements.sort(key=lambda t: t[0], reverse=True)
+        for start, end, chunk in replacements:
+            lines[start:end] = chunk.split('\n')
+
+        files_changed += 1
+        if args.apply:
+            with open(path, 'w', encoding='utf-8', newline='\n') as handle:
+                handle.write('\n'.join(lines))
+
+    print('methods fixed: %d across %d files' % (renames, files_changed))
+    if skipped:
+        print('unresolved %d:' % len(skipped))
+        for line in sorted(set(skipped)):
+            print('  %s' % line)
+    if not args.apply:
+        print('dry run, pass --apply to write')
+    return 0
+
+
+if __name__ == '__main__':
+    raise SystemExit(main())
\ No newline at end of file
diff --git a/rsx-client/tools/merge_srg_mcp.py b/rsx-client/tools/merge_srg_mcp.py
new file mode 100644
index 0000000..019d46b
--- /dev/null
+++ b/rsx-client/tools/merge_srg_mcp.py
@@ -0,0 +1,139 @@
+#!/usr/bin/env python3
+"""Merge a vanilla SRG with the MCP CSVs into a single obf -> MCP SRG.
+
+Why this exists
+---------------
+The vanilla mapping for 1.12.2 arrives as `joined.srg`, whose right hand side is
+in SRG names: classes already carry their MCP names, but fields and methods are
+still `field_96303_A` / `func_175744_a`. The MCP CSVs are the table that turns
+those into `fancyStyling` / `isFancyStyling`.
+
+SpecialSource can then apply the merged file in one pass, which avoids two
+things that a tiny v2 pipeline would require: reading every class file to learn
+member descriptors, and chaining two remappers.
+
+    python merge_srg_mcp.py joined.srg mcp-stable out.srg
+
+Any member missing from the CSVs keeps its SRG name. That is deliberate and
+matches how a normal development jar looks: mapped where MCP knows the name,
+`field_XXXX_` where it does not.
+"""
+
+import argparse
+import csv
+import sys
+
+
+def read_csv(path):
+    """searge name -> MCP name."""
+    table = {}
+    try:
+        with open(path, newline='', encoding='utf-8') as handle:
+            reader = csv.reader(handle)
+            header = next(reader, None)
+            if header is None:
+                return table
+            # the first column is the SRG name, the second the MCP name
+            for row in reader:
+                if len(row) < 2:
+                    continue
+                searge = row[0].strip()
+                named = row[1].strip()
+                if searge and named:
+                    table[searge] = named
+    except OSError as exc:
+        print('cannot read %s: %s' % (path, exc), file=sys.stderr)
+    return table
+
+
+def merge_member_name(token, table):
+    """`owner/srgName` -> `owner/mcpName`, keeping the owner untouched."""
+    if '/' not in token:
+        return token
+    owner, _, name = token.rpartition('/')
+    return owner + '/' + table.get(name, name)
+
+
+def main():
+    parser = argparse.ArgumentParser(description=__doc__)
+    parser.add_argument('srg', help='vanilla joined.srg')
+    parser.add_argument('mcp_dir', help='directory holding fields.csv and methods.csv')
+    parser.add_argument('out', help='merged SRG to write')
+    args = parser.parse_args()
+
+    fields = read_csv(args.mcp_dir + '/fields.csv')
+    methods = read_csv(args.mcp_dir + '/methods.csv')
+    print('csv: %d fields, %d methods' % (len(fields), len(methods)))
+
+    stats = {'CL': 0, 'FD': 0, 'MD': 0, 'PK': 0, 'renamed_f': 0, 'renamed_m': 0}
+    skipped = 0
+
+    with open(args.srg, 'r', encoding='utf-8', errors='replace') as src, \
+            open(args.out, 'w', encoding='utf-8', newline='\n') as dst:
+
+        for line in src:
+            line = line.rstrip('\r\n')
+            if not line.strip():
+                continue
+
+            if line.startswith('CL: '):
+                # class names in the SRG already are the MCP names
+                stats['CL'] += 1
+                dst.write(line + '\n')
+                continue
+
+            if line.startswith('FD: '):
+                parts = line.split(' ')
+                if len(parts) < 3:
+                    skipped += 1
+                    continue
+                before = parts[2].rpartition('/')[2]
+                after = merge_member_name(parts[2], fields)
+                if after.rpartition('/')[2] != before:
+                    stats['renamed_f'] += 1
+                stats['FD'] += 1
+                dst.write('FD: %s %s\n' % (parts[1], after))
+                continue
+
+            if line.startswith('MD: '):
+                # MD: /  / ;
+                #
+                # The trailing semicolon belongs to the descriptor and is absent
+                # for primitive and void returns: ()I, ()Z, ()V carry none, while
+                # ()Ljava/lang/String; does. Appending one unconditionally turns
+                # ()I into the invalid ()I; and the remapper rejects it with
+                # "Unrecognized type in method descriptor: ;". Keep it verbatim.
+                trail = ';' if line.rstrip().endswith(';') else ''
+                parts = line.rstrip().rstrip(';').split(' ')
+                if len(parts) < 5:
+                    skipped += 1
+                    continue
+                before = parts[3].rpartition('/')[2]
+                after = merge_member_name(parts[3], methods)
+                if after.rpartition('/')[2] != before:
+                    stats['renamed_m'] += 1
+                stats['MD'] += 1
+                dst.write('MD: %s %s %s %s%s\n'
+                          % (parts[1], parts[2], after, parts[4], trail))
+                continue
+
+            if line.startswith('PK: '):
+                # Package lines exist for MCP's source-jar layout: `PK: . net/minecraft/src`
+                # moves the whole root package under net/minecraft/src. The CL lines
+                # already place every class at its final path, so applying this on top
+                # would double-prefix them, and SpecialSource's parser rejects the
+                # combination. Not needed for remapping a client jar.
+                stats['PK'] += 1
+                continue
+
+            skipped += 1
+
+    print('wrote %s' % args.out)
+    print('  CL=%(CL)d FD=%(FD)d MD=%(MD)d PK=%(PK)d' % stats)
+    print('  renamed %d fields, %d methods' % (stats['renamed_f'], stats['renamed_m']))
+    if skipped:
+        print('  skipped %d unrecognised lines' % skipped)
+
+
+if __name__ == '__main__':
+    main()
\ No newline at end of file
diff --git a/rsx-client/tools/show_errors.py b/rsx-client/tools/show_errors.py
new file mode 100644
index 0000000..235a910
--- /dev/null
+++ b/rsx-client/tools/show_errors.py
@@ -0,0 +1,103 @@
+#!/usr/bin/env python3
+"""Show compile errors next to the source line that caused them.
+
+javac prints file:line: error: message, which is not enough to work from: the
+interesting part is usually the statement, and hunting for it in a 3000 line
+decompiled file by hand is what turns a 160 error job into an afternoon.
+
+    python show_errors.py  [ ...] [--top N] [--file NAME]
+"""
+
+import argparse
+import collections
+import os
+import re
+
+LINE = re.compile(r'^(?P[A-Za-z]:.*?\.java):(?P\d+): error: (?P.*)$')
+DETAIL = re.compile(r'^ {2}(?Psymbol|location|where|required|found|reason):\s*(?P.*)$')
+
+
+def load(paths):
+    """Returns [(file, line, message, [detail lines])].
+
+    javac puts the useful part after the message: which symbol was missing, and
+    which types were expected. Those continuation lines are indented and carry
+    no filename, so they are attached to the error above them.
+    """
+    found = []
+    for path in paths:
+        pending = None
+        with open(path, encoding='utf-8', errors='replace') as handle:
+            for raw in handle:
+                line = raw.rstrip('\n').rstrip('\r')
+                m = LINE.match(line)
+                if m:
+                    if pending:
+                        found.append(pending)
+                    pending = (m.group('file'), int(m.group('line')),
+                               m.group('msg'), [])
+                    continue
+                if pending is not None:
+                    # The quoted source line and its caret sit between the error
+                    # and the symbol block, so anything that is not a detail line
+                    # is skipped rather than treated as the end of the error.
+                    d = DETAIL.match(line)
+                    if d:
+                        text = d.group('rest').strip()
+                        pending[3].append('%s: %s' % (d.group('key'), text)
+                                         if text else d.group('key'))
+        if pending:
+            found.append(pending)
+    return found
+
+
+def main():
+    parser = argparse.ArgumentParser(description=__doc__)
+    parser.add_argument('errors', nargs='+')
+    parser.add_argument('--top', type=int, default=0, help='only the N worst files')
+    parser.add_argument('--file', help='show only errors in this file (substring match)')
+    parser.add_argument('--context', type=int, default=0, help='lines around the error')
+    args = parser.parse_args()
+
+    found = load(args.errors)
+    if not found:
+        print('no errors')
+        return 0
+
+    counts = collections.Counter(f for f, _, _, _ in found)
+    if args.file:
+        targets = [f for f in counts if args.file in f]
+    elif args.top:
+        targets = [f for f, _ in counts.most_common(args.top)]
+    else:
+        targets = sorted(counts)
+
+    shown = 0
+    for path in targets:
+        rows = [(ln, msg, det) for f, ln, msg, det in found if f == path]
+        print('%s  (%d errors)' % (path, len(rows)))
+        cache = {}
+        for ln, msg, details in rows:
+            if path not in cache:
+                try:
+                    with open(path, encoding='utf-8', errors='replace') as handle:
+                        cache[path] = handle.readlines()
+                except OSError:
+                    cache[path] = []
+            body = cache[path]
+            print('  %d: %s' % (ln, msg))
+            for extra in details:
+                print('       %s' % extra)
+            lo = max(0, ln - 1 - args.context)
+            hi = min(len(body), ln + args.context)
+            for i in range(lo, hi):
+                mark = '>' if i == ln - 1 else ' '
+                print('     %s %5d | %s' % (mark, i + 1, body[i].rstrip()))
+            shown += 1
+            print()
+    print('---- %d error(s) across %d file(s)' % (len(found), len(counts)))
+    return 0
+
+
+if __name__ == '__main__':
+    raise SystemExit(main())
\ No newline at end of file
diff --git a/rsx-client/tools/stage_run.py b/rsx-client/tools/stage_run.py
new file mode 100644
index 0000000..44f1cf4
--- /dev/null
+++ b/rsx-client/tools/stage_run.py
@@ -0,0 +1,159 @@
+"""Stage a runnable 1.12.2 client from the compiled fork.
+
+Resolves the exact library set the launcher already downloaded for 1.12.2 rather
+than globbing the whole meta folder, which holds every version ever installed
+and would put four LWJGL generations on one classpath.
+
+    python stage_run.py   
+"""
+
+import json
+import os
+import sys
+import zipfile
+
+
+def allowed(entry):
+    """Honours the launcher rules block: natives are per-OS."""
+    rules = entry.get('rules')
+    if not rules:
+        return True
+    verdict = False
+    for rule in rules:
+        os_name = rule.get('os', {}).get('name')
+        if os_name is None or os_name == 'windows':
+            verdict = rule['action'] == 'allow'
+    return verdict
+
+
+def resolve(entry, lib_roots):
+    """Yields (path, kind) for one library coordinate, kind in class/native.
+
+    1.12.2 keeps natives in downloads.classifiers rather than in the maven
+    coordinate, so the artifact path and the natives-windows path are read
+    separately. Some entries are natives-only and carry no artifact at all,
+    which is why an absent artifact must not be treated as a fallback signal
+    on its own. Roots are searched in order, so a local mirror wins.
+    """
+    def locate(rel):
+        for root in lib_roots:
+            jar = os.path.join(root, rel.replace('/', os.sep))
+            if os.path.isfile(jar):
+                return jar
+        return None
+
+    downloads = entry.get('downloads') or {}
+    artifact = (downloads.get('artifact') or {}).get('path')
+    seen_artifact = False
+    if artifact:
+        jar = locate(artifact)
+        if jar:
+            seen_artifact = True
+            yield jar, 'class'
+
+    classifiers = downloads.get('classifiers') or {}
+    natives_only = False
+    for key in sorted(classifiers):
+        if not key.startswith('natives-windows'):
+            continue
+        path = (classifiers[key] or {}).get('path')
+        natives_only = True
+        if not path:
+            continue
+        jar = locate(path)
+        if jar:
+            yield jar, 'native'
+
+    if seen_artifact or natives_only or artifact:
+        return
+
+    # pre-1.13 style coordinates: group:artifact:version[:classifier]
+    parts = entry['name'].split(':')
+    classifier = parts[3] if len(parts) > 3 and parts[3] else ''
+    rel = '/'.join([parts[0].replace('.', '/'), parts[1], parts[2],
+                    '%s-%s%s.jar' % (parts[1], parts[2],
+                                     '-' + classifier if classifier else '')])
+    jar = locate(rel)
+    if jar:
+        yield jar, 'native' if classifier.startswith('natives') else 'class'
+
+
+def extract(jar, dest):
+    """Unpacks a natives jar, returning False when it is not a real archive.
+
+    Some downloads in the local meta folder are truncated stubs rather than
+    jars. Losing a natives jar only costs an optional feature, so a bad file is
+    reported and skipped instead of aborting the whole staging.
+    """
+    try:
+        with zipfile.ZipFile(jar) as archive:
+            archive.extractall(dest)
+        return True
+    except (zipfile.BadZipFile, OSError):
+        return False
+
+
+def main():
+    run, classes, meta = sys.argv[1], sys.argv[2], sys.argv[3]
+    version_json = os.path.join(meta, 'versions', '1.12.2', '1.12.2.json')
+    with open(version_json, encoding='utf-8') as handle:
+        manifest = json.load(handle)
+
+    for name in ('assets', 'config', 'saves', 'resourcepacks', 'logs',
+                 'screenshots', 'shaderpacks'):
+        os.makedirs(os.path.join(run, name), exist_ok=True)
+
+    lib_roots = [os.path.join(run, 'libraries'), os.path.join(meta, 'libraries')]
+    cp = [classes]
+    natives = []
+    missing = []
+    for entry in manifest['libraries']:
+        if not allowed(entry):
+            continue
+        found = list(resolve(entry, lib_roots))
+        if not found:
+            missing.append(entry['name'])
+            continue
+        for jar, kind in found:
+            (natives if kind == 'native' else cp).append(jar)
+
+    # the unpacked jar carries pack.png, log4j2.xml and the in-jar assets;
+    # the compiled classes go first so they shadow the original bytecode
+    unpacked = os.path.join(os.path.dirname(os.path.dirname(classes)), 'classes')
+    if os.path.isdir(unpacked):
+        cp.append(unpacked)
+
+    # the mod loader is part of the game now, not an agent beside it, so it
+    # belongs on the classpath of the same JVM that renders the client
+    loader_jar = os.path.join(os.path.dirname(os.path.dirname(
+        os.path.abspath(__file__))), 'dist', 'rsx-loader.jar')
+    if os.path.isfile(loader_jar):
+        cp.append(loader_jar)
+    else:
+        print('NOTE: %s missing, run scripts\\build.ps1' % loader_jar)
+
+    with open(os.path.join(run, 'classpath.txt'), 'w', encoding='utf-8') as handle:
+        handle.write(os.pathsep.join(cp))
+
+    native_dir = os.path.join(run, 'natives')
+    os.makedirs(native_dir, exist_ok=True)
+    skipped = []
+    for jar in natives:
+        if not extract(jar, native_dir):
+            skipped.append(os.path.basename(jar))
+
+    print('classpath entries: %d' % len(cp))
+    print('natives: %d jars -> %s' % (len(natives), native_dir))
+    if missing:
+        print('MISSING libraries (%d):' % len(missing))
+        for name in missing:
+            print('  %s' % name)
+    if skipped:
+        print('unusable native jars (%d), skipped:' % len(skipped))
+        for name in skipped:
+            print('  %s' % name)
+    return 1 if missing else 0
+
+
+if __name__ == '__main__':
+    raise SystemExit(main())
\ No newline at end of file
diff --git a/rsx-client/tools/struct_diff.py b/rsx-client/tools/struct_diff.py
new file mode 100644
index 0000000..3e6bdb0
--- /dev/null
+++ b/rsx-client/tools/struct_diff.py
@@ -0,0 +1,288 @@
+"""Structural comparison between the original bytecode and the rebuilt fork.
+
+javac proves a file compiles. It proves nothing about whether the compiler
+emitted the same class the decompiler read, and a misdecompiled class can be
+silently wrong: dropping an interface, losing a method, or emitting the wrong
+superclass all produce clean output.
+
+Both trees are on disk, so the honest check is to parse the class files
+directly and compare shape. This is a structural pass only, not a bytecode
+equivalence proof, but it catches the whole class of errors that compiling
+cannot: missing classes, lost interfaces, absent methods and fields.
+
+    python struct_diff.py   [--json out.json]
+"""
+
+import json
+import os
+import struct
+import sys
+
+CONSTANT_UTF8 = 1
+CONSTANT_CLASS = 7
+CONSTANT_STRING = 8
+CONSTANT_LONG = 5
+CONSTANT_DOUBLE = 6
+SKIP_TWO_SLOT = (CONSTANT_LONG, CONSTANT_DOUBLE)
+
+INTERFACE = 0x0200
+ABSTRACT = 0x0400
+SYNTHETIC = 0x1000
+
+
+class Reader:
+    """Minimal big-endian cursor over a class file."""
+
+    def __init__(self, data):
+        self.data = data
+        self.pos = 0
+
+    def u1(self):
+        value = self.data[self.pos]
+        self.pos += 1
+        return value
+
+    def u2(self):
+        value = struct.unpack_from('>H', self.data, self.pos)[0]
+        self.pos += 2
+        return value
+
+    def u4(self):
+        value = struct.unpack_from('>I', self.data, self.pos)[0]
+        self.pos += 4
+        return value
+
+    def take(self, size):
+        value = self.data[self.pos:self.pos + size]
+        self.pos += size
+        return value
+
+
+def read_constant_pool(reader):
+    """Returns a list indexed by pool index; entries are str or int."""
+    count = reader.u2()
+    pool = [None] * count
+    index = 1
+    while index < count:
+        tag = reader.u1()
+        if tag == CONSTANT_UTF8:
+            length = reader.u2()
+            pool[index] = reader.take(length).decode('utf-8', 'replace')
+        elif tag in (CONSTANT_CLASS, CONSTANT_STRING):
+            pool[index] = ('ref', reader.u2())
+        elif tag in (3, 4):                      # Integer, Float
+            pool[index] = reader.take(4)
+        elif tag in SKIP_TWO_SLOT:               # Long, Double
+            pool[index] = reader.take(8)
+        elif tag in (9, 10, 11, 12, 17, 18):     # refs, NameAndType, Dynamic
+            reader.take(4)
+            pool[index] = None
+        elif tag == 15:                          # MethodHandle
+            reader.take(3)
+            pool[index] = None
+        elif tag in (16, 19, 20):                # MethodType, Module, Package
+            reader.take(2)
+            pool[index] = None
+        else:
+            raise ValueError('unknown constant pool tag %d' % tag)
+        index += 2 if tag in SKIP_TWO_SLOT else 1
+    return pool
+
+
+def utf(pool, index):
+    entry = pool[index]
+    return entry if isinstance(entry, str) else '?'
+
+
+def class_name(pool, index):
+    entry = pool[index]
+    return utf(pool, entry[1]) if isinstance(entry, tuple) else '?'
+
+
+def skip_attributes(reader, pool):
+    count = reader.u2()
+    for _ in range(count):
+        reader.u2()                              # name index
+        length = reader.u4()
+        reader.take(length)
+
+
+def parse(path):
+    """Returns (name, superName, interfaces, methods, fields, referenced)."""
+    with open(path, 'rb') as handle:
+        data = handle.read()
+    reader = Reader(data)
+    if reader.u4() != 0xCAFEBABE:
+        raise ValueError('not a class file: %s' % path)
+    reader.u2()                                  # minor
+    reader.u2()                                  # major
+    pool = read_constant_pool(reader)
+    reader.u2()                                  # access flags
+    this_class = reader.u2()
+    super_class = reader.u2()
+    referenced = set()
+    for entry in pool:
+        if isinstance(entry, tuple) and entry[0] == 'ref':
+            target = pool[entry[1]]
+            if isinstance(target, str):
+                referenced.add(target)
+    interfaces = []
+    for _ in range(reader.u2()):
+        interfaces.append(class_name(pool, reader.u2()))
+    fields = set()
+    for _ in range(reader.u2()):
+        flags = reader.u2()
+        name = utf(pool, reader.u2())
+        descriptor = utf(pool, reader.u2())
+        if not flags & SYNTHETIC:
+            fields.add('%s %s' % (name, descriptor))
+        skip_attributes(reader, pool)
+    methods = set()
+    for _ in range(reader.u2()):
+        flags = reader.u2()
+        name = utf(pool, reader.u2())
+        descriptor = utf(pool, reader.u2())
+        if not flags & SYNTHETIC and name not in ('',):
+            methods.add('%s %s' % (name, descriptor))
+        skip_attributes(reader, pool)
+    return (class_name(pool, this_class), class_name(pool, super_class),
+            interfaces, methods, fields, referenced)
+
+
+def collect(root):
+    found = {}
+    for base, _dirs, files in os.walk(root):
+        for name in files:
+            if not name.endswith('.class'):
+                continue
+            full = os.path.join(base, name)
+            key = os.path.relpath(full, root).replace(os.sep, '/')
+            try:
+                found[key] = parse(full)
+            except (ValueError, struct.error, IndexError) as error:
+                found[key] = None
+                sys.stderr.write('unreadable %s: %s\n' % (key, error))
+    return found
+
+
+def describe(kind, items):
+    return sorted(items)
+
+
+def main():
+    original_root, rebuilt_root = sys.argv[1], sys.argv[2]
+    report_path = None
+    if '--json' in sys.argv:
+        report_path = sys.argv[sys.argv.index('--json') + 1]
+
+    original = collect(original_root)
+    rebuilt = collect(rebuilt_root)
+
+    only_original = sorted(set(original) - set(rebuilt))
+    only_rebuilt = sorted(set(rebuilt) - set(original))
+
+    lost_interfaces = []
+    lost_methods = []
+    lost_fields = []
+    changed_super = []
+    unreadable = []
+    referenced_by_rebuild = set()
+
+    for key in sorted(set(rebuilt)):
+        entry = rebuilt[key]
+        if entry is not None:
+            referenced_by_rebuild |= entry[5]
+
+    for key in sorted(set(original) & set(rebuilt)):
+        left, right = original[key], rebuilt[key]
+        if left is None or right is None:
+            unreadable.append(key)
+            continue
+        if set(left[2]) != set(right[2]):
+            lost_interfaces.append({
+                'class': key,
+                'missing': describe('i', set(left[2]) - set(right[2])),
+                'added': describe('i', set(right[2]) - set(left[2])),
+            })
+        if left[1] != right[1]:
+            changed_super.append({
+                'class': key,
+                'original': left[1],
+                'rebuilt': right[1],
+            })
+        gone_methods = left[3] - right[3]
+        if gone_methods:
+            lost_methods.append({'class': key, 'missing': sorted(gone_methods)})
+        gone_fields = left[4] - right[4]
+        if gone_fields:
+            lost_fields.append({'class': key, 'missing': sorted(gone_fields)})
+
+    # a class the rebuild never mentions cannot be reached at runtime, so the
+    # original bytecode on the classpath is never consulted for it
+    orphans = [k for k in only_original
+               if k[:-6] not in referenced_by_rebuild]
+    still_needed = [k for k in only_original
+                    if k[:-6] in referenced_by_rebuild]
+
+    report = {
+        'classes_original': len(original),
+        'classes_rebuilt': len(rebuilt),
+        'only_in_original': only_original,
+        'only_in_rebuilt': only_rebuilt,
+        'lost_interfaces': lost_interfaces,
+        'changed_super': changed_super,
+        'lost_methods': lost_methods,
+        'lost_fields': lost_fields,
+        'unreadable': unreadable,
+        'orphans': orphans,
+        'still_needed': still_needed,
+    }
+
+    print('classes: original %d, rebuilt %d' % (len(original), len(rebuilt)))
+    print('classes only in original : %d' % len(only_original))
+    print('classes only in rebuilt  : %d' % len(only_rebuilt))
+    print('lost interfaces          : %d' % len(lost_interfaces))
+    print('changed superclass       : %d' % len(changed_super))
+    print('lost methods             : %d' % sum(len(r['missing']) for r in lost_methods))
+    print('lost fields              : %d' % sum(len(r['missing']) for r in lost_fields))
+    print('missing but unreferenced : %d (dead weight, never loaded)' % len(orphans))
+    print('missing and still needed : %d (would fail without the fallback)' % len(still_needed))
+
+    if lost_interfaces:
+        print('\n--- LOST INTERFACES (fatal class of bug) ---')
+        for row in lost_interfaces:
+            print('  %s' % row['class'])
+            print('      - %s' % ', '.join(row['missing']))
+            if row['added']:
+                print('      + %s' % ', '.join(row['added']))
+    if changed_super:
+        print('\n--- CHANGED SUPERCLASS ---')
+        for row in changed_super:
+            print('  %-60s %s -> %s' % (row['class'], row['original'], row['rebuilt']))
+    if lost_methods:
+        print('\n--- LOST METHODS ---')
+        for row in lost_methods:
+            print('  %s' % row['class'])
+            for name in row['missing']:
+                print('      - %s' % name)
+    if lost_fields:
+        print('\n--- LOST FIELDS ---')
+        for row in lost_fields:
+            print('  %s' % row['class'])
+            for name in row['missing']:
+                print('      - %s' % name)
+    if only_original:
+        print('\n--- ONLY IN ORIGINAL (first 60) ---')
+        for key in only_original[:60]:
+            print('  %s' % key)
+        if len(only_original) > 60:
+            print('  ... and %d more' % (len(only_original) - 60))
+
+    if report_path:
+        with open(report_path, 'w', encoding='utf-8') as handle:
+            json.dump(report, handle, indent=2)
+        print('\nreport written to %s' % report_path)
+
+
+if __name__ == '__main__':
+    main()
\ No newline at end of file
diff --git a/rsx-mods/rsxm-demo/resources/META-INF/rsxmod.properties b/rsx-mods/rsxm-demo/resources/META-INF/rsxmod.properties
new file mode 100644
index 0000000..1b35f0e
--- /dev/null
+++ b/rsx-mods/rsxm-demo/resources/META-INF/rsxmod.properties
@@ -0,0 +1,7 @@
+id=rsxm-demo
+name=RustNotMC Demo
+version=1.1.0
+env=CLIENT
+entrypoint=net.rustnotmc.demo.DemoMod
+api=1
+access=hud,commands,files
\ No newline at end of file
diff --git a/rsx-mods/rsxm-demo/src/main/java/net/rustnotmc/demo/DemoMod.java b/rsx-mods/rsxm-demo/src/main/java/net/rustnotmc/demo/DemoMod.java
new file mode 100644
index 0000000..74bc88d
--- /dev/null
+++ b/rsx-mods/rsxm-demo/src/main/java/net/rustnotmc/demo/DemoMod.java
@@ -0,0 +1,142 @@
+package net.rustnotmc.demo;
+
+import java.io.File;
+
+import net.rustnotmc.api.Rsx;
+import net.rustnotmc.api.RustNotMcMod;
+import net.rustnotmc.api.client.ClientCommand;
+import net.rustnotmc.api.client.CommandArgs;
+import net.rustnotmc.api.client.CommandScope;
+import net.rustnotmc.api.client.ScreenKind;
+import net.rustnotmc.api.hud.Anchor;
+import net.rustnotmc.api.hud.HudLayout;
+import net.rustnotmc.api.hud.HudSurface;
+
+/**
+ * Shows that the client, the loader and the mod API are actually connected.
+ *
+ * Nothing here is cosmetic: every value comes from a jar in the mods folder,
+ * loaded by the loader, ticked by the client loop and drawn with the game's own
+ * font. The command registration also shows the scope rule, where a CLIENT
+ * command runs here and a SERVER one is deliberately left for the server.
+ */
+public final class DemoMod implements RustNotMcMod {
+
+    private static final int ACCENT = 0x63D2FF;
+    private static final int PADDING = 3;
+
+    private Rsx context;
+    private String lastCommand = "none yet";
+    private int commandHits;
+    private File directory;
+    private boolean sawWorld;
+
+    @Override
+    public void onConstruct(Rsx context) {
+        this.context = context;
+        // registering here rather than in onEnable keeps a disable/enable cycle safe
+        HudLayout.Slot slot = context.hud("demo", new Panel(), Anchor.TOP_RIGHT,
+                HudLayout.NORMAL, PADDING, PADDING);
+        if (slot == null) {
+            context.log().warn("no hud capability, no overlay");
+        }
+
+        // a client command: carried out here, never sent anywhere
+        context.command("rsxdemo", new ClientCommand() {
+            @Override
+            public void run(CommandArgs args) {
+                note("/rsxdemo handled locally: " + args.raw());
+            }
+        }, CommandScope.CLIENT);
+
+        // a server command: registered so the name is claimed, and deliberately
+        // not executed here, because the server decides what it does
+        context.command("rsxadmin", new ClientCommand() {
+            @Override
+            public void run(CommandArgs args) {
+                note("this handler must never run on the client");
+            }
+        }, CommandScope.SERVER);
+
+        this.directory = context.modDirectory();
+    }
+
+    @Override
+    public void onLoad() {
+        this.context.log().info("hello from inside the client");
+    }
+
+    @Override
+    public void onEnable() {
+        this.context.log().info("enabled as api v" + this.context.apiVersion());
+        this.context.log().info("screen is " + this.context.client().screen().kind());
+        if (this.directory != null) {
+            this.context.log().info("my directory is " + this.directory);
+        }
+    }
+
+    private void note(String message) {
+        this.lastCommand = message;
+        this.commandHits++;
+        this.context.log().info(message);
+    }
+
+    private String[] lines() {
+        ScreenKind kind = this.context.client().screen().kind();
+        if (kind.inWorld()) {
+            this.sawWorld = true;
+        }
+        return new String[]{
+                "RustNotMC " + this.context.modVersion() + "  (api v"
+                        + this.context.apiVersion() + ")",
+                kind.name().toLowerCase() + "  |  tick " + this.context.tick(),
+                this.context.client().frame() + " frames"
+                        + (this.context.hasClient() ? "" : "  [detached]"),
+                "/rsxdemo x" + (this.commandHits == 0
+                        ? "   |  /rsxadmin -> server"
+                        : "  (" + this.commandHits + ")"
+                        + "   |  /rsxadmin -> server"),
+                this.lastCommand,
+        };
+    }
+
+    /** Draw is a plain interface with three methods, not a lambda target. */
+    private final class Panel implements HudLayout.Draw {
+
+        @Override
+        public int width(HudSurface hud) {
+            int widest = 0;
+            String[] lines = lines();
+            for (int i = 0; i < lines.length; i++) {
+                widest = Math.max(widest, (int) hud.textWidth(lines[i]));
+            }
+            return widest;
+        }
+
+        @Override
+        public int height(HudSurface hud) {
+            return lines().length * (hud.textHeight() + 2) + 2;
+        }
+
+        @Override
+        public void draw(HudSurface hud, int x, int y, int width, int height) {
+            hud.rect(x, y, width, height, ACCENT, 0.16f);
+            String[] lines = lines();
+            int lineHeight = hud.textHeight() + 2;
+            for (int i = 0; i < lines.length; i++) {
+                int tint = i == 0 ? ACCENT : (i == lines.length - 1 ? 0xFFD98C : 0xE8E8E8);
+                hud.text(x + 2, y + 2 + i * lineHeight, lines[i], tint, 0.9f);
+            }
+        }
+    }
+
+    @Override
+    public void onDisable() {
+        this.context.log().info("disabled, saw a world: " + this.sawWorld);
+    }
+
+    @Override
+    public void onUnload() {
+        this.context.clearHud();
+    }
+}
\ No newline at end of file
diff --git a/rsx-plugin-api/LICENSE b/rsx-plugin-api/LICENSE
new file mode 100644
index 0000000..ea7e6d4
--- /dev/null
+++ b/rsx-plugin-api/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 RustNotMC
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/rsx-protocol/LICENSE b/rsx-protocol/LICENSE
new file mode 100644
index 0000000..ea7e6d4
--- /dev/null
+++ b/rsx-protocol/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 RustNotMC
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/rsx-server-core/LICENSE b/rsx-server-core/LICENSE
new file mode 100644
index 0000000..8ff7d67
--- /dev/null
+++ b/rsx-server-core/LICENSE
@@ -0,0 +1,9 @@
+RustNotMC - rsx-server-core
+Copyright (c) 2026 RustNotMC. All rights reserved.
+
+This repository is proprietary and is not published. No part of it may be
+copied, modified, redistributed or used to build a derivative work without
+written permission from the copyright holder.
+
+Third-party components vendored under third_party\ remain under their own
+licenses; see the notices in those directories.