Skip to content
Open
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
60 changes: 59 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ through the inventory title and HUD, and generates a typed Kotlin view for each
server. If you rename a button in the UI, the server code stops compiling instead of breaking at runtime.

Inventory UIs support real items, scrolling collections, paging, toggles, choices, tooltips, and native anvil text
input. HUDs are positioned with core shaders and rendered by the server whenever it wants to update them.
input. HUDs are positioned with core shaders and rendered by the server whenever it wants to update them. Full-screen
windows are drawn over the whole screen with no screen open, and the player aims a cursor by turning.

> [!WARNING]
>
Expand Down Expand Up @@ -191,6 +192,63 @@ You own the rest:
- Per-player state and cleanup, such as stopping the resend task when the player leaves.
- Threading: a `HudView` or `HudStack` is not thread-safe, so render it from one thread at a time.

## Full-screen windows

A full-screen window is authored with [`<Fullscreen>`](docs/AUTHORING.md#full-screen-windows) and generates a typed
view, such as `LobbyFullscreen`, that extends `FullscreenView`. With the `minestom` target it takes a `Player`; with
other targets it takes a `FullscreenHost`, which only `window-minestom` implements (`MinestomFullscreenHost`). The
player aims by turning, clicks with the attack key, and leaves by sneaking. An excerpt of the example's
[`MyLobby`](jvm/example/src/main/kotlin/com/chunkzero/window/example/MyLobby.kt):

```kotlin
class MyLobby(player: Player) : LobbyFullscreen(player) {
private var name by state("${player.username}'s Foundry")
private val worlds = list(cells = 3, key = { it: String -> it }, step = 1) { WORLDS }

override fun foundryName(): Component = Component.text(name)

override fun worldName(index: Int): Component = Component.text(worlds.at(index).orEmpty())

// A click opens a native dialog; the canvas is back once the player confirms or cancels.
override fun onRename(click: FullscreenClick) {
promptText(Component.text("Name your foundry"), name) { text -> text?.let { name = it } }
}

override fun onScroll(steps: Int) = repeat(abs(steps)) { if (steps > 0) worlds.next() else worlds.previous() }

// The other generated bindings and handlers are omitted.
}

MyLobby(player).open()
```

- Handlers receive a `FullscreenClick` with the cursor's canvas position. `onScroll(steps)` gets the scroll wheel as
signed steps, and `onRightClick(click)` gets right clicks anywhere on the canvas.
- `list(...)` pages a list through a `<Repeater fixed>`'s cells, like `WindowView.list(...)`.
- `promptText(title, initial) { text -> }` shows a native dialog with a text field and Confirm and Cancel buttons. The
callback gets the text, or `null` on Cancel or when the window closes. Escape does not dismiss it.
- Views are single-use, as with windows, and a player has at most one full-screen window open.

While the window is open, the Minestom host:

- keeps the player in spectator mode on the server and tells only their client it is in survival, so clicks, the scroll
wheel, and number keys reach the server. Game mode, ability, hotbar, time, and max health updates the server sends
meanwhile still reach the client, followed by packets that hide them again. Closing restores the player's game mode
(or the one the server set meanwhile), abilities, position, and held slot.
- hides the HUD by holding the client's world age negative, which the pack's shaders read. The hidden strip at the
bottom of the screen can clip the player's own inventory (E) at small GUI heights, since the server is not told it
opened.
- closes the window when the player dies or spawns into another instance. Opening one while the player is dead or
respawning throws, so a respawn listener should open it on the next tick.

Limitations:

- First person only: third person (F5) mirrors the camera, so the drawn cursor and the server's hit-test disagree.
- Shader packs that replace world rendering, such as Iris, are not supported.
- The client sends at most one held-slot change per tick, so `onScroll` gets one update per tick of at most 4 steps
either way, more than 4 notches in one tick read as a scroll the other way, and number keys look like scrolling.
- Chat and other GUI overlays draw over the canvas, and there are no real items.

## Supported versions

Window targets Minecraft 26.2 (resource pack format 88). The hosts and example server use Minestom builds for 26.2, and
Expand Down
34 changes: 28 additions & 6 deletions docs/AUTHORING.md
Original file line number Diff line number Diff line change
Expand Up @@ -564,10 +564,11 @@ const raised = (fill: Token<Color>) =>

### Roots

| root | `raw` equivalent | notes |
| ---------------------------------------------------- | ---------------- | ---------------------------------------------------------------- |
| `<Window name container frame? bleed? text?>` | `ui(...)` | `frame` is drawn first over the GUI grown by `bleed` |
| `<Hud name channel? width? height? anchor? offset?>` | `hud(...)` | children lay out as a column; without a size it fits its content |
| root | `raw` equivalent | notes |
| ---------------------------------------------------- | ----------------- | ---------------------------------------------------------------- |
| `<Window name container frame? bleed? text?>` | `ui(...)` | `frame` is drawn first over the GUI grown by `bleed` |
| `<Hud name channel? width? height? anchor? offset?>` | `hud(...)` | children lay out as a column; without a size it fits its content |
| `<Fullscreen name mode? backdrop?>` | `fullscreen(...)` | see [Full-screen windows](#full-screen-windows) |

`text={{ ... }}` on a window, HUD, section, box, or switch sets default text style (`color`, `shadow`, `bold`, `italic`,
`underlined`, `strikethrough`, `obfuscated`, `font`, `smallCaps`, `align`) for the text inside it; the nearest setting
Expand Down Expand Up @@ -612,8 +613,25 @@ which the client still draws once the shader has moved them.

### Full-screen windows

`<Fullscreen name mode? backdrop?>` lays its children out on a 480x270 canvas drawn over the whole screen with no screen
open; the player aims a cursor by turning. It takes the same elements as a window, except those that need a container:
`<Fullscreen name mode? backdrop?>` (`raw.fullscreen(...)`) lays its children out on a 480x270 canvas drawn over the
whole screen with no screen open; the player aims a cursor by turning. List it in `defineWindows({ fullscreens })`. The
canvas is letterboxed or pillarboxed to the screen and does not move with FOV. Its children lay out as a column in a
root box that takes box props such as `align`, `justify`, `text`, and `theme`. A pack with a full-screen window emits
the core shaders that draw it, with no `coreShaders` option, and needs pack format 85 through 88.

```tsx
<Fullscreen name="lobby" backdrop="#140b08" align="center" justify="center">
<Button onClick={action("play")} width={60} height={18} tooltip="Joins the round">
Play
</Button>
</Fullscreen>
```

- `mode`: `"unlocked"`, the default, keeps the camera on the player, so the world turns behind the canvas as they aim.
`"horse"` locks the view in place and draws the cursor about a tick behind the mouse.
- `backdrop`: a hex color filling the screen behind the canvas, letterbox included. Omit it to show the world.

It takes the same elements as a window, except those that need a container:

- **Supported**: art, static and bound text, switches and conditions, tints, bars, regions and the industrial controls
built on them (with hover outlines), `toggle` and `selection` state, and indexed handles. A `<Repeater>`'s cells are
Expand All @@ -629,6 +647,10 @@ open; the player aims a cursor by turning. It takes the same elements as a windo
region's `itemModel`, and text `shadow` are also rejected.
- `span` and `at` count grid tracks, which are slots only in a section. A `<Repeater fixed>` sizes its `cell`s at 18px
per slot; give other industrial controls a size or a grid to fill on a full-screen window.
- Text costs a [text-row font](#what-things-cost) per distinct row, as in windows, and full-screen rows rarely line up
with container rows. Each tooltip line counts as a row.

The server side is in the README's [Full-screen windows](../README.md#full-screen-windows).

## Industrial

Expand Down
4 changes: 4 additions & 0 deletions docs/VALIDATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ sprites, container/hotbar item sync, cards that render a real item in one of the
routes clicks to the card's region, left/right/shift/hotbar clicks, reactive title invalidation, anvil input, HUD
diagnostics, GUI scale 2, and clean disconnect.

It does not cover full-screen windows. Their compiler output, cursor mapping, session, and host behavior are covered by
the deterministic checks and the `fullscreen` golden case; check how they draw and take input in a real client with the
example's `/lobby` and `/lookout`, in first person, at a few window sizes and GUI scales.

The `just mc-validation` recipe defaults to the sibling `../mc-validation` checkout and passes its absolute path to both
Gradle builds. Override it with:

Expand Down
45 changes: 28 additions & 17 deletions example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ example/
│ ├── rpp.config.ts # pack metadata, build, and Window plugin configuration
│ ├── src/
│ │ └── window/ # UI sources — stripped from the built pack
│ │ ├── index.ts # defineWindows entry: the sprite catalog, windows, HUDs, and previews
│ │ ├── index.ts # defineWindows entry: the sprite catalog, windows, HUDs, full-screen windows, and previews
│ │ ├── handles.ts # typed bindings shared by the windows
│ │ ├── art.ts # inline art: icons, coin, vents, bars, and the rank sprites
│ │ ├── shop.tsx # paged catalog, selections, toggles, and buttons
│ │ ├── search.tsx # linked native-anvil catalog search
│ │ ├── upgrades.tsx # themed workshop: styles, vars, cards with a face per stage, choices, and a hotspot
│ │ ├── lobby.tsx # full-screen main menu: paged worlds, a rename prompt, tooltips, and a ready switch
│ │ ├── lookout.tsx # horse-mode full-screen window with a signal bar
│ │ ├── status_hud.tsx # the shader-relocated actionbar HUDs
│ │ └── clock_hud.ts # the clock HUD, written with the function-style raw API
│ ├── build/previews/ # preview PNGs of the windows and HUDs, rewritten by every build
Expand All @@ -24,13 +26,14 @@ example/
```

The Minestom server is the Gradle module `jvm/example`, which depends on `window-minestom`. `Main.kt` boots the server,
`Market.kt` and `Workshop.kt` are a throwaway domain model, and `MyShop.kt` and `MyUpgrades.kt` implement the
**generated** `ShopView` and `UpgradesView`. `MyStatusHud.kt` implements the **generated** HUD views. The pack uses
`target: "minestom"`, so a window view takes the `Player` it is shown to: `Main.kt` opens the shop with
`MyShop(player, market).open()`. Generated HUD views take no player; the handwritten ones that need it take it
themselves. `Main.kt` joins the status HUDs into one action-bar `HudStack` and resends it with `player.sendHud(stack)`
every tick, which keeps the wave bar moving smoothly, re-renders the clocks, and stays ahead of the action bar fading
out. The bindings live under `generated/`, produced by the rpp plugin — committed, do not hand-edit.
`Market.kt` and `Workshop.kt` are a throwaway domain model, `MyShop.kt` and `MyUpgrades.kt` implement the **generated**
`ShopView` and `UpgradesView`, and `MyLobby.kt` and `MyLookout.kt` the full-screen `LobbyFullscreen` and
`LookoutFullscreen`. `MyStatusHud.kt` implements the **generated** HUD views. The pack uses `target: "minestom"`, so a
window view takes the `Player` it is shown to: `Main.kt` opens the shop with `MyShop(player, market).open()`. Generated
HUD views take no player; the handwritten ones that need it take it themselves. `Main.kt` joins the status HUDs into one
action-bar `HudStack` and resends it with `player.sendHud(stack)` every tick, which keeps the wave bar moving smoothly,
re-renders the clocks, and stays ahead of the action bar fading out. The bindings live under `generated/`, produced by
the rpp plugin — committed, do not hand-edit.

## Run it

Expand All @@ -51,6 +54,12 @@ levels, each card shows its level as text and is marked new, shows a bar, or is
the shared tone. A maxed card takes no clicks, so hovering it shows "Fully upgraded" and no outline. Its levels last
until the server stops.

`/lobby` opens a full-screen main menu over the world; aim by turning and sneak to leave. Click the foundry name to
rename it in a native dialog, which returns to the lobby on Confirm or Cancel. The arrows or the scroll wheel move the
world list, a click selects a world, and right clicking anywhere toggles ready, which switches the line under the list
and retints the status. Play leaves once you are ready. `/lookout` opens a horse-mode window, which locks the view while
the cursor follows the mouse; Charge fills its signal bar.

The actionbar-backed HUD demo is disabled by default so the inventory examples stay visually focused. Enable it with
`-Dwindow.hud.enabled=true`; use `-Dwindow.hud.spriteDebug=true` to print each HUD's static art to chat. The pack blanks
vanilla's experience bar, and one of the HUDs draws its wave bar there.
Expand All @@ -59,6 +68,8 @@ vanilla's experience bar, and one of the HUDs draws its wave bar there.
| -------------------------------- | ---------------------------------------------------------------------- |
| `/shop` | opens the catalog, like the hotbar emerald |
| `/upgrades` | opens the workshop, like the catalog's Workshop button |
| `/lobby` | opens the full-screen lobby |
| `/lookout` | opens the horse-mode lookout |
| `/tone <ok\|warn\|danger\|auto>` | pins the tone the HUDs and newly opened windows draw in; `auto` unpins |
| `/bar <fraction\|auto>` | pins how full the wave bar and a newly opened shop's bar are |

Expand All @@ -71,12 +82,12 @@ when testing from another machine.
## The typed contract

The rpp plugin emits `internal` pack data (`WindowPackData`, `WindowFonts`, `WindowSpacers`, and friends), the public
`WindowDefinitions` and `WindowHudDefinitions`, and one public abstract class for each window/HUD. Each handle in
`handles.ts` becomes a member of the views that use it: `text` handles such as `balance` and `status` are functions
returning a `Component`, `flag` handles such as `canBuy` are Boolean functions, `action` handles such as `buy` are
`onBuy(click)` handlers, `products` is a `WindowCollection` that `MyShop` backs with a `WindowList`, and the
`category`/`sort` selections and `favorites`/`affordable` toggles are UI-owned `var`s with `on…Changed` hooks. In
`UpgradesView` the indexed handles take the card's index (`upgradeLevel(index)`, `onUpgrade(click)` with `click.index`),
the `upgradeStage` value and the `rank` sprite return generated enums, and `upgradeProgress` and `tone` return a bar's
fraction and a tint's palette entry. `MyStatusHud` implements each HUD's handles. Omit one, rebuild, and the breakage
surfaces at compile time.
`WindowDefinitions`, `WindowHudDefinitions`, and `WindowFullscreenDefinitions`, and one public abstract class for each
window, HUD, and full-screen window. Each handle in `handles.ts` becomes a member of the views that use it: `text`
handles such as `balance` and `status` are functions returning a `Component`, `flag` handles such as `canBuy` are
Boolean functions, `action` handles such as `buy` are `onBuy(click)` handlers, `products` is a `WindowCollection` that
`MyShop` backs with a `WindowList`, and the `category`/`sort` selections and `favorites`/`affordable` toggles are
UI-owned `var`s with `on…Changed` hooks. In `UpgradesView` the indexed handles take the card's index
(`upgradeLevel(index)`, `onUpgrade(click)` with `click.index`), the `upgradeStage` value and the `rank` sprite return
generated enums, and `upgradeProgress` and `tone` return a bar's fraction and a tint's palette entry. `MyStatusHud`
implements each HUD's handles. Omit one, rebuild, and the breakage surfaces at compile time.
2 changes: 1 addition & 1 deletion example/pack/rpp.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export default defineConfig({
namespace: "window",
coreShaders: { hud: true, hover: true },
vanilla: { anvil: { field: "search_field" }, hud: { experience: false }, slotHighlight: false },
size: { report: true, budget: 1_000_000 },
size: { report: true, budget: 1_150_000 },
previews: { output: "build/previews" },
kotlin: {
package: "com.chunkzero.window.example.generated",
Expand Down
17 changes: 17 additions & 0 deletions example/pack/src/window/handles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,20 @@ export const amount = selection("amount", ["one", "five", "max"]);
export const rank = sprite("rank", { only: ["rank_1", "rank_2", "rank_3"] });
export const rankName = text("rank_name");
export const openShop = action("open_shop");

// Lobby and lookout, the full-screen windows; `tone` is shared
export const WORLDS = 3;
const worlds = { shape: [WORLDS] } as const;
export const worldName = text("world_name", worlds);
export const pickWorld = action("pick_world", worlds);
export const previousWorld = action("previous_world");
export const nextWorld = action("next_world");
export const foundryName = text("foundry_name");
export const rename = action("rename");
export const lobbyStatus = text("lobby_status");
export const ready = flag("ready");
export const play = action("play");

/** How far the lookout's signal has charged. */
export const signal = fraction("signal");
export const charge = action("charge");
9 changes: 9 additions & 0 deletions example/pack/src/window/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { art } from "plugin:window/theme/industrial";
import { ranks } from "./art.ts";
import clock, { runtime } from "./clock_hud.ts";
import * as handles from "./handles.ts";
import lobby from "./lobby.tsx";
import lookout from "./lookout.tsx";
import search from "./search.tsx";
import shop from "./shop.tsx";
import statusHuds, { bottomCenter, coins, hint, power, rate, topLeft, waveBar, waveCharge } from "./status_hud.tsx";
Expand Down Expand Up @@ -33,6 +35,7 @@ export default defineWindows({
sprites: { searchField: art.inputField, ...ranks },
windows: [shop, search, upgrades],
huds: statusHuds,
fullscreens: [lobby, lookout],
// `rpp build` writes these to build/previews/, one PNG per preview.
previews: [
preview(shop, [
Expand Down Expand Up @@ -76,5 +79,11 @@ export default defineWindows({
{ name: "status_bottom_center_danger", scale: 3 },
),
preview(waveBar, [[waveCharge, 0.6]], { scale: 3 }),
preview(lobby, [
[handles.foundryName, "Steve's Foundry"],
[handles.lobbyStatus, "Ready for Emberly"],
[handles.worldName, ["Ironvale", "Emberly", "Ashmoor"]],
[handles.ready, true],
]),
],
});
Loading
Loading