diff --git a/common/src/api/java/dev/engine_room/flywheel/api/backend/Backend.java b/common/src/api/java/dev/engine_room/flywheel/api/backend/Backend.java
index 6cc6228f9..c6f5251a6 100644
--- a/common/src/api/java/dev/engine_room/flywheel/api/backend/Backend.java
+++ b/common/src/api/java/dev/engine_room/flywheel/api/backend/Backend.java
@@ -13,6 +13,13 @@ public interface Backend {
*/
Engine createEngine(LevelAccessor level);
+ /**
+ * The version of flywheel this backend was written against.
+ *
+ * @return A backend version.
+ */
+ BackendVersion version();
+
/**
* The priority of this backend.
*
The backend with the highest priority upon first launch will be chosen as the default backend.
diff --git a/common/src/api/java/dev/engine_room/flywheel/api/backend/BackendVersion.java b/common/src/api/java/dev/engine_room/flywheel/api/backend/BackendVersion.java
new file mode 100644
index 000000000..87f798174
--- /dev/null
+++ b/common/src/api/java/dev/engine_room/flywheel/api/backend/BackendVersion.java
@@ -0,0 +1,11 @@
+package dev.engine_room.flywheel.api.backend;
+
+public record BackendVersion(int major, int minor) implements Comparable {
+ @Override
+ public int compareTo(BackendVersion o) {
+ if (major != o.major) {
+ return Integer.compare(major, o.major);
+ }
+ return Integer.compare(minor, o.minor);
+ }
+}
diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/Backends.java b/common/src/backend/java/dev/engine_room/flywheel/backend/Backends.java
index c6de7eb47..da5a71372 100644
--- a/common/src/backend/java/dev/engine_room/flywheel/backend/Backends.java
+++ b/common/src/backend/java/dev/engine_room/flywheel/backend/Backends.java
@@ -2,6 +2,7 @@ package dev.engine_room.flywheel.backend;
import dev.engine_room.flywheel.api.Flywheel;
import dev.engine_room.flywheel.api.backend.Backend;
+import dev.engine_room.flywheel.api.backend.BackendVersion;
import dev.engine_room.flywheel.backend.compile.IndirectPrograms;
import dev.engine_room.flywheel.backend.compile.InstancingPrograms;
import dev.engine_room.flywheel.backend.engine.EngineImpl;
@@ -12,11 +13,14 @@ import dev.engine_room.flywheel.lib.backend.SimpleBackend;
import dev.engine_room.flywheel.lib.util.ShadersModHelper;
public final class Backends {
+ private static final BackendVersion VERSION = new BackendVersion(1, 0);
+
/**
* Use GPU instancing to render everything.
*/
public static final Backend INSTANCING = SimpleBackend.builder()
.engineFactory(level -> new EngineImpl(level, new InstancedDrawManager(InstancingPrograms.get()), 256))
+ .version(VERSION)
.priority(500)
.supported(() -> GlCompat.SUPPORTS_INSTANCING && InstancingPrograms.allLoaded() && !ShadersModHelper.isShaderPackInUse())
.register(Flywheel.rl("instancing"));
@@ -26,6 +30,7 @@ public final class Backends {
*/
public static final Backend INDIRECT = SimpleBackend.builder()
.engineFactory(level -> new EngineImpl(level, new IndirectDrawManager(IndirectPrograms.get()), 256))
+ .version(VERSION)
.priority(1000)
.supported(() -> GlCompat.SUPPORTS_INDIRECT && IndirectPrograms.allLoaded() && !ShadersModHelper.isShaderPackInUse())
.register(Flywheel.rl("indirect"));
diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/backend/SimpleBackend.java b/common/src/lib/java/dev/engine_room/flywheel/lib/backend/SimpleBackend.java
index bc09227c1..0ac537c3b 100644
--- a/common/src/lib/java/dev/engine_room/flywheel/lib/backend/SimpleBackend.java
+++ b/common/src/lib/java/dev/engine_room/flywheel/lib/backend/SimpleBackend.java
@@ -4,7 +4,10 @@ import java.util.Objects;
import java.util.function.BooleanSupplier;
import java.util.function.Function;
+import org.jetbrains.annotations.Nullable;
+
import dev.engine_room.flywheel.api.backend.Backend;
+import dev.engine_room.flywheel.api.backend.BackendVersion;
import dev.engine_room.flywheel.api.backend.Engine;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.LevelAccessor;
@@ -13,11 +16,13 @@ public final class SimpleBackend implements Backend {
private final Function engineFactory;
private final int priority;
private final BooleanSupplier isSupported;
+ private final BackendVersion version;
- public SimpleBackend(int priority, Function engineFactory, BooleanSupplier isSupported) {
+ public SimpleBackend(int priority, Function engineFactory, BooleanSupplier isSupported, BackendVersion version) {
this.priority = priority;
this.engineFactory = engineFactory;
this.isSupported = isSupported;
+ this.version = version;
}
public static Builder builder() {
@@ -29,6 +34,11 @@ public final class SimpleBackend implements Backend {
return engineFactory.apply(level);
}
+ @Override
+ public BackendVersion version() {
+ return version;
+ }
+
@Override
public int priority() {
return priority;
@@ -40,9 +50,13 @@ public final class SimpleBackend implements Backend {
}
public static final class Builder {
- private Function engineFactory;
private int priority = 0;
+ @Nullable
+ private Function engineFactory;
+ @Nullable
private BooleanSupplier isSupported;
+ @Nullable
+ private BackendVersion version;
public Builder engineFactory(Function engineFactory) {
this.engineFactory = engineFactory;
@@ -59,11 +73,17 @@ public final class SimpleBackend implements Backend {
return this;
}
+ public Builder version(BackendVersion version) {
+ this.version = version;
+ return this;
+ }
+
public Backend register(ResourceLocation id) {
Objects.requireNonNull(engineFactory);
Objects.requireNonNull(isSupported);
+ Objects.requireNonNull(version);
- return Backend.REGISTRY.registerAndGet(id, new SimpleBackend(priority, engineFactory, isSupported));
+ return Backend.REGISTRY.registerAndGet(id, new SimpleBackend(priority, engineFactory, isSupported, version));
}
}
}
diff --git a/common/src/main/java/dev/engine_room/flywheel/impl/BackendManagerImpl.java b/common/src/main/java/dev/engine_room/flywheel/impl/BackendManagerImpl.java
index 75133adeb..6697857cf 100644
--- a/common/src/main/java/dev/engine_room/flywheel/impl/BackendManagerImpl.java
+++ b/common/src/main/java/dev/engine_room/flywheel/impl/BackendManagerImpl.java
@@ -1,9 +1,11 @@
package dev.engine_room.flywheel.impl;
import java.util.ArrayList;
+import java.util.List;
import dev.engine_room.flywheel.api.Flywheel;
import dev.engine_room.flywheel.api.backend.Backend;
+import dev.engine_room.flywheel.api.backend.BackendVersion;
import dev.engine_room.flywheel.impl.visualization.VisualizationManagerImpl;
import dev.engine_room.flywheel.lib.backend.SimpleBackend;
import net.minecraft.client.multiplayer.ClientLevel;
@@ -15,6 +17,7 @@ public final class BackendManagerImpl {
throw new UnsupportedOperationException("Cannot create engine when backend is off.");
})
.supported(() -> true)
+ .version(new BackendVersion(0, 0))
.register(Flywheel.rl("off"));
public static final Backend DEFAULT_BACKEND = findDefaultBackend();
@@ -33,7 +36,7 @@ public final class BackendManagerImpl {
}
// Don't store this statically because backends can theoretically change their priorities at runtime.
- private static ArrayList backendsByPriority() {
+ private static List backendsByPriority() {
var backends = new ArrayList<>(Backend.REGISTRY.getAll());
// Sort with keys backwards so that the highest priority is first.
@@ -53,27 +56,46 @@ public final class BackendManagerImpl {
}
private static void chooseBackend() {
+ var requirements = FlwImplXplat.INSTANCE.modRequirements();
+
var preferred = FlwConfig.INSTANCE.backend();
if (preferred.isSupported()) {
- backend = preferred;
- return;
+ if (meetsModRequirements(requirements, preferred)) {
+ backend = preferred;
+ return;
+ } else {
+ var minVersion = requirements.minimumBackendVersion();
+ FlwImpl.LOGGER.warn("Preferred backend '{}' does not meet minimum version requirement {}.{}", Backend.REGISTRY.getIdOrThrow(preferred), minVersion.major(), minVersion.minor());
+ }
}
+ backend = fallback(preferred, requirements);
+
+ FlwImpl.LOGGER.warn("Flywheel backend fell back from '{}' to '{}'", Backend.REGISTRY.getIdOrThrow(preferred), Backend.REGISTRY.getIdOrThrow(backend));
+ }
+
+ private static Backend fallback(Backend preferred, ModRequirements requirements) {
var backendsByPriority = backendsByPriority();
var startIndex = backendsByPriority.indexOf(preferred) + 1;
// For safety in case we don't find anything
- backend = OFF_BACKEND;
+ var out = OFF_BACKEND;
for (int i = startIndex; i < backendsByPriority.size(); i++) {
var candidate = backendsByPriority.get(i);
if (candidate.isSupported()) {
- backend = candidate;
- break;
+ if (meetsModRequirements(requirements, candidate)) {
+ out = candidate;
+ break;
+ }
}
}
+ return out;
+ }
- FlwImpl.LOGGER.warn("Flywheel backend fell back from '{}' to '{}'", Backend.REGISTRY.getIdOrThrow(preferred), Backend.REGISTRY.getIdOrThrow(backend));
+ private static boolean meetsModRequirements(ModRequirements requirements, Backend candidate) {
+ return candidate.version()
+ .compareTo(requirements.minimumBackendVersion()) >= 0;
}
public static String getBackendString() {
diff --git a/common/src/main/java/dev/engine_room/flywheel/impl/FlwImplXplat.java b/common/src/main/java/dev/engine_room/flywheel/impl/FlwImplXplat.java
index c8fb9fec1..380c91b06 100644
--- a/common/src/main/java/dev/engine_room/flywheel/impl/FlwImplXplat.java
+++ b/common/src/main/java/dev/engine_room/flywheel/impl/FlwImplXplat.java
@@ -17,4 +17,6 @@ public interface FlwImplXplat {
boolean useSodium0_6Compat();
boolean useIrisCompat();
+
+ ModRequirements modRequirements();
}
diff --git a/common/src/main/java/dev/engine_room/flywheel/impl/ModRequirements.java b/common/src/main/java/dev/engine_room/flywheel/impl/ModRequirements.java
new file mode 100644
index 000000000..199788316
--- /dev/null
+++ b/common/src/main/java/dev/engine_room/flywheel/impl/ModRequirements.java
@@ -0,0 +1,10 @@
+package dev.engine_room.flywheel.impl;
+
+import java.util.List;
+
+import dev.engine_room.flywheel.api.backend.BackendVersion;
+
+public record ModRequirements(BackendVersion minimumBackendVersion, List entries) {
+ public record Entry(String modId, BackendVersion version) {
+ }
+}
diff --git a/fabric/src/main/java/dev/engine_room/flywheel/impl/FlwImplXplatImpl.java b/fabric/src/main/java/dev/engine_room/flywheel/impl/FlwImplXplatImpl.java
index d1c346906..775440cf9 100644
--- a/fabric/src/main/java/dev/engine_room/flywheel/impl/FlwImplXplatImpl.java
+++ b/fabric/src/main/java/dev/engine_room/flywheel/impl/FlwImplXplatImpl.java
@@ -1,9 +1,17 @@
package dev.engine_room.flywheel.impl;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.jetbrains.annotations.Nullable;
+
+import dev.engine_room.flywheel.api.backend.BackendVersion;
import dev.engine_room.flywheel.api.event.ReloadLevelRendererCallback;
import dev.engine_room.flywheel.impl.compat.CompatMod;
import dev.engine_room.flywheel.impl.compat.FabricSodiumCompat;
import net.fabricmc.loader.api.FabricLoader;
+import net.fabricmc.loader.api.ModContainer;
+import net.fabricmc.loader.api.metadata.CustomValue;
import net.minecraft.client.multiplayer.ClientLevel;
public class FlwImplXplatImpl implements FlwImplXplat {
@@ -36,4 +44,54 @@ public class FlwImplXplatImpl implements FlwImplXplat {
public boolean useIrisCompat() {
return CompatMod.IRIS.isLoaded;
}
+
+ @Override
+ public ModRequirements modRequirements() {
+ List entries = new ArrayList<>();
+
+ for (ModContainer mod : FabricLoader.getInstance()
+ .getAllMods()) {
+ var metadata = mod.getMetadata();
+
+ var custom = metadata.getCustomValue("flywheel");
+
+ if (custom != null && custom.getType() == CustomValue.CvType.OBJECT) {
+ var object = custom.getAsObject();
+
+ var major = getCustomValueAsInt(object.get("backend_major_version"));
+ var minor = getCustomValueAsInt(object.get("backend_minor_version"));
+
+ // Major version is required
+ if (major != null) {
+ // Minor version defaults to 0
+ var version = new BackendVersion(major, minor == null ? 0 : minor);
+
+ entries.add(new ModRequirements.Entry(metadata.getId(), version));
+ } else {
+ FlwImpl.LOGGER.warn("Mod {} has invalid required backend version", metadata.getId());
+ }
+ }
+ }
+
+ if (!entries.isEmpty()) {
+ var minVersion = entries.stream()
+ .map(ModRequirements.Entry::version)
+ .min(BackendVersion::compareTo)
+ .get();
+
+ return new ModRequirements(minVersion, entries);
+ } else {
+ return new ModRequirements(new BackendVersion(0, 0), List.of());
+ }
+ }
+
+ @Nullable
+ private static Integer getCustomValueAsInt(@Nullable CustomValue major) {
+ if (major != null && major.getType() == CustomValue.CvType.NUMBER) {
+ return major.getAsNumber()
+ .intValue();
+ }
+
+ return null;
+ }
}
diff --git a/fabric/src/main/resources/fabric.mod.json b/fabric/src/main/resources/fabric.mod.json
index d90f772c9..24caeb709 100644
--- a/fabric/src/main/resources/fabric.mod.json
+++ b/fabric/src/main/resources/fabric.mod.json
@@ -35,5 +35,11 @@
"breaks": {
"sodium": ["<0.5.0", "~0.6.0- <0.6.0-beta.2"],
"embeddium": "*"
+ },
+ "custom" : {
+ "flywheel" : {
+ "backend_major_version" : 1,
+ "backend_minor_version" : 0
+ }
}
}
diff --git a/forge/src/main/java/dev/engine_room/flywheel/impl/FlwImplXplatImpl.java b/forge/src/main/java/dev/engine_room/flywheel/impl/FlwImplXplatImpl.java
index 78e99c9e0..bd28b6d7e 100644
--- a/forge/src/main/java/dev/engine_room/flywheel/impl/FlwImplXplatImpl.java
+++ b/forge/src/main/java/dev/engine_room/flywheel/impl/FlwImplXplatImpl.java
@@ -1,10 +1,18 @@
package dev.engine_room.flywheel.impl;
+import java.util.ArrayList;
+import java.util.List;
+
+import com.electronwill.nightconfig.core.Config;
+
+import dev.engine_room.flywheel.api.backend.BackendVersion;
import dev.engine_room.flywheel.api.event.ReloadLevelRendererEvent;
import dev.engine_room.flywheel.impl.compat.CompatMod;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraftforge.common.MinecraftForge;
+import net.minecraftforge.fml.ModList;
import net.minecraftforge.fml.loading.LoadingModList;
+import net.minecraftforge.forgespi.language.IModInfo;
public class FlwImplXplatImpl implements FlwImplXplat {
@Override
@@ -36,4 +44,46 @@ public class FlwImplXplatImpl implements FlwImplXplat {
public boolean useIrisCompat() {
return CompatMod.IRIS.isLoaded || CompatMod.OCULUS.isLoaded;
}
+
+ @Override
+ public ModRequirements modRequirements() {
+ List entries = new ArrayList<>();
+
+ ModList.get()
+ .forEachModFile(file -> {
+ var info = file.getModFileInfo();
+
+ for (IModInfo mod : info.getMods()) {
+ var modProperties = mod.getModProperties()
+ .get("flywheel");
+
+ // There's no well-defined API for custom properties like in fabric.
+ // It just returns an object, but internally it's represented with nightconfig.
+ if (modProperties instanceof Config config) {
+ // Minor version defaults to 0, major is required
+ int major = config.getIntOrElse("backend_major_version", Integer.MIN_VALUE);
+ int minor = config.getIntOrElse("backend_minor_version", 0);
+
+ if (major != Integer.MIN_VALUE) {
+ entries.add(new ModRequirements.Entry(mod.getModId(), new BackendVersion(major, minor)));
+ } else {
+ FlwImpl.LOGGER.warn("Mod {} has invalid required backend version", mod.getModId());
+ }
+ } else {
+ FlwImpl.LOGGER.warn("Mod {} has invalid flywheel properties", mod.getModId());
+ }
+ }
+ });
+
+ if (!entries.isEmpty()) {
+ var minVersion = entries.stream()
+ .map(ModRequirements.Entry::version)
+ .min(BackendVersion::compareTo)
+ .get();
+
+ return new ModRequirements(minVersion, entries);
+ } else {
+ return new ModRequirements(new BackendVersion(0, 0), List.of());
+ }
+ }
}
diff --git a/forge/src/main/resources/META-INF/mods.toml b/forge/src/main/resources/META-INF/mods.toml
index be991ac88..991e1095a 100644
--- a/forge/src/main/resources/META-INF/mods.toml
+++ b/forge/src/main/resources/META-INF/mods.toml
@@ -14,6 +14,10 @@ authors = "Jozufozu, PepperCode1"
displayURL = "${mod_homepage}"
displayTest = "IGNORE_ALL_VERSION"
+[modproperties.${ mod_id }.flywheel]
+backend_major_version = 1
+backend_minor_version = 0
+
[[dependencies.${mod_id}]]
modId = "minecraft"
mandatory = true