init
This commit is contained in:
+495
@@ -0,0 +1,495 @@
|
||||
# Porting PhotoSync to a new Minecraft version
|
||||
|
||||
PhotoSync supports Minecraft 1.20 through 26.2 from one source tree. This
|
||||
document explains how that is arranged, what actually changes between versions,
|
||||
and what you have to do when a new one comes out.
|
||||
|
||||
Everything in the breakpoint table below is *measured*, not remembered. It is
|
||||
produced by `tools/probe-api.py`, which javaps the Minecraft jars Loom has
|
||||
already downloaded and prints the signatures this mod depends on:
|
||||
|
||||
```sh
|
||||
./gradlew buildAllPlatforms # once, so Loom caches every jar
|
||||
python3 tools/probe-api.py > /tmp/api.txt
|
||||
```
|
||||
|
||||
Re-run it whenever you add a bucket. A member that vanishes from the output is
|
||||
itself the finding — that is how the 26.x `GuiGraphics` removal surfaced.
|
||||
|
||||
`probe-api.py` carries a fixed member list, so for anything not already in it
|
||||
there is `tools/probe-class.py`, which takes classes and an optional filter and
|
||||
javaps them across every cached bucket:
|
||||
|
||||
```sh
|
||||
python3 tools/probe-class.py net.minecraft.client.Screenshot takeScreenshot
|
||||
python3 tools/probe-class.py net.minecraft.client.gui.GuiGraphics,net.minecraft.client.gui.Hud blit
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. Buckets, not versions
|
||||
|
||||
There are 23 Minecraft releases in the supported range and nine `:platform`
|
||||
projects. A **bucket** is a contiguous run of versions whose client API is
|
||||
identical in every respect PhotoSync touches. We compile and test against one
|
||||
representative version per bucket; the jar it produces runs on the whole range,
|
||||
which each bucket declares in its own `gradle.properties`:
|
||||
|
||||
| Bucket | Representative | `minecraft_range` | Java | Mappings |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 1 | 1.20.1 | `>=1.20 <1.20.2` | 17 | Mojang |
|
||||
| 2 | 1.20.4 | `>=1.20.2 <1.20.5` | 17 | Mojang |
|
||||
| 3 | 1.20.6 | `>=1.20.5 <1.21` | 21 | Mojang |
|
||||
| 4 | 1.21.1 | `>=1.21 <1.21.2` | 21 | Mojang |
|
||||
| 5 | 1.21.4 | `>=1.21.2 <1.21.5` | 21 | Mojang |
|
||||
| 6 | 1.21.5 | `>=1.21.5 <1.21.6` | 21 | Mojang |
|
||||
| 7 | 1.21.8 | `>=1.21.6 <1.21.11` | 21 | Mojang |
|
||||
| 8 | 1.21.11 | `>=1.21.11 <1.22` | 21 | Mojang |
|
||||
| 9 | 26.2 | `>=26.1 <26.3` | 25 | identity |
|
||||
|
||||
The boundaries are not aesthetic. Each one is a signature in section 3 that
|
||||
changed, and merging two buckets means one of those adapters stops compiling.
|
||||
|
||||
## 2. Module layers
|
||||
|
||||
```
|
||||
shared/core no Minecraft, no GUI. Providers, upload queue, config,
|
||||
timeline paging, ThumbHash decoding. Unit-testable.
|
||||
shared/mc-api the seam. Interfaces describing what the mod needs from the
|
||||
game. No Minecraft imports -- it is the shape of the
|
||||
dependency, not the dependency.
|
||||
shared/ui every screen and widget, drawn through mc-api only.
|
||||
shared/client wiring: builds the queue, providers and screens from config.
|
||||
Owns the language file and the rest of assets/photosync.
|
||||
platform/common the compat layer. Imports net.minecraft, but only members
|
||||
that are identical on all nine buckets: the entrypoint, the
|
||||
bridge assembly, Component.literal, the clipboard.
|
||||
platform/<v> one bucket's adapters, and its mixins. Everything whose
|
||||
signature moved lives here.
|
||||
```
|
||||
|
||||
`platform/common` is a source root, not a project: the root build wires
|
||||
`platform/common/src/main/{java,resources}` into every bucket's `sourceSets.main`.
|
||||
It exists because roughly two thirds of the platform code does not vary, and
|
||||
copying it nine times would mean nine places to fix a bug. The split rule is
|
||||
mechanical — a class goes in `common` until the day it stops compiling on some
|
||||
bucket, and then it moves down into all nine.
|
||||
|
||||
The nine copies under `platform/<v>` share fully-qualified names on purpose
|
||||
(`dev.photosync.platform.impl.RenderAdapter` exists nine times). Only one is ever
|
||||
on a classpath, `platform/common` can name them without indirection, and diffing
|
||||
two buckets is a plain `diff -r`.
|
||||
|
||||
`shared/*` compiles to **Java 17 bytecode** (`shared_java` in
|
||||
`gradle.properties`), because buckets 1 and 2 run on a Java 17 JVM. That is a
|
||||
constraint on the code, not just a compiler flag: no virtual threads, no Java 21
|
||||
pattern matching in shared code. The platform modules compile at their own
|
||||
bucket's level, so 26.2's adapter may use Java 25 freely.
|
||||
|
||||
Shared classes are folded into each platform jar by `jar { from ... }` in the
|
||||
root build. There is no shading and no jar-in-jar — these are our own classes
|
||||
and they carry no third-party dependencies. Gson and slf4j-api are `compileOnly`
|
||||
because Minecraft and Fabric Loader supply both at runtime on every supported
|
||||
version.
|
||||
|
||||
### Why the seam is only eleven drawing calls
|
||||
|
||||
The seam is deliberately narrow. `RenderBridge` exposes eleven primitives —
|
||||
`fill`, `gradient`, `border`, `text`, `textWidth`, `lineHeight`, two `image`
|
||||
overloads, `pushClip`/`popClip`, and the frame's `width`/`height`/`tickDelta`.
|
||||
PhotoSync draws every button, scrollbar, text field and tile itself out of those.
|
||||
|
||||
That is a real cost — we reimplement widgets Minecraft already has — and it buys
|
||||
the thing that matters: **vanilla's widget classes churn far more than its
|
||||
drawing primitives do.** `Button`'s constructor, `AbstractWidget`'s render
|
||||
signature and the whole `Layout` package changed repeatedly across this range,
|
||||
while `fill` and `enableScissor` did not change once in six years. Section 3 is
|
||||
the evidence: nine versions, and the drawing primitives collapse to three
|
||||
adapter shapes.
|
||||
|
||||
This is also why the mod does **not** need imgui, which `PROMPT.md` raised as
|
||||
the fallback if every version needed its own render logic. Every version does
|
||||
need its own adapter, but each adapter is a few dozen lines of delegation, not a
|
||||
GUI toolkit.
|
||||
|
||||
A method belongs in `mc-api` only if its Minecraft implementation genuinely
|
||||
differs across versions. Nothing there exposes a Minecraft concept under another
|
||||
name.
|
||||
|
||||
## 3. Measured API breakpoints
|
||||
|
||||
`✓` means "same as the column to its left".
|
||||
|
||||
### Drawing
|
||||
|
||||
| | 1.20.1 | 1.20.4 | 1.20.6 | 1.21.1 | 1.21.4 | 1.21.5 | 1.21.8 | 1.21.11 | 26.2 |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| host class | `GuiGraphics` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | **`GuiGraphicsExtractor`** |
|
||||
| `fill` / `fillGradient` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| `enableScissor` / `disableScissor` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| text | `drawString(Font,String,int,int,int,boolean)` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | **`text(…)`**, same args |
|
||||
| `blit` | pixel UVs + `ResourceLocation` | ✓ | ✓ | ✓ | **`Function<RL,RenderType>` first arg, dest size moves** | ✓ | **`RenderPipeline` first arg** | `Identifier` rename | ✓ |
|
||||
|
||||
Three adapter shapes, then. All of them use the overload that carries an
|
||||
explicit source rectangle, since `RenderBridge.image` has to be able to draw part
|
||||
of a texture:
|
||||
|
||||
* **1.20.1 – 1.21.1** —
|
||||
`blit(ResourceLocation, int x, int y, int w, int h, float u, float v, int uw, int vh, int texW, int texH)`.
|
||||
* **1.21.4 – 1.21.5** —
|
||||
`blit(Function<ResourceLocation,RenderType>, ResourceLocation, int x, int y, float u, float v, int w, int h, int uw, int vh, int texW, int texH)`.
|
||||
Two things changed: a `Function` is prepended (`RenderType::guiTextured`), *and
|
||||
the destination width/height moved from before the UV offsets to after them.*
|
||||
The tail — `uw, vh, texW, texH` — is unchanged, which makes the swap easy to
|
||||
miss when reading a diff.
|
||||
* **1.21.8 – 26.2** — `RenderPipeline` replaces the function
|
||||
(`RenderPipelines.GUI_TEXTURED`); argument order is otherwise the 1.21.4 one.
|
||||
|
||||
UVs are texel coordinates in every version, so `TextureHandle`'s own dimensions
|
||||
convert them from the normalised `u0..u1` the bridge takes. That conversion is
|
||||
the same three lines on all nine adapters.
|
||||
|
||||
The reorder is at least loud when you get it wrong: `u`/`v` are `float` and the
|
||||
sizes are `int`, so passing the old order to the new method fails to compile
|
||||
rather than drawing garbage. Do not "fix" that by casting.
|
||||
|
||||
A normalised-UV overload `blit(Identifier, x, y, w, h, u0, v0, u1, v1)` appears
|
||||
at 1.21.8 and would suit `RenderBridge.image` better on paper. The adapters
|
||||
deliberately do not use it: its argument order differs again from the texel
|
||||
form's, and buckets 1–5 have no equivalent, so adopting it would buy a shorter
|
||||
method on four buckets in exchange for two spellings to keep straight. All nine
|
||||
adapters stay on the explicit texel form.
|
||||
|
||||
26.2 is the interesting one and it is less disruptive than it looks. `GuiGraphics`
|
||||
is gone entirely, replaced by an extract/render-state pipeline: `Screen.render`
|
||||
becomes `extractRenderState(GuiGraphicsExtractor, int, int, float)`. But
|
||||
`GuiGraphicsExtractor` still exposes `fill`, `fillGradient`, `enableScissor`,
|
||||
`disableScissor`, `text` and `blit` with recognisable signatures, so all eleven
|
||||
primitives survive the transition. The adapter binds to the extractor instead of
|
||||
the graphics object; nothing above the seam notices.
|
||||
|
||||
### Screen
|
||||
|
||||
| | 1.20.1 | 1.20.4 → 1.21.8 | 1.21.11 | 26.2 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| render entry | `render(GuiGraphics,int,int,float)` | ✓ | ✓ | **`extractRenderState(GuiGraphicsExtractor,…)`** |
|
||||
| background | `renderBackground(GuiGraphics)` | **4 args** | ✓ | **`extractBackground(…)`** |
|
||||
| `init` / `resize` | `(Minecraft,int,int)` | ✓ | **`(int,int)`** — no `Minecraft` | ✓ |
|
||||
| `tick`, `removed`, `isPauseScreen`, `shouldCloseOnEsc` | ✓ | ✓ | ✓ | ✓ |
|
||||
|
||||
### HUD — where notifications are drawn
|
||||
|
||||
| | 1.20.1 → 1.20.6 | 1.21.1 → 1.21.11 | 26.2 |
|
||||
| --- | --- | --- | --- |
|
||||
| class | `Gui` | ✓ | **`Hud`** |
|
||||
| hook | `render(GuiGraphics, float)` | **`render(GuiGraphics, DeltaTracker)`** | **`extractRenderState(GuiGraphicsExtractor, DeltaTracker)`** |
|
||||
| partial tick | the `float` argument | `delta.getGameTimeDeltaPartialTick(false)` | ✓ |
|
||||
|
||||
`false` asks for the real partial tick rather than the frozen-while-paused one,
|
||||
so notifications keep animating while the game is paused behind a PhotoSync
|
||||
screen.
|
||||
|
||||
26.1 split `Gui` in two: the in-world overlay moved to `Hud`, and `Gui` kept an
|
||||
outer `extractRenderState` that draws the current screen *after* the HUD.
|
||||
Injecting into `Gui`'s would put notifications underneath our own GUI, so
|
||||
`HudMixin` targets `Hud`. Grep for the class before assuming the method name is
|
||||
the whole change.
|
||||
|
||||
Because the injection point is the in-world HUD on every bucket, **notifications
|
||||
are invisible on the title screen and inside our own screens.** That is by
|
||||
design — an upload that finishes while the player is in the queue screen shows up
|
||||
in the queue list, which is better feedback than a corner toast — but it does
|
||||
mean a capture taken from a menu reports nothing until the player is back in a
|
||||
world.
|
||||
|
||||
### Input — `GuiEventListener`
|
||||
|
||||
| | 1.20.1 | 1.20.4 → 1.21.8 | 1.21.11 → 26.2 |
|
||||
| --- | --- | --- | --- |
|
||||
| `mouseClicked` | `(double,double,int)` | ✓ | **`(MouseButtonEvent, boolean)`** |
|
||||
| `mouseReleased` | `(double,double,int)` | ✓ | **`(MouseButtonEvent)`** |
|
||||
| `mouseDragged` | `(double,double,int,double,double)` | ✓ | **`(MouseButtonEvent,double,double)`** |
|
||||
| `mouseScrolled` | `(double,double,double)` | **4 args** | `(double,double,double,double)` |
|
||||
| `keyPressed` | `(int,int,int)` | ✓ | **`(KeyEvent)`** |
|
||||
| `charTyped` | `(char,int)` | ✓ | **`(CharacterEvent)`** |
|
||||
|
||||
`ScreenModel` keeps the flat 1.20-era signatures because they carry every field
|
||||
the event objects do; the 1.21.11+ adapters unpack. `mouseScrolled` gaining a
|
||||
fourth argument at 1.20.4 is the *only* reason buckets 1 and 2 are separate —
|
||||
that plus `renderBackground` above.
|
||||
|
||||
Two details the table cannot hold:
|
||||
|
||||
* `CharacterEvent` is `(int codepoint, int modifiers)` at 1.21.11 and
|
||||
**`(int codepoint)`** at 26.2 — the modifiers were dropped. The 26.2 adapter
|
||||
passes `0`, which costs nothing: nothing downstream reads them (`TextField`
|
||||
filters on the character alone, and the shortcuts that care about Ctrl arrive
|
||||
through `keyPressed`).
|
||||
* The event carries a *code point*, not a `char`, so anything outside the basic
|
||||
plane arrives as a surrogate pair. Both adapters loop over
|
||||
`Character.toChars(...)` and deliver two `char`s — which is what a `String`
|
||||
would have held anyway.
|
||||
|
||||
### Textures
|
||||
|
||||
| | 1.20.1 → 1.21.1 | 1.21.4 | 1.21.5 → 1.21.8 | 1.21.11 → 26.2 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `DynamicTexture(NativeImage)` | ✓ | ✓ | **`(Supplier<String>, NativeImage)`** | ✓ |
|
||||
| `NativeImage.setPixelRGBA` | ✓ | **`setPixel`** | ✓ | ✓ |
|
||||
| `TextureManager.register(String, DynamicTexture)` → id | ✓ | **removed** | — | — |
|
||||
| `TextureManager.register(id, AbstractTexture)` | ✓ | ✓ | ✓ | ✓ |
|
||||
| `TextureManager.release(id)` | ✓ | ✓ | ✓ | ✓ |
|
||||
| id type | `ResourceLocation` | ✓ | ✓ | **`Identifier`** |
|
||||
|
||||
Because the auto-naming `register(String, DynamicTexture)` overload disappears at
|
||||
1.21.4 while `register(id, AbstractTexture)` exists everywhere, all nine adapters
|
||||
mint their own id and use the two-argument form. One code shape, no breakpoint —
|
||||
worth knowing so nobody "simplifies" the older buckets back onto the overload.
|
||||
|
||||
The `setPixelRGBA` → `setPixel` rename at 1.21.4 is not just a rename: **the
|
||||
channel order changed with it.** `setPixelRGBA` takes ABGR despite the name;
|
||||
`setPixel` takes plain ARGB, which is what `ThumbImage` already holds. That is
|
||||
why buckets 1–4 have a private `abgr()` swap in `TextureAdapter` and buckets 5–9
|
||||
do not. Copying an adapter across that boundary in either direction produces
|
||||
images with red and blue exchanged — it renders, so nothing tells you but your
|
||||
eyes.
|
||||
|
||||
At 1.21.5 `DynamicTexture` gained a `Supplier<String>` label, which means the id
|
||||
has to be minted *before* the texture rather than after. The texture still takes
|
||||
ownership of the `NativeImage` and closes it with itself; only the registration
|
||||
needs releasing.
|
||||
|
||||
### Key bindings
|
||||
|
||||
| | 1.20.1 → 1.21.8 | 1.21.11 | 26.2 |
|
||||
| --- | --- | --- | --- |
|
||||
| category | translation key `String` | **`KeyMapping.Category.register(Identifier)`** | ✓ |
|
||||
| Fabric module | `fabric-key-binding-api-v1` | ✓ | **`fabric-key-mapping-api-v1`** |
|
||||
| Fabric helper | `KeyBindingHelper.registerKeyBinding` | ✓ | **`KeyMappingHelper.registerKeyMapping`** |
|
||||
|
||||
The 1.21.11 category derives its own label: `photosync:main` becomes
|
||||
`key.category.photosync.main`. The older buckets use whatever string you hand
|
||||
them, which here is `key.categories.photosync`. `en_us.json` carries **both**
|
||||
keys — they are two lines, and the alternative is a missing-translation string in
|
||||
the controls screen on half the buckets.
|
||||
|
||||
### Screenshots
|
||||
|
||||
| | 1.20.1 → 1.21.4 | 1.21.5 | 1.21.8 → 26.2 |
|
||||
| --- | --- | --- | --- |
|
||||
| `Screenshot._grab(File,String,RenderTarget,Consumer<Component>)` | ✓ | **removed** | — |
|
||||
| `Screenshot.takeScreenshot(RenderTarget)` → `NativeImage` | ✓ | **`(RenderTarget, Consumer<NativeImage>)`** | ✓ |
|
||||
| `Screenshot.grab(File,String,RenderTarget,…,Consumer<Component>)` | ✓ | ✓ | **extra `int` (downscale)** |
|
||||
| `NativeImage.writeToFile(File)` / `(Path)` | ✓ | ✓ | ✓ |
|
||||
|
||||
26.2 additionally gains `grab(Minecraft, boolean)`. `Minecraft.getMainRenderTarget()`
|
||||
also moved at 26.1 — the target now hangs off `gameRenderer.mainRenderTarget()`.
|
||||
|
||||
`writeToFile` is the anchor: it is the one member in this entire table that is
|
||||
byte-for-byte identical on all nine versions. The capture mixin therefore
|
||||
redirects `NativeImage.writeToFile` rather than trying to intercept `grab`, whose
|
||||
shape changes three times.
|
||||
|
||||
The mixin declares `method = "*"` deliberately. On **every** bucket the call site
|
||||
is a private static helper rather than `grab` itself, and the helper is named
|
||||
after nothing:
|
||||
|
||||
```
|
||||
private static void <helper>(NativeImage, File, Consumer<Component>)
|
||||
1.20.1 – 1.21.4 method_1661
|
||||
1.21.5 – 1.21.11 method_22691
|
||||
26.2 lambda$grab$3
|
||||
```
|
||||
|
||||
Those names are unspellable in a Mojmap bucket — Mojang's mappings do not name
|
||||
synthetics, so the intermediary name is what survives, and it changes. Matching
|
||||
every method in the class and letting the redirect's target descriptor pick the
|
||||
call site is the only formulation that survives all nine.
|
||||
|
||||
Two facts make redirecting the write safe rather than merely convenient:
|
||||
|
||||
* `NativeImage.writeToFile(File)` *delegates* to `writeToFile(Path)` on every
|
||||
bucket (checked in bytecode, not assumed), so redirecting the `File` overload
|
||||
catches vanilla's F2 exactly once and never fires for the mod's own
|
||||
`writeToFile(Path)` calls. Redirect the `Path` overload instead and every
|
||||
automatic capture publishes twice.
|
||||
* The exact `Path` is known synchronously, at the moment the file exists. No
|
||||
directory watching, no guessing at the filename, no race with the player
|
||||
taking a second screenshot.
|
||||
|
||||
Automatic captures never go through this path. They are taken by
|
||||
`ScreenshotService`, which builds its own filename (that is where the
|
||||
configurable `_auto` suffix comes from) and publishes to `ScreenshotBus` itself,
|
||||
so the mixin only ever reports vanilla F2 presses and reports them as `MANUAL`.
|
||||
The origin of a capture is known by construction rather than by a thread-local
|
||||
flag.
|
||||
|
||||
`CaptureAdapter` does not call `Screenshot.grab` for the same reason: `grab`
|
||||
names the file itself and writes a chat message, and since we intercept its write
|
||||
to notice F2, routing automatic captures through it would make the two origins
|
||||
indistinguishable. It grabs the frame and writes the PNG itself — on the render
|
||||
thread for the readback, on `Util.ioPool()` for the encode.
|
||||
|
||||
That readback turned **asynchronous at 1.21.5**: `takeScreenshot` no longer
|
||||
returns the frame, it hands it to a callback once the GPU fence clears, possibly
|
||||
several frames later. The filename is therefore reserved *before* the grab on
|
||||
those buckets, so captures land in the order they were requested. Reservation is
|
||||
a `synchronized` `Files.createFile` — claiming the name by creating the file
|
||||
empty, not by testing for absence — because two captures a second apart would
|
||||
otherwise agree on a name and one would overwrite the other. A failed write
|
||||
deletes the placeholder.
|
||||
|
||||
### Client
|
||||
|
||||
| | 1.20.1 → 1.21.1 | 1.21.4 → 1.21.8 | 1.21.11 | 26.2 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| current screen | `Minecraft.screen` field | ✓ | ✓ | **`Minecraft.gui.screen()`** |
|
||||
| set screen | `Minecraft.setScreen` | ✓ | ✓ | **`Minecraft.gui.setScreen`** |
|
||||
| render target | `Minecraft.getMainRenderTarget()` | ✓ | ✓ | **`gameRenderer.mainRenderTarget()`** |
|
||||
| `Minecraft.stop()`, `getWindow()` | ✓ | ✓ | ✓ | ✓ |
|
||||
| `execute(Runnable)`, `isSameThread()` | inherited from `BlockableEventLoop`, unchanged | ✓ | ✓ | ✓ |
|
||||
| `Util` package | `net.minecraft.Util` | ✓ | **`net.minecraft.util.Util`** | ✓ |
|
||||
| `Util.ioPool()` | `ExecutorService` | **`TracingExecutor`** | ✓ | ✓ |
|
||||
|
||||
26.1 moved screen ownership off `Minecraft` and onto `Gui`, which holds it now.
|
||||
`Minecraft.setScreenAndShow` still exists and would also work, but it forces a
|
||||
synchronous frame on top of the switch; `Gui.setScreen` is where the plain setter
|
||||
went, and it is what `ScreenAdapter` uses.
|
||||
|
||||
`Util.ioPool()` returning `TracingExecutor` rather than `ExecutorService` from
|
||||
1.21.4 is invisible at the call site — both have `execute(Runnable)` — but it is
|
||||
a hard break if you ever assign the result to a typed local. Don't; call it
|
||||
inline.
|
||||
|
||||
## 4. Mappings: Mojang for 1.20–1.21, identity for 26.x
|
||||
|
||||
Buckets 1–8 use `loom.officialMojangMappings()`. Minecraft 26.x ships already
|
||||
deobfuscated, so there is nothing to remap — but Loom still requires a mappings
|
||||
artifact that declares a `named` namespace. The root build writes a header-only
|
||||
tiny-v2 jar for those buckets:
|
||||
|
||||
```
|
||||
tiny 2 0 official intermediary named
|
||||
```
|
||||
|
||||
Three namespaces, zero rows, which Loom reads as "every name maps to itself". It
|
||||
is generated during configuration rather than by a task because Loom resolves
|
||||
the mappings configuration while configuring the project, so a task output would
|
||||
not exist yet. It is deterministic and 47 bytes; `deobfuscated=true` in a
|
||||
bucket's `gradle.properties` turns it on.
|
||||
|
||||
Consequence when reading code: in a 26.x adapter the names you see *are* the
|
||||
runtime names. In a 1.20–1.21 adapter they are Mojmap names that Loom remaps on
|
||||
build.
|
||||
|
||||
## 5. Adding a new Minecraft version
|
||||
|
||||
Most releases need no work at all — if the new version falls inside an existing
|
||||
bucket's `minecraft_range`, widen the range and ship. Do that check first.
|
||||
|
||||
When it does not:
|
||||
|
||||
1. **Probe before writing anything.**
|
||||
|
||||
```sh
|
||||
./gradlew :platform:<nearest-existing>:build
|
||||
python3 tools/probe-api.py > /tmp/api.txt
|
||||
```
|
||||
|
||||
Add the new version to a scratch platform project first so Loom caches its
|
||||
jar, then diff its section against the nearest existing bucket. If nothing in
|
||||
section 3 moved, you do not need a new bucket.
|
||||
|
||||
2. **Copy the nearest bucket.** `cp -r platform/26.2 platform/<new>`, then edit
|
||||
its `gradle.properties`: `minecraft_version`, `minecraft_range`, `mc_java`,
|
||||
`deobfuscated`, `loader_version`, `fabric_api_version`. Add
|
||||
`include 'platform:<new>'` to `settings.gradle`, and narrow the *previous*
|
||||
bucket's upper bound so the two ranges do not overlap.
|
||||
|
||||
Copy the newest bucket, not the nearest by number — a new release is almost
|
||||
always a continuation of the most recent one, and starting from an older
|
||||
bucket means re-doing every change made since.
|
||||
|
||||
3. **Fix what the compiler complains about.** This is the whole point of the
|
||||
layering: the errors are confined to `platform/<new>` and they are the diff
|
||||
in section 3. Nothing in `shared/` should need to change. If it does, that is
|
||||
a signal the seam is leaking a Minecraft concept and is worth pushing back on
|
||||
before working around.
|
||||
|
||||
If a class in `platform/common` is among the failures, it has stopped being
|
||||
common: move it down into **all** buckets (`for v in platform/*/; do ...`),
|
||||
then change the one copy that needed changing. Do not add version checks to
|
||||
`common` — that is the thing the bucket layout exists to avoid.
|
||||
|
||||
`tools/probe-class.py <class[,class...]> [regex]` javaps one class across
|
||||
every cached bucket jar at once, which is usually faster than reading release
|
||||
notes. For argument *order* questions the notes are useless anyway; javap the
|
||||
jar under `~/.gradle/caches/fabric-loom/minecraftMaven/`.
|
||||
|
||||
4. **Update this document.** Add a column to the affected tables in section 3
|
||||
and a row to the bucket table in section 1. The tables are the reason step 1
|
||||
is cheap next time. If a new API needs a translation key that older buckets
|
||||
do not (as `KeyMapping.Category` did at 1.21.11), add it to
|
||||
`shared/client/src/main/resources/assets/photosync/lang/en_us.json` alongside
|
||||
the old one rather than replacing it — the file is shared by all nine jars.
|
||||
|
||||
5. **Run it.** `./gradlew :platform:<new>:runClient`, then walk the checklist
|
||||
below.
|
||||
|
||||
### Manual checklist
|
||||
|
||||
The seam is not unit-testable — its whole job is to talk to the game — so these
|
||||
are checked by hand once per bucket:
|
||||
|
||||
- [ ] Screens open, close on Escape, and survive a window resize
|
||||
- [ ] Mouse wheel scrolls the timeline; drag scrolls the scrollbar
|
||||
- [ ] Text field accepts typing, and **paste works** (an Immich API key is not
|
||||
typed by hand)
|
||||
- [ ] Thumbnails decode and draw, and are released when the screen closes —
|
||||
watch for a texture leak across repeated opens
|
||||
- [ ] Clipping: the timeline's tiles do not draw outside their viewport
|
||||
- [ ] F2 produces a queue entry with the correct path
|
||||
- [ ] Automatic capture produces a file with the configured suffix
|
||||
- [ ] Quitting with an upload in flight shows the progress dialog, and
|
||||
"quit anyway" actually quits
|
||||
|
||||
## 6. Things that will bite you
|
||||
|
||||
**`lombok.config` changes need a clean build.** Gradle does not track it as a
|
||||
compile input, so changing it leaves stale generated accessors on the classpath
|
||||
and produces "cannot find symbol" errors on methods you can see in the source.
|
||||
`./gradlew :shared:core:clean` first.
|
||||
|
||||
**Compile against the oldest Gson, not the newest.** `gson_api_version=2.10` is
|
||||
what 1.20.1 bundles; 26.2 has 2.14. Compiling against 2.14 would let code
|
||||
reference API that is missing at runtime on older buckets, and the failure would
|
||||
appear only in the field. Same reasoning for `slf4j_api_version=2.0.1`.
|
||||
|
||||
**Minecraft's image decoder is stb_image, which reads PNG and JPEG only** — not
|
||||
WebP, not AVIF. Immich's default thumbnail format is WebP, so `ImmichProvider`
|
||||
sniffs the first thumbnail's magic bytes and falls back to previews (JPEG) for
|
||||
the rest of the session if it sees a RIFF/WEBP header. Any new provider has to
|
||||
honour the same contract, which `PhotoProvider.thumbnail` states explicitly.
|
||||
|
||||
**Everything touching a texture, screen or framebuffer must go through
|
||||
`GameContext.submit`.** Uploads, thumbnail fetches and the capture timer all run
|
||||
on worker threads; the render thread is the only one allowed to touch GPU state.
|
||||
|
||||
**Mixin annotations are remapped in place; there is no refmap.** Loom's
|
||||
non-legacy path rewrites `@Mixin`/`@Inject` targets into intermediary with
|
||||
tiny-remapper, so no `*-refmap.json` is produced and none should be configured.
|
||||
If you want to confirm a mixin actually resolved, unzip the built jar and
|
||||
`javap -p -v` the mixin class — the annotation holds the remapped target
|
||||
(`method_1592` on a Mojmap bucket, the plain name on 26.x). A mixin that silently
|
||||
does nothing usually has a target that remapped to something that no longer
|
||||
exists, and this is the only place that shows.
|
||||
|
||||
**`@Redirect` on an overloaded method needs the delegation checked, not
|
||||
assumed.** The `writeToFile(File)` → `writeToFile(Path)` delegation above is the
|
||||
example: get it backwards and the mod publishes every automatic capture twice,
|
||||
with no error anywhere.
|
||||
|
||||
**26.x buckets are already deobfuscated, so `runClient` output names differ.** A
|
||||
stack trace from bucket 9 reads like source; one from buckets 1–8 reads like
|
||||
Mojmap only because Loom mapped it on the way in. Do not paste a 26.x trace into
|
||||
a search for an older bucket's symbol.
|
||||
Reference in New Issue
Block a user