Skip to content

Commit 550e8fb

Browse files
committed
Add runtime Molang support for ModelEngine
Evaluate preserved YSM expressions per animation frame, expose MythicMobs variable mechanics, and verify the distributable with GitHub Actions.
0 parents  commit 550e8fb

21 files changed

Lines changed: 754 additions & 0 deletions

.github/workflows/build.yml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: Build
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
workflow_dispatch:
9+
10+
permissions:
11+
contents: read
12+
13+
jobs:
14+
build:
15+
runs-on: ubuntu-latest
16+
timeout-minutes: 15
17+
18+
steps:
19+
- name: Check out repository
20+
uses: actions/checkout@v4
21+
22+
- name: Set up Java 21
23+
uses: actions/setup-java@v4
24+
with:
25+
distribution: temurin
26+
java-version: '21'
27+
28+
- name: Set up Gradle
29+
uses: gradle/actions/setup-gradle@v4
30+
with:
31+
gradle-version: '8.14.3'
32+
33+
- name: Build and test
34+
run: gradle --no-daemon clean build
35+
36+
- name: Upload plugin artifact
37+
uses: actions/upload-artifact@v4
38+
with:
39+
name: YsmModelEngineMolang
40+
path: build/libs/YsmModelEngineMolang-*.jar
41+
if-no-files-found: error
42+
retention-days: 14

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.gradle/
2+
build/
3+
out/
4+
.idea/
5+
*.iml
6+
/lib/

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 OpenYSM
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

THIRD_PARTY_NOTICES

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Third-party components
2+
3+
Molang compiler
4+
- Artifact: gg.moonflower:molang-compiler:3.1.1.19
5+
- License: MIT
6+
- Project: Ocelot5836/molang-compiler
7+
8+
ModelEngine and Paper APIs are compile-time/runtime dependencies supplied by
9+
those projects and are not bundled into the plugin JAR.

build.gradle.kts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
plugins {
2+
java
3+
id("com.gradleup.shadow") version "8.3.6"
4+
}
5+
6+
group = property("group") as String
7+
version = property("version") as String
8+
9+
java {
10+
toolchain.languageVersion.set(JavaLanguageVersion.of(21))
11+
withSourcesJar()
12+
}
13+
14+
dependencies {
15+
compileOnly("io.papermc.paper:paper-api:1.21.8-R0.1-SNAPSHOT")
16+
compileOnly("com.ticxo.modelengine:ModelEngine:R4.1.0") {
17+
isTransitive = false
18+
}
19+
compileOnly("io.lumine:Mythic-Dist:5.13.0-SNAPSHOT") {
20+
isTransitive = false
21+
}
22+
23+
implementation("gg.moonflower:molang-compiler:3.1.1.19")
24+
25+
testImplementation("org.junit.jupiter:junit-jupiter:5.12.2")
26+
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
27+
testCompileOnly("io.papermc.paper:paper-api:1.21.8-R0.1-SNAPSHOT")
28+
testCompileOnly("com.ticxo.modelengine:ModelEngine:R4.1.0") {
29+
isTransitive = false
30+
}
31+
testCompileOnly("io.lumine:Mythic-Dist:5.13.0-SNAPSHOT") {
32+
isTransitive = false
33+
}
34+
}
35+
36+
tasks.withType<JavaCompile>().configureEach {
37+
options.encoding = "UTF-8"
38+
options.release.set(21)
39+
}
40+
41+
tasks.test {
42+
useJUnitPlatform()
43+
}
44+
45+
tasks.processResources {
46+
filesMatching("plugin.yml") {
47+
expand("version" to project.version)
48+
}
49+
}
50+
51+
tasks.shadowJar {
52+
archiveClassifier.set("")
53+
archiveFileName.set("YsmModelEngineMolang-${project.version}.jar")
54+
}
55+
56+
tasks.build {
57+
dependsOn(tasks.shadowJar)
58+
}

gradle.properties

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
group=com.ysm
2+
version=0.1.0-SNAPSHOT
3+
org.gradle.jvmargs=-Xmx1G -Dfile.encoding=UTF-8

settings.gradle.kts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
pluginManagement {
2+
repositories {
3+
gradlePluginPortal()
4+
mavenCentral()
5+
}
6+
}
7+
8+
dependencyResolutionManagement {
9+
repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)
10+
repositories {
11+
mavenCentral()
12+
maven("https://repo.papermc.io/repository/maven-public/")
13+
maven("https://mvn.lumine.io/repository/maven-public/") {
14+
metadataSources {
15+
mavenPom()
16+
artifact()
17+
}
18+
}
19+
maven("https://maven.blamejared.com/")
20+
}
21+
}
22+
23+
rootProject.name = "ysm-modelengine-molang"
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.ysm.modelengine.molang;
2+
3+
import io.lumine.mythic.api.adapters.AbstractEntity;
4+
import io.lumine.mythic.api.config.MythicLineConfig;
5+
import io.lumine.mythic.api.skills.INoTargetSkill;
6+
import io.lumine.mythic.api.skills.ITargetedEntitySkill;
7+
import io.lumine.mythic.api.skills.SkillMetadata;
8+
import io.lumine.mythic.api.skills.SkillResult;
9+
10+
final class ClearMolangVariableMechanic implements INoTargetSkill, ITargetedEntitySkill {
11+
private final String key;
12+
private final EntityVariableStore variables;
13+
14+
ClearMolangVariableMechanic(MythicLineConfig config, EntityVariableStore variables) {
15+
this.key = config.getString(new String[]{"key", "name", "variable"}, "");
16+
this.variables = variables;
17+
}
18+
19+
@Override
20+
public SkillResult cast(SkillMetadata metadata) {
21+
return clear(metadata.getCaster().getEntity());
22+
}
23+
24+
@Override
25+
public SkillResult castAtEntity(SkillMetadata metadata, AbstractEntity target) {
26+
return clear(target);
27+
}
28+
29+
private SkillResult clear(AbstractEntity target) {
30+
if (target == null || target.getUniqueId() == null) return SkillResult.INVALID_TARGET;
31+
variables.clear(target.getUniqueId(), key);
32+
return SkillResult.SUCCESS;
33+
}
34+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package com.ysm.modelengine.molang;
2+
3+
import java.util.Map;
4+
import java.util.UUID;
5+
import java.util.concurrent.ConcurrentHashMap;
6+
7+
public final class EntityVariableStore {
8+
private final Map<UUID, Map<String, Float>> values = new ConcurrentHashMap<>();
9+
private final float defaultValue;
10+
11+
public EntityVariableStore(float defaultValue) {
12+
this.defaultValue = defaultValue;
13+
}
14+
15+
public float get(UUID entityId, String name) {
16+
if (entityId == null || name == null || name.isBlank()) return defaultValue;
17+
Map<String, Float> entityValues = values.get(entityId);
18+
if (entityValues == null) return defaultValue;
19+
return entityValues.getOrDefault(normalize(name), defaultValue);
20+
}
21+
22+
public void set(UUID entityId, String name, float value) {
23+
if (entityId == null || name == null || name.isBlank()) return;
24+
values.computeIfAbsent(entityId, ignored -> new ConcurrentHashMap<>())
25+
.put(normalize(name), value);
26+
}
27+
28+
public void clear(UUID entityId, String name) {
29+
if (entityId == null) return;
30+
if (name == null || name.isBlank()) {
31+
values.remove(entityId);
32+
return;
33+
}
34+
Map<String, Float> entityValues = values.get(entityId);
35+
if (entityValues == null) return;
36+
entityValues.remove(normalize(name));
37+
if (entityValues.isEmpty()) values.remove(entityId, entityValues);
38+
}
39+
40+
public Map<String, Float> snapshot(UUID entityId) {
41+
if (entityId == null) return Map.of();
42+
Map<String, Float> entityValues = values.get(entityId);
43+
return entityValues == null ? Map.of() : Map.copyOf(entityValues);
44+
}
45+
46+
public void clearAll() {
47+
values.clear();
48+
}
49+
50+
private static String normalize(String name) {
51+
return normalizeName(name);
52+
}
53+
54+
static String normalizeName(String name) {
55+
if (name == null) return "";
56+
String normalized = name.trim().toLowerCase(java.util.Locale.ROOT);
57+
if (normalized.startsWith("v.")) return normalized.substring(2);
58+
if (normalized.startsWith("variable.")) return normalized.substring("variable.".length());
59+
if (normalized.startsWith("ctrl.")) return "ctrl_" + normalized.substring("ctrl.".length());
60+
return normalized;
61+
}
62+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package com.ysm.modelengine.molang;
2+
3+
import gg.moonflower.molangcompiler.api.GlobalMolangCompiler;
4+
import gg.moonflower.molangcompiler.api.MolangCompiler;
5+
import gg.moonflower.molangcompiler.api.MolangExpression;
6+
import gg.moonflower.molangcompiler.api.exception.MolangException;
7+
8+
import java.util.Map;
9+
import java.util.concurrent.ConcurrentHashMap;
10+
11+
final class MolangCompilerFacade {
12+
private final MolangCompiler compiler = GlobalMolangCompiler.get();
13+
private final Map<String, MolangExpression> cache = new ConcurrentHashMap<>();
14+
private final int maxEntries;
15+
private final RateLimitedDiagnostics diagnostics;
16+
17+
MolangCompilerFacade(int maxEntries, RateLimitedDiagnostics diagnostics) {
18+
this.maxEntries = Math.max(64, maxEntries);
19+
this.diagnostics = diagnostics;
20+
}
21+
22+
MolangExpression compile(String source) {
23+
String normalized = normalize(source);
24+
if (normalized.isBlank()) {
25+
throw new IllegalArgumentException("Molang expression cannot be blank");
26+
}
27+
28+
MolangExpression cached = cache.get(normalized);
29+
if (cached != null) return cached;
30+
31+
MolangExpression expression = compileUncached(normalized);
32+
if (cache.size() >= maxEntries) {
33+
diagnostics.warn("cache-full", "Molang expression cache is full; using uncached expression: " + normalized);
34+
return expression;
35+
}
36+
37+
MolangExpression existing = cache.putIfAbsent(normalized, expression);
38+
return existing == null ? expression : existing;
39+
}
40+
41+
void clear() {
42+
cache.clear();
43+
}
44+
45+
int size() {
46+
return cache.size();
47+
}
48+
49+
private MolangExpression compileUncached(String source) {
50+
try {
51+
return compiler.compile(source);
52+
} catch (MolangException | RuntimeException exception) {
53+
diagnostics.warn("compile:" + source,
54+
"Unable to compile Molang expression '" + source + "': " + exception.getMessage());
55+
throw new IllegalArgumentException("Invalid Molang expression: " + source, exception);
56+
}
57+
}
58+
59+
static String normalize(String source) {
60+
if (source == null) return "";
61+
String normalized = source.trim();
62+
if (normalized.startsWith("molang:")) normalized = normalized.substring("molang:".length()).trim();
63+
normalized = normalized.replace("ysm.head_yaw", "query.head_y_rotation");
64+
normalized = normalized.replace("ysm.head_pitch", "query.head_x_rotation");
65+
normalized = normalized.replace("ysm.body_yaw", "query.body_y_rotation");
66+
normalized = normalized.replace("variable.", "v.");
67+
normalized = normalized.replace("ctrl.", "v.ctrl_");
68+
return normalized;
69+
}
70+
}

0 commit comments

Comments
 (0)