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