# 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/<v>     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/<name>`, 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/<v>/…/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.
