Initial Commit
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
package me.zacharias.ollama;
|
||||
|
||||
import me.neurodock.ollama.OllamaFunctionArgument;
|
||||
import me.neurodock.ollama.OllamaFunctionTool;
|
||||
import me.neurodock.ollama.OllamaPerameter;
|
||||
import me.neurodock.ollama.OllamaToolResponse;
|
||||
import me.neurodock.ollama.exceptions.OllamaToolErrorException;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.crafting.*;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class GetCraftibles extends OllamaFunctionTool {
|
||||
@Override
|
||||
public @NotNull String name() {
|
||||
return "getcraftibles";
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull OllamaPerameter parameters() {
|
||||
return OllamaPerameter.builder()
|
||||
.addEnumProperty("container", Container.class, "Set what container to use as the craftable", true)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull OllamaToolResponse function(OllamaFunctionArgument... ollamaFunctionArguments) {
|
||||
if(ollamaFunctionArguments.length != 1) {
|
||||
throw new OllamaToolErrorException(name(), "Invalid number of arguments");
|
||||
}
|
||||
|
||||
if(!ollamaFunctionArguments[0].argument().equals("container")) {
|
||||
throw new OllamaToolErrorException(name(), "Invalid argument");
|
||||
}
|
||||
|
||||
Container container = ollamaFunctionArguments[0].getValue(Container.class);
|
||||
|
||||
if(Minecraft.getInstance().getSingleplayerServer() == null) {
|
||||
throw new OllamaToolErrorException(name(), "Not on a single player server");
|
||||
}
|
||||
|
||||
if(Minecraft.getInstance().player == null) {
|
||||
throw new OllamaToolErrorException(name(), "Cant get a player");
|
||||
}
|
||||
|
||||
if(Minecraft.getInstance().level == null) {
|
||||
throw new OllamaToolErrorException(name(), "Cant get a level");
|
||||
}
|
||||
|
||||
Inventory inventory = Minecraft.getInstance().player.getInventory();
|
||||
|
||||
List<ItemStack> inputItems = Minecraft.getInstance().player.getInventory().getNonEquipmentItems();
|
||||
|
||||
RecipeManager recipes = Minecraft.getInstance().getSingleplayerServer().getRecipeManager();
|
||||
|
||||
JSONArray recipies = new JSONArray();
|
||||
|
||||
if(container != Container.FURNACE) {
|
||||
|
||||
List<RecipeHolder<CraftingRecipe>> allRecipes = recipes.getRecipes();
|
||||
}
|
||||
else if(container == Container.FURNACE) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public enum Container {
|
||||
INVENTORY(2,2),
|
||||
CRAFTING_TABLE(3,3),
|
||||
FURNACE(1,1);
|
||||
|
||||
private final int width, height;
|
||||
|
||||
Container(int width, int height) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return height;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package me.zacharias.ollama;
|
||||
|
||||
import me.neurodock.ollama.OllamaFunctionArgument;
|
||||
import me.neurodock.ollama.OllamaFunctionTool;
|
||||
import me.neurodock.ollama.OllamaPerameter;
|
||||
import me.neurodock.ollama.OllamaToolResponse;
|
||||
import me.neurodock.ollama.exceptions.OllamaToolErrorException;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class GetItemTool extends OllamaFunctionTool {
|
||||
@Override
|
||||
public @NotNull String name() {
|
||||
return "getiteminhand";
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull OllamaPerameter parameters() {
|
||||
return OllamaPerameter.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull OllamaToolResponse function(OllamaFunctionArgument... ollamaFunctionArguments) {
|
||||
|
||||
if(Minecraft.getInstance() == null) {
|
||||
throw new OllamaToolErrorException(name(), "Minecraft is not initialized");
|
||||
}
|
||||
|
||||
if(Minecraft.getInstance().player == null) {
|
||||
throw new OllamaToolErrorException(name(), "Player is not initialized");
|
||||
}
|
||||
|
||||
if(Minecraft.getInstance().player.getInventory().getSelectedItem() == null) {
|
||||
throw new OllamaToolErrorException(name(), "Selected item is null");
|
||||
}
|
||||
|
||||
ItemStack stack = Minecraft.getInstance().player.getInventory().getSelectedItem();
|
||||
|
||||
JSONObject item = new JSONObject();
|
||||
|
||||
Identifier id = BuiltInRegistries.ITEM.getKey(stack.getItem());
|
||||
|
||||
item.put("item", id.toString());
|
||||
item.put("count", stack.getCount());
|
||||
item.put("name", stack.getDisplayName().getString());
|
||||
|
||||
return new OllamaToolResponse(name(), item.toString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package me.zacharias.ollama;
|
||||
|
||||
import me.neurodock.ollama.OllamaFunctionArgument;
|
||||
import me.neurodock.ollama.OllamaFunctionTool;
|
||||
import me.neurodock.ollama.OllamaPerameter;
|
||||
import me.neurodock.ollama.OllamaToolResponse;
|
||||
import me.neurodock.ollama.exceptions.OllamaToolErrorException;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class GetLocation extends OllamaFunctionTool {
|
||||
@Override
|
||||
public @NotNull String name() {
|
||||
return "getlocation";
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull OllamaPerameter parameters() {
|
||||
return OllamaPerameter.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull OllamaToolResponse function(OllamaFunctionArgument... ollamaFunctionArguments) {
|
||||
if(Minecraft.getInstance() == null) {
|
||||
throw new OllamaToolErrorException(name(), "Minecraft is not initialized");
|
||||
}
|
||||
|
||||
if(Minecraft.getInstance().player == null) {
|
||||
throw new OllamaToolErrorException(name(), "Player is not initialized");
|
||||
}
|
||||
|
||||
Vec3 pos = Minecraft.getInstance().player.position();
|
||||
|
||||
return new OllamaToolResponse(name(), new JSONObject().put("x", pos.x).put("y", pos.y).put("z", pos.z).toString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
package me.zacharias.ollama;
|
||||
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.logging.LogUtils;
|
||||
import me.neurodock.core.Core;
|
||||
import me.neurodock.core.LaunchOptions;
|
||||
import me.neurodock.core.PrintAdvanceMessageHandler;
|
||||
import me.neurodock.ollama.OllamaMessage;
|
||||
import me.neurodock.ollama.OllamaMessageRole;
|
||||
import me.neurodock.ollama.OllamaObject;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.Commands;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.PlayerChatMessage;
|
||||
import net.minecraft.world.food.FoodProperties;
|
||||
import net.minecraft.world.item.BlockItem;
|
||||
import net.minecraft.world.item.CreativeModeTab;
|
||||
import net.minecraft.world.item.CreativeModeTabs;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockBehaviour;
|
||||
import net.minecraft.world.level.material.MapColor;
|
||||
import net.neoforged.api.distmarker.Dist;
|
||||
import net.neoforged.bus.api.IEventBus;
|
||||
import net.neoforged.bus.api.SubscribeEvent;
|
||||
import net.neoforged.fml.ModContainer;
|
||||
import net.neoforged.fml.common.EventBusSubscriber;
|
||||
import net.neoforged.fml.common.Mod;
|
||||
import net.neoforged.fml.config.ModConfig;
|
||||
import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent;
|
||||
import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent;
|
||||
import net.neoforged.fml.loading.FMLPaths;
|
||||
import net.neoforged.neoforge.common.NeoForge;
|
||||
import net.neoforged.neoforge.event.BuildCreativeModeTabContentsEvent;
|
||||
import net.neoforged.neoforge.event.RegisterCommandsEvent;
|
||||
import net.neoforged.neoforge.event.server.ServerStartingEvent;
|
||||
import net.neoforged.neoforge.registries.DeferredBlock;
|
||||
import net.neoforged.neoforge.registries.DeferredHolder;
|
||||
import net.neoforged.neoforge.registries.DeferredItem;
|
||||
import net.neoforged.neoforge.registries.DeferredRegister;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
// The value here should match an entry in the META-INF/neoforge.mods.toml file
|
||||
@Mod(Ollama.MODID)
|
||||
public class Ollama {
|
||||
// Define mod id in a common place for everything to reference
|
||||
public static final String MODID = "ollama";
|
||||
// Directly reference a slf4j logger
|
||||
private static final Logger LOGGER = LogUtils.getLogger();
|
||||
|
||||
private static CommandSourceStack sourceStack;
|
||||
|
||||
PrintAdvanceMessageHandler printAdvanceMessageHandler = new PrintAdvanceMessageHandler() {
|
||||
@Override
|
||||
public void printMessage(OllamaMessage ollamaMessage) {
|
||||
String sender = ollamaMessage.getRole().getRole();
|
||||
if(ollamaMessage.getRole() == OllamaMessageRole.ASSISTANT)
|
||||
{
|
||||
sender = "Merl";
|
||||
}
|
||||
LOGGER.info("{}: {}", sender, ollamaMessage.getContent());
|
||||
if(sourceStack != null) {
|
||||
sourceStack.sendSystemMessage(Component.literal(sender + "> " + ollamaMessage.getContent()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void printErrorMessage(OllamaMessage ollamaMessage) {
|
||||
LOGGER.warn("{}: {}", ollamaMessage.getRole().getRole(), ollamaMessage.getContent());
|
||||
if(sourceStack != null) {
|
||||
sourceStack.sendSystemMessage(Component.literal(ollamaMessage.getRole().getRole() + "> " + ollamaMessage.getContent()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void printToolCalling(String s) {
|
||||
//LOGGER.warn("Tool calling: {}", s);
|
||||
//if(sourceStack != null) {
|
||||
// sourceStack.sendSystemMessage(Component.literal("Tool calling: " + s));
|
||||
|
||||
// We will not send any info about tool info bc well fuck that :)
|
||||
}
|
||||
};
|
||||
|
||||
private static Core core;
|
||||
|
||||
// The constructor for the mod class is the first code that is run when your mod is loaded.
|
||||
// FML will recognize some parameter types like IEventBus or ModContainer and pass them in automatically.
|
||||
public Ollama(IEventBus modEventBus, ModContainer modContainer) {
|
||||
// Register the commonSetup method for modloading
|
||||
modEventBus.addListener(this::commonSetup);
|
||||
|
||||
// Register ourselves for server and other game events we are interested in.
|
||||
// Note that this is necessary if and only if we want *this* class (Ollama) to respond directly to events.
|
||||
// Do not add this line if there are no @SubscribeEvent-annotated functions in this class, like onServerStarting() below.
|
||||
NeoForge.EVENT_BUS.register(this);
|
||||
}
|
||||
|
||||
private void commonSetup(final FMLCommonSetupEvent event) {
|
||||
// Some common setup code
|
||||
LOGGER.info("HELLO FROM COMMON SETUP");
|
||||
|
||||
Path ollamaPath = FMLPaths.CONFIGDIR.get().resolve("ollama");
|
||||
if (!Files.exists(ollamaPath)) {
|
||||
try {
|
||||
Files.createDirectory(ollamaPath);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
Core.setDataDirectory(ollamaPath.toString(), true);
|
||||
Core.setLogDirectory(ollamaPath.toString() + "/logs");
|
||||
LaunchOptions.getInstance().setLoadOld(false);
|
||||
|
||||
initOllama();
|
||||
}
|
||||
|
||||
private void initOllama() {
|
||||
core = new Core(printAdvanceMessageHandler, "127.0.0.1");
|
||||
|
||||
core.setOllamaObjectNoMemory(OllamaObject.builder()
|
||||
.setModel("llama3.2")
|
||||
.keep_alive(10)
|
||||
.addMessage(new OllamaMessage(OllamaMessageRole.SYSTEM, "You are an assistant named \"Merl\" who is tasked to help the player in Minecraft"))
|
||||
.build());
|
||||
|
||||
core.addTool(new GetItemTool(), Core.Source.CTP);
|
||||
core.addTool(new GetLocation(), Core.Source.CTP);
|
||||
core.addTool(new getPlayerInventory(), Core.Source.CTP);
|
||||
}
|
||||
|
||||
// You can use SubscribeEvent and let the Event Bus discover methods to call
|
||||
@SubscribeEvent
|
||||
public void onServerStarting(ServerStartingEvent event) {
|
||||
// Do something when the server starts
|
||||
LOGGER.info("HELLO from server starting");
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public void registercommands(RegisterCommandsEvent event) {
|
||||
event.getDispatcher().register(Commands.literal("chat")
|
||||
.then(Commands.argument("message", StringArgumentType.greedyString())
|
||||
.executes(context -> {
|
||||
String message = StringArgumentType.getString(context, "message");
|
||||
|
||||
sourceStack = context.getSource();
|
||||
|
||||
core.getOllamaObject().addMessage(new OllamaMessage(OllamaMessageRole.USER, message));
|
||||
core.handleResponse(core.qurryOllama());
|
||||
|
||||
return 1;
|
||||
})
|
||||
));
|
||||
event.getDispatcher().register(Commands.literal("reset")
|
||||
.executes(context -> {
|
||||
initOllama();
|
||||
|
||||
return 1;
|
||||
}));
|
||||
}
|
||||
|
||||
// You can use EventBusSubscriber to automatically register all static methods in the class annotated with @SubscribeEvent
|
||||
@EventBusSubscriber(modid = MODID, value = Dist.CLIENT)
|
||||
public static class ClientModEvents {
|
||||
@SubscribeEvent
|
||||
public static void onClientSetup(FMLClientSetupEvent event) {
|
||||
// Some client setup code
|
||||
LOGGER.info("HELLO FROM CLIENT SETUP");
|
||||
LOGGER.info("MINECRAFT NAME >> {}", Minecraft.getInstance().getUser().getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package me.zacharias.ollama;
|
||||
|
||||
import me.neurodock.ollama.OllamaFunctionArgument;
|
||||
import me.neurodock.ollama.OllamaFunctionTool;
|
||||
import me.neurodock.ollama.OllamaPerameter;
|
||||
import me.neurodock.ollama.OllamaToolResponse;
|
||||
import me.neurodock.ollama.exceptions.OllamaToolErrorException;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.entity.SlotAccess;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class getPlayerInventory extends OllamaFunctionTool {
|
||||
@Override
|
||||
public @NotNull String name() {
|
||||
return "getinventory";
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull OllamaPerameter parameters() {
|
||||
return OllamaPerameter.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull OllamaToolResponse function(OllamaFunctionArgument... ollamaFunctionArguments) {
|
||||
if(Minecraft.getInstance() == null) {
|
||||
throw new OllamaToolErrorException(name(), "Minecraft is not initialized");
|
||||
}
|
||||
|
||||
if(Minecraft.getInstance().player == null) {
|
||||
throw new OllamaToolErrorException(name(), "Player is not initialized");
|
||||
}
|
||||
|
||||
if(Minecraft.getInstance().player.getInventory() == null) {
|
||||
throw new OllamaToolErrorException(name(), "Selected item is null");
|
||||
}
|
||||
|
||||
JSONObject inventory = new JSONObject();
|
||||
|
||||
for(int i = 0; i < Minecraft.getInstance().player.getInventory().getContainerSize(); i++) {
|
||||
JSONObject item = new JSONObject();
|
||||
SlotAccess slot = Minecraft.getInstance().player.getInventory().getSlot(i);
|
||||
|
||||
if(slot == null) continue;
|
||||
|
||||
ItemStack stack = slot.get();
|
||||
|
||||
if(stack.getItem() == Items.AIR) continue;
|
||||
|
||||
Identifier id = BuiltInRegistries.ITEM.getKey(stack.getItem());
|
||||
|
||||
item.put("item", id.toString());
|
||||
item.put("count", stack.getCount());
|
||||
item.put("name", stack.getDisplayName().getString());
|
||||
|
||||
inventory.put(i+"", item);
|
||||
}
|
||||
|
||||
return new OllamaToolResponse(name(), inventory.toString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
# This is an example mods.toml file. It contains the data relating to the loading mods.
|
||||
# There are several mandatory fields (#mandatory), and many more that are optional (#optional).
|
||||
# The overall format is standard TOML format, v0.5.0.
|
||||
# Note that there are a couple of TOML lists in this file.
|
||||
# Find more information on toml format here: https://github.com/toml-lang/toml
|
||||
# The name of the mod loader type to load - for regular FML @Mod mods it should be javafml
|
||||
modLoader = "javafml" #mandatory
|
||||
# A version range to match for said mod loader - for regular FML @Mod it will be the the FML version. This is currently 47.
|
||||
loaderVersion = "${loader_version_range}" #mandatory
|
||||
# The license for you mod. This is mandatory metadata and allows for easier comprehension of your redistributive properties.
|
||||
# Review your options at https://choosealicense.com/. All rights reserved is the default copyright stance, and is thus the default here.
|
||||
license = "${mod_license}"
|
||||
# A URL to refer people to when problems occur with this mod
|
||||
#issueTrackerURL="https://change.me.to.your.issue.tracker.example.invalid/" #optional
|
||||
# A list of mods - how many allowed here is determined by the individual mod loader
|
||||
[[mods]] #mandatory
|
||||
# The modid of the mod
|
||||
modId = "${mod_id}" #mandatory
|
||||
# The version number of the mod
|
||||
version = "${mod_version}" #mandatory
|
||||
# A display name for the mod
|
||||
displayName = "${mod_name}" #mandatory
|
||||
# A URL to query for updates for this mod. See the JSON update specification https://docs.neoforge.net/docs/misc/updatechecker/
|
||||
#updateJSONURL="https://change.me.example.invalid/updates.json" #optional
|
||||
# A URL for the "homepage" for this mod, displayed in the mod UI
|
||||
#displayURL="https://change.me.to.your.mods.homepage.example.invalid/" #optional
|
||||
# A file name (in the root of the mod JAR) containing a logo for display
|
||||
#logoFile="ollama.png" #optional
|
||||
# A text field displayed in the mod UI
|
||||
#credits="" #optional
|
||||
# A text field displayed in the mod UI
|
||||
authors = "${mod_authors}" #optional
|
||||
|
||||
# The description text for the mod (multi line!) (#mandatory)
|
||||
description = '''${mod_description}'''
|
||||
|
||||
# The [[mixins]] block allows you to declare your mixin config to FML so that it gets loaded.
|
||||
#[[mixins]]
|
||||
#config="${mod_id}.mixins.json"
|
||||
|
||||
# The [[accessTransformers]] block allows you to declare where your AT file is.
|
||||
# If this block is omitted, a fallback attempt will be made to load an AT from META-INF/accesstransformer.cfg
|
||||
#[[accessTransformers]]
|
||||
#file="META-INF/accesstransformer.cfg"
|
||||
|
||||
# The coremods config file path is not configurable and is always loaded from META-INF/coremods.json
|
||||
|
||||
# A dependency - use the . to indicate dependency for a specific modid. Dependencies are optional.
|
||||
[[dependencies."${mod_id}"]] #optional
|
||||
# the modid of the dependency
|
||||
modId = "neoforge" #mandatory
|
||||
# The type of the dependency. Can be one of "required", "optional", "incompatible" or "discouraged" (case insensitive).
|
||||
# 'required' requires the mod to exist, 'optional' does not
|
||||
# 'incompatible' will prevent the game from loading when the mod exists, and 'discouraged' will show a warning
|
||||
type = "required" #mandatory
|
||||
# Optional field describing why the dependency is required or why it is incompatible
|
||||
# reason="..."
|
||||
# The version range of the dependency
|
||||
versionRange = "${neo_version_range}" #mandatory
|
||||
# An ordering relationship for the dependency.
|
||||
# BEFORE - This mod is loaded BEFORE the dependency
|
||||
# AFTER - This mod is loaded AFTER the dependency
|
||||
ordering = "NONE"
|
||||
# Side this dependency is applied on - BOTH, CLIENT, or SERVER
|
||||
side = "BOTH"
|
||||
# Here's another dependency
|
||||
[[dependencies."${mod_id}"]]
|
||||
modId = "minecraft"
|
||||
type = "required"
|
||||
# This version range declares a minimum of the current minecraft version up to but not including the next major version
|
||||
versionRange = "${minecraft_version_range}"
|
||||
ordering = "NONE"
|
||||
side = "BOTH"
|
||||
|
||||
# Features are specific properties of the game environment, that you may want to declare you require. This example declares
|
||||
# that your mod requires GL version 3.2 or higher. Other features will be added. They are side aware so declaring this won't
|
||||
# stop your mod loading on the server for example.
|
||||
#[features."${mod_id}"]
|
||||
#openGLVersion="[3.2,)"
|
||||
Reference in New Issue
Block a user