# Working on PhotoSync A Fabric client mod that uploads Minecraft screenshots to Immich and browses the album in game, built from one source tree for nine Minecraft versions (1.20 → 26.2). Read [`README.md`](README.md) for what it does and [`docs/PORTING.md`](docs/PORTING.md) before touching anything that imports `net.minecraft`. This file is the orientation an agent needs before its first edit: where code goes, what may import what, and which oddities are deliberate. ## Commands ```sh ./gradlew test # the shared modules' unit tests ./gradlew :platform:1.21.8:build # one bucket, remapped jar included ./gradlew buildAllPlatforms # all nine — the real check before finishing ./gradlew :platform:1.21.8:runClient # dev client for one bucket ``` `buildAllPlatforms` takes a few minutes and is the only thing that proves a change compiles everywhere. A change that only touches `shared/` still has to pass it, because each bucket compiles the shared sources at its own bytecode level. If the machine has no network but a populated Gradle cache, add `--offline`. Two probes answer "what does this method look like on every version", by running `javap` across the cached Minecraft jars: ```sh python3 tools/probe-api.py # the fixed list of members the bridge depends on python3 tools/probe-class.py net.minecraft.client.gui.GuiGraphics '(?i)blit' ``` Use them instead of recalling a signature. Every table in `docs/PORTING.md` was produced this way, and the ones that were not were wrong. ## The module map ``` shared/core no Minecraft, no GUI. Config, uploads, providers, timeline, ThumbHash. Where behaviour that can be unit-tested lives. shared/mc-api the seam: interfaces the game must implement, no bodies. shared/ui every screen and widget, drawn through mc-api only. shared/client wiring — screenshot becomes upload becomes notification. platform/common a source root, not a project: the adapter code that is textually identical on all nine buckets. platform/ one bucket's adapters (dev.photosync.platform.impl) and mixins. ``` The dependency arrow points one way: `platform` → `client` → `ui` → `mc-api` → `core`. Each module's `package-info.java` states its own rule and is worth reading before adding a file to it. The two that get violated by accident: - **Nothing under `shared/` may import `net.minecraft`, `net.fabricmc` or `org.lwjgl`.** If shared code needs something from the game, the answer is a new method on an `mc-api` interface plus nine adapter implementations — never an import. Adding to that seam is expensive on purpose, so first check whether the thing can be computed from what `RenderBridge` already exposes. - **`shared/` compiles to Java 17 bytecode** (`shared_java` in `gradle.properties`), because 1.20–1.20.4 run on a Java 17 JVM. No virtual threads, no Java 21 pattern matching, no `HttpClient.close()`. Platform modules compile at their own bucket's level, which is why the same idiom can be legal in `platform/26.2` and rejected in `shared/core`. ## Where a change goes | You want to | Put it in | | --- | --- | | change upload, retry or queue behaviour | `shared/core/upload` | | add a setting | `shared/core/config` (a record + `normalized()`), then `SettingsScreen` and `en_us.json` | | support another photo service | `shared/core/provider/`, implementing `PhotoProvider` + `ProviderFactory`, added to the list in `PhotoSync`'s one-argument constructor. No UI or platform change needed — the settings screen builds its fields from `ProviderDescriptor` | | change a screen | `shared/ui/screen` | | add a drawing primitive | `RenderBridge` **and all nine** `RenderAdapter`s. Read `docs/PORTING.md` §3 first | | fix something on one Minecraft version | that bucket's `platform//…/impl` | | fix something on every Minecraft version | `platform/common` — but only if it compiles on all nine | The `platform/common` rule has no third case: a class lives there while all nine compilations accept it, and the day one of them stops, it moves down into all nine copies of `impl`. Do not add a version check to keep it in `common`. ## Conventions The compiler does not enforce these; reviewers do. - **Lombok with fluent accessors.** `lombok.config` at the repo root sets `lombok.accessors.fluent = true`, so a `@Getter` on `kind` generates `kind()`, not `getKind()`. Records are the default for data; `@Builder(toBuilder = true)` for config records. - **No utility classes, no scattered constants.** A constant belongs to the class that uses it, as a `private static final` next to that use. If a static helper is tempting, it usually means the behaviour belongs on one of the objects. - **No new dependencies.** Gson and SLF4J are `compileOnly` because Minecraft and Fabric Loader already ship them; nothing is shaded and nothing is jar-in-jar'd, which is why each jar is ~270 KB. Do not write a JSON parser, an HTTP client or a base64 encoder — the platform has all three. - **Comments explain why, not what.** The existing ones record measurements and decisions ("Immich's default thumbnail format is WebP, which the game's decoder cannot read"). Match that; a comment restating the code is noise. - **Don't over-abstract.** One provider interface exists because a second provider is a stated goal. An interface with one implementation and no second in sight is not the house style. - **User-visible strings are translation keys**, in `shared/client/src/main/resources/assets/photosync/lang/en_us.json`. Adding keys is safe on every version; renaming them is not. ## Things that are the way they are on purpose Verify before "fixing" any of these — each one cost a measurement. - **No refmap.** Loom's non-legacy mixin remapping rewrites annotations in place. There is no `refmap.json` and none is needed; `docs/PORTING.md` §6 shows how to confirm it with `javap -p -v` on a built jar. - **`CaptureMixin` targets `method = "*"`** because the private helper that writes the screenshot has three different names across the range, and it redirects the `File` overload rather than the `Path` one because the two delegate — redirecting both publishes every capture twice. - **26.x uses a generated 47-byte identity mappings jar** (root `build.gradle`), because those Minecraft jars ship deobfuscated and Loom still demands a mappings artifact. - **The UI draws its own widgets.** Minecraft's drawing primitives have been stable since 1.20; its widget constructors have not. - **Immich renditions are a ladder, not a constant** (`ImmichProvider.thumbnail`): the grid asks for `thumbnail` then `preview`, an opened photo asks for `fullsize` then `preview`, and a rendition that answers with WebP, a 404 or a 403 is retired for the session. Minecraft decodes PNG and JPEG only — stb_image reads neither WebP nor AVIF — so a rendition's format is a correctness concern, not a preference. - **`ThumbnailCache` floors capacity at 1, not 8.** The grid's real floor comes from `BrowserSettings.normalized()`; the detail cache wants two, because full-resolution textures are tens of megabytes each. ## Testing `shared/core` is the only module with meaningful test coverage, and it is thin — `ThumbHashTest` is what exists. Anything you add to `core` that can be tested without a game should come with tests; `ui` and `platform` are verified by compiling all nine buckets and running a dev client. There is no test that catches a broken adapter. If you change anything under `platform/`, run `runClient` on that bucket and look at the screen.