Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
e8e37db
Let definitions be read from text already in memory
jaymar921 Nov 5, 2026
a7c7b7e
Add EnginePosition and FuelTankPosition definition keys
jaymar921 Nov 5, 2026
3c9155b
Write pack manifests back out and expose the id check
jaymar921 Nov 5, 2026
79845a0
Split larger OBJ faces into fans and name the line of a bad one
jaymar921 Nov 5, 2026
709e3e0
Add the exception the workshop reports to players
jaymar921 Nov 5, 2026
a01fb7e
Add an editable definition that keeps every line it does not change
jaymar921 Nov 5, 2026
d3fa228
Add snapshot undo and redo with merged steps
jaymar921 Nov 5, 2026
909fae1
List the vehicle kinds the workshop can build
jaymar921 Nov 5, 2026
a09e79d
Describe one value of a part line
jaymar921 Nov 5, 2026
49d2559
Add the table of parts the workshop can place
jaymar921 Nov 5, 2026
ceed368
Read and change the values of a placed part
jaymar921 Nov 5, 2026
5242dc6
List the values on the workshop's tuning page
jaymar921 Nov 5, 2026
bff216b
Add the orthographic view around the model
jaymar921 Nov 5, 2026
bf4bb1f
Check OBJ files before the workshop takes them in
jaymar921 Nov 5, 2026
ebe408a
Add the workshop project, stored as a pack folder
jaymar921 Nov 5, 2026
6a438c5
Test that definition edits leave other lines alone
jaymar921 Nov 5, 2026
d9dabe2
Test undo, redo and merged steps
jaymar921 Nov 5, 2026
163e7dd
Check the part table against the real parsers
jaymar921 Nov 5, 2026
076bd4e
Test the workshop view's projection and framing
jaymar921 Nov 5, 2026
c529639
Test the model checks and their messages
jaymar921 Nov 5, 2026
38d58c9
Test that workshop projects load as ordinary packs
jaymar921 Nov 5, 2026
2767fce
Add the workshop's English text
jaymar921 Nov 5, 2026
7258053
Search the pack open in the workshop first for assets
jaymar921 Nov 5, 2026
a70f4e0
Mark vehicles drawn inside a screen
jaymar921 Nov 5, 2026
ebeca5a
Skip CPU glass culling for vehicles drawn in a GUI
jaymar921 Nov 5, 2026
536fe20
Add test flight vehicles and use the engine and fuel tank positions
jaymar921 Nov 5, 2026
bd5c7fa
Start test flights for the owner of a hosted world
jaymar921 Nov 5, 2026
e24bd21
Add the test flight packet
jaymar921 Nov 5, 2026
97e9339
Register the test flight packet and bump the protocol to 10
jaymar921 Nov 5, 2026
f183aca
Take the player to a test flight and back to the editor
jaymar921 Nov 5, 2026
4d14a91
Add the workshop's 3D editor screen
jaymar921 Nov 5, 2026
87ce9db
Add the workshop's list and new vehicle form
jaymar921 Nov 5, 2026
2d4c197
Open the workshop from the drafting table
jaymar921 Nov 5, 2026
8a52536
Tick the workshop client and clear it on logout
jaymar921 Nov 5, 2026
0200bcb
Add the workshop guide
jaymar921 Nov 5, 2026
acfb308
Document the engine and fuel tank keys
jaymar921 Nov 5, 2026
c61c22f
Point the modeling guide at the workshop and update the polygon rule
jaymar921 Nov 5, 2026
8a20e4d
Point the pack guide at the workshop
jaymar921 Nov 5, 2026
b7c5701
Describe the workshop in the architecture
jaymar921 Nov 5, 2026
862e918
Mark the first workshop phase done and settle its entry point
jaymar921 Nov 5, 2026
f28479b
Add the phase 11 checklist
jaymar921 Nov 5, 2026
dbc53c9
Add the workshop editor screenshot
jaymar921 Nov 5, 2026
995358f
Add the workshop tuning screenshot
jaymar921 Nov 5, 2026
6af171b
Add the workshop import screenshot
jaymar921 Nov 5, 2026
b2d5f1e
Add the workshop test-flight screenshot
jaymar921 Nov 5, 2026
5bfd078
Add the workshop pack screenshot
jaymar921 Nov 5, 2026
013ca24
Add the workshop finished-vehicle screenshot
jaymar921 Nov 5, 2026
70a73a5
Add phase 11 to the screenshot gallery
jaymar921 Nov 5, 2026
a8d5653
Show the workshop in the README
jaymar921 Nov 5, 2026
86bc23e
Add the workshop to the changelog
jaymar921 Nov 5, 2026
04fc0f8
Set the version to 1.1.0-beta.7
jaymar921 Nov 5, 2026
762d3a1
Date the 1.1.0-beta.7 changelog section
jaymar921 Nov 5, 2026
f871d59
Mark phase 11 released
jaymar921 Nov 5, 2026
c220485
Update the README status for 1.1.0-beta.7
jaymar921 Nov 5, 2026
55c984d
Name the phase 11 screenshots after 1.1.0-beta.7
jaymar921 Nov 5, 2026
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
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,39 @@ way to 1.0.0; see [docs/ROADMAP.md](docs/ROADMAP.md).

## [Unreleased]

## [1.1.0-beta.7] - 2026-11-06

Phase 11 of the road to 1.1.0: the first half of the vehicle workshop. Betas are not uploaded to
CurseForge, and this one ships the Forge 26.3 jar only. Worlds from 1.1.0-beta.6 load as before.
Players and servers need the same version, since the network protocol changed (10).

### Added

- Placeholder loader modules for every supported version that is not ported yet: Forge, NeoForge
and Fabric on 26.2, 26.1, 1.21.11, 1.21.5 and 1.20.6, and Forge and Fabric on 1.20.2 and 1.19.4.
Each downloads its loader and only logs at startup; none of them is built by default or shipped
in a beta. See docs/DEVELOPMENT.md.
- The vehicle workshop, first half: build a vehicle in game from a Blockbench or Blender model. The
drafting table's Workshop button opens it; the guide is docs/content/workshop.md.
- Import an OBJ by dropping it on the window or pasting its path. Files that cannot be used are
refused with the reason, and suspicious ones (no `$body`, no texture coordinates, a size that
suggests the wrong scale) open with a warning.
- A 3D editor around the model, drawn by the game's own vehicle renderer. Gizmos place seats,
cameras, rotors, propellers, wings, flaps, nozzles, wheels, tracks, landing gear, canopies,
hatches, weapons, hit boxes, the engine and the fuel tank, with a snap to 1/16 of a block.
- A tuning page for speed, turn rates, gravity, fuel, hit points, armor and so on, listing only
what the kind has and showing the real defaults.
- Test flight from the editor in a world you host, on a vehicle that takes no damage and goes away
when you get out, back to the editor.
- Undo and redo. Save writes an ordinary pack folder to `mcheli/packs`, keeping comments and any
keys the editor has no panel for. Export writes a zip to share to `mcheli/workshop`. Any pack
vehicle with an OBJ model can be opened; one in a zip opens from an `-edit` copy.
- `EnginePosition` and `FuelTankPosition` definition keys: where a wreck burns and where a crash
explodes.

### Changed

- OBJ models may have faces with more than four corners, and a bad line now names its line number.
- The supported version list for final releases is now 26.3, 26.2, 26.1, 1.21.11, 1.21.5, 1.21.1,
and 1.20.6 on Forge, NeoForge and Fabric, and 1.20.2 and 1.19.4 on Forge and Fabric (25 jars). 1.20.1
is no longer planned, and older versions are no longer frozen after 1.1.0.
Expand Down Expand Up @@ -643,6 +667,7 @@ Phase 1: foundation and flyable helicopters.
- Free look moved from Left Ctrl to Left Alt, because Left Ctrl is now the vanilla sprint key.
- Roll damping runs per tick instead of per frame, so handling no longer depends on frame rate.

[1.1.0-beta.7]: https://github.com/JnH-Projects/MCHeli-Rework-Forge/releases/tag/v1.1.0-beta.7
[1.1.0-beta.6]: https://github.com/JnH-Projects/MCHeli-Rework-Forge/releases/tag/v1.1.0-beta.6
[1.1.0-beta.5]: https://github.com/JnH-Projects/MCHeli-Rework-Forge/releases/tag/v1.1.0-beta.5
[1.1.0-beta.4]: https://github.com/JnH-Projects/MCHeli-Rework-Forge/releases/tag/v1.1.0-beta.4
Expand Down
29 changes: 15 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ MCHeli Rework is a ground-up port of MCHeli 1.0.3, a mod that stopped at Minecra
system is rewritten for the modern game, while the original vehicle definitions, models,
textures and sounds load unchanged.

> **Status:** on the way to 1.1.0, now at phase 10 (`1.1.0-beta.6`). Every helicopter, plane,
> **Status:** on the way to 1.1.0, now at phase 11 (`1.1.0-beta.7`). Every helicopter, plane,
> tank, car and ground vehicle can be placed, crewed and driven with the original handling, all
> 229 weapons fire, and the scripted HUDs, fuel, the vehicle screen and all of the original's
> equipment are in, down to UAV stations, the drafting table and the team manager. Betas ship the
> equipment are in, down to UAV stations, the drafting table and the team manager. The vehicle
> workshop builds new vehicles in game from a Blockbench or Blender model. Betas ship the
> Forge 26.3 jar only; 1.1.0 ships on Forge and Fabric for every version from 1.19.4 to 26.3, and
> on NeoForge from 1.20.6 (see [supported versions](docs/ROADMAP.md#supported-versions)). See the
> [roadmap](docs/ROADMAP.md).
Expand All @@ -32,20 +33,19 @@ Phase 4 clips (click a picture to open the video):

## Screenshots

Phase 10 in a development build (`1.1.0-beta.6`): smoke grenades, the GLTD target designator, the
rangefinder, cargo containers and the tow chain. Every earlier phase is in the
The vehicle workshop in a development build (`1.1.0-beta.7`): bring a model
from Blockbench or Blender, place its parts on it, tune it and fly it. The helicopter here is the
example pack's Rework Scout. Every earlier phase is in the
[screenshot gallery](docs/SCREENSHOTS.md).

| | |
|---|---|
| ![A green M18 smoke cloud rising from a hilltop by a lake](docs/images/phase-10/m18-green-smoke.png) | ![The view through the GLTD sight over a desert village, a red cross on the target and the readout "A-10 on call"](docs/images/phase-10/gltd-sight.png) |
| An M18 smoke grenade marks a spot in green | Through the GLTD's sight, with the A-10 on call |
| ![Two A-10s pulling away over a desert hill after strafing it, the readout counting down "A-10 rearming"](docs/images/phase-10/gltd-a-10-strike.png) | ![A player sitting behind the GLTD tripod, its red laser running out over the sand](docs/images/phase-10/gltd-tripod.png) |
| The A-10 makes its gun runs over the marked spot | The GLTD from outside: the head follows the aim, with a faint laser |
| ![The rangefinder scope on an AH-64 at 163 metres, the spot modes listed at the side](docs/images/phase-10/rangefinder-scope.png) | ![A cargo container coming down over a desert under two parachutes](docs/images/phase-10/container-parachute-drop.png) |
| The rangefinder reads the range to an AH-64 | A container dropped from a rack comes down under a parachute |
| ![A cargo container chained to the side of an M1A2 tank](docs/images/phase-10/chain-tow-m1a2.png) | ![A CH-47 flying over canyons with a container hanging below it on the tow chain](docs/images/phase-10/ch47-sling-load.png) |
| An M1A2 with a container on the tow chain | A CH-47 carries a container as a sling load |
| ![The workshop editor: a small helicopter on a grid, a hit box outlined around its cockpit, coloured handles on its parts, and the main rotor selected with arrows, its axis and its values in the side panel](docs/images/phase-11/workshop-editor.png) | ![The tuning page beside the helicopter, listing hit points, top speed, gravity, turn rates, fuel and armor](docs/images/phase-11/workshop-tuning.png) |
| Place every part by dragging it on the model | Tune how it flies; values you changed stand out from the defaults |
| ![The workshop's first screen: vehicles in the packs on the left, and a form for a new vehicle with a dropped OBJ file on the right](docs/images/phase-11/workshop-import.png) | ![The view from the helicopter's cockpit in flight over a desert coast, with the instruments on the HUD](docs/images/phase-11/workshop-test-flight.png) |
| Drop an OBJ on the window to start a vehicle | Test flight straight from the editor |
| ![The pack page with the vehicle's name, author, version and description, and a message that the pack was exported to a zip](docs/images/phase-11/workshop-pack.png) | ![The finished helicopter parked on a red desert hill, a CH-47 flying past behind it](docs/images/phase-11/workshop-finished-vehicle.png) |
| Name the pack and export a zip to share | The result is an ordinary content pack |

## Credits

Expand Down Expand Up @@ -135,7 +135,8 @@ chain), troops, the team manager and the commands and server settings are in
Content packs add vehicles and weapons: drop a pack (a zip or a folder) into
`<game dir>/mcheli/packs`. On a server only the server needs it; players download the server's
packs automatically when they join. [examples/rework-example-pack](examples/rework-example-pack)
adds a small helicopter, the Rework Scout. To make your own, see
adds a small helicopter, the Rework Scout. To make your own, use the
[workshop](docs/content/workshop.md) in game, or write one by hand with
[docs/content/packs.md](docs/content/packs.md).

## Documentation
Expand All @@ -147,7 +148,7 @@ adds a small helicopter, the Rework Scout. To make your own, see
* [Architecture](docs/ARCHITECTURE.md): how the mod is put together
* [Development](docs/DEVELOPMENT.md): building, running, testing and releasing
* [In-game test plan](docs/TESTING.md)
* Content authoring: [content packs](docs/content/packs.md), [modeling in Blockbench or Blender](docs/content/modeling.md), [vehicles](docs/content/aircraft.md), [weapons](docs/content/weapons.md), [HUD scripts](docs/content/hud.md)
* Content authoring: [the workshop](docs/content/workshop.md), [content packs](docs/content/packs.md), [modeling in Blockbench or Blender](docs/content/modeling.md), [vehicles](docs/content/aircraft.md), [weapons](docs/content/weapons.md), [HUD scripts](docs/content/hud.md)
* [Contributing](CONTRIBUTING.md): rules for pull requests and bug reports

## Building from source
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,15 @@ private DefinitionReader() {
}

public static <T extends Definition> Result<T> read(Path file, T definition) throws IOException {
return read(decode(Files.readAllBytes(file)), definition);
}

/** Reads text that is already in memory, such as the definition open in the workshop. */
public static <T extends Definition> Result<T> read(String text, T definition) throws IOException {
List<String> problems = new ArrayList<>();
Set<String> unhandled = new LinkedHashSet<>();

try (BufferedReader reader = new BufferedReader(new StringReader(decode(Files.readAllBytes(file))))) {
try (BufferedReader reader = new BufferedReader(new StringReader(text))) {
String line;
int lineNumber = 0;
while ((line = reader.readLine()) != null) {
Expand Down Expand Up @@ -78,7 +83,7 @@ public static <T extends Definition> Result<T> read(Path file, T definition) thr
* Files are meant to be UTF-8, but a few of the shipped ones were saved as Shift-JIS by the
* original author's editor. Fall back to Windows-31J rather than mangling those comments.
*/
static String decode(byte[] bytes) {
public static String decode(byte[] bytes) {
try {
return StandardCharsets.UTF_8.newDecoder()
.onMalformedInput(CodingErrorAction.REPORT)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ public abstract class AircraftDefinition extends Definition {
public float repairOtherVehiclesRange;
public int repairOtherVehiclesValue = 10;
public int inventorySize;
/** {@code EnginePosition}, not in the original: where a wreck burns. Its middle when unset. */
public @Nullable Vector3 enginePosition;
/** {@code FuelTankPosition}, not in the original: where a crash goes up. Its origin when unset. */
public @Nullable Vector3 fuelTankPosition;

// Body and physics
public float bodyWidth = 2.0F;
Expand Down Expand Up @@ -368,6 +372,18 @@ protected void registerKeys(KeyTable keys) {
}
});
keys.put("InventorySize", v -> inventorySize = toInt(v, 0, 54));
keys.put("EnginePosition", v -> {
String[] s = split(v);
if (s.length >= 3) {
enginePosition = toVec(s[0], s[1], s[2]);
}
});
keys.put("FuelTankPosition", v -> {
String[] s = split(v);
if (s.length >= 3) {
fuelTankPosition = toVec(s[0], s[1], s[2]);
}
});

keys.put("Width", v -> bodyWidth = toFloat(v, 0.1F, 1000.0F));
keys.put("Height", v -> bodyHeight = toFloat(v, 0.1F, 1000.0F));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,21 @@ public static PackManifest read(InputStream in) throws IOException, PackExceptio
value(p, "version", "1.0.0", 32), value(p, "description", "", 256));
}

/** The file's text, for the workshop. {@link #read} gives back the same values. */
public String text() {
return "id = " + escape(id) + "\nname = " + escape(name) + "\nauthor = " + escape(author)
+ "\nversion = " + escape(version) + "\ndescription = " + escape(description) + "\n";
}

private static String escape(String value) {
return value.replace("\\", "\\\\").replace("\n", " ").replace("\r", " ");
}

/** Whether a text can be a pack id. */
public static boolean isValidId(String id) {
return ID.matcher(id).matches() && !id.equals(ContentSource.ORIGINAL);
}

private static String value(Properties p, String key, String fallback, int maxLength) {
String value = p.getProperty(key, "").trim();
if (value.isEmpty()) {
Expand Down
87 changes: 50 additions & 37 deletions common/src/main/java/com/jnhprojects/mcheli/model/ObjLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@
* Reader for Wavefront {@code .obj} files.
*
* <p>Follows {@code W_WavefrontObject}: {@code g} always starts a new group, {@code o} only does
* so when its name begins with {@code $}. Texture V is flipped, positions are used unscaled, and
* quads are split into two triangles. Normals from the file are used when present, otherwise the
* face normal is computed.
* so when its name begins with {@code $}. Texture V is flipped and positions are used unscaled.
* Faces with more than three corners are split into a fan of triangles. Normals from the file are
* used when present, otherwise the face normal is computed.
*
* <p>A line that cannot be read fails the whole file with a {@link ModelFormatException} naming
* the line, because players see these messages when they import a model into the workshop.
*/
public final class ObjLoader {
private ObjLoader() {
Expand All @@ -39,36 +42,42 @@ public static Mesh load(String source, InputStream in) throws IOException {
continue;
}
String[] t = line.split(" ");
switch (t[0]) {
case "v" -> positions.add(floats(t, 3));
case "vn" -> normals.add(floats(t, 3));
case "vt" -> {
float[] uv = floats(t, 2);
uv[1] = 1.0F - uv[1];
uvs.add(uv);
}
case "g", "o" -> {
if (t.length < 2 || (t[0].equals("o") && !t[1].startsWith("$"))) {
continue;
try {
switch (t[0]) {
case "v" -> positions.add(floats(t, 3));
case "vn" -> normals.add(floats(t, 3));
case "vt" -> {
float[] uv = floats(t, 2);
uv[1] = 1.0F - uv[1];
uvs.add(uv);
}
if (builder != null) {
groups.add(builder.build(groupName));
}
groupName = t[1];
builder = new TriangleBuilder();
}
case "f" -> {
if (builder == null) {
case "g", "o" -> {
if (t.length < 2 || (t[0].equals("o") && !t[1].startsWith("$"))) {
continue;
}
if (builder != null) {
groups.add(builder.build(groupName));
}
groupName = t[1];
builder = new TriangleBuilder();
}
if (t.length != 4 && t.length != 5) {
throw new ModelFormatException("%s:%d only triangles and quads are supported".formatted(source, lineNumber));
case "f" -> {
if (builder == null) {
builder = new TriangleBuilder();
}
if (t.length < 4) {
throw new ModelFormatException("a face needs at least three corners");
}
addFace(builder, t, positions, normals, uvs);
}
default -> {
// mtllib, usemtl, s and friends carry nothing the renderer needs.
}
addFace(builder, t, positions, normals, uvs);
}
default -> {
// mtllib, usemtl, s and friends carry nothing the renderer needs.
}
} catch (ModelFormatException e) {
throw new ModelFormatException("%s, line %d: %s".formatted(source, lineNumber, e.getMessage()));
} catch (NumberFormatException e) {
throw new ModelFormatException("%s, line %d: not a number in \"%s\"".formatted(source, lineNumber, line));
}
}
}
Expand All @@ -78,20 +87,20 @@ public static Mesh load(String source, InputStream in) throws IOException {
return new Mesh(source, groups);
}

private static void addFace(TriangleBuilder builder, String[] t, List<float[]> positions, List<float[]> normals, List<float[]> uvs) {
private static void addFace(TriangleBuilder builder, String[] t, List<float[]> positions, List<float[]> normals, List<float[]> uvs)
throws ModelFormatException {
int corners = t.length - 1;
float[][] p = new float[corners][];
float[][] n = new float[corners][];
float[][] uv = new float[corners][];
for (int i = 0; i < corners; i++) {
String[] parts = t[i + 1].split("/", -1);
p[i] = positions.get(index(parts[0], positions.size()));
uv[i] = parts.length > 1 && !parts[1].isEmpty() ? uvs.get(index(parts[1], uvs.size())) : new float[2];
n[i] = parts.length > 2 && !parts[2].isEmpty() ? normals.get(index(parts[2], normals.size())) : null;
p[i] = element(positions, parts[0], "vertex");
uv[i] = parts.length > 1 && !parts[1].isEmpty() ? element(uvs, parts[1], "texture coordinate") : new float[2];
n[i] = parts.length > 2 && !parts[2].isEmpty() ? element(normals, parts[2], "normal") : null;
}
emit(builder, p, n, uv, 0, 1, 2);
if (corners == 4) {
emit(builder, p, n, uv, 0, 2, 3);
for (int i = 1; i + 1 < corners; i++) {
emit(builder, p, n, uv, 0, i, i + 1);
}
}

Expand All @@ -104,9 +113,13 @@ private static void emit(TriangleBuilder builder, float[][] p, float[][] n, floa
}

/** OBJ indices are one based and may be negative, counting back from the last element. */
private static int index(String token, int size) {
private static float[] element(List<float[]> list, String token, String what) throws ModelFormatException {
int i = Integer.parseInt(token);
return i < 0 ? size + i : i - 1;
int index = i < 0 ? list.size() + i : i - 1;
if (index < 0 || index >= list.size()) {
throw new ModelFormatException("a face uses %s %d, but only %d exist at this point".formatted(what, i, list.size()));
}
return list.get(index);
}

private static float[] floats(String[] t, int count) {
Expand Down
Loading
Loading