Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 68 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ dependencies {

## Compatibility

`library-gui` 0.2.x targets Minecraft 26.2 with Minestom `2026.07.22-26.2` and JVM 25.
The host supplies Minestom; consumers must not shade a second copy.
This migration branch targets Minecraft 26.2 with Minestom `2026.07.22-26.2`
and JVM 25. The host supplies Minestom; consumers must not shade a second copy.
The typed-contribution flow below is unreleased on this migration branch; the
eventual `library-gui` release version has not yet been assigned.

## Basics

Expand Down Expand Up @@ -150,45 +152,84 @@ anvilInput(player, Component.text("Party name")) { text ->
## Themes: custom graphics

A theme is the one declaration behind both halves of a custom-looking GUI — the
resource pack the client downloads, and the components the server sends. Keeping
those two in sync by hand is what usually breaks; here neither can be generated
without the other.
resource-pack contribution the product composes, and the components the server
sends.

```kotlin
val guiTheme = theme("grounds", PackFormat(88, minInclusive = 84, maxInclusive = 88)) {
description = "Grounds GUI"
// Background artwork. width/height are the PNG's real size — the generator
// fails the build if the file on disk ever disagrees.
import gg.grounds.gui.pack.toPackContribution
import gg.grounds.gui.theme.PackFormat
import gg.grounds.gui.theme.theme

val guiTheme = theme("grounds", PackFormat(88)) {
frame("hover", "frame/hover.png")
panel("shop", "panels/shop.png", 176, 166, offsetY = -6)
// A graphic that replaces an item's whole appearance.
icon("coin", "icons/coin.png")
// A hover effect: this button's tooltip is drawn with these sprites.
tooltip("gold", "tooltips/gold_bg.png", "tooltips/gold_frame.png")
}

val contribution = guiTheme.toPackContribution(assets)
```

`PackFormat` is per Minecraft version — read `pack_version.resource_major` out of
that version's `version.json` rather than guessing (26.2 is 88). The range says
which clients the pack claims to serve.
`assets` is the product's artwork root. `PackFormat` is per Minecraft version —
read `pack_version.resource_major` out of that version's `version.json` rather
than guessing (26.2 is 88). A theme with any `frame` provides the rendering
capability `grounds:text-marker-shader/v1` and must declare **exactly format
88** (`PackFormat(88)`, with no range); it overrides the 26.2 text shader.

### Compose and write in the product

Building the pack is a build-time step, not a server one:
`library-gui` declares a typed contribution. The product owns the
`PackDefinition`, chooses its other contributions and policy, composes the
complete pack, and writes the final ZIP artifact. This is also where it serves
the artifact and couples the URL sent to clients to that artifact's SHA-1.

This separate product/demo composition code needs the builder explicitly:

```kotlin
writePack(guiTheme, assets = Path.of("art"), out = Path.of("build/pack"))
val sha1 = zipPack(Path.of("build/pack"), Path.of("build/grounds-gui.zip"))
dependencies {
implementation("gg.grounds:resource-pack-builder:0.1.0")
}
```

Host that zip and hand the client both halves:
Ordinary `library-gui` consumers remain API-only and do not add the builder.

```kotlin
player.sendResourcePacks(
ResourcePackRequest.resourcePackRequest()
.packs(ResourcePackInfo.resourcePackInfo(id, URI.create(url), sha1))
.required(true)
.build(),
import gg.grounds.gui.pack.toResourcePackFormat
import gg.grounds.resourcepack.api.PackDefinition
import gg.grounds.resourcepack.api.PackPolicy
import gg.grounds.resourcepack.api.VanillaPathPolicy
import gg.grounds.resourcepack.builder.ResourcePackComposer
import gg.grounds.resourcepack.builder.ZipPackWriter
import java.nio.file.Path

val definition = PackDefinition(
description = guiTheme.description,
format = guiTheme.packFormat.toResourcePackFormat(),
policy = PackPolicy(vanillaPaths = VanillaPathPolicy.ALLOW_CLAIMED),
)
val composed = ResourcePackComposer().compose(definition, listOf(contribution))
val artifact = ZipPackWriter().write(composed, Path.of("build/grounds-gui.zip"))
// Serve artifact.path and pass artifact.sha1 with that exact URL to the client.
```

`ResourcePackComposer` performs validation before it composes: contributions
must be compatible and every claimed vanilla path must be allowed by the
definition. There is **no JSON merge**: a contribution owns complete emitted
files, including each `assets/minecraft/**` file it emits. Two contributions
cannot share a vanilla path.

### Compatibility facades

`writePack` and `zipPack` remain deprecated **0.x facades** for existing callers;
new code uses `Theme.toPackContribution(assets)` and product-owned composition.
They are not the production flow. `library-gui` production consumers need
`resource-pack-api`, but do not need `resource-pack-builder`; builder usage here
is demo/product integration only.

`Theme.vanillaOverrides()` is also a compatibility view: it returns paths
relative to `assets/minecraft`, while `VanillaPathClaim` uses complete pack paths
(for example, `assets/minecraft/shaders/core/text.vsh`).

Using it is the normal DSL — the theme only supplies the title and two item ids:

```kotlin
Expand Down Expand Up @@ -351,6 +392,10 @@ Ask a theme what it claims before shipping it beside another pack:
theme.vanillaOverrides()
```

This list is relative to `assets/minecraft`; composition uses complete
`VanillaPathClaim` paths. Every emitted `assets/minecraft/**` file is exclusively
claimed, because the composer never merges JSON or any other file contents.

It is derived from the theme rather than written down, because a hand-kept list
goes stale: the first one here said "the text shader and the slot highlight" and
was missing the bundle sprites, their `.mcmeta` files and — most quietly —
Expand Down
4 changes: 4 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ group = "gg.grounds"
version = findProperty("versionOverride")?.toString() ?: "0.1.0-SNAPSHOT"

val minestomVersion = "2026.07.22-26.2"
val resourcePackVersion = "0.1.0"

kotlin { jvmToolchain(25) }

Expand Down Expand Up @@ -49,9 +50,12 @@ dependencies {
// Per-player GUIs render per-player language; adventure itself comes from
// Minestom (library-i18n declares it compileOnly), so nothing doubles up.
api("gg.grounds:library-i18n:0.1.1")
api("gg.grounds:resource-pack-api:$resourcePackVersion")

testImplementation(kotlin("test"))
testImplementation("org.junit.jupiter:junit-jupiter:5.11.4")
testImplementation("gg.grounds:resource-pack-builder:$resourcePackVersion")
testImplementation("gg.grounds:resource-pack-testkit:$resourcePackVersion")
// compileOnly is not on the test classpath; the Click-dispatch tests
// construct Minestom click records directly.
testImplementation("net.minestom:minestom:$minestomVersion")
Expand Down
142 changes: 57 additions & 85 deletions examples/theme-demo/README.md
Original file line number Diff line number Diff line change
@@ -1,102 +1,74 @@
# theme-demo

A runnable server for settling a theme's title offsets against a real client.

The library positions a panel with numbers taken from how vanilla lays a
container title out. Nothing in it has been measured against a running client,
and it cannot be — so this demo exists to close that gap: join, open the GUI,
nudge until the artwork's slot grid sits under the real slots, and paste what
`/tune show` prints back into your theme.
A runnable Minecraft 26.2 server that demonstrates typed GUI resource-pack
contributions, title calibration, marker-driven UI, and runtime map imagery.

```bash
./gradlew :examples:theme-demo:run
```

Then connect a **Minecraft 26.2** client to `localhost:25565` and type `/gui`.

## Tuning

The panel is a calibration target, not decoration: 8px rulers along the top and
left edge, and a slot grid at the positions a 3-row container uses. A misalign
is readable in pixels instead of guessable.

| Command | Effect |
| --- | --- |
| `/gui` | Opens the themed GUI |
| `/hover` | Opens the hover-only screen (see below) |
| `/glow` | Toggles the slot glow — **rebuilds the pack**, client refetches |
| `/tune x <px>` | Horizontal offset — instant, no download |
| `/tune y <px>` | Vertical offset — **rebuilds the pack**, client refetches |
| `/tune show` | Prints the current values as a pasteable `panel(...)` line |
| `/tune reset` | Back to the library's defaults |

`x` only changes the string the server puts in the window title, so it applies on
the next open. `y` becomes the glyph's *ascent*, which lives in the font file —
so it is a new pack with a new hash, and the client downloads it again. `/tune y`
therefore does not reopen the GUI for you: reopen with `/gui` once the download
lands, or you will be looking at the old artwork. `DemoThemeTest` pins exactly
this split.

A panel's *advance* is deliberately not tunable. The generator reproduces the
client's own measurement — it trims fully transparent columns off the right edge
before measuring — and fails the build with the correct number, so a hand-set
value could only ever be the wrong one.
Connect a **Minecraft 26.2** client, then use `/gui`. The demo's frame theme
declares **exactly format 88**: frames provide
`grounds:text-marker-shader/v1`, which is the 26.2 text-shader capability.

While a pack is still on its way, `/tune y` and `/tune reset` refuse to send
another. Replacing a push that has not settled makes the client report the old
pack as discarded, and because the push is required, that terminal status would
kick you out of your own session.
## Pack lifecycle

## The hover-only screen
The demo is intentionally product integration, not the `library-gui` production
dependency model. It converts `DemoTheme` with `toPackContribution(ART)`, owns a
`PackDefinition`, composes with `ResourcePackComposer`, and writes the completed
artifact with `ZipPackWriter`.

`/hover` opens a five-slot hopper with everything except the hover effect switched
off: no panel behind the window, no `item_model` on the items, a plain vanilla
title. The middle pickaxe carries a `tooltip_style`; the axe and shovel beside it
carry nothing. Whatever differs when you hover the middle one *is* the effect,
with its neighbours as the control.
Each rebuilt ZIP is published at an immutable, content-addressed URL of the form
`/packs/<sha1>.zip`. The exact artifact SHA-1 is both checked against the bytes
the host snapshots and sent to the client with that same URL. Older URLs remain
valid for the process lifetime, while the pack identity is stable and requests
use `replace(false)` so a rebuild does not discard unrelated server packs.

Five compartments rather than a chest's twenty-seven, so the comparison is one
glance instead of a hunt.
## Commands

The theme also replaces the **slot highlight** with a glow around the hovered
item — the bloom sits on the `back` layer, so it spills into the 4px around the
item rather than washing over it, and the `front` layer is left empty.

That one is a vanilla sprite override, so unlike the tooltip skin it is global: it
changes the hover glow in every container, the player's own inventory included,
and cannot be limited to a single GUI. `/glow` toggles it and re-sends the pack,
which is the only honest way to decide between everywhere and nowhere — those are
the two options.

Between them those two are the full extent of what a server can do to hover. The
client never reports what the cursor is over — it draws *this* item's tooltip
with *these* sprites, and its own highlight wherever it likes. Effects tied to
one specific slot need a core shader in the pack, which this demo does not ship.
| Command | Effect |
| --- | --- |
| `/gui` | Opens the calibrated themed GUI. |
| `/overview` | Opens the marker and hover overview. |
| `/hover` | Opens the hover-only comparison screen. |
| `/glow` | Toggles the global slot glow and rebuilds/re-sends the pack. |
| `/highlight` | Blanks or restores the vanilla highlight and rebuilds/re-sends the pack. |
| `/tint` | Toggles runtime-only empty-tile tinting; reopen the screen, no pack rebuild. |
| `/tune x <px>` | Changes the title offset; reopen the GUI, no pack download. |
| `/tune y <px>` | Changes glyph ascent; rebuilds/re-sends the pack. |
| `/tune show` | Prints a pasteable `panel(...)` declaration. |
| `/tune reset` | Restores the tuning offset defaults and rebuilds/re-sends the pack. |
| `/story`, `/market`, `/menu`, `/dialog`, `/ui` | Open the other GUI and dialog demonstrations. |
| `/mapdemo` | Shows a per-player image sent as map data. |

Do not send another rebuilding command while a pack request is pending: the demo
refuses it so an in-flight required request is not discarded. `/tune y`,
`/tune reset`, `/glow`, and `/highlight` need a fresh download; reopen the
relevant GUI after the client has loaded it.

## Hover and markers

`/hover` isolates a tooltip-style effect. The global slot-highlight override is
deliberately separate: it affects every container, including the player's own
inventory. Marker frames are different: their glyphs ride in the hovered item's
tooltip and the shader relocates them, allowing a selected slot or control to
draw its own frame without the server learning the cursor position.

`/mapdemo` demonstrates the boundary. A marker sprite must already exist in the
pack, so it cannot show per-player or newly generated artwork. The cartography
table demo instead draws a map per player and sends map data; the image is not a
marker and is not stored in the resource pack.

## Configuration

| Variable | Default | Meaning |
| --- | --- | --- |
| `SERVER_PORT` | `25565` | Minecraft listen port |
| `PACK_PORT` | `8080` | HTTP port the pack is served on |
| `PACK_HOST` | `127.0.0.1` | Host **the client** resolves to fetch the pack |

`PACK_HOST` is the one that catches people out: the client downloads the pack,
not the server. If the client runs on another machine, `127.0.0.1` points it at
itself — set an address that machine can actually reach.

## Things worth knowing before you run it

- **The server is in offline mode.** `MinecraftServer.init()` is
`init(Auth.Offline())` — there is no `MojangAuth` in 26.2. It accepts any
username with no session check. Local development only; never expose it.
- **The pack is sent as required.** Minestom kicks on any terminal pack status
that is not `SUCCESSFULLY_LOADED`, so a declined or failed download drops the
player with "Required resource pack was not loaded." That is deliberate: a
silent fallback to vanilla would waste your time wondering why nothing looks
themed. The console logs every terminal status.
- **The hash cannot drift.** The SHA-1 handed to the client is computed from the
exact bytes the HTTP host serves, in the same call. A stale hash would kick
every player who joins, which is the failure this arrangement removes.
- **Artwork is generated, not drawn.** `art/generate.py` rebuilds all four PNGs;
run it if you want to change the calibration target.
| `SERVER_PORT` | `25565` | Minecraft listen port. |
| `PACK_PORT` | `8080` | HTTP port for immutable artifacts. |
| `PACK_HOST` | routable local address | Host the **client** resolves to fetch the artifact. |
| `PACK_REQUIRED` | `true` | Set `false` only while iterating on a broken shader. |

`PACK_HOST` must be reachable from the client; `127.0.0.1` is the client's
loopback when the server and client are on different machines. The demo runs in
offline mode for local development only. Artwork is generated by the demo's
Gradle task when needed: `./gradlew :examples:theme-demo:paintArt`.
1 change: 1 addition & 0 deletions examples/theme-demo/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ repositories {

dependencies {
implementation(project(":"))
implementation("gg.grounds:resource-pack-builder:0.1.0")
// The library declares Minestom compileOnly, so a runnable example has to bring a real one.
implementation("net.minestom:minestom:2026.07.22-26.2")
runtimeOnly("org.slf4j:slf4j-simple:2.0.18")
Expand Down
Loading