From 599304f8b1001cb8963b2a0dcf35b8456c788106 Mon Sep 17 00:00:00 2001 From: dima_dencep Date: Sun, 4 Oct 2026 17:26:03 +0700 Subject: [PATCH 1/3] Add the mochafloats documentation A new docs section at /mochafloats: introduction, why mochafloats, getting started (installation, evaluating MoLang, bindings), features (compiling to bytecode, network format), MoLang support and migration (from Mocha, upgrading to 6.1). The pages describe mochafloats 6.1.0. Every result on them was run on Mocha 3.0.1, mochafloats 6.0.2 and 6.1.0, and the Bedrock results come from a behaviour pack run in the game. The homepage, the navbar and the footer link to the section, PAL's pages link to it instead of the GitHub repository, and the installation page shows the latest version from the Maven metadata. Co-Authored-By: Claude Opus 5.5 --- docusaurus.config.ts | 21 ++ mochafloats/features/_category_.json | 4 + mochafloats/features/compiling.md | 116 ++++++++ mochafloats/features/index.md | 10 + mochafloats/features/serialization.md | 65 +++++ mochafloats/gettingstarted/_category_.json | 4 + mochafloats/gettingstarted/bindings.md | 211 +++++++++++++++ mochafloats/gettingstarted/index.md | 11 + mochafloats/gettingstarted/installation.md | 106 ++++++++ mochafloats/gettingstarted/usage.md | 145 ++++++++++ mochafloats/intro.md | 66 +++++ mochafloats/language.md | 134 +++++++++ mochafloats/migration/_category_.json | 4 + mochafloats/migration/index.md | 8 + mochafloats/migration/migrating_from_mocha.md | 65 +++++ mochafloats/migration/upgrading_to_6_1.md | 71 +++++ mochafloats/why_mochafloats.md | 256 ++++++++++++++++++ pal/intro.md | 2 +- pal/molang.md | 2 +- plugins/mochafloats-version-plugin.js | 27 ++ src/components/MochaFloatsVersion.tsx | 12 + src/pages/index.md | 8 + static/_redirects | 1 + 23 files changed, 1347 insertions(+), 2 deletions(-) create mode 100644 mochafloats/features/_category_.json create mode 100644 mochafloats/features/compiling.md create mode 100644 mochafloats/features/index.md create mode 100644 mochafloats/features/serialization.md create mode 100644 mochafloats/gettingstarted/_category_.json create mode 100644 mochafloats/gettingstarted/bindings.md create mode 100644 mochafloats/gettingstarted/index.md create mode 100644 mochafloats/gettingstarted/installation.md create mode 100644 mochafloats/gettingstarted/usage.md create mode 100644 mochafloats/intro.md create mode 100644 mochafloats/language.md create mode 100644 mochafloats/migration/_category_.json create mode 100644 mochafloats/migration/index.md create mode 100644 mochafloats/migration/migrating_from_mocha.md create mode 100644 mochafloats/migration/upgrading_to_6_1.md create mode 100644 mochafloats/why_mochafloats.md create mode 100644 plugins/mochafloats-version-plugin.js create mode 100644 src/components/MochaFloatsVersion.tsx diff --git a/docusaurus.config.ts b/docusaurus.config.ts index dd0e8fd..ad5b83e 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -63,6 +63,7 @@ const config: Config = { plugins: [ './plugins/maven-versions-plugin', + './plugins/mochafloats-version-plugin', [ '@docusaurus/plugin-content-docs', { @@ -81,6 +82,15 @@ const config: Config = { sidebarPath: './sidebars.ts', }, ], + [ + '@docusaurus/plugin-content-docs', + { + id: 'mochafloats', + path: "mochafloats", + routeBasePath: "mochafloats", + sidebarPath: './sidebars.ts', + }, + ], ], themeConfig: { @@ -115,6 +125,13 @@ const config: Config = { docsPluginId: "emotecraft", label: "Emotecraft Documentation", }, + { + type: "docSidebar", + sidebarId: "mainSidebar", + position: "left", + docsPluginId: "mochafloats", + label: "MochaFloats Documentation", + }, { href: "https://github.com/PlayerAnimationLibrary/Documentation", label: "GitHub", @@ -135,6 +152,10 @@ const config: Config = { { to: "/emotecraft/gettingstarted", label: "Emotecraft Documentation", + }, + { + to: "/mochafloats/intro", + label: "MochaFloats Documentation", } ], }, diff --git a/mochafloats/features/_category_.json b/mochafloats/features/_category_.json new file mode 100644 index 0000000..59287f7 --- /dev/null +++ b/mochafloats/features/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Features", + "position": 4 +} diff --git a/mochafloats/features/compiling.md b/mochafloats/features/compiling.md new file mode 100644 index 0000000..20559e6 --- /dev/null +++ b/mochafloats/features/compiling.md @@ -0,0 +1,116 @@ +--- +sidebar_position: 1 +description: "Compiling MoLang expressions to JVM bytecode with MolangCompiler: typed functions, Java calls and what the compiler supports." +--- + +# Compiling to bytecode + +The `runtime-compiler` module can turn an expression into a small JVM class. +Calling it is as fast as calling a hand-written Java method: there's no syntax tree to walk, and the JIT can inline it like any other code. + +Compile expressions that run very often and only use math and your own Java methods. +For everything else — and especially for MoLang written by users or artists — use the [interpreter](../gettingstarted/usage.md): it supports the whole language, while the compiler only supports [a subset](#what-the-compiler-supports). + +## Compiling an expression + +```java +MochaEngine engine = MochaEngine.createStandard(); + +MochaFunction function = engine.compiler().compile("math.sqrt(3 * 3 + 4 * 4)"); +float result = function.evaluate(); // 5.0 +``` + +Constant parts are evaluated while compiling, so the class generated for this example simply returns `5.0` — its whole method body is `ldc 5.0; freturn`. + +Compile once and keep the function: every `compile` call parses the text and defines a new class. +The class is a [hidden class](https://openjdk.org/jeps/371), so the JVM can unload it once you no longer reference the function. + +`MolangCompiler` can also be created directly — `new MolangCompiler(entity, scope)` — to compile against any scope, for example an interpreter's `scope()`. + +## Functions with parameters + +Declare an interface with a single method that extends `MochaCompiledFunction`, and the compiled class will implement it. +Parameters are available in the expression by name: + +```java +public interface Falloff extends MochaCompiledFunction { + float apply(@Named("distance") float distance, @Named("radius") float radius); +} + +Falloff falloff = engine.compiler().compile("math.clamp(1 - distance / radius, 0, 1)", Falloff.class); +falloff.apply(2, 8); // 0.75 +``` + +- The interface must have exactly one abstract method. Default and static methods are fine. +- Name every parameter with `@Named`, or compile your code with `javac -parameters` so the names are kept. +- Parameters and the return value can be `float`, `double`, `int`, `long` or `boolean`; values are converted as needed. A `boolean` result is `true` when the expression is non-zero. + +## Calling Java code + +Bindings made with [`@Binding`](../gettingstarted/bindings.md#java-classes-with-binding) are called directly from the generated code — no reflection, no boxing: + +```java +@Binding("game") +public final class GameBindings { + @Binding("gravity") + public static final float GRAVITY = 0.08F; + + @Binding("ticks_to_seconds") + public static float ticksToSeconds(float ticks) { + return ticks / 20F; + } +} + +engine.interpreter().bind(GameBindings.class); +engine.compiler().compile("game.ticks_to_seconds(40) * game.gravity").evaluate(); // 0.16 +``` + +- Static methods become `invokestatic`, methods of an instance bound with `bindInstance` become `invokevirtual` on that instance. +- `public static final` number fields are copied into the class as constants; other static fields are read every time. +- String literals can be passed to methods that take a `String`. +- A parameter annotated with `@Entity` receives the compiler's entity. + +## What the compiler supports + +| Supported | Not supported — use the interpreter | +|-------------------------------------------------------------|-------------------------------------------------------------| +| Numbers, `true`, `false` | `variable` / `v` | +| `+` `-` `*` `/`, unary `-` | Bindings made of lambdas or `ObjectValue`s, such as a `query` built from `Function`s | +| `<` `<=` `>` `>=` `==` `!=`, `&&` `\|\|` `!` | `loop`, `for_each`, `break`, `continue`, blocks `{ ... }` | +| `c ? a : b`, `c ? a`, `return`, several statements | `->`, `??` and `[]` | +| `temp` / `t` variables | Non-static `@Binding` fields | +| The interface's parameters | Methods that take an `ExecutionContext`, a `Lazy` or varargs | +| `math.*` and other `@Binding` static methods and fields | | +| Methods of instances bound with `bindInstance` | | + +An unsupported construct makes `compile` throw an `UnsupportedOperationException` that names it. +Parts of an expression that only use constants are computed at compile time, so `1 ?? 2` compiles fine; `t.a ?? 3` doesn't. + +Compiled code follows the interpreter's number rules: dividing by zero gives `0`, and `NaN` or infinities, from arithmetic or returned by a Java method, become `0`. +A method whose `@Binding` sets `skipChecking = true` is trusted to return finite values and isn't checked. + +## Debugging + +- `postCompile(bytes -> ...)` receives the bytecode of every generated class before it's loaded. Write it to a `.class` file and open it in your IDE or with `javap`. +- `handleParseExceptions(handler)` is called when the source doesn't parse. The compiler then compiles an empty body, which returns `0` (or `false`). + +```java +engine.compiler() + .postCompile(bytes -> { + try { + Files.write(Path.of("molang.class"), bytes); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }) + .handleParseExceptions(e -> LOGGER.warn("Invalid MoLang", e)); +``` + +## Constant folding + +By default the compiler runs every expression through an `ExpressionInliner` first, which evaluates parts made only of literals and `pure` functions. +Pass your own inliner as the third constructor argument, or `null` to compile expressions exactly as written: + +```java +MolangCompiler compiler = new MolangCompiler(null, engine.scope(), null); +``` diff --git a/mochafloats/features/index.md b/mochafloats/features/index.md new file mode 100644 index 0000000..fbf7418 --- /dev/null +++ b/mochafloats/features/index.md @@ -0,0 +1,10 @@ +--- +description: "What mochafloats does beyond interpreting MoLang: compiling it to bytecode and sending it over the network." +--- + +# Features + +Beyond interpreting MoLang, mochafloats can: + +- [Compile expressions to bytecode](./compiling.md) — turn hot expressions into JVM classes that run as fast as hand-written Java. +- [Send expressions over the network](./serialization.md) — write parsed expressions to a Netty `ByteBuf` and read them back without parsing the text again, or print them as MoLang. diff --git a/mochafloats/features/serialization.md b/mochafloats/features/serialization.md new file mode 100644 index 0000000..ff18f9a --- /dev/null +++ b/mochafloats/features/serialization.md @@ -0,0 +1,65 @@ +--- +sidebar_position: 2 +sidebar_label: "Network format" +description: "Writing parsed MoLang expressions to a Netty ByteBuf and reading them back, and printing them as text." +--- + +# Sending expressions over the network + +The `parser` module can write parsed expressions to a Netty `ByteBuf` and read them back. +The receiving side gets a ready syntax tree: it never parses text, so it can't hit a syntax error. + +Minecraft's `FriendlyByteBuf` is a `ByteBuf`, so you can pass the packet buffer directly. + +```java +List expressions = MolangParser.parseAll("math.sin(q.anim_time * 360) * 30"); + +// sender +ExprBytesUtils.writeExpressions(expressions, buf); + +// receiver +List received = ExprBytesUtils.readExpressions(buf); +float value = molang.eval(received); +``` + +`writeExpression` and `readExpression` do the same for a single expression. +Evaluating an expression doesn't change it, so one parsed list can be written to many players and shared between interpreters. + +PlayerAnimationLibrary's binary animation format, which is what it sends over the network, stores every keyframe value this way. + +## The format + +Every expression is one byte with its type, followed by its contents: + +| Id | Expression | Contents | +|----|------------------------------------|--------------------------------------------| +| 0 | Unary (`-x`, `!x`, `return x`) | operator, expression | +| 1 | Conditional (`c ? a : b`) | three expressions | +| 2 | String | string | +| 3 | `break` / `continue` | operator | +| 4 | Name (`math`, `v`) | string | +| 5 | Number | 4-byte float | +| 6 | Block (`{ ... }`) | list of expressions | +| 7 | Call | expression, list of arguments | +| 8 | Binary (`a + b`, `a = b`, ...) | operator, two expressions | +| 9 | Array index (`a[i]`) | two expressions | +| 10 | Property access (`a.b`) | expression, string | + +Strings are UTF-8 with a [VarInt](https://minecraft.wiki/w/Java_Edition_protocol/Data_types#VarInt_and_VarLong) length in front, lists are a VarInt count followed by the elements, and operators are a single byte. + +- The format has no version number, and operators are stored by their position in mochafloats' operator lists. Make sure both sides use the same mochafloats version. +- It saves parsing, not bandwidth: the result is usually a little longer than the source text. + +`VarIntUtils` and `ProtocolUtils` are public too, if you want the same VarInts, strings and lists in your own packets. + +## Printing expressions as text + +`toString()` on a parsed expression prints it in MoLang syntax, and `ExpressionListUtils.toString(list)` joins a whole script with `;`: + +```java +List expressions = MolangParser.parseAll("v.a = 2; return math.sin(q.anim_time * 50) * 45;"); +ExpressionListUtils.toString(expressions); // v.a=2;return (math.sin(q.anim_time*50)*45) +``` + +The printed text keeps the parentheses the expression needs — `(1 + 2) * 3` prints as `(1+2)*3` — so parsing it again gives the same expression. +It isn't the original text, though: spaces are gone, and parentheses that don't change the meaning may be added or left out. Keep the source if you want to show people what they wrote. diff --git a/mochafloats/gettingstarted/_category_.json b/mochafloats/gettingstarted/_category_.json new file mode 100644 index 0000000..c1998c5 --- /dev/null +++ b/mochafloats/gettingstarted/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Getting Started", + "position": 3 +} diff --git a/mochafloats/gettingstarted/bindings.md b/mochafloats/gettingstarted/bindings.md new file mode 100644 index 0000000..6c017cc --- /dev/null +++ b/mochafloats/gettingstarted/bindings.md @@ -0,0 +1,211 @@ +--- +sidebar_position: 3 +description: "Exposing your own values, functions and Java classes to MoLang expressions." +--- + +# Bindings + +A binding is anything an expression can reach by name: `math`, `v`, your `query` object, a Java class. +All of them live in the interpreter's scope, which you get with `scope()`. + +## How names are resolved + +`q.health` looks up `q` in the scope, then asks that object for its `health` property. +`q.distance_to(2)` does the same, then calls the property as a function. + +- Names are **case-insensitive**: `Math.PI`, `math.pi` and `MATH.PI` are the same thing. mochafloats lowercases them with `Locale.ROOT`, so the player's language doesn't matter. +- Names are made of letters, digits and `_`, and can't start with a digit. A dot always means "property of", so `q.mymod.speed` is the `speed` property of the `mymod` property of `q` — use `q.mymod_speed` for a flat name. +- A name that doesn't exist evaluates to `0`, without an error. + +## Values + +Everything an expression can touch is a `Value`: + +| Type | Holds | +|------------------|--------------------------------------------------------------| +| `NumberValue` | A `float`. `NumberValue.of(1.5F)`, `Value.of(true)` (which is `1`). | +| `StringValue` | A string. MoLang strings use single quotes: `'minecraft:pig'`. | +| `ArrayValue` | An array of values, indexed with `[]`. | +| `ObjectValue` | Something with named properties, like `math` or `query`. | +| `Function` | Something you can call with `()`. | + +`Value.of(object)` converts plain Java values: numbers, strings, booleans and arrays. + +## Functions as lambdas + +The quickest way to add a function is a `MutableObjectBinding` holding `Function` lambdas: + +```java +MolangInterpreter molang = MolangInterpreter.standard(player); + +MutableObjectBinding query = new MutableObjectBinding(); +query.set("health", (Function) (ctx, args) -> NumberValue.of(ctx.entity().getHealth())); +query.set("distance_to", (Function) (ctx, args) -> { + float x = args.next().eval().getAsNumber(); + return NumberValue.of(Math.abs(ctx.entity().getX() - x)); +}); + +molang.scope().set("query", query); +molang.scope().set("q", query); + +molang.eval("q.health() / 2"); +molang.eval("q.distance_to(10)"); +``` + +- `ctx.entity()` is the interpreter's entity. +- `args.next()` returns the next argument; `eval()` evaluates it. Arguments are evaluated only when you ask, so a function decides whether and how often to evaluate them. `args.length()` tells you how many were passed. +- Return `Value.nil()` or `null` for "nothing"; both read as `0`. + +For plain number functions, `setFunction` saves the casting: + +```java +query.setFunction("half", x -> x / 2); +query.setFunction("random_angle", () -> (float) (Math.random() * 360)); +``` + +Once everything is registered, `query.block()` makes the object read-only: later `set` calls return `false` and change nothing. + +Lambdas are the fastest bindings for the interpreter. The [compiler](../features/compiling.md) can't call them, though — use [`@Binding` classes](#java-classes-with-binding) for compiled expressions. + +## Properties computed when read + +Bedrock writes most queries without parentheses: `q.health`, `q.is_sneaking`. +A `Function` stored in a `MutableObjectBinding` only runs when called with `()`, so for that style implement `ObjectValue` and compute the value in `getProperty`: + +```java +public final class PlayerQueries implements ObjectValue { + private final Player player; + + public PlayerQueries(Player player) { + this.player = player; + } + + @Override + public @Nullable ObjectProperty getProperty(@NotNull String name) { + return switch (name.toLowerCase(Locale.ROOT)) { + case "health" -> ObjectProperty.property(NumberValue.of(player.getHealth()), false); + case "is_sneaking" -> ObjectProperty.property(Value.of(player.isCrouching()), false); + default -> null; + }; + } +} + +molang.scope().set("q", new PlayerQueries(player)); +molang.eval("q.is_sneaking ? q.health : 0"); +``` + +:::warning +`getProperty` receives the name exactly as it was written in the expression — `Health` for `q.Health`. +Lowercase it with `Locale.ROOT` yourself, like above. The built-in bindings already do. +::: + +PlayerAnimationLibrary does the same in its `QueryBinding`: a `MutableObjectBinding` whose `getProperty` calls the stored `Function` instead of returning it. + +## Java classes with `@Binding` + +Annotate a class and its static members with `@Binding`, then bind the class: + +```java +@Binding("random") +public final class RandomBinding { + @Binding("select") + public static float select(float a, float b) { + return Math.random() < 0.5 ? a : b; + } + + @Binding("max_roll") + public static final float MAX_ROLL = 6F; +} + +molang.bind(RandomBinding.class); +molang.eval("random.select(1, random.max_roll)"); // 1 or 6 +``` + +- The class's `@Binding` names it in the scope. Give several names for aliases: `@Binding({"random", "rng"})`. +- Methods can take and return `float`, `double`, `int`, `long`, `short`, `boolean` and `String`; arguments are converted from MoLang values. A `void` method evaluates to `0`. +- A parameter annotated with `@Entity` receives the entity, and a parameter of type `ExecutionContext` receives the execution context. Neither consumes a MoLang argument. +- A `Lazy` parameter receives its argument unevaluated: `get()` evaluates it, so the method decides whether to. +- A varargs parameter, such as `float... values`, receives all the remaining arguments. +- The compiler only supports `@Entity` of these and rejects methods with the others, so call those from interpreted code. + +To bind the non-static members of an object instead, use `bindInstance` and choose the names yourself: + +```java +public final class PlayerApi { + private final Player player; + + public PlayerApi(Player player) { + this.player = player; + } + + @Binding("health") + public float health() { + return player.getHealth(); + } +} + +molang.bindInstance(PlayerApi.class, new PlayerApi(player), "player", "p"); +molang.eval("p.health()"); +``` + +### Existing methods + +`@BindExternalFunction` exposes a static method of another class without writing a wrapper. +Put it on your binding class, once per function: + +```java +@Binding("geometry") +@BindExternalFunction(at = Math.class, name = "hypot", args = {double.class, double.class}, pure = true) +@BindExternalFunction(at = Math.class, name = "cbrt", args = {double.class}, pure = true) +public final class GeometryBinding { +} + +molang.bind(GeometryBinding.class); +molang.eval("geometry.hypot(3, 4)"); // 5.0 +molang.eval("geometry.cbrt(27)"); // 3.0 +``` + +`as = "..."` publishes it under a different name. + +### Pure functions + +`pure = true` on `@Binding` or `@BindExternalFunction` promises that the function always returns the same result for the same arguments and has no side effects. +The compiler then calls it **while compiling** when all its arguments are constants and puts the result in the class instead. +Don't mark anything that reads game state or randomness as pure. + +## Interpreter speed of Java bindings + +The compiler calls `@Binding` methods directly. The interpreter calls them through a method handle prepared once, which is a little slower than calling a lambda. +Turn on `warnOnReflectiveFunctionUsage(true)` on the interpreter to print each method called this way to `System.err`, once, and find the ones worth a lambda. + +To give the interpreter a lambda while compiled code keeps calling the method, bind the class with a **backing object** that holds a `Function` under the same name. +The backing object must return its properties from `entries()`: + +```java +Map backing = Map.of("select", ObjectProperty.property( + (Function) (ctx, args) -> NumberValue.of(RandomBinding.select( + args.next().eval().getAsNumber(), + args.next().eval().getAsNumber())), + false)); + +ObjectValue backingObject = new ObjectValue() { + @Override + public @Nullable ObjectProperty getProperty(@NotNull String name) { + return backing.get(name); + } + + @Override + public @NotNull Map entries() { + return backing; + } +}; + +engine.scope().set("random", JavaObjectBinding.of(RandomBinding.class, null, backingObject)); +``` + +Now `engine.interpreter()` runs the lambda for `random.select(...)` and `engine.compiler()` calls the static method. +A backing function must have the same `pure` flag as the method, otherwise `JavaObjectBinding.of` throws. + +:::note +`JavaObjectBinding` is marked as an internal API, so this may change between versions. +::: diff --git a/mochafloats/gettingstarted/index.md b/mochafloats/gettingstarted/index.md new file mode 100644 index 0000000..f915243 --- /dev/null +++ b/mochafloats/gettingstarted/index.md @@ -0,0 +1,11 @@ +--- +description: "Adding mochafloats to your project, evaluating MoLang and exposing your own values to it." +--- + +# Getting Started + +This section takes you from an empty project to MoLang that reads your game's state: + +1. [Installation](./installation.md) — add the library and pick a module. +2. [Evaluating MoLang](./usage.md) — create an interpreter, evaluate expressions and parse them once for repeated use. +3. [Bindings](./bindings.md) — expose your own values, functions and Java classes to expressions, such as a `query` object. diff --git a/mochafloats/gettingstarted/installation.md b/mochafloats/gettingstarted/installation.md new file mode 100644 index 0000000..48226ad --- /dev/null +++ b/mochafloats/gettingstarted/installation.md @@ -0,0 +1,106 @@ +--- +sidebar_position: 1 +description: "How to add mochafloats to a Gradle or Maven project, and which module to pick." +--- + +import MochaFloatsVersion from '@site/src/components/MochaFloatsVersion'; + +# Installation + +## Requirements + +mochafloats needs **Java 24 or newer**: every module is compiled for Java 24, and the compiler uses the ClassFile API that became final in that release. +Minecraft 26.1 and newer already runs on Java 25. + +The latest version is . + +## Which module do I need? + +| If you want to… | Depend on | +|--------------------------------------------------------------|--------------------| +| Parse MoLang, inspect the syntax tree or send it over the network | `parser` | +| Evaluate MoLang | `runtime` | +| Evaluate MoLang **and** compile it to bytecode | `runtime-compiler` | + +Every module pulls in the ones below it (`runtime-compiler` → `runtime` → `parser` → `lexer`), so declare only one. + +:::tip +If your mod already depends on [PlayerAnimationLibrary](/pal/intro), you already have mochafloats: PAL exposes `runtime` as an API dependency and ships `lexer`, `parser` and `runtime` inside its jar. +Don't bundle a second copy. If you also need the compiler, add `runtime-compiler` with the same version PAL uses. +::: + +## Gradle + +`gradle.properties`: + +```properties +mochafloats_version = +``` + +`build.gradle` / `build.gradle.kts` (the snippet works in both the Groovy and the Kotlin DSL): + +```gradle +repositories { + mavenCentral() + maven { + name = "RedlanceMinecraft" + url = uri("https://repo.redlance.org/public") + } +} + +dependencies { + implementation("org.redlance.mochafloats:runtime:${property("mochafloats_version")}") +} +``` + +Swap `runtime` for `parser` or `runtime-compiler` according to the table above. + +## Maven + +```xml + + + redlance + https://repo.redlance.org/public + + + + + + org.redlance.mochafloats + runtime + 6.1.0 + + +``` + +## Transitive dependencies + +- `parser` depends on Netty's `netty-buffer`, which it uses for the [network format](../features/serialization.md). Minecraft already ships Netty. +- The POMs also list `org.jetbrains:annotations` and `com.google.j2objc:j2objc-annotations`. They only contain annotations and aren't needed at runtime. + +Nothing else: there's no Javassist or other bytecode library. + +## Bundling it inside a Minecraft mod + +If your mod ships mochafloats inside its own jar (jar-in-jar), embed the mochafloats modules themselves and nothing they depend on: + +- `lexer`, `parser` and `runtime` — plus `runtime-compiler` if you compile expressions. +- **Not** Netty: Minecraft provides it, so disable transitive dependencies for the embedded artifacts. + +With Fabric Loom, for example: + +```groovy +dependencies { + implementation("org.redlance.mochafloats:runtime:${property("mochafloats_version")}") + + include("org.redlance.mochafloats:lexer:${property("mochafloats_version")}") { transitive = false } + include("org.redlance.mochafloats:parser:${property("mochafloats_version")}") { transitive = false } + include("org.redlance.mochafloats:runtime:${property("mochafloats_version")}") { transitive = false } +} +``` + +In the Kotlin DSL the closure is `{ isTransitive = false }`. + +On NeoForge, use your build plugin's jar-in-jar configuration (`jarJar` in ModDevGradle) the same way. +The jars declare `FMLModType: GAMELIBRARY` in their manifest, so NeoForge loads them next to your mod, and compiled functions can implement your mod's interfaces — something that crashes with Mocha ([#27](https://github.com/unnamed/mocha/issues/27)). diff --git a/mochafloats/gettingstarted/usage.md b/mochafloats/gettingstarted/usage.md new file mode 100644 index 0000000..9f34951 --- /dev/null +++ b/mochafloats/gettingstarted/usage.md @@ -0,0 +1,145 @@ +--- +sidebar_position: 2 +description: "Creating an interpreter, evaluating and caching MoLang expressions, scopes and parse errors." +--- + +# Evaluating MoLang + +## Creating an interpreter + +`MolangInterpreter` lives in the `runtime` module. `MolangInterpreter.standard()` gives you one with the usual bindings already in place: + +```java +MolangInterpreter molang = MolangInterpreter.standard(); +``` + +| Name | What it is | +|--------------------|----------------------------------------------------------| +| `math` | The [standard math library](../language.md#math-functions). | +| `variable` / `v` | Variables that are kept between evaluations. | +| `temp` / `t` | Variables that only live for one evaluation (added on every call). | + +`query` is **not** part of the standard set: queries describe your game objects, so you [bind them yourself](./bindings.md). + +An interpreter can carry an **entity**, the object its expressions are about — an animated player, a mob, a particle. +Functions you bind receive it through their execution context: + +```java +MolangInterpreter molang = MolangInterpreter.standard(player); +``` + +To start from an empty scope instead, use `MolangInterpreter.create(entity, builder -> builder.set("name", value))`. + +## Evaluating text + +`eval` parses a string and runs it. The result is always a `float`: + +```java +float a = molang.eval("math.sqrt(3 * 3 + 4 * 4)"); // 5.0 +float b = molang.eval("math.abs(-5) + 5"); // 10.0 +float c = molang.eval("v.x = 2; return v.x * 3;"); // 6.0 +``` + +A script with several statements separated by `;` returns the value of its `return`. + +## Parsing once, evaluating many times + +`eval(String)` parses the text again on every call. +For anything that runs repeatedly — a keyframe evaluated every frame, for example — parse it once with `MolangParser` and keep the result: + +```java +List wave = MolangParser.parseAll("math.sin(v.t * 360) * 30"); // throws ParseException + +molang.eval("v.t = 0.25"); +float value = molang.eval(wave); // 30.0 +``` + +`prepareEval` does the same in one step and hands you a `Supplier`: + +```java +Supplier wave = molang.prepareEval("math.sin(v.t * 360) * 30"); +float value = wave.get(); +``` + +:::tip +`Supplier` boxes the result into a new `Float` on every call. In hot code, keep the `List` and call `eval(list)`, which returns a primitive `float`. +::: + +An expression that is just a number — `5`, `-1.5` — is returned straight away, without allocating anything. + +## Where values live + +Every call to `eval` works on its own copy of the interpreter's scope: + +- `temp` / `t` is created fresh for each call, so temporary variables never leak from one evaluation to the next. +- `variable` / `v` is shared: the copy points at the same object, so a value written by one evaluation is visible to the next one. +- While an expression runs, its scope is read-only: a script can write `v.x = 1`, but it can't replace `math` or any other top-level binding. + +```java +molang.eval("v.counter = 1"); +molang.eval("v.counter = v.counter + 1"); +molang.eval("v.counter"); // 2.0 + +molang.eval("t.value = 5"); +molang.eval("t.value"); // 0.0, temp variables are gone after each call +``` + +## Bindings for a single evaluation + +Pass a `Consumer` as the second argument to add bindings that only exist for that one call. +It runs on the copied scope, before the scope becomes read-only, so the interpreter's own scope is never touched: + +```java +List scaled = MolangParser.parseAll("this * 2"); + +molang.eval(scaled); // 0.0 +molang.eval(scaled, scope -> scope.set("this", NumberValue.of(21))); // 42.0 +molang.eval(scaled); // 0.0 again +``` + +This is handy when the same parsed expression is evaluated for many different objects — give each call its own `this`, `context` or query object instead of creating an interpreter per object. + +## Parse errors + +`MolangParser.parseAll` throws a `ParseException` (an `IOException`) that tells you where the problem is: + +```java +try { + MolangParser.parseAll("math.sin("); +} catch (ParseException e) { + Cursor cursor = e.cursor(); // line and column, may be null +} +``` + +`eval(String)` and `prepareEval` never throw for bad syntax: they return `0` instead. +Register a handler to find out when that happens: + +```java +molang.handleParseExceptions(e -> LOGGER.warn("Invalid MoLang", e)); +``` + +## What evaluation returns + +- Booleans are numbers: comparisons and `!` give `1` or `0`, and `true`/`false` are `1`/`0`. +- Dividing by zero gives `0`, and so does any `NaN` or infinity — the interpreter never returns them. +- Names that don't exist evaluate to `0` without an error, so a misspelled query silently gives `0`. +- If the final value isn't a number (a string, an object), the result is `0`. + +## Interpreter and compiler together + +The `runtime-compiler` module adds `MochaEngine`, which pairs an interpreter with a [compiler](../features/compiling.md) that share one scope: + +```java +MochaEngine engine = MochaEngine.createStandard(); + +engine.interpreter().eval("v.x = 3"); +MochaFunction function = engine.compiler().compile("math.pow(2, 10)"); +float result = function.evaluate(); // 1024.0 +``` + +`MochaEngine.from(interpreter)` wraps an interpreter you already have. + +## Threads + +The interpreter doesn't lock anything, and the standard `variable` binding is a plain map. +Give each independent owner its own interpreter — PlayerAnimationLibrary creates one per animation controller — instead of sharing one between threads. diff --git a/mochafloats/intro.md b/mochafloats/intro.md new file mode 100644 index 0000000..144bceb --- /dev/null +++ b/mochafloats/intro.md @@ -0,0 +1,66 @@ +--- +sidebar_position: 1 +description: "mochafloats — a fast MoLang lexer, parser, interpreter and bytecode compiler for Java, forked from Mocha." +--- + +# Introduction + +mochafloats is a Java library that reads and runs [MoLang](https://bedrock.dev/docs/stable/Molang), the small expression language Minecraft Bedrock uses for data-driven values. +It contains a lexer, a parser, an interpreter and a compiler that turns expressions into JVM bytecode. + +It started as a fork of [Mocha](https://github.com/unnamed/mocha) by Unnamed Team and is maintained by the PlayerAnimationLibrary team. +[PlayerAnimationLibrary](/pal/intro) and [Emotecraft](/emotecraft/gettingstarted) run every MoLang expression in their animations through it. + +```java +MolangInterpreter molang = MolangInterpreter.standard(); + +float result = molang.eval("math.sqrt(3 * 3 + 4 * 4)"); // 5.0 +float swing = molang.eval("v.t = 0.25; return -math.sin(v.t * 360) * 30;"); // -30.0 +``` + +## What MoLang looks like + +Everything in MoLang evaluates to a number: `true` is `1`, `false` is `0`, and a whole script is basically one formula. +It has variables (`variable.x`, or `v.x` for short), host-provided values (`query.anim_time`), a `math` library, conditionals and loops: + +``` +v.speed = q.ground_speed * 4; +return q.is_on_ground ? math.sin(q.anim_time * 360 * v.speed) * 20 : 0; +``` + +See [MoLang support](./language.md) for everything mochafloats understands. + +## What is different from Mocha + +mochafloats computes with 32-bit `float` values instead of `double`, splits the library into modules, drops Javassist for the JDK's own ClassFile API and adds a binary format for sending parsed expressions over the network. +It also fixes bugs that are still present in Mocha 3.0.1, such as `-math.sin(90) * 30` evaluating to `0`, and its results were checked against Bedrock itself. + +[Why mochafloats?](./why_mochafloats.md) lists every difference, with the results of running the same expressions on both libraries. + +## Modules + +| Module | Contains | Depends on | +|--------------------|-------------------------------------------------------------------------------------|--------------------------| +| `lexer` | The tokenizer. | — | +| `parser` | The parser, the syntax tree and the network format for it. | `lexer`, Netty `ByteBuf` | +| `runtime` | `MolangInterpreter`, scopes, values, bindings and the standard `math` library. | `parser` | +| `runtime-compiler` | `MolangCompiler`, which compiles expressions to bytecode, and `MochaEngine`. | `runtime` | + +Each module brings the ones it depends on, so you only ever declare one of them. +Most projects only need `runtime`. + +## Where to start + +1. [Add the library to your project](./gettingstarted/installation.md) +2. [Evaluate your first expressions](./gettingstarted/usage.md) +3. [Expose your own values and functions to MoLang](./gettingstarted/bindings.md) +4. [Compile hot expressions to bytecode](./features/compiling.md) if interpreting them isn't fast enough + +Already using Mocha? Read [Migrating from Mocha](./migration/migrating_from_mocha.md). + +## Credits and license + +mochafloats is a fork of [Mocha](https://github.com/unnamed/mocha) © Unnamed Team, and like Mocha it is distributed under the [MIT license](https://github.com/PlayerAnimationLibrary/mochafloats/blob/HEAD/license.txt). +Parts of these pages are based on [Mocha's documentation](https://github.com/unnamed/mocha/tree/v3.0.0/docs). + +The source code lives at [PlayerAnimationLibrary/mochafloats](https://github.com/PlayerAnimationLibrary/mochafloats). diff --git a/mochafloats/language.md b/mochafloats/language.md new file mode 100644 index 0000000..d55171e --- /dev/null +++ b/mochafloats/language.md @@ -0,0 +1,134 @@ +--- +sidebar_position: 5 +description: "The MoLang syntax, operators, keywords and math functions mochafloats supports, and where it differs from Bedrock." +--- + +# MoLang support + +mochafloats follows [Bedrock's MoLang](https://bedrock.dev/docs/stable/Molang), and 6.1 was checked against Bedrock itself. +This page lists exactly what it understands, so you know what you can write in an expression. + +## Literals + +| Literal | Examples | Notes | +|----------|------------------------------------------|-------------------------------------------------------------| +| Number | `5`, `0.25`, `-1.5`, `1.5e3`, `2.5E-4` | Stored as a 32-bit `float`. Scientific notation is supported. | +| Boolean | `true`, `false` | The same as `1` and `0`. | +| String | `'minecraft:pig'` | Single quotes only, no escape sequences. `"text"` is a syntax error. | + +## Names and scopes + +Names are case-insensitive and made of letters, digits and `_`. +Every expression starts from these top-level names: + +| Name | Provided by | Lifetime | +|------------------|-----------------------------|-----------------------------------------------------------| +| `math` | `MolangInterpreter.standard()` | Read-only. | +| `variable` / `v` | `MolangInterpreter.standard()` | Kept between evaluations of the same interpreter. | +| `temp` / `t` | every evaluation | Thrown away when the evaluation ends. | +| `query` / `q` and anything else | you, see [Bindings](./gettingstarted/bindings.md) | Whatever you decide. | + +A name that doesn't exist evaluates to `0`. + +## Operators + +From the tightest binding to the loosest: + +| Operators | Meaning | +|-------------------------------|---------------------------------------------------------------| +| `.` `()` `[]` | Property access, call, array index | +| `-x` `!x` | Negation, logical not | +| `->` | Evaluate the right side with the Java object on the left as the entity | +| `*` `/` | Multiplication, division (dividing by zero gives `0`) | +| `+` `-` | Addition, subtraction | +| `<` `<=` `>` `>=` | Comparison, gives `1` or `0` | +| `==` `!=` | Equality. Two strings compare by their text | +| `&&` | Logical and | +| `\|\|` | Logical or | +| `??` | The left side, or the right side if the left side doesn't exist. `0` counts as existing | +| `c ? a : b`, `c ? a` | Conditional. Without `:` it gives `0` when `c` is false. Chained conditionals group from the right: `a ? 1 : b ? 2 : 3` is `a ? 1 : (b ? 2 : 3)` | +| `=` | Assignment, to `v.*` and `t.*`. It takes everything on its right: `v.x = c ? 5 : 6` assigns the conditional, and `v.a = v.b = 1` assigns both | + +There is no `%` operator; use `math.mod(a, b)`. + +## Statements and control flow + +- `;` separates statements. A script with several statements returns the value of its `return`, and stops there. Empty statements (`;;`) are skipped. +- `{ ... }` groups statements into a block. +- `c ? { ... } : { ... }` runs the block of the chosen branch, and `c ? { ... }` runs its block when `c` is true. +- `loop(count, { ... })` runs a block `count` times. +- `for_each(t.item, array, { ... })` runs a block once for every element of an array, with `t.item` set to the element. +- `break` and `continue` work inside both loops. + +``` +t.total = 0; +loop(10, { + t.total = t.total + 1; + (t.total >= 5) ? break; +}); +return t.total; // 5 +``` + +A missing operand (`1 +`) or a trailing comma in a call (`math.abs(1,)`) is a syntax error, as in Bedrock. + +## Math functions + +Angles are in **degrees**: `math.sin` and `math.cos` take degrees, and `math.asin`, `math.acos`, `math.atan` and `math.atan2` return degrees. + +| Function | Result | +|--------------------------------------------|-------------------------------------------------------------------------| +| `math.abs(x)` | Absolute value. | +| `math.sign(x)` | `-1`, `0` or `1`. | +| `math.copy_sign(x, sign)` | `x` with the sign of `sign`. | +| `math.min(a, b)`, `math.max(a, b)` | The smaller / bigger value. | +| `math.clamp(x, min, max)` | `x` limited to the range. | +| `math.floor(x)`, `math.ceil(x)` | Round down / up. | +| `math.round(x)` | Round to the nearest whole number; halves round away from zero (`-2.5` → `-3`). | +| `math.trunc(x)` | Drop the fractional part (`-2.7` → `-2`). | +| `math.mod(a, b)` | Remainder of `a / b`, with the sign of `a` (`math.mod(-7, 3)` → `-1`). | +| `math.sqrt(x)`, `math.pow(a, b)` | Square root, power. | +| `math.exp(x)`, `math.ln(x)` | `e` to the power of `x`, natural logarithm. | +| `math.sin(deg)`, `math.cos(deg)` | Sine and cosine of an angle in degrees. | +| `math.asin(x)`, `math.acos(x)`, `math.atan(x)` | Inverse functions, in degrees. | +| `math.atan2(y, x)` | Angle of the point `(x, y)`, in degrees. | +| `math.d2r(deg)`, `math.r2d(rad)` | Degrees to radians and back. | +| `math.lerp(a, b, t)` | Linear interpolation from `a` to `b`. | +| `math.inverse_lerp(a, b, x)` | Where `x` lies between `a` and `b`, as `0`–`1` (`0` if `a == b`). | +| `math.lerprotate(a, b, t)` | Interpolation between two angles the short way round. | +| `math.min_angle(deg)` | The angle wrapped into `-180`–`180`. | +| `math.hermite_blend(t)` | `3t² - 2t³`, a smooth `0`–`1` curve. | +| `math.random(min, max)` | A random number between the two bounds, in either order. | +| `math.random_integer(min, max)` | A random whole number from the smaller bound to the bigger one, both included. | +| `math.die_roll(count, low, high)` | The sum of `count` random numbers between `low` and `high`. | +| `math.die_roll_integer(count, low, high)` | The sum of `count` random whole numbers from `low` to `high`, both included. | +| `math.pi`, `math.e` | The constants π and e. | + +`math.sign`, `math.copy_sign`, `math.inverse_lerp`, `math.d2r`, `math.r2d` and `math.e` don't exist in Mocha. + +:::info +PlayerAnimationLibrary adds the `math.ease_*` easing functions on top of this list; they belong to PAL, not to mochafloats. See [PAL's MoLang page](/pal/molang#easing-functions). +::: + +## Results + +- Results are 32-bit floats: about 7 significant digits, and whole numbers are exact up to 16,777,216. +- Dividing by zero gives `0`, and `NaN` and infinities are turned into `0`, both interpreted and in [compiled functions](./features/compiling.md). +- Strings, objects and functions evaluate to `0` when a number is needed. + +## Differences from Bedrock and Blockbench + +Of everything checked against Bedrock, one difference is left, and it's deliberate: `math.sqrt` of a negative number and `math.ln(0)` give `0`, where Bedrock gives `NaN` and `-Infinity`. A `NaN` that reaches a bone breaks the model. + +:::note +Blockbench previews animations with [MolangJS](https://github.com/JannisX11/MolangJS), which differs from Bedrock and mochafloats in a few places. In Blockbench: + +- `'a' == 'b'` is `1`: strings count as `0`. +- `math.round(-2.5)` is `-2`. +- `math.random_integer(3, 1)` is always `2`. +- `math.ln(0)` is `-Infinity`. +- `1 +` and `math.abs(1,)` are accepted. + +Avoid these in animations if what you see in Blockbench has to match the game exactly. +::: + +Versions before 6.1 differ from Bedrock in more places; see [Upgrading to 6.1](./migration/upgrading_to_6_1.md). diff --git a/mochafloats/migration/_category_.json b/mochafloats/migration/_category_.json new file mode 100644 index 0000000..0f64ee1 --- /dev/null +++ b/mochafloats/migration/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Migration", + "position": 6 +} diff --git a/mochafloats/migration/index.md b/mochafloats/migration/index.md new file mode 100644 index 0000000..e628d6e --- /dev/null +++ b/mochafloats/migration/index.md @@ -0,0 +1,8 @@ +--- +description: "Moving to mochafloats from Mocha, and updating between mochafloats versions." +--- + +# Migration + +- [Migrating from Mocha](./migrating_from_mocha.md) — move a project from Mocha 3 to mochafloats: the dependency, the packages and every changed call. +- [Upgrading to 6.1](./upgrading_to_6_1.md) — what evaluates differently after updating from mochafloats 6.0. diff --git a/mochafloats/migration/migrating_from_mocha.md b/mochafloats/migration/migrating_from_mocha.md new file mode 100644 index 0000000..d1dbf3b --- /dev/null +++ b/mochafloats/migration/migrating_from_mocha.md @@ -0,0 +1,65 @@ +--- +sidebar_position: 1 +description: "Moving a project from Mocha 3.x to mochafloats: dependency, packages and every changed call." +--- + +# Migrating from Mocha + +mochafloats keeps Mocha's concepts — scopes, bindings, `@Binding`, compiled interfaces — so most code only needs new imports and a few renamed calls. + +## 1. Swap the dependency + +Remove `team.unnamed:mocha` (and Javassist, if you declared it yourself) and add mochafloats as described in [Installation](../gettingstarted/installation.md). +Make sure the project builds and runs on **Java 24 or newer**. + +To keep both the interpreter and the compiler, as Mocha had them, depend on `runtime-compiler`. + +## 2. Update the imports + +Everything moved from `team.unnamed.mocha` to `org.redlance.mocha`, and a few classes changed packages: + +| Mocha 3 | mochafloats | +|--------------------------------------------------|----------------------------------------------------------| +| `team.unnamed.mocha.MochaEngine` | `org.redlance.mocha.runtime.MochaEngine` | +| `team.unnamed.mocha.util.CaseInsensitiveStringHashMap` | `org.redlance.mocha.runtime.util.CaseInsensitiveStringHashMap` | +| `team.unnamed.mocha.parser.ast.DoubleExpression` | `org.redlance.mocha.parser.ast.FloatExpression` | +| any other `team.unnamed.mocha.X` | `org.redlance.mocha.X` | + +## 3. Update the calls + +`MochaEngine` used to do everything. In mochafloats it only holds an interpreter and a compiler that share one scope: + +| Mocha 3 | mochafloats | +|---------------------------------------------------|---------------------------------------------------------------------| +| `MochaEngine.createStandard()` | `MochaEngine.createStandard()`, or `MolangInterpreter.standard()` if you don't compile | +| `MochaEngine.create(entity, builder -> ...)` | `MolangInterpreter.create(entity, builder -> ...)`, wrapped with `MochaEngine.from(...)` if you compile | +| `engine.eval(code)` → `double` | `engine.interpreter().eval(code)` → `float` | +| `engine.prepareEval(code)` → `MochaFunction` | `engine.interpreter().prepareEval(code)` → `Supplier` | +| `engine.parse(code)` | `MolangParser.parseAll(code)` | +| `engine.compile(code)`, `compile(code, type)` | `engine.compiler().compile(code)`, `compile(code, type)` | +| `engine.bind(type)`, `bindInstance(...)` | `engine.interpreter().bind(type)`, `bindInstance(...)` | +| `engine.warnOnReflectiveFunctionUsage(true)` | `engine.interpreter().warnOnReflectiveFunctionUsage(true)` | +| `engine.handleParseExceptions(handler)` | set it on both `engine.interpreter()` and `engine.compiler()` | +| `engine.postCompile(consumer)` | `engine.compiler().postCompile(consumer)` | +| `engine.classPool()` | removed, there's no Javassist | +| `new MolangCompiler(entity, classLoader, scope)` | `new MolangCompiler(entity, scope)` | +| `MochaFunction.evaluate()` → `double` | `MochaFunction.evaluate()` → `float` | + +`engine.scope()` still works and returns the shared scope. + +## 4. From `double` to `float` + +- `Value.getAsNumber()` and `NumberValue.value()` return `float`. `NumberValue.of` accepts both `float` and `double`. +- `ObjectValue.setFunction` takes `FloatFunction1`, `FloatFunction2` and `FloatFunction3` instead of `DoubleFunction1`–`3`, plus a new `FloatFunction` with no arguments. Lambdas usually compile unchanged once their arithmetic is `float`. +- `@Binding` methods may still use `double`; arguments and results are converted. Using `float` avoids the conversions. +- A custom `ExpressionVisitor` overrides `visitFloat` instead of `visitDouble`. Create literals with `FloatExpression.of(value)`; the constructor taking a number is private. +- Compiled interfaces can keep `double` parameters and results, but `float` matches what the compiler computes with. + +## 5. Things that behave differently + +- `Expression` and `Value` are `sealed`: you can no longer implement `Expression` yourself, and every expression has a `write(ByteBuf)` method for the [network format](../features/serialization.md). +- `toString()` on expressions prints MoLang instead of debug trees like `Call(Access(...))`. +- Expressions that Mocha got wrong now give the right result, for example `-math.sin(90) * 30` is `-30` instead of `0` and `1e3` is `1000` instead of `0`. If your content was tuned around the old results, check it again — see [Why mochafloats?](../why_mochafloats.md) for the full list. +- MoLang evaluates like Bedrock where Mocha didn't: `v.x = c ? 5 : 6` assigns `5` or `6` instead of `c`, strings compare by their text, `??` keeps a `0`, `math.round(-2.5)` is `-3`, and the random functions include both bounds. See [MoLang that evaluates like Bedrock](../why_mochafloats.md#molang-that-evaluates-like-bedrock). +- Syntax Mocha accepted, such as `1 +` or a trailing comma in `math.abs(1,)`, is a parse error. +- Results are floats, so very large or very precise values round differently: `16777217` becomes `16777216`, `0.1 + 0.2` becomes `0.3`. diff --git a/mochafloats/migration/upgrading_to_6_1.md b/mochafloats/migration/upgrading_to_6_1.md new file mode 100644 index 0000000..b73248a --- /dev/null +++ b/mochafloats/migration/upgrading_to_6_1.md @@ -0,0 +1,71 @@ +--- +sidebar_position: 2 +description: "What changes when you update mochafloats from 6.0 to 6.1: MoLang that now evaluates like Bedrock, syntax errors, compiled functions, Java bindings and the network format." +--- + +# Upgrading to 6.1 + +6.1 doesn't remove or change any public API, so code written for 6.0 compiles against it unchanged. +What changes is the result of some MoLang: 6.1 was checked against Bedrock in the game and now agrees with it, and bugs in the parser, the compiler and the Java bindings are fixed. + +## MoLang that evaluates differently + +| Expression | 6.0 | 6.1 | Bedrock | +|---------------------------------------------------------|-------------|-------------|-------------| +| `v.x = 1 ? 5 : 6; return v.x;` | `1` | `5` | `5` | +| `1 ? 2 : 3 ? 4 : 5` | `4` | `2` | `2` | +| `v.w = 0; 1 ? { v.w = 4; } : { v.w = 5; }; return v.w;` | `0` | `4` | `4` | +| `v.zero = 0; return v.zero ?? 7;` | `7` | `0` | `0` | +| `'a' == 'b'` | `1` | `0` | `0` | +| `math.round(-2.5)` | `-2` | `-3` | `-3` | +| `math.random_integer(1, 3)` | 1 or 2 | 1 to 3 | 1 to 3 | +| `math.random(1, 1)`, `math.random(5, 1)` | throws | `1`, 1 to 5 | `1`, 1 to 5 | +| `math.die_roll(1, 5, 6)` | 1.25 to 2.5 | 5 to 6 | 5 to 6 | +| `v.a = 1;; v.b = 2; return v.b;` | `1` | `2` | `2` | + +What to look for in existing animations: + +- **`v.x = condition ? a : b`** assigns `a` or `b`; 6.0 assigned the condition. Content that wraps the conditional in parentheses, `v.x = (condition ? a : b)`, gives the same result in both versions. For the same reason, `v.a = v.b = 1` sets both variables, where 6.0 set only `v.a`. +- **Chained conditionals:** `a ? 1 : b ? 2 : 3` means `a ? 1 : (b ? 2 : 3)`; 6.0 read it as `(a ? 1 : b) ? 2 : 3`. +- **Blocks in a full conditional**, `c ? { ... } : { ... }`, run; in 6.0 neither block did. +- **`??`** falls back only when the left side doesn't exist: a variable set to `0` is kept. +- **Strings** compare by their text; in 6.0 any two strings were equal. +- **`math.round`** rounds halves away from zero. +- **Random functions** include both bounds and accept them in either order, and `math.die_roll` returns a sum between `count × low` and `count × high`. In 6.0 an equal or reversed pair of bounds made `math.random` and `math.random_integer` throw an exception out of `eval`. + +## Syntax errors are reported + +| Source | 6.0 | 6.1 | +|----------------------|----------------------------------------|-----------------| +| `1 +` | an empty script, evaluates to `0` | parse error | +| `math.abs(1,)` | `1`, with `null` as a second argument | parse error | +| `v.a = 1;; v.b = 2` | only `v.a = 1`, the rest is dropped | both statements | + +For the first two, `MolangParser.parseAll` throws a `ParseException`, and `eval(String)` returns `0` and passes the exception to the handler set with `handleParseExceptions`, as for any other syntax error. 6.0 reported neither, and Bedrock rejects both. + +## Compiled functions + +- A fraction counts as true: with `a = 0.5`, `a && 1` is `1` and `a ? 10 : 20` is `10`. 6.0 cut fractions to whole numbers first, so they were false. +- Dividing by zero gives `0`, and `NaN` or infinities, from arithmetic or returned by a Java method, become `0`, as in the interpreter. A method whose `@Binding` sets `skipChecking = true` is trusted and its result returned as it is. +- `t.*` assignments work in functions that return `double`, `int` or `boolean`; in 6.0 they failed verification. +- `c ? a` without `:` compiles. +- A construct the compiler doesn't support makes `compile` throw an `UnsupportedOperationException` that names it. In 6.0 some of them failed with an `IllegalArgumentException` about the operand stack, `loop` and `for_each` compiled to nothing, so the function returned a wrong value, and methods that take an `ExecutionContext`, a `Lazy` or varargs received `null`. + +If `compile` now throws for one of your expressions, evaluate that expression with the interpreter. See [what the compiler supports](../features/compiling.md#what-the-compiler-supports). + +## Java bindings + +- Interpreted calls are faster: `math.*` functions are called directly instead of through reflection, and other `@Binding` methods through a method handle that is prepared once. An expression with `math.sin` and `math.cos` takes 135 ns instead of 304 ns, and a call to a `@Binding` method 102 ns instead of 174 ns. +- `warnOnReflectiveFunctionUsage(true)` reports each reflectively called method once. 6.0 printed a line on every call, `math.*` included. +- Parameters of type `ExecutionContext`, `Lazy` and varargs receive their values in interpreted code. In 6.0 they received `null`, and primitive varargs threw a `ClassCastException`. +- A class with a single `@BindExternalFunction` binds it; 6.0 only read repeated annotations. +- A `Function` that returns `null` evaluates to `0`; in 6.0, `q.f() + 1` threw a `NullPointerException`. + +## Network format and printing + +- The binary format is unchanged, so 6.0 and 6.1 can exchange expressions. The exception is an empty string, `''`: 6.0 can't read it back, whichever version wrote it, and 6.1 reads it from both. +- An unknown operator byte throws an `IllegalArgumentException`; 6.0 read it as the first operator. +- `toString()` keeps the parentheses an expression needs: `(1 + 2) * 3` prints as `(1+2)*3` instead of `1+2*3`, so the printed text parses back into the same expression. +- `break` and `continue` expressions implement `equals` and `hashCode`. + +*Times measured with JMH on JDK 25 (Apple Silicon), average per call.* diff --git a/mochafloats/why_mochafloats.md b/mochafloats/why_mochafloats.md new file mode 100644 index 0000000..cb0b827 --- /dev/null +++ b/mochafloats/why_mochafloats.md @@ -0,0 +1,256 @@ +--- +sidebar_position: 2 +description: "What mochafloats fixes and adds compared to Mocha 3.0.1, with the results of running the same expressions on both." +--- + +# Why mochafloats? + +Mocha 3.0.1, released in March 2025, is the last version of the original library. +Its `main` branch hasn't changed since, the bug reports [#27](https://github.com/unnamed/mocha/issues/27) and [#29](https://github.com/unnamed/mocha/issues/29) are still open, and the pull request that ported it to `float` ([#22](https://github.com/unnamed/mocha/pull/22)) was never merged. + +mochafloats picked up from there: well over a hundred commits on top of Mocha 3.0.1, released as versions up to 6.1.0. +This page lists what that changed. + +## At a glance + +| | Mocha 3.0.1 | mochafloats 6.1.0 | +|------------------------------------------|---------------------------------------|------------------------------------------------------| +| Number type | `double` | `float` | +| Java | 8+ | 24+ | +| Bytecode generation | Javassist 3.30.2 (≈ 795 KB extra jar) | JDK ClassFile API, no extra jar | +| What you ship to only interpret MoLang | ≈ 930 KB (Mocha + Javassist) | ≈ 125 KB (`lexer` + `parser` + `runtime`) | +| Interpreting `math.sin(...) * 45 + math.cos(...)` | 248 ns | 157 ns | +| Compiled functions | regular classes, never unloaded | hidden classes, unloaded with the function | +| Sending parsed expressions over network | — | built in, on Netty `ByteBuf` | +| Checked against Bedrock | — | [yes](#molang-that-evaluates-like-bedrock) | +| `-math.sin(90) * 30` | `0` | `-30` | +| `v.x = 1 ? 5 : 6; return v.x;` | `1` | `5`, like Bedrock | +| `1.0E-4 * 10000` | `0` and a parse error | `1` | +| `math.PI` on a Turkish system | `0` | `3.1415927` | +| `math.e`, `math.sign`, `math.inverse_lerp` | `0` (missing) | implemented | +| Bundled inside a NeoForge mod | crashes when compiling ([#27](https://github.com/unnamed/mocha/issues/27)) | works | +| Compiled calls to instance methods | fail verification ([#29](https://github.com/unnamed/mocha/issues/29)) | work | + +:::info How these results were measured +Every expression on this page was run on both libraries, interpreted and compiled, with a standard engine (`createStandard()`) on JDK 25. +Where a query appears, `q.length('abc')` is a Java method bound with `@Binding` that returns the length of its argument. +The Bedrock results come from a behaviour pack whose animation controllers evaluate each expression in the game and report the result. +::: + +## Bugs fixed only in mochafloats + +### A minus or `!` in front of a function call + +Mocha attaches a leading `-` or `!` to the function name instead of the call, so `-math.abs(5)` is read as `(-math.abs)(5)`: it calls a negated function, which evaluates to `0`. +Negating a `sin` or `cos` is one of the most common things an animation does, and every such expression silently produced `0`. + +| Expression | Mocha 3.0.1 | mochafloats | +|-----------------------|-------------|-------------| +| `-math.sin(90) * 30` | `0` | `-30` | +| `-math.abs(3) * 2` | `0` | `-6` | +| `!math.abs(0)` | `0` | `1` | +| `-q.length('abc')` | `0` | `-3` | +| `!q.length('')` | `0` | `1` | + +The results are the same interpreted and compiled. Fixed in [#55](https://github.com/PlayerAnimationLibrary/mochafloats/pull/55). + +### MoLang that evaluates like Bedrock + +mochafloats 6.1 was checked against Bedrock itself, and against five other MoLang implementations where Bedrock wasn't tested. +These expressions give different results in Mocha: + +| Expression | Mocha 3.0.1 | mochafloats 6.1 | Bedrock | +|--------------------------------------------------------|-------------|-----------------|---------| +| `v.x = 1 ? 5 : 6; return v.x;` | `1` | `5` | `5` | +| `1 ? 2 : 3 ? 4 : 5` | `4` | `2` | `2` | +| `v.w = 0; 1 ? { v.w = 4; } : { v.w = 5; }; return v.w;` | `0` | `4` | `4` | +| `v.zero = 0; return v.zero ?? 7;` | `7` | `0` | `0` | +| `'a' == 'b'` | `1` | `0` | `0` | +| `math.round(-2.5)` | `-2` | `-3` | `-3` | +| `math.random_integer(1, 3)` | 1 or 2 | 1 to 3 | 1 to 3 | +| `math.random(1, 1)` | throws | `1` | `1` | +| `math.die_roll(1, 5, 6)` | 1.25 to 2.5 | 5 to 6 | 5 to 6 | + +- Mocha binds `=` tighter than `? :`, so `v.x = c ? 5 : 6` assigns `c`; and it groups chained conditionals from the left. +- In `c ? { ... } : { ... }`, Mocha never runs either block. +- Mocha's `??` falls back on `0` as well as on a missing value. +- Mocha compares strings as numbers, so any two strings are equal. +- Mocha's `math.random`, `math.random_integer` and `math.die_roll_integer` throw an `IllegalArgumentException` out of `eval` when both bounds are equal or reversed, and `random_integer` never returns its upper bound. +- Mocha's `math.die_roll` sums whole numbers from `low` to `low + high - 1` and divides the total by 4. + +The one deliberate difference from Bedrock is in [MoLang support](./language.md#differences-from-bedrock-and-blockbench). + +### Numbers in scientific notation + +Mocha's lexer stops reading a number at the `e`, so anything written in scientific notation fails to parse and the whole expression evaluates to `0`. +That's the format Java, JavaScript and many exporters print very small and very large numbers in. + +| Expression | Mocha 3.0.1 | mochafloats | +|-------------------|------------------------------------------------------------|-------------| +| `1.0E-4 * 10000` | `0`, parse error *Expected a semicolon, but was IDENTIFIER(E)* | `1` | +| `2.5e-1` | `0` | `0.25` | +| `1.5E+2` | `0` | `150` | + +### Names on a Turkish (or Azerbaijani) system + +MoLang names are case-insensitive, and Mocha lowercases them with the system's language rules. +In Turkish the lowercase of `I` is the dotless `ı`, so on a Turkish system `math.PI` looks up `pı`, finds nothing and evaluates to `0`. + +| Expression (Turkish system locale) | Mocha 3.0.1 | mochafloats | +|------------------------------------|-------------|-------------| +| `math.PI` | `0` | `3.1415927` | +| `MATH.MIN(1, 2)` | `0` | `1` | + +mochafloats lowercases with `Locale.ROOT`, so the result no longer depends on the player's language. Fixed in [#18](https://github.com/PlayerAnimationLibrary/mochafloats/pull/18). + +### Compiling calls to Java methods + +Mocha's compiler generates invalid bytecode for several kinds of Java calls, so the expression can't be compiled at all: + +| Expression | Mocha 3.0.1 | mochafloats | +|-----------------------------------------|---------------------------------------------------|-------------| +| `q.length('abc') + 1` | compile error: *stack underflow* | `4` | +| `q.body_float('chest', 'power') - 100` | compile error: *stack underflow* | `100` | +| `offset.calc(1)` (an instance method) | `VerifyError`: *Bad type on operand stack* | `4` | + +- A call left the type of its last parameter as the "expected type" for the rest of the expression, so an arithmetic operation after a call with a `String`, `int`, `long`, `double` or `boolean` parameter broke. Fixed in [#55](https://github.com/PlayerAnimationLibrary/mochafloats/pull/55) ([#54](https://github.com/PlayerAnimationLibrary/mochafloats/issues/54)). +- For instance methods, the arguments were pushed before the object the method is called on. This is Mocha's open issue [#29](https://github.com/unnamed/mocha/issues/29). + +### Compiled code that disagrees with the interpreter + +| Compiled | Mocha 3.0.1 | mochafloats | +|-------------------------------------------------------|---------------------------------------------------|-------------| +| `a && 1` with `a = 0.5` | `0` | `1` | +| `a ? 10 : 20` with `a = 0.5` | `20` | `10` | +| `1 / a` with `a = 0` | `Infinity` | `0` | +| `-(a > b) ? 10 : 20` with `a = 5`, `b = 4` | `20` | `10` | +| `!(a > b)` with `a = 1`, `b = 2`, returning `boolean` | `ArrayIndexOutOfBoundsException` while compiling | `true` | + +Mocha's compiler turns a number into a boolean by cutting it to an integer first, so every value between `-1` and `1` is false. +It doesn't apply MoLang's division-by-zero rule, it flips the truth of a negated boolean, and it emits a broken jump for `!` when the surrounding code expects a `boolean`. +mochafloats' compiled functions give the interpreter's results, and constructs the compiler doesn't support are rejected with an error that names them instead of turning into invalid bytecode. See [what the compiler supports](./features/compiling.md#what-the-compiler-supports). + +### Java bindings + +| With bindings made with `@Binding` / lambdas | Mocha 3.0.1 | mochafloats | +|---------------------------------------------------------------------|-----------------------------|------------------| +| A class with a single `@BindExternalFunction` | nothing is bound | bound | +| A method parameter of type `ExecutionContext`, `Lazy` or varargs | receives `null`, or throws | works | +| `q.f() + 1` where the `Function` returns `null` | `NullPointerException` | `1` | +| `warnOnReflectiveFunctionUsage(true)`, 3 evaluations with 2 calls | 6 warnings, `math.*` included | each reflective method once | + +### Compiled classes that collide and never go away + +Mocha names every compiled class after the current millisecond plus a random number below 2024. +When many expressions are compiled at once — for example while resources load — two of them eventually get the same name, and the second fails with `frozen class (cannot edit)`. +The classes it defines are also never unloaded, and Javassist's global `ClassPool` keeps a copy of each one. + +Compiling 20,000 expressions in a loop: + +| | Mocha 3.0.1 | mochafloats | +|-----------------------------------|-------------|-------------| +| Failed compilations | 242 | 0 | +| Generated classes unloaded by GC | 0 | 20,000 | +| Heap still used after GC | 75 MB | 2 MB | + +mochafloats numbers its classes with an atomic counter and defines them as [hidden classes](https://openjdk.org/jeps/371), which the JVM can unload once nothing references the compiled function anymore. + +### Crash when bundled inside a NeoForge mod + +When Mocha is shipped inside a mod's jar, NeoForge loads it in a separate module layer, and compiling a function that implements one of the mod's interfaces crashes with `IllegalAccessError: superinterface check failed` ([#27](https://github.com/unnamed/mocha/issues/27), still open). +mochafloats' jars declare `FMLModType: GAMELIBRARY`, so NeoForge loads them next to the mods that use them. + +## Features only in mochafloats + +### A faster interpreter + +| Interpreted, pre-parsed | Mocha 3.0.1 | mochafloats 6.1 | +|-------------------------------------------------------------------|--------------------|---------------------| +| `5` | 71 ns, 664 bytes | **0.9 ns, 0 bytes** | +| `t.t = 3; return 3*t.t*t.t - 2*t.t*t.t*t.t;` | 180 ns, 1000 bytes | **167 ns, 712 bytes** | +| `t.a = 1.25; return math.sin(t.a * 50) * 45 + math.cos(t.a * 20);` | 248 ns, 1408 bytes | **157 ns, 824 bytes** | +| A method bound with `@Binding` | 137 ns, 968 bytes | **102 ns, 696 bytes** | + +*Measured with JMH on JDK 25 (Apple Silicon), average time and allocation per call.* + +- An expression that is just a number, like most keyframe values, is returned straight away; Mocha copies the whole scope and creates an interpreter for it on every call. +- `math.*` functions are called directly instead of through reflection. +- Other `@Binding` methods are called through a method handle that is prepared once. + +Compiled functions run in about half a nanosecond for simple expressions in Mocha, mochafloats and Moonflower's molang-compiler alike. On the arithmetic and math expressions above, mochafloats' compiled code is 10 to 15% slower than the other two, because it also applies MoLang's rules for division by zero and `NaN`. + +
+Compared with other Java MoLang libraries + +| Interpreted | mochafloats 6.1 | Mocha 3.0.1 | bedrockk/MoLang | +|-------------------------------------------|-----------------|-------------|-----------------| +| `5` | 0.9 ns | 71 ns | 74 ns | +| arithmetic with temp variables (above) | 167 ns | 180 ns | 2212 ns | +| `math.sin` and `math.cos` (above) | 157 ns | 248 ns | 2159 ns | + +| Compiled | mochafloats 6.1 | Mocha 3.0.1 | molang-compiler | +|-------------------------------------------|-----------------|-------------|-----------------| +| `5` | 0.50 ns | 0.46 ns | 0.52 ns | +| arithmetic with temp variables (above) | 0.51 ns | 0.45 ns | 0.44 ns | +| `math.sin` and `math.cos` (above) | 5.3 ns | 4.7 ns | 4.6 ns | + +In this test molang-compiler 3.1.1.19 returned `189` instead of `-27` for the arithmetic expression, and bedrockk/MoLang computed `math.sin` and `math.cos` in radians; their times are shown as measured. + +
+ +### Floats everywhere + +Literals, values, function arguments and results are 32-bit floats, the same type Minecraft's models use for part rotations, offsets and scales. +A keyframe value goes from MoLang to the model without being converted from `double` to `float` and back. + +### Pick only what you need + +The library is split into [four modules](./intro.md#modules). +A mod that only interprets MoLang ships `lexer`, `parser` and `runtime` — about 125 KB — while Mocha can't even create an interpreter without Javassist on the classpath. + +### No Javassist + +The compiler builds classes with the JDK's ClassFile API (`java.lang.classfile`, final since Java 24) and defines them as hidden classes. +There is no third-party bytecode library to ship, shade or conflict with another mod's copy. + +### Sending expressions over the network + +Parsed expressions can be written to a Netty `ByteBuf` and read back on the other side, without turning them back into text and parsing them again. +PlayerAnimationLibrary's binary animation format, the one that goes over the network, stores expressions this way. See [Sending expressions over the network](./features/serialization.md). + +### More of the `math` library + +`math.e`, `math.sign`, `math.copy_sign`, `math.inverse_lerp`, `math.d2r` and `math.r2d` are implemented; in Mocha they evaluate to `0`. + +| Expression | Mocha 3.0.1 | mochafloats | +|----------------------------------|-------------|-------------| +| `math.e` | `0` | `2.7182817` | +| `math.sign(-4)` | `0` | `-1` | +| `math.copy_sign(3, -1)` | `0` | `-3` | +| `math.inverse_lerp(0, 10, 2.5)` | `0` | `0.25` | +| `math.d2r(180)` | `0` | `3.1415927` | +| `math.r2d(math.pi)` | `0` | `180` | + +### Values that only exist for one evaluation + +`MolangInterpreter.eval(expressions, scope -> ...)` lets you add bindings that only that one evaluation can see — for example a `this` or a `context` object — without touching the interpreter's shared scope. See [Bindings for a single evaluation](./gettingstarted/usage.md#bindings-for-a-single-evaluation). + +### Syntax trees that print as MoLang + +`toString()` on a parsed expression prints it in MoLang syntax — `math.sin(q.anim_time*50)*45` instead of `Call(Access(Identifier(math), sin), [...])` — with parentheses wherever they're needed, so the text parses back into the same expression. See [Printing expressions as text](./features/serialization.md#printing-expressions-as-text). + +### Smaller changes + +- `Expression`, `Value`, `MolangLexer` and `MolangParser` are `sealed`, so a `switch` over them can be checked for exhaustiveness. +- The compiler's constant folding can be replaced or turned off: `new MolangCompiler(entity, scope, null)`. +- `ObjectValue.setFunction` accepts functions with no arguments. +- The runtime is annotated for [J2ObjC](https://developers.google.com/j2objc) and falls back to plain reflection where `java.lang.invoke` isn't available. +- The test suite checks 141 expressions against [MolangJS](https://github.com/JannisX11/MolangJS), the MoLang implementation Blockbench uses, both interpreted and compiled, and CI builds and tests every pull request. + +## What it costs you + +- **Java 24 or newer.** The ClassFile API doesn't exist before Java 24. Minecraft 26.1 and newer runs on Java 25. +- **Float precision.** A float holds about 7 significant digits, and whole numbers are only exact up to 16,777,216: `16777217` evaluates to `16777216`. That's plenty for animation, but large counters lose precision — a world's game time in ticks (`q.time_stamp` in PAL) passes that limit after about 9.7 days of running, so prefer values that stay small, like `q.anim_time`. +- **A different API.** Packages moved to `org.redlance.mocha`, and `MochaEngine` was split into an interpreter and a compiler. [Migrating from Mocha](./migration/migrating_from_mocha.md) maps every old call to its new place. + +Already on mochafloats 6.0? [Upgrading to 6.1](./migration/upgrading_to_6_1.md) lists what changed. diff --git a/pal/intro.md b/pal/intro.md index 9711305..f041ee8 100644 --- a/pal/intro.md +++ b/pal/intro.md @@ -14,7 +14,7 @@ Here are a couple of the features the mod provides that you might find very usef - Provides an easy way to play JSON animations made in Blockbench **and** Blender, while allowing you to modify them using code. - Enables smooth transitions between all animations. - Has a priority system, so conflicts between two mods trying to animate the player at the same time can be resolved. -- Almost full [MoLang](./molang.md) support using [Mocha](https://unnamed.team/docs/mocha) — even stuff like arrow `->` operators and variables! +- Almost full [MoLang](./molang.md) support using [mochafloats](/mochafloats/intro), our fork of Mocha — even stuff like arrow `->` operators and variables! ## Why should I use this over PlayerAnimator? diff --git a/pal/molang.md b/pal/molang.md index e343468..dcef447 100644 --- a/pal/molang.md +++ b/pal/molang.md @@ -9,7 +9,7 @@ description: "MoLang in PAL: syntax, built-in queries, math functions and how to If you are unfamiliar with what MoLang is and want to use it to create an animation, refer to the [MoLang page in the Emotecraft wiki](/emotecraft/creatingemotes/molang). ::: -PAL evaluates MoLang with [mochafloats](https://github.com/PlayerAnimationLibrary/mochafloats), a fork of [Mocha](https://unnamed.team/docs/mocha) that computes with floats instead of doubles. +PAL evaluates MoLang with [mochafloats](/mochafloats/intro), our fork of [Mocha](https://github.com/unnamed/mocha) that computes with floats instead of doubles and fixes bugs Mocha still has — see [Why mochafloats?](/mochafloats/why_mochafloats). Almost all of Bedrock MoLang's functionality is present, and all MoLang queries and functions have been implemented — [here is the full documentation for Bedrock MoLang](https://bedrock.dev/docs/stable/Molang). MoLang lets you compute a keyframe value while the animation plays instead of baking it into keyframes, so you can react to the player's state or build motion that never repeats itself. diff --git a/plugins/mochafloats-version-plugin.js b/plugins/mochafloats-version-plugin.js new file mode 100644 index 0000000..602b488 --- /dev/null +++ b/plugins/mochafloats-version-plugin.js @@ -0,0 +1,27 @@ +const MAVEN_METADATA_URL = + 'https://repo.redlance.org/public/org/redlance/mochafloats/runtime/maven-metadata.xml'; + +module.exports = function mochafloatsVersionPlugin() { + return { + name: 'mochafloats-version-plugin', + + async loadContent() { + try { + const response = await fetch(MAVEN_METADATA_URL); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + const xml = await response.text(); + const release = xml.match(/([^<]+)<\/release>/); + return {release: release ? release[1] : null}; + } catch (error) { + console.warn('[mochafloats-version-plugin] Failed to fetch the version:', error); + return {release: null}; + } + }, + + async contentLoaded({content, actions}) { + actions.setGlobalData(content); + }, + }; +}; diff --git a/src/components/MochaFloatsVersion.tsx b/src/components/MochaFloatsVersion.tsx new file mode 100644 index 0000000..d2b686f --- /dev/null +++ b/src/components/MochaFloatsVersion.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import {usePluginData} from '@docusaurus/useGlobalData'; + +export default function MochaFloatsVersion() { + const data = usePluginData('mochafloats-version-plugin') as {release: string | null} | undefined; + + if (!data?.release) { + return not found; + } + + return {data.release}; +} diff --git a/src/pages/index.md b/src/pages/index.md index 2a6769e..829a8c6 100644 --- a/src/pages/index.md +++ b/src/pages/index.md @@ -26,5 +26,13 @@ Official documentation for PlayerAnimationLibrary, Emotecraft and other projects linkTitle="View Documentation" /> +
+ +
\ No newline at end of file diff --git a/static/_redirects b/static/_redirects index 20c6ca9..f2f8b50 100644 --- a/static/_redirects +++ b/static/_redirects @@ -2,6 +2,7 @@ /emotecraft /emotecraft/gettingstarted 308 /pal /pal/intro 308 /playeranimationlibrary /pal/intro 308 +/mochafloats /mochafloats/intro 308 /blog/2025-07-12-emotectaft-300 /blog/2025-07-12-emotecraft-300 308 /pal/how_to_add_lib_to_mod /pal/gettingstarted/how_to_add_lib_to_mod 308 /pal/how_to_play_animations /pal/gettingstarted/how_to_play_animations 308 From feaed61405f8d77e4e4635ee5b668e9cf2558d5b Mon Sep 17 00:00:00 2001 From: dima_dencep Date: Sun, 4 Oct 2026 17:26:03 +0700 Subject: [PATCH 2/3] Make the typecheck work with TypeScript 7 TypeScript 7 removed baseUrl, which @docusaurus/tsconfig still sets, so tsc stopped at the configuration without checking any file. Reset it and resolve @site/* from the project root instead. Co-Authored-By: Claude Opus 5.5 --- tsconfig.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 920d7a6..4e6f77d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,11 @@ // This file is not used in compilation. It is here just for a nice editor experience. "extends": "@docusaurus/tsconfig", "compilerOptions": { - "baseUrl": "." + // TypeScript 7 removed baseUrl, which @docusaurus/tsconfig still sets + "baseUrl": null, + "paths": { + "@site/*": ["./*"] + } }, "exclude": [".docusaurus", "build"] } From a6113087b86a19ff938a8a3bd6d578574a14ab0d Mon Sep 17 00:00:00 2001 From: dima_dencep Date: Sun, 4 Oct 2026 17:41:40 +0700 Subject: [PATCH 3/3] Cover mochafloats 5 and earlier in the migration guide The guide now moves a project to mochafloats 6 from Mocha 3 or from com.zigythebird:mochafloats 1.0 to 5.0.1, which kept Mocha's packages. It adds a before-and-after example that prints the same results on Mocha 3, mochafloats 5 and 6.1.0, the classes that moved to another package, a command that renames the imports, and what evaluates differently from each starting point. Co-Authored-By: Claude Opus 5.5 --- mochafloats/intro.md | 2 +- mochafloats/migration/index.md | 2 +- mochafloats/migration/migrating_from_mocha.md | 229 +++++++++++++++--- mochafloats/migration/upgrading_to_6_1.md | 1 + mochafloats/why_mochafloats.md | 2 +- 5 files changed, 194 insertions(+), 42 deletions(-) diff --git a/mochafloats/intro.md b/mochafloats/intro.md index 144bceb..5fd9428 100644 --- a/mochafloats/intro.md +++ b/mochafloats/intro.md @@ -56,7 +56,7 @@ Most projects only need `runtime`. 3. [Expose your own values and functions to MoLang](./gettingstarted/bindings.md) 4. [Compile hot expressions to bytecode](./features/compiling.md) if interpreting them isn't fast enough -Already using Mocha? Read [Migrating from Mocha](./migration/migrating_from_mocha.md). +Coming from Mocha or an older mochafloats? Read [Migrating to mochafloats 6](./migration/migrating_from_mocha.md). ## Credits and license diff --git a/mochafloats/migration/index.md b/mochafloats/migration/index.md index e628d6e..ee9499d 100644 --- a/mochafloats/migration/index.md +++ b/mochafloats/migration/index.md @@ -4,5 +4,5 @@ description: "Moving to mochafloats from Mocha, and updating between mochafloats # Migration -- [Migrating from Mocha](./migrating_from_mocha.md) — move a project from Mocha 3 to mochafloats: the dependency, the packages and every changed call. +- [Migrating to mochafloats 6](./migrating_from_mocha.md) — move a project from Mocha 3, or from mochafloats 5 and earlier: the dependency, the packages and every changed call. - [Upgrading to 6.1](./upgrading_to_6_1.md) — what evaluates differently after updating from mochafloats 6.0. diff --git a/mochafloats/migration/migrating_from_mocha.md b/mochafloats/migration/migrating_from_mocha.md index d1dbf3b..599718f 100644 --- a/mochafloats/migration/migrating_from_mocha.md +++ b/mochafloats/migration/migrating_from_mocha.md @@ -1,65 +1,216 @@ --- sidebar_position: 1 -description: "Moving a project from Mocha 3.x to mochafloats: dependency, packages and every changed call." +sidebar_label: "Migrating to mochafloats 6" +description: "Moving a project from Mocha 3, or from mochafloats 5 and earlier, to mochafloats 6: dependencies, packages, the split MochaEngine, floats and what evaluates differently." --- -# Migrating from Mocha +# Migrating to mochafloats 6 + +This page moves a project to mochafloats 6 from: + +- **Mocha 3** — `team.unnamed:mocha`, the original library. +- **mochafloats 5 or earlier** — `com.zigythebird:mochafloats`, versions 1.0 to 5.0.1. They kept Mocha's `team.unnamed.mocha` packages and its all-in-one `MochaEngine`, and already computed with floats. -mochafloats keeps Mocha's concepts — scopes, bindings, `@Binding`, compiled interfaces — so most code only needs new imports and a few renamed calls. +mochafloats 6 keeps the same concepts — scopes, bindings, `@Binding`, compiled interfaces — so most code needs a new dependency, new imports and a few renamed calls. +Each step says whom it applies to. + +## Before and after + +The same code on Mocha 3 and on mochafloats 6. Both print `2.0 30.0 5.0 0.75` for `seconds`, `angle`, `scaled` and `strength`. + +```java title="Mocha 3" +import team.unnamed.mocha.MochaEngine; +import team.unnamed.mocha.parser.ast.Expression; +import team.unnamed.mocha.runtime.MochaFunction; +import team.unnamed.mocha.runtime.binding.Binding; +import team.unnamed.mocha.runtime.compiled.MochaCompiledFunction; +import team.unnamed.mocha.runtime.compiled.Named; +import team.unnamed.mocha.runtime.value.MutableObjectBinding; +import team.unnamed.mocha.runtime.value.NumberValue; + +@Binding("game") +public final class GameBindings { + @Binding("ticks_to_seconds") + public static double ticksToSeconds(double ticks) { + return ticks / 20; + } +} + +public interface Falloff extends MochaCompiledFunction { + double apply(@Named("distance") double distance, @Named("radius") double radius); +} + +MochaEngine engine = MochaEngine.createStandard(); +engine.handleParseExceptions(e -> LOGGER.warn("Invalid MoLang", e)); +engine.bind(GameBindings.class); + +MutableObjectBinding query = new MutableObjectBinding(); +query.set("anim_time", NumberValue.of(1.25)); +query.setFunction("scaled", value -> value * 4); +engine.scope().set("query", query); +engine.scope().set("q", query); + +double seconds = engine.eval("game.ticks_to_seconds(40)"); + +List wave = engine.parse("math.sin(q.anim_time * 72) * 30"); +double angle = engine.eval(wave); + +MochaFunction prepared = engine.prepareEval("q.scaled(q.anim_time)"); +double scaled = prepared.evaluate(); + +Falloff falloff = engine.compile("math.clamp(1 - distance / radius, 0, 1)", Falloff.class); +double strength = falloff.apply(2, 8); +``` + +```java title="mochafloats 6" +import org.redlance.mocha.parser.MolangParser; +import org.redlance.mocha.parser.ast.Expression; +import org.redlance.mocha.runtime.MochaEngine; +import org.redlance.mocha.runtime.binding.Binding; +import org.redlance.mocha.runtime.compiled.MochaCompiledFunction; +import org.redlance.mocha.runtime.compiled.Named; +import org.redlance.mocha.runtime.value.MutableObjectBinding; +import org.redlance.mocha.runtime.value.NumberValue; + +@Binding("game") +public final class GameBindings { + @Binding("ticks_to_seconds") + public static float ticksToSeconds(float ticks) { + return ticks / 20; + } +} + +public interface Falloff extends MochaCompiledFunction { + float apply(@Named("distance") float distance, @Named("radius") float radius); +} + +MochaEngine engine = MochaEngine.createStandard(); +engine.interpreter().handleParseExceptions(e -> LOGGER.warn("Invalid MoLang", e)); +engine.compiler().handleParseExceptions(e -> LOGGER.warn("Invalid MoLang", e)); +engine.interpreter().bind(GameBindings.class); + +MutableObjectBinding query = new MutableObjectBinding(); +query.set("anim_time", NumberValue.of(1.25F)); +query.setFunction("scaled", value -> value * 4); +engine.scope().set("query", query); +engine.scope().set("q", query); + +float seconds = engine.interpreter().eval("game.ticks_to_seconds(40)"); + +List wave = MolangParser.parseAll("math.sin(q.anim_time * 72) * 30"); +float angle = engine.interpreter().eval(wave); + +Supplier prepared = engine.interpreter().prepareEval("q.scaled(q.anim_time)"); +float scaled = prepared.get(); + +Falloff falloff = engine.compiler().compile("math.clamp(1 - distance / radius, 0, 1)", Falloff.class); +float strength = falloff.apply(2, 8); +``` + +On mochafloats 5 the first version works once `double` becomes `float`. ## 1. Swap the dependency -Remove `team.unnamed:mocha` (and Javassist, if you declared it yourself) and add mochafloats as described in [Installation](../gettingstarted/installation.md). -Make sure the project builds and runs on **Java 24 or newer**. +*Everyone.* + +| You depend on | Replace it with | +|------------------------------------------------------------------------|----------------------------------------------| +| `team.unnamed:mocha`, and `org.javassist:javassist` if you declared it | `org.redlance.mochafloats:runtime-compiler` | +| `com.zigythebird:mochafloats` | `org.redlance.mochafloats:runtime-compiler` | + +The old artifacts were a single jar with both the interpreter and the compiler; `runtime-compiler` is the module that has both. +If you never compile expressions, depend on `runtime` instead. The repository and the build snippets are on [Installation](../gettingstarted/installation.md). -To keep both the interpreter and the compiler, as Mocha had them, depend on `runtime-compiler`. +- mochafloats 6 needs **Java 24 or newer**, like mochafloats 5. Mocha 3 and mochafloats 1.0 ran on Java 8, and mochafloats 1.1 to 4.x on Java 16 or 17. +- A mod that bundles the library in its jar now bundles `lexer`, `parser`, `runtime` and, if it compiles, `runtime-compiler` — see [Bundling it inside a Minecraft mod](../gettingstarted/installation.md#bundling-it-inside-a-minecraft-mod). +- If your mod gets mochafloats through PlayerAnimationLibrary, don't add it yourself — see the tip in [Which module do I need?](../gettingstarted/installation.md#which-module-do-i-need) ## 2. Update the imports -Everything moved from `team.unnamed.mocha` to `org.redlance.mocha`, and a few classes changed packages: +*Everyone.* + +Replace `team.unnamed.mocha` with `org.redlance.mocha`. Every class keeps its name and subpackage, except these: + +| Before | mochafloats 6 | +|-----------------------------------------------------------------|---------------------------------------------------------------------| +| `team.unnamed.mocha.MochaEngine` | `org.redlance.mocha.runtime.MochaEngine` | +| `team.unnamed.mocha.util.CaseInsensitiveStringHashMap` | `org.redlance.mocha.runtime.util.CaseInsensitiveStringHashMap` | +| `team.unnamed.mocha.util.ExprBytesUtils` | `org.redlance.mocha.parser.util.ExprBytesUtils` | +| `team.unnamed.mocha.util.ExpressionListUtils` | `org.redlance.mocha.parser.util.ExpressionListUtils` | +| `team.unnamed.mocha.util.network.ProtocolUtils`, `VarIntUtils` | `org.redlance.mocha.parser.util.network.ProtocolUtils`, `VarIntUtils` | +| `team.unnamed.mocha.parser.ast.DoubleExpression` (Mocha 3) | `org.redlance.mocha.parser.ast.FloatExpression` | -| Mocha 3 | mochafloats | -|--------------------------------------------------|----------------------------------------------------------| -| `team.unnamed.mocha.MochaEngine` | `org.redlance.mocha.runtime.MochaEngine` | -| `team.unnamed.mocha.util.CaseInsensitiveStringHashMap` | `org.redlance.mocha.runtime.util.CaseInsensitiveStringHashMap` | -| `team.unnamed.mocha.parser.ast.DoubleExpression` | `org.redlance.mocha.parser.ast.FloatExpression` | -| any other `team.unnamed.mocha.X` | `org.redlance.mocha.X` | +This command renames them all in your Java and Kotlin sources, the moved classes included: + +```bash +grep -rl --include='*.java' --include='*.kt' 'team\.unnamed\.mocha' . | xargs perl -pi -e ' + s/team\.unnamed\.mocha\.MochaEngine\b/org.redlance.mocha.runtime.MochaEngine/g; + s/team\.unnamed\.mocha\.util\.CaseInsensitiveStringHashMap\b/org.redlance.mocha.runtime.util.CaseInsensitiveStringHashMap/g; + s/team\.unnamed\.mocha\.util\.(ExprBytesUtils|ExpressionListUtils|network)\b/org.redlance.mocha.parser.util.$1/g; + s/team\.unnamed\.mocha\.parser\.ast\.DoubleExpression\b/org.redlance.mocha.parser.ast.FloatExpression/g; + s/team\.unnamed\.mocha\./org.redlance.mocha./g' +``` ## 3. Update the calls -`MochaEngine` used to do everything. In mochafloats it only holds an interpreter and a compiler that share one scope: - -| Mocha 3 | mochafloats | -|---------------------------------------------------|---------------------------------------------------------------------| -| `MochaEngine.createStandard()` | `MochaEngine.createStandard()`, or `MolangInterpreter.standard()` if you don't compile | -| `MochaEngine.create(entity, builder -> ...)` | `MolangInterpreter.create(entity, builder -> ...)`, wrapped with `MochaEngine.from(...)` if you compile | -| `engine.eval(code)` → `double` | `engine.interpreter().eval(code)` → `float` | -| `engine.prepareEval(code)` → `MochaFunction` | `engine.interpreter().prepareEval(code)` → `Supplier` | -| `engine.parse(code)` | `MolangParser.parseAll(code)` | -| `engine.compile(code)`, `compile(code, type)` | `engine.compiler().compile(code)`, `compile(code, type)` | -| `engine.bind(type)`, `bindInstance(...)` | `engine.interpreter().bind(type)`, `bindInstance(...)` | -| `engine.warnOnReflectiveFunctionUsage(true)` | `engine.interpreter().warnOnReflectiveFunctionUsage(true)` | -| `engine.handleParseExceptions(handler)` | set it on both `engine.interpreter()` and `engine.compiler()` | -| `engine.postCompile(consumer)` | `engine.compiler().postCompile(consumer)` | -| `engine.classPool()` | removed, there's no Javassist | -| `new MolangCompiler(entity, classLoader, scope)` | `new MolangCompiler(entity, scope)` | -| `MochaFunction.evaluate()` → `double` | `MochaFunction.evaluate()` → `float` | - -`engine.scope()` still works and returns the shared scope. +*Everyone.* + +`MochaEngine` used to parse, evaluate, compile and bind by itself. In mochafloats 6 it's a class that holds an interpreter and a compiler sharing one scope: + +| Before | mochafloats 6 | +|------------------------------------------------------|------------------------------------------------------------------------| +| `MochaEngine.createStandard()`, `create()`, and their forms taking an entity | unchanged, or `MolangInterpreter.standard()` and `create()` if you don't compile | +| `MochaEngine.create(entity, builder -> ...)` | `MochaEngine.from(MolangInterpreter.create(entity, builder -> ...))` | +| `engine.eval(...)`, every overload | `engine.interpreter().eval(...)` | +| `engine.prepareEval(...)` → `MochaFunction` | `engine.interpreter().prepareEval(...)` → `Supplier` | +| `engine.parse(code)` | `MolangParser.parseAll(code)` | +| `engine.compile(...)`, every overload | `engine.compiler().compile(...)` | +| `engine.bind(type)`, `engine.bindInstance(...)` | `engine.interpreter().bind(type)`, `bindInstance(...)` | +| `engine.warnOnReflectiveFunctionUsage(true)` | `engine.interpreter().warnOnReflectiveFunctionUsage(true)` | +| `engine.handleParseExceptions(handler)` | set it on both `engine.interpreter()` and `engine.compiler()` | +| `engine.postCompile(consumer)` | `engine.compiler().postCompile(consumer)` | +| `engine.scope()` | unchanged: the scope both share | + +A `Supplier` boxes every result. In hot code, keep the parsed `List` and call `eval(list)`, which returns a primitive `float`. + +*Mocha 3 and mochafloats 4 or earlier*, which compiled with Javassist: + +| Before | mochafloats 6 | +|--------------------------------------------------|--------------------------------------| +| `new MolangCompiler(entity, classLoader, scope)` | `new MolangCompiler(entity, scope)` | +| `engine.classPool()`, `compiler.classPool()` | removed, there's no Javassist | ## 4. From `double` to `float` -- `Value.getAsNumber()` and `NumberValue.value()` return `float`. `NumberValue.of` accepts both `float` and `double`. -- `ObjectValue.setFunction` takes `FloatFunction1`, `FloatFunction2` and `FloatFunction3` instead of `DoubleFunction1`–`3`, plus a new `FloatFunction` with no arguments. Lambdas usually compile unchanged once their arithmetic is `float`. +*Mocha 3 only.* Every mochafloats version already computes with floats. + +- `Value.getAsNumber()`, `NumberValue.value()` and `MochaFunction.evaluate()` return `float`. `Value.of` and `NumberValue.of` accept both `float` and `double`. +- `ObjectValue.setFunction` takes `FloatFunction1`, `FloatFunction2` and `FloatFunction3` instead of `DoubleFunction1`–`3`, plus `FloatFunction` with no arguments. Lambdas usually compile unchanged once their arithmetic is `float`. +- The static methods of `MochaMath` and its `PI` constant are `float`. - `@Binding` methods may still use `double`; arguments and results are converted. Using `float` avoids the conversions. - A custom `ExpressionVisitor` overrides `visitFloat` instead of `visitDouble`. Create literals with `FloatExpression.of(value)`; the constructor taking a number is private. - Compiled interfaces can keep `double` parameters and results, but `float` matches what the compiler computes with. -## 5. Things that behave differently +## 5. Other API changes + +*Mocha 3 and early mochafloats versions.* mochafloats 5 already worked this way. + +- `Expression` and `Value` are `sealed`: you can no longer implement `Expression` yourself. +- Every expression has a `write(ByteBuf)` method for the [network format](../features/serialization.md). +- `toString()` on an expression prints MoLang instead of debug trees like `Call(Access(...))`. + +## 6. What evaluates differently -- `Expression` and `Value` are `sealed`: you can no longer implement `Expression` yourself, and every expression has a `write(ByteBuf)` method for the [network format](../features/serialization.md). -- `toString()` on expressions prints MoLang instead of debug trees like `Call(Access(...))`. -- Expressions that Mocha got wrong now give the right result, for example `-math.sin(90) * 30` is `-30` instead of `0` and `1e3` is `1000` instead of `0`. If your content was tuned around the old results, check it again — see [Why mochafloats?](../why_mochafloats.md) for the full list. +Content tuned around the old results may need another look. + +**From Mocha 3:** + +- Expressions Mocha got wrong give the right result: `-math.sin(90) * 30` is `-30` instead of `0`, and `1e3` is `1000` instead of `0`. [Why mochafloats?](../why_mochafloats.md) has the full list. - MoLang evaluates like Bedrock where Mocha didn't: `v.x = c ? 5 : 6` assigns `5` or `6` instead of `c`, strings compare by their text, `??` keeps a `0`, `math.round(-2.5)` is `-3`, and the random functions include both bounds. See [MoLang that evaluates like Bedrock](../why_mochafloats.md#molang-that-evaluates-like-bedrock). - Syntax Mocha accepted, such as `1 +` or a trailing comma in `math.abs(1,)`, is a parse error. -- Results are floats, so very large or very precise values round differently: `16777217` becomes `16777216`, `0.1 + 0.2` becomes `0.3`. +- Results are floats, so very large or very precise values round differently: `16777217` becomes `16777216`, and `0.1 + 0.2` is `0.3` instead of `0.30000000000000004`. + +**From mochafloats 5 or earlier:** + +- mochafloats 5.0.1 evaluates like mochafloats 6.0, so everything in [Upgrading to 6.1](./upgrading_to_6_1.md) applies: the same table of Bedrock results, syntax errors, compiled functions and Java bindings. +- Before 5.0.1, a minus or `!` in front of a function call evaluated to `0`: `-math.sin(90) * 30` was `0` instead of `-30`, and `!math.abs(0)` was `0` instead of `1`. diff --git a/mochafloats/migration/upgrading_to_6_1.md b/mochafloats/migration/upgrading_to_6_1.md index b73248a..da72580 100644 --- a/mochafloats/migration/upgrading_to_6_1.md +++ b/mochafloats/migration/upgrading_to_6_1.md @@ -6,6 +6,7 @@ description: "What changes when you update mochafloats from 6.0 to 6.1: MoLang t # Upgrading to 6.1 6.1 doesn't remove or change any public API, so code written for 6.0 compiles against it unchanged. +Coming from mochafloats 5 or earlier? Start with [Migrating to mochafloats 6](./migrating_from_mocha.md). What changes is the result of some MoLang: 6.1 was checked against Bedrock in the game and now agrees with it, and bugs in the parser, the compiler and the Java bindings are fixed. ## MoLang that evaluates differently diff --git a/mochafloats/why_mochafloats.md b/mochafloats/why_mochafloats.md index cb0b827..6a94cca 100644 --- a/mochafloats/why_mochafloats.md +++ b/mochafloats/why_mochafloats.md @@ -251,6 +251,6 @@ PlayerAnimationLibrary's binary animation format, the one that goes over the net - **Java 24 or newer.** The ClassFile API doesn't exist before Java 24. Minecraft 26.1 and newer runs on Java 25. - **Float precision.** A float holds about 7 significant digits, and whole numbers are only exact up to 16,777,216: `16777217` evaluates to `16777216`. That's plenty for animation, but large counters lose precision — a world's game time in ticks (`q.time_stamp` in PAL) passes that limit after about 9.7 days of running, so prefer values that stay small, like `q.anim_time`. -- **A different API.** Packages moved to `org.redlance.mocha`, and `MochaEngine` was split into an interpreter and a compiler. [Migrating from Mocha](./migration/migrating_from_mocha.md) maps every old call to its new place. +- **A different API.** Packages moved to `org.redlance.mocha`, and `MochaEngine` was split into an interpreter and a compiler. [Migrating to mochafloats 6](./migration/migrating_from_mocha.md) maps every old call to its new place. Already on mochafloats 6.0? [Upgrading to 6.1](./migration/upgrading_to_6_1.md) lists what changed.