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
9 changes: 9 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,15 @@ jobs:
# setup-java left in JAVA_HOME: RetroFuturaGradle and ForgeGradle run game tooling
# inside the Gradle process itself, so the daemon's JVM is part of the build
# definition. `-D` on the command line overrides the project's gradle.properties.
# The odd one out: RetroFuturaGradle covers only 1.7.10 and 1.12.2, so 1.8.9 has to
# use the era-correct ForgeGradle 2.1 — which needs Gradle 2.14 on Java 8. That is
# why this adapter carries its own wrapper and ignores the pinned Gradle above.
- name: Build forge-1.8.9
working-directory: mod/adapters/forge-1.8.9
env:
JAVA_HOME: ${{ env.JAVA_HOME_8_X64 }}
run: ./gradlew build --no-daemon

- name: Build forge-1.12.2
working-directory: mod/adapters/forge-1.12.2
run: gradle build ${{ env.TOOLCHAINS }} -Dorg.gradle.java.home=$JAVA_HOME_17_X64
Expand Down
9 changes: 8 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,20 @@ release/
launcher/build/
launcher/resources/

# tsc's incremental cache. It changes on every typecheck and describes one machine's last
# build, so committing it makes every branch conflict over a file nobody reads.
*.tsbuildinfo

# Java / Gradle
build/
.gradle/
bin/
*.class
*.jar
!gradle/wrapper/gradle-wrapper.jar
# Anchored with `**/` so it reaches an adapter's own wrapper, not just a root one. The
# 1.8.9 adapter is the only build here that needs a wrapper — without its jar committed, a
# fresh clone and CI both fail at `./gradlew`.
!**/gradle/wrapper/gradle-wrapper.jar

# Forge / Minecraft dev
run/
Expand Down
42 changes: 39 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,27 @@ Targets **Minecraft 1.8.9 → 26.2** on Forge.

## Status

Early but working end to end in code: the launcher builds and runs, both adapter jars
compile against real Forge toolchains, and 202 launcher tests plus 23 Java tests pass.
Early but working end to end in code: the launcher builds and runs, all three built adapter
jars compile against real Forge toolchains, and 304 launcher tests plus 23 Java tests pass.

Ella opens on a five-step guide — install a version, create a project, add a block, point
at Blockbench, launch — where every step reads its own state rather than being ticked, so
it can never claim something is done when it is not, and a step that stops being true goes
back to undone on its own. Once all five pass, the guide folds away and the same space
becomes a dashboard. Every control that is disabled says in its tooltip why, because a
greyed-out button with no reason reads as a broken app rather than a missing prerequisite.

**A project is bound to the version it was authored for.** Opening it preselects that
version, and launching a different one stops first to say what the change would do — listed
against the project's own files, entry by entry, with an offer to rewrite the ones Ella can.
The two divergences that matter are both silent in game: a `parent` overrides a model's own
geometry below 1.9, and vanilla's texture folders were renamed in 1.13. Finding either from
a black block in the world is exactly what that dialog exists to prevent.

**Changes that touch files can be taken back from the notification that reports them** — a
deletion, a rename, a texture variable removed, a model Ella rewrote. Files a deletion took
wait in a per-session stash rather than being destroyed, so the offer is real rather than a
promise the disk cannot keep.

| Component | State |
|---|---|
Expand All @@ -22,16 +41,23 @@ compile against real Forge toolchains, and 202 launcher tests plus 23 Java tests
| Adapter presence check on every launch | done |
| Version install / uninstall | done |
| Quick launch from any view | done |
| Guided setup — five steps read off live state | done |
| Animated splash while the launcher starts | done |
| Project and entry deletion | done |
| Undo a change from its notification | done |
| Projects bound to a Minecraft version, with model migration | done |
| Turntable previews of blocks and items | done |
| Block/item editor with capability gating | done |
| Blockbench open + file watch | done |
| Blockbench live-sync plugin | done |
| Resource pack `.zip` export | done |
| Windows installer + portable `.exe` | done — built and smoke-tested |
| GitHub Actions build & release workflow | written, not yet run against a remote |
| Forge adapter 1.12.2 (`[1.12, 1.13)`) | **verified in game** — blocks render in hand and placed |
| Forge adapter 1.8.9 (`[1.8.8, 1.9)`) | **verified in game** — mod loads, blocks register and place |
| Forge adapter 1.21.1 (`[1.21.1, 1.21.2)`) | builds and loads; in-game rendering not yet confirmed |
| Forge adapters 1.8.9 and 1.16.5–1.20.1 | not started |
| Forge adapter 1.16.5–1.20.1 | not started |
| Minecraft 1.7.10 and older | **not possible** — see below |
| OBJ model source | designed for, not implemented |

The full loop works on 1.12.2: the launcher installs Minecraft and Forge, picks the right
Expand All @@ -43,6 +69,16 @@ in [`docs/building.md`](docs/building.md) and the cross-version traps in
[`docs/architecture.md`](docs/architecture.md). Six of the seven were invisible to the
compiler and only surfaced in a running game.

**1.7.10 and older cannot be supported, and the reason is not effort.** The JSON model
system arrived in 1.8: the 1.7.10 client jar contains zero model and blockstate files
against 1.8.9's 1935, and its `assets/minecraft/` holds only fonts, languages, shaders,
texts and textures. Block shape on 1.7.10 lives in Java code and item appearance in
atlas sprites, so there is no file for Blockbench to edit and no file for a save to
update. Ella could only fake it by reimplementing a model interpreter inside the mod —
which is precisely the "renders something close to what the file says" failure this
design exists to avoid. Texture-only live editing on plain cubes would be possible; ask
if that is worth having.

**Version coverage is narrow and deliberately honest.** An adapter claims only the
versions it was compiled against, because Forge changed the block properties and item
component APIs inside every bucket — "compiles for 1.21.1" says nothing about 1.21.11.
Expand Down
113 changes: 113 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,73 @@ that merge are easy to get wrong, and both fail far from their cause:
Both mistakes share a shape worth remembering: a modded version id looks enough like a
version to pass through code unnoticed, and fails somewhere else entirely.

## A parent overrides the child's geometry on 1.8.x

`ModelBlock.getElements()` on 1.8.9 is, in full:

```java
return this.hasParent() ? this.parent.getElements() : this.elements;
```

with `hasParent()` being nothing more than `parent != null`. The parent wins outright. That
is why vanilla 1.8.9's own `block/cube.json` declares **no** parent and inlines its
elements — it only gained `"parent": "block/block"` in 1.9, once the semantics flipped to
"the child's elements win if it has any".

Blockbench keeps whatever parent it finds. So a model that starts life as
`{ parent: "block/cube_all" }` and then gains geometry carries that parent forever, and on
1.8.x renders as a plain cube — in the missing-texture checkerboard, because Blockbench
also rewrites the texture keys and the parent's `#all` stops resolving. The author sees a
magenta cube and reasonably concludes Ella lost their model. It loaded and was overruled.

Two changes, because one alone is not enough:

- The generated starting model is **self-contained** — its own full-cube elements, `all`
and `particle` declared, no parent. There is then no parent for Blockbench to keep. It
also has to declare `particle` explicitly, which `block/cube_all` used to supply for free.
- Models that already carry the trap are detected from the parsed model the editor already
holds, and the warning offers to drop the `parent`. Dropping it is the whole fix and
costs nothing: from 1.9 onwards that parent's geometry was being ignored anyway.

Ella does not rewrite the author's file on its own. Saving in Blockbench has to stay the
only thing that changes it, so the fix is a button rather than a repair on load.

## Two generations of Forge installer

Ella runs the official Forge installer headlessly rather than reimplementing it. From 1.13
onwards that is not a preference: installation runs binary-patch and deobfuscation
processors, and reproducing those would mean tracking a toolchain that is not ours.

It stops being possible going the other way. `--installClient` was added to the installer
around 2018; older builds abort with *"'installClient' is not a recognized option"*. That
covers every Forge build for 1.8.x — so the whole 1.8 line installed as vanilla, and
because a Forge failure is collected as a warning rather than thrown, the version still
appeared installed. The symptom was "I have no Forge for 1.8.8", a long way from the cause.

Those same builds predate the processors, so their install genuinely is just unpacking:
write `versionInfo` from `install_profile.json` as the version json — it carries
`inheritsFrom`, so the vanilla document supplies the rest — drop the universal jar at the
path `install.path` names inside `libraries/`, and download the twenty-odd libraries the
version file lists.

That last step is the one to get wrong, because the official installer does it invisibly.
Skip it and the game dies before its window opens with
`NoClassDefFoundError: org/objectweb/asm/ClassVisitor`: ASM is listed in the 1.8.x Forge
file with no download url at all, so nothing else in the pipeline would ever fetch it. The
launcher already knows how to resolve the three shapes a library entry can take — a direct
url, a repository base, or nothing but a coordinate — so the legacy path reuses that code
rather than carrying a second copy of it.

A missing jar does not stop Java from starting, which is why `buildLaunchCommand` now
refuses to launch with an incomplete classpath. Checking costs a few stat calls and turns
a stack trace naming a *class* into one sentence naming the *file* and the fix.

Which path applies is read from the installer's own profile, not from the Minecraft
version: the old generation carries a `versionInfo` block, the new one carries
`processors`. The change came with an installer release rather than a game release, and
1.12.2 sits on the new side of it while 1.8.9 sits on the old one — a version-number rule
would have put them the wrong way round.

## Install and uninstall

Downloaded content falls into three groups, and the difference decides what an uninstall
Expand Down Expand Up @@ -188,6 +255,52 @@ version cannot honour and says why. One UI serves the whole range honestly.

See [`protocol.md`](protocol.md) and [`project-format.md`](project-format.md).

## Guided setup, derived rather than stored

Ella's loop needs five things true at once — a version installed, a project open, an entry
in it, Blockbench reachable, and the mod connected — and the order is not guessable from
the navigation. The home view walks them as an ordered list.

Nothing about that list is persisted. `shared/workflow.ts` takes a flat snapshot of the
same state the rest of the UI reads and returns which steps are done and which one is
current; there is no "onboarding completed" flag anywhere. Two things fall out of that:
a step cannot claim to be done when it is not, and a step that stops being true — the game
exits, the project is closed — reopens on its own without anything having to invalidate it.
It is also the reason the checklist is safe to keep showing after setup is complete, where
it collapses into a toggle rather than disappearing.

The same principle covers disabled controls. Every one of them carries the reason in its
`title` — *launch the game first*, *Java 21 was not found*, *stop the running game* —
because the three states that dim the give/place buttons have three different fixes, and a
greyed-out button with no reason reads as a broken app rather than a missing prerequisite.

Whether Blockbench is reachable is asked of the main process (`blockbench:resolve`) rather
than inferred from the configured path, since the usual case is an empty setting and a
successful auto-detection.

## Two write paths for the slot namespace

`writeSlotNamespace` wipes and regenerates everything: four files per block slot plus one
per item slot, so a default pool of 128 + 128 is 640 files, about 450 ms. That is the right
answer for a change that moves bindings, renames the namespace or deletes an entry, because
a stale blockstate left behind keeps rendering a ghost.

It is the wrong answer for a settings change, which arrives once per slider tick. Settings
reach the pack through exactly one thing — the render layer, baked into that slot's
redirect model — so `writeEntrySlot` rewrites that slot alone, four files and about 2 ms.
`entries:patchLive` takes that path.

Two related rules follow from the same reasoning:

- The `ella` namespace is excluded from the file watcher. It is Ella's own output, and
watching it meant every settings change fed its own writes back in as a model change:
a reload the game did not need, plus a preview refresh in the editor for each one.
- The editor's settings form is driven by a local draft, not by the saved project. A
control bound to the round trip cannot follow the mouse. Writes are coalesced behind
the draft, one at a time, and a spinner reports that they are in flight — the controls
are never disabled while saving, since that would reintroduce exactly the stall the
draft exists to remove.

## Language policy

Code, comments, identifiers and documentation are English. Every user-facing string is
Expand Down
38 changes: 28 additions & 10 deletions docs/building.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# Building Ella

Three independent builds. Nothing is shared through a repository — the adapters compile
Four independent builds. Nothing is shared through a repository — the adapters compile
`ella-core` from source — so they can be built in any order.

## Launcher

```bash
cd launcher
npm install
npm test # 202 tests, runs straight off the TypeScript sources
npm test # 232 tests, runs straight off the TypeScript sources
npm run typecheck
npm run dev # Electron with hot reload
npm run build # production bundle into out/
Expand Down Expand Up @@ -38,7 +38,11 @@ instance's `mods` folder.

```bash
cd mod/adapters/forge-1.12.2 && gradle build
cd mod/adapters/forge-modern && gradle build
cd mod/adapters/forge-modern && gradle build

# The 1.8.9 adapter carries its own wrapper and needs a Java 8 JVM to run Gradle itself.
cd mod/adapters/forge-1.8.9
JAVA_HOME=/path/to/jdk-8 ./gradlew build # or ./gradlew build -Dorg.gradle.java.home=...
```

Output lands in `build/libs/`. The launcher finds it automatically in development; there
Expand All @@ -49,10 +53,24 @@ launcher explicitly filters it out for that reason.

### Toolchains

| Adapter | Plugin | Gradle JVM | Compiles to |
|---|---|---|---|
| `forge-1.12.2` | RetroFuturaGradle 1.4.x | Java 17 | Java 8 |
| `forge-modern` | ForgeGradle 6 | Java 21 | Java 21 |
| Adapter | Plugin | Gradle | Gradle JVM | Compiles to |
|---|---|---|---|---|
| `forge-1.8.9` | ForgeGradle 2.1 | 2.14.1 (own wrapper) | Java 8 | Java 8 |
| `forge-1.12.2` | RetroFuturaGradle 1.4.x | 8.2.1 | Java 17 | Java 8 |
| `forge-modern` | ForgeGradle 6 | 8.2.1 | Java 21 | Java 21 |

**Why 1.8.9 is the odd one out.** RetroFuturaGradle — which is what lets 1.12.2 build on a
modern Gradle — supports exactly two Minecraft versions, 1.7.10 and 1.12.2, because those
are the ones its authors ship modpacks for. No release of it adds 1.8.9, so that adapter
has to use the era-correct ForgeGradle 2.1, which is pinned to Gradle 2.x and Java 8. It
therefore has its own wrapper: `./gradlew` in that directory launches Gradle 2.14.1, not
the 8.2.1 everything else uses. Adapters were always independent builds for exactly this
kind of reason.

Its `gradle.properties` raises the heap to 3 GB. Gradle 2.14's default is far too small for
the 1.8.9 deobfuscation pass, which dies partway through `deobfMcMCP` with *GC overhead
limit exceeded* — a confusing failure, because nothing in the message suggests memory is
the fixable part.

**Why RetroFuturaGradle for 1.12.2.** The original ForgeGradle 2.3 is pinned to Gradle 4.4
and Java 8. RFG provides the same deobfuscation toolchain on modern Gradle. It is pinned
Expand Down Expand Up @@ -96,12 +114,12 @@ executable. Both are around 80 MB, which is Electron.
**The adapters must be built first.** `stage:adapters` fails the build if it finds no jars
at all, because an installer without them still launches Minecraft but silently loses live
editing — the feature the tool exists for. Adapters that are merely *not written yet*
(`forge-1.8.9`, `forge-mid`) are skipped without complaint, and the launcher reports those
(`forge-mid`) are skipped without complaint, and the launcher reports those
versions as vanilla-only at runtime.

The jars are packaged as `extraResources`, so they sit next to the asar as ordinary files
rather than inside it. The injector copies them into an instance's `mods` folder, and
keeping their real filename (`ella-forge-1.12.2-0.1.0.jar`) matters: the stale-jar cleanup
keeping their real filename (`ella-forge-1.12.2-0.2.0.jar`) matters: the stale-jar cleanup
matches `ella-*.jar`, so renaming them would let two Ella mods accumulate in one instance.

### Signing
Expand Down Expand Up @@ -145,7 +163,7 @@ JDKs `setup-java` installed and reports the Java 8 toolchain as missing.
Cutting a release is one command:

```bash
git tag v0.1.0 && git push origin v0.1.0
git tag v0.2.0 && git push origin v0.2.0
```

## Verifying an API before using it
Expand Down
15 changes: 12 additions & 3 deletions docs/project-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,23 @@ the mod injects into the resource stack.
"formatVersion": 1,
"name": "My Project",
"namespace": "myproject",
"targetVersions": ["1.12.2", "1.21.1"],
"targetVersion": "1.12.2",
"slotPool": { "block": 128, "item": 128 },
"entries": []
}
```

`targetVersions` drives validation: the editor warns when a setting is used that some
listed target cannot honour.
`targetVersion` is the Minecraft version the project is authored against. The launcher
preselects it whenever the project is opened, and launching any other version asks first —
listing what would break in this project's files, and offering to rewrite the ones it can.
See `shared/version-compat.ts` for what "break" means here; both cases are silent in game,
which is why they are worth a dialog.

`null` means the project is not bound yet: the next launch adopts its version. That is how
a project created before the field existed acquires one.

> Replaces a `targetVersions` array that nothing ever read past creation. A manifest still
> carrying it is migrated on load — the first entry becomes `targetVersion`.

## Entries

Expand Down
2 changes: 1 addition & 1 deletion docs/protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ Sent immediately on connect.
"loader": "forge",
"loaderVersion": "14.23.5.2859",
"adapter": "forge-1.12.2",
"adapterVersion": "0.1.0",
"adapterVersion": "0.2.0",
"javaVersion": "8",
"slots": { "block": 128, "item": 128 },
"capabilities": ["render_layer.cutout", "..."]
Expand Down
Loading
Loading