Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
50e2596
Carry the pack manifest on a content source
jaymar921 Nov 6, 2026
db5afa2
Pass the manifest along with a pack's content source
jaymar921 Nov 6, 2026
a26ce30
Keep the pack's manifest on a definition for credits
jaymar921 Nov 6, 2026
5b6a586
Credit each definition to the pack it came from
jaymar921 Nov 6, 2026
6a1ad96
Add pitch curves that follow the throttle
jaymar921 Nov 6, 2026
e48f7e0
Add a vehicle's own sound for a weapon
jaymar921 Nov 6, 2026
fbdb497
Sort a part's objects by the texture they draw with
jaymar921 Nov 6, 2026
7f2727e
Add the PartTexture, RotorSound, pitch curve and WeaponSound keys
jaymar921 Nov 6, 2026
3b617c5
Keep packs from players until an operator decides
jaymar921 Nov 6, 2026
a035db8
Check PNG and OGG files before the workshop takes them in
jaymar921 Nov 6, 2026
3f569b2
Add the data behind the pack browser and installing a zip
jaymar921 Nov 6, 2026
ff375a3
Edit lines of repeating keys by their first value
jaymar921 Nov 6, 2026
78c6ebe
Import textures and sounds into a workshop project
jaymar921 Nov 6, 2026
9b7c00e
Add the text for textures, sounds, the browser and submissions
jaymar921 Nov 6, 2026
9216787
Test pitch curves
jaymar921 Nov 6, 2026
1f17179
Test sorting objects by part texture
jaymar921 Nov 6, 2026
ca336ee
Test what submitted packs can and cannot do
jaymar921 Nov 6, 2026
5618f2d
Test importing assets and the pack catalog
jaymar921 Nov 6, 2026
474e28d
Upload a single texture again for the live preview
jaymar921 Nov 6, 2026
c7f921c
Cache a part's objects by texture
jaymar921 Nov 6, 2026
91e51bd
Draw part textures and the cockpit object
jaymar921 Nov 6, 2026
fb410db
Follow pitch curves and play as a rotor loop
jaymar921 Nov 6, 2026
3f60160
Play up to two loops per vehicle
jaymar921 Nov 6, 2026
ace0bdc
Add a one-off sound that streams from a pack
jaymar921 Nov 6, 2026
fdfbc19
Add a packet for one-off sounds from packs
jaymar921 Nov 6, 2026
4d4a968
Route pack sounds to the client
jaymar921 Nov 6, 2026
d1cf166
Play pack sounds and expose the server's packs
jaymar921 Nov 6, 2026
57bdfad
Send pack sounds by packet so they are not silent
jaymar921 Nov 6, 2026
7775231
Track pack sounds and the pending folder
jaymar921 Nov 6, 2026
0b43dce
Add the allowSubmissions and requireApproval settings
jaymar921 Nov 6, 2026
447e4d9
Hold a vehicle's own sound on a weapon set
jaymar921 Nov 6, 2026
e2f5a51
Set the vehicle's own sound on its weapon sets
jaymar921 Nov 6, 2026
c2d2d66
Fire with the vehicle's own weapon sound and pitch
jaymar921 Nov 6, 2026
c4f9f2b
Add the packet that carries a pack to the server
jaymar921 Nov 6, 2026
51e86e3
Receive packs from players and install approved ones
jaymar921 Nov 6, 2026
c3f866d
Tell operators about waiting packs when they join
jaymar921 Nov 6, 2026
4b580da
Register the new packets and raise the protocol to 11
jaymar921 Nov 6, 2026
b680bcb
Add /mcheli pending, approve and reject, and show authors
jaymar921 Nov 6, 2026
8a9330b
Show a pack vehicle's author on its item
jaymar921 Nov 6, 2026
0addecd
Update the test flight comment for submissions
jaymar921 Nov 6, 2026
c01b4ad
Send a pack to the server a few chunks a tick
jaymar921 Nov 6, 2026
95a7d0e
Add the pack browser
jaymar921 Nov 6, 2026
74824fd
Open the pack browser from the workshop
jaymar921 Nov 6, 2026
febd50b
Add the Paint and Sound tabs and sending to a server
jaymar921 Nov 6, 2026
79d5ce1
Add the workshop paint screenshot
jaymar921 Nov 6, 2026
9bbba08
Add the workshop sound screenshot
jaymar921 Nov 6, 2026
4c8c92a
Add the pack browser screenshot
jaymar921 Nov 6, 2026
8845875
Explain the cockpit object and part textures
jaymar921 Nov 6, 2026
3e35f18
Document the new sound and texture keys
jaymar921 Nov 6, 2026
39c629c
Document vehicles from players
jaymar921 Nov 6, 2026
c1b5c2a
Add paint, sound, the browser and sending to the workshop guide
jaymar921 Nov 6, 2026
6b5c980
List the pending, approve and reject commands
jaymar921 Nov 6, 2026
b5c9058
Describe part textures, pack sounds and submissions
jaymar921 Nov 6, 2026
e75dd07
Add the phase 12 checklist
jaymar921 Nov 6, 2026
8340806
Add phase 12 to the screenshot gallery
jaymar921 Nov 6, 2026
f60a767
Update the README for 1.1.0-beta.8
jaymar921 Nov 6, 2026
d0a819f
Set the version to 1.1.0-beta.8
jaymar921 Nov 6, 2026
9519f15
Add the 1.1.0-beta.8 changelog section
jaymar921 Nov 6, 2026
e6a4e9c
Mark phase 12 released and drop the texture brush
jaymar921 Nov 6, 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
39 changes: 39 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,44 @@ way to 1.0.0; see [docs/ROADMAP.md](docs/ROADMAP.md).

## [Unreleased]

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

Phase 12 of the road to 1.1.0: the second 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.7 load as before.
Players and servers need the same version, since the network protocol changed (11).

### Added

- Workshop Paint tab: import a PNG for the whole vehicle or for any part, by dropping it on the window
or pasting its path, and see it on the model straight away. Painting itself happens in your own image
editor. Files that are not real PNGs are refused with the reason.
- `PartTexture` definition key: draws one object of a model with its own texture, whatever paint the
vehicle wears. An object named `$cockpit` now draws with the hull, so a cockpit can have its own.
- Workshop Sound tab: import an OGG for the engine, the rotor or any of the vehicle's weapons, play it,
set its volume or pitch, and drag a pitch curve. OGG files the game cannot play, such as Opus, are
refused with the reason.
- `SoundPitchCurve`, `RotorSound`, `RotorSoundVolume`, `RotorSoundPitchCurve` and `WeaponSound`
definition keys. A second loop plays alongside the engine, loops can follow the throttle along a
curve, and a vehicle can have its own sound for a weapon without changing the weapon's file.
- Pack browser, from the Packs button in the workshop: every pack you have or the server sent, with its
author, version, size and contents, and the reason for any that cannot be used. Drop a zip on it to
install the pack.
- Author credits: a pack vehicle's item names its pack and author, and so does `/mcheli packs`.
- Sending a vehicle to a server: Send to server in the workshop offers your pack to the server's
operators, who decide with `/mcheli pending`, `/mcheli approve` and `/mcheli reject`. Approved packs
reach every player with no restart. This is off unless the server sets `packs.allowSubmissions`;
`packs.requireApproval` is on by default.

### Fixed

- A sound a content pack brought for a weapon or anything else played once by the server was silent. Only
engine loops could use pack sounds.

### Changed

- Network protocol 11.
- The roadmap's texture brush is dropped: textures are imported per part as PNGs.

## [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
Expand Down Expand Up @@ -667,6 +705,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.8]: https://github.com/JnH-Projects/MCHeli-Rework-Forge/releases/tag/v1.1.0-beta.8
[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
Expand Down
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ 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 11 (`1.1.0-beta.7`). Every helicopter, plane,
> **Status:** on the way to 1.1.0, now at phase 12 (`1.1.0-beta.8`). 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. The vehicle
> workshop builds new vehicles in game from a Blockbench or Blender model. Betas ship the
> workshop builds new vehicles in game from a Blockbench or Blender model, with their own textures
> and sounds, and sends them to a server for its operators to approve. 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 @@ -33,13 +34,14 @@ Phase 4 clips (click a picture to open the video):

## Screenshots

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).
The vehicle workshop in a development build (`1.1.0-beta.8`): bring a model
from Blockbench or Blender, place its parts on it, tune it and fly it, then give it textures and
sounds of its own. Every earlier phase is in the [screenshot gallery](docs/SCREENSHOTS.md).

| | |
|---|---|
| ![The Paint tab: the model's parts listed on the left, the helicopter in the middle with blue rotor blades, a red cockpit panel and a green hull, and the blade texture previewed on the right](docs/images/phase-12/workshop-paint.png) | ![The Sound tab: Engine, Rotor and a weapon listed on the left, and on the right the engine's sound file, Play and Clear buttons, its volume, and a pitch curve with five points](docs/images/phase-12/workshop-sound.png) |
| A PNG per part, showing on the model straight away | An OGG for the engine, with its pitch following the throttle along a curve |
| ![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) |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ <T extends Definition> void folder(ContentSource source, String folder, Function
if (result.isValid()) {
T def = result.definition();
def.setPack(source.pack());
def.setCredit(source.manifest());
if (def instanceof AircraftDefinition vehicle) {
fillHull(source, vehicle);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.jnhprojects.mcheli.content;

import com.jnhprojects.mcheli.content.pack.PackManifest;
import org.jspecify.annotations.Nullable;

import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
Expand All @@ -15,6 +18,7 @@ public abstract class Definition {
private final String name;
private final Map<String, Consumer<String>> handlers = new HashMap<>();
private String pack = "";
private @Nullable PackManifest credit;

protected Definition(String name) {
this.name = name;
Expand All @@ -38,6 +42,15 @@ void setPack(String pack) {
this.pack = pack;
}

/** What the pack this came from says about itself, for credits; {@code null} for the import. */
public @Nullable PackManifest credit() {
return credit;
}

void setCredit(@Nullable PackManifest credit) {
this.credit = credit;
}

protected abstract void registerKeys(KeyTable keys);

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import com.jnhprojects.mcheli.content.aircraft.part.WeaponBayPart;
import com.jnhprojects.mcheli.content.aircraft.part.WeaponPart;
import com.jnhprojects.mcheli.content.aircraft.part.WheelPart;
import com.jnhprojects.mcheli.model.PartTextures;
import org.jspecify.annotations.Nullable;

import java.util.ArrayList;
Expand Down Expand Up @@ -51,6 +52,8 @@ public abstract class AircraftDefinition extends Definition {
public final Map<String, String> localizedNames = new LinkedHashMap<>();
public String category = "zzz";
public final List<String> textures = new ArrayList<>();
/** Texture names by part, which a part draws with whatever paint the vehicle wears. */
public final Map<String, String> partTextures = new LinkedHashMap<>();
public boolean smoothShading = true;
public boolean hideEntity;
public float particlesScale = 1.0F;
Expand Down Expand Up @@ -164,6 +167,14 @@ public abstract class AircraftDefinition extends Definition {
public float soundRange;
public float soundVolume = 1.0F;
public float soundPitch = 1.0F;
/** How the engine's pitch follows the throttle, or {@code null} for what the kind does. */
public @Nullable PitchCurve soundPitchCurve;
/** A second loop alongside the engine, or empty for none. */
public String rotorSound = "";
public float rotorSoundVolume = 1.0F;
public @Nullable PitchCurve rotorSoundPitchCurve;
/** This vehicle's own sounds for weapons, by weapon name. */
public final Map<String, WeaponSound> weaponSounds = new LinkedHashMap<>();

// Cargo racks
public final List<RackDefinition> racks = new ArrayList<>();
Expand Down Expand Up @@ -330,6 +341,13 @@ protected void registerKeys(KeyTable keys) {
});
keys.put("Category", v -> category = v.toUpperCase(Locale.ROOT).replaceAll("[,;:]", ".").replaceAll("[ \t]", ""));
keys.put("AddTexture", v -> textures.add(lower(v)));
keys.put("PartTexture", v -> {
String[] s = split(v);
if (s.length != 2) {
throw new IllegalArgumentException("PartTexture needs a part and a texture");
}
partTextures.put(PartTextures.partName(s[0]), lower(s[1]));
});
keys.put("SmoothShading", v -> smoothShading = toBool(v));
keys.put("HideEntity", v -> hideEntity = toBool(v));
keys.put("ParticlesScale", v -> particlesScale = toFloat(v, 0.0F, 50.0F));
Expand Down Expand Up @@ -525,6 +543,18 @@ protected void registerKeys(KeyTable keys) {
keys.put("SoundRange", v -> soundRange = toFloat(v, 1.0F, 1000.0F));
keys.put("SoundVolume", v -> soundVolume = toFloat(v, 0.0F, 10.0F));
keys.put("SoundPitch", v -> soundPitch = toFloat(v, 0.0F, 10.0F));
keys.put("SoundPitchCurve", v -> soundPitchCurve = PitchCurve.parse(v));
keys.put("RotorSound", v -> rotorSound = lower(v));
keys.put("RotorSoundVolume", v -> rotorSoundVolume = toFloat(v, 0.0F, 10.0F));
keys.put("RotorSoundPitchCurve", v -> rotorSoundPitchCurve = PitchCurve.parse(v));
keys.put("WeaponSound", v -> {
String[] s = split(v);
if (s.length < 2) {
throw new IllegalArgumentException("WeaponSound needs a weapon and a sound");
}
Float pitch = s.length >= 3 ? toFloat(s[2], PitchCurve.MIN_PITCH, PitchCurve.MAX_PITCH) : null;
weaponSounds.put(lower(s[0]), new WeaponSound(lower(s[1]), pitch));
});

keys.put("AddRecipe", v -> {
shapedRecipe = true;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package com.jnhprojects.mcheli.content.aircraft;

import java.util.Locale;

/**
* How an engine or rotor loop's pitch follows the throttle, from {@code SoundPitchCurve} and
* {@code RotorSoundPitchCurve}: points of {@code throttle:pitch} with straight lines between them.
* <pre>
* SoundPitchCurve = 0:0.6, 0.5:0.9, 1:1.4
* </pre>
* Throttle runs from 0 to 1. Pitch stays within {@value #MIN_PITCH} to {@value #MAX_PITCH}, all the
* sound engine will play, so a curve never promises something that cannot be heard.
*/
public final class PitchCurve {
public static final float MIN_PITCH = 0.5F;
public static final float MAX_PITCH = 2.0F;

private final float[] throttle;
private final float[] pitch;

private PitchCurve(float[] throttle, float[] pitch) {
this.throttle = throttle;
this.pitch = pitch;
}

/**
* @throws IllegalArgumentException when the text is not a curve; the message is meant for the
* pack's author
*/
public static PitchCurve parse(String text) {
String[] points = text.split(",");
if (points.length < 2) {
throw new IllegalArgumentException("a pitch curve needs at least two throttle:pitch points");
}
float[] x = new float[points.length];
float[] y = new float[points.length];
for (int i = 0; i < points.length; i++) {
String[] parts = points[i].split(":");
if (parts.length != 2) {
throw new IllegalArgumentException("\"" + points[i].trim() + "\" is not throttle:pitch");
}
x[i] = Float.parseFloat(parts[0].trim());
y[i] = Math.clamp(Float.parseFloat(parts[1].trim()), MIN_PITCH, MAX_PITCH);
if (x[i] < 0.0F || x[i] > 1.0F) {
throw new IllegalArgumentException("throttle " + parts[0].trim() + " is outside 0 to 1");
}
if (i > 0 && x[i] <= x[i - 1]) {
throw new IllegalArgumentException("the throttle values of a pitch curve must rise from left to right");
}
}
return new PitchCurve(x, y);
}

/** The pitch at a throttle. Beyond its first and last points the curve stays level. */
public float at(float value) {
if (value <= throttle[0]) {
return pitch[0];
}
for (int i = 1; i < throttle.length; i++) {
if (value <= throttle[i]) {
float f = (value - throttle[i - 1]) / (throttle[i] - throttle[i - 1]);
return pitch[i - 1] + (pitch[i] - pitch[i - 1]) * f;
}
}
return pitch[pitch.length - 1];
}

public int size() {
return throttle.length;
}

public float throttle(int point) {
return throttle[point];
}

public float pitch(int point) {
return pitch[point];
}

/** The same curve with one point at another pitch. */
public PitchCurve withPitch(int point, float value) {
float[] y = pitch.clone();
y[point] = Math.clamp(value, MIN_PITCH, MAX_PITCH);
return new PitchCurve(throttle, y);
}

/** The curve as a definition value, which {@link #parse} reads back. */
public String text() {
StringBuilder out = new StringBuilder();
for (int i = 0; i < throttle.length; i++) {
out.append(i > 0 ? ", " : "").append(number(throttle[i])).append(':').append(number(pitch[i]));
}
return out.toString();
}

private static String number(float value) {
String text = String.format(Locale.ROOT, "%.3f", value);
text = text.replaceAll("0+$", "");
return text.endsWith(".") ? text.substring(0, text.length() - 1) : text;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.jnhprojects.mcheli.content.aircraft;

import org.jspecify.annotations.Nullable;

/**
* What one weapon sounds like on this vehicle, from {@code WeaponSound = weapon, sound[, pitch]}.
* Weapon files are shared between vehicles, so a vehicle with its own gun sound says so here rather
* than in the weapon's file.
*
* @param sound the sound's name, as in {@code sounds/<name>.ogg}
* @param pitch the pitch to play it at, or {@code null} for what the weapon's file says
*/
public record WeaponSound(String sound, @Nullable Float pitch) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ public Path assets() {
}

public ContentSource contentSource() {
return new ContentSource(id(), assets());
return new ContentSource(id(), assets(), manifest);
}

/** Paths of the pack's files relative to its top, in order. */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
package com.jnhprojects.mcheli.content.pack;

import org.jspecify.annotations.Nullable;

import java.nio.file.Path;

/**
* One place definitions are read from: the {@code assets/mcheli} folder of a content pack, or of
* the imported 1.0.3 content.
*
* @param pack id of the pack, {@link #ORIGINAL} for the import
* @param assets the {@code assets/mcheli} folder, possibly inside a zip file system; it need not
* exist
* @param pack id of the pack, {@link #ORIGINAL} for the import
* @param assets the {@code assets/mcheli} folder, possibly inside a zip file system; it need not
* exist
* @param manifest what the pack says about itself, for crediting its author, or {@code null} for
* the import
*/
public record ContentSource(String pack, Path assets) {
public record ContentSource(String pack, Path assets, @Nullable PackManifest manifest) {
/** Pack id of the imported 1.0.3 content. No content pack may use it. */
public static final String ORIGINAL = "original";

public ContentSource(String pack, Path assets) {
this(pack, assets, null);
}

public boolean isOriginal() {
return ORIGINAL.equals(pack);
}
Expand Down
Loading
Loading