Some refactor and add missing behaviors in some GUIs.

This commit is contained in:
LambdAurora
2020-02-13 00:28:58 +01:00
parent 0050b0216c
commit 8063116820
23 changed files with 123 additions and 70 deletions

View File

@@ -42,7 +42,7 @@ dependencies {
// Fabric API. This is technically optional, but you probably want it anyway.
modApi "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}"
modCompile "io.github.prospector:modmenu:1.8.5+build.23"
modCompile "io.github.prospector:modmenu:${project.modmenu_version}"
modCompile "com.github.lambdaurora:spruceui:${project.spruceui_version}"
include "com.github.lambdaurora:spruceui:${project.spruceui_version}"

View File

@@ -9,6 +9,7 @@
package me.lambdaurora.lambdacontrols.client;
import io.github.prospector.modmenu.api.ConfigScreenFactory;
import io.github.prospector.modmenu.api.ModMenuApi;
import me.lambdaurora.lambdacontrols.LambdaControlsConstants;
import me.lambdaurora.lambdacontrols.client.gui.LambdaControlsSettingsScreen;
@@ -21,7 +22,7 @@ import java.util.function.Function;
* Represents the API implementation of ModMenu for LambdaControls.
*
* @author LambdAurora
* @version 1.1.0
* @version 1.1.1
* @since 1.1.0
*/
public class LambdaControlsModMenu implements ModMenuApi
@@ -33,8 +34,8 @@ public class LambdaControlsModMenu implements ModMenuApi
}
@Override
public Function<Screen, ? extends Screen> getConfigScreenFactory()
public ConfigScreenFactory<?> getModConfigScreenFactory()
{
return screen -> new LambdaControlsSettingsScreen(screen, MinecraftClient.getInstance().options, false);
return parent -> new LambdaControlsSettingsScreen(parent, false);
}
}

View File

@@ -14,6 +14,7 @@ import me.lambdaurora.lambdacontrols.client.controller.Controller;
import me.lambdaurora.lambdacontrols.client.controller.InputManager;
import me.lambdaurora.lambdacontrols.client.gui.ControllerControlsScreen;
import me.lambdaurora.lambdacontrols.client.gui.TouchscreenOverlay;
import me.lambdaurora.lambdacontrols.client.mixin.AdvancementsScreenAccessor;
import me.lambdaurora.lambdacontrols.client.mixin.CreativeInventoryScreenAccessor;
import me.lambdaurora.lambdacontrols.client.mixin.EntryListWidgetAccessor;
import me.lambdaurora.lambdacontrols.client.util.ContainerScreenAccessor;
@@ -22,6 +23,7 @@ import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.Element;
import net.minecraft.client.gui.ParentElement;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.screen.advancement.AdvancementTab;
import net.minecraft.client.gui.screen.advancement.AdvancementsScreen;
import net.minecraft.client.gui.screen.ingame.ContainerScreen;
import net.minecraft.client.gui.screen.ingame.CreativeInventoryScreen;
@@ -55,7 +57,7 @@ import static org.lwjgl.glfw.GLFW.*;
* Represents the LambdaControls' input handler.
*
* @author LambdAurora
* @version 1.1.0
* @version 1.1.1
* @since 1.0.0
*/
public class LambdaInput
@@ -311,6 +313,7 @@ public class LambdaInput
slotAction = SlotActionType.CLONE;
}
client.interactionManager.clickSlot(((ContainerScreen) client.currentScreen).getContainer().syncId, slot.id, GLFW.GLFW_MOUSE_BUTTON_1, slotAction, client.player);
client.player.playerContainer.sendContentUpdates();
this.actionGuiCooldown = 5;
return;
} else if (button == GLFW.GLFW_GAMEPAD_BUTTON_B) {
@@ -381,6 +384,8 @@ public class LambdaInput
}
}
double deadZone = this.config.getDeadZone();
if (client.currentScreen instanceof ControllerControlsScreen) {
ControllerControlsScreen screen = (ControllerControlsScreen) client.currentScreen;
if (screen.focusedBinding != null) {
@@ -397,9 +402,27 @@ public class LambdaInput
}
return;
}
} else if (client.currentScreen instanceof CreativeInventoryScreen) {
if (axis == GLFW_GAMEPAD_AXIS_RIGHT_Y) {
CreativeInventoryScreen screen = (CreativeInventoryScreen) client.currentScreen;
CreativeInventoryScreenAccessor accessor = (CreativeInventoryScreenAccessor) screen;
if (accessor.lambdacontrols_hasScrollbar() && absValue >= deadZone) {
screen.mouseScrolled(0.0, 0.0, -value);
}
return;
}
} else if (client.currentScreen instanceof AdvancementsScreen) {
if (axis == GLFW_GAMEPAD_AXIS_RIGHT_X || axis == GLFW_GAMEPAD_AXIS_RIGHT_Y) {
AdvancementsScreen screen = (AdvancementsScreen) client.currentScreen;
AdvancementsScreenAccessor accessor = (AdvancementsScreenAccessor) screen;
if (absValue >= deadZone) {
AdvancementTab tab = accessor.getSelectedTab();
tab.move(axis == GLFW_GAMEPAD_AXIS_RIGHT_X ? -value * 5.0 : 0.0, axis == GLFW_GAMEPAD_AXIS_RIGHT_Y ? -value * 5.0 : 0.0);
}
return;
}
}
double deadZone = this.config.getDeadZone();
if (client.currentScreen == null) {
// Handles the look direction.
this.handleLook(client, axis, (float) (absValue / (1.0 - this.config.getDeadZone())), state);
@@ -590,8 +613,8 @@ public class LambdaInput
if (screen instanceof ContainerScreen) {
ContainerScreen inventoryScreen = (ContainerScreen) screen;
ContainerScreenAccessor accessor = (ContainerScreenAccessor) inventoryScreen;
int guiLeft = accessor.lambdacontrols_getX();
int guiTop = accessor.lambdacontrols_getY();
int guiLeft = accessor.getX();
int guiTop = accessor.getY();
int mouseX = (int) (targetMouseX * (double) client.getWindow().getScaledWidth() / (double) client.getWindow().getWidth());
int mouseY = (int) (targetMouseY * (double) client.getWindow().getScaledHeight() / (double) client.getWindow().getHeight());

View File

@@ -35,7 +35,7 @@ import java.util.stream.Collectors;
* Represents some input handlers.
*
* @author LambdAurora
* @version 1.1.0
* @version 1.1.1
* @since 1.1.0
*/
public class InputHandlers
@@ -59,7 +59,7 @@ public class InputHandlers
return true;
} else if (client.currentScreen instanceof CreativeInventoryScreen) {
CreativeInventoryScreenAccessor inventory = (CreativeInventoryScreenAccessor) client.currentScreen;
int currentSelectedTab = inventory.lambdacontrols_getSelectedTab();
int currentSelectedTab = inventory.getSelectedTab();
int nextTab = currentSelectedTab + (right ? 1 : -1);
if (nextTab < 0)
nextTab = ItemGroup.GROUPS.length - 1;
@@ -69,8 +69,8 @@ public class InputHandlers
return true;
} else if (client.currentScreen instanceof AdvancementsScreen) {
AdvancementsScreenAccessor screen = (AdvancementsScreenAccessor) client.currentScreen;
List<AdvancementTab> tabs = screen.lambdacontrols_getTabs().values().stream().distinct().collect(Collectors.toList());
AdvancementTab tab = screen.lambdacontrols_getSelectedTab();
List<AdvancementTab> tabs = screen.getTabs().values().stream().distinct().collect(Collectors.toList());
AdvancementTab tab = screen.getSelectedTab();
for (int i = 0; i < tabs.size(); i++) {
if (tabs.get(i).equals(tab)) {
int nextTab = i + (right ? 1 : -1);
@@ -78,7 +78,7 @@ public class InputHandlers
nextTab = tabs.size() - 1;
else if (nextTab >= tabs.size())
nextTab = 0;
screen.lambdacontrols_getAdvancementManager().selectTab(tabs.get(nextTab).getRoot(), true);
screen.getAdvancementManager().selectTab(tabs.get(nextTab).getRoot(), true);
break;
}
}
@@ -134,8 +134,8 @@ public class InputHandlers
ContainerScreen inventory = (ContainerScreen) client.currentScreen;
ContainerScreenAccessor accessor = (ContainerScreenAccessor) inventory;
int guiLeft = accessor.lambdacontrols_getX();
int guiTop = accessor.lambdacontrols_getY();
int guiLeft = accessor.getX();
int guiTop = accessor.getY();
double mouseX = client.mouse.getX() * (double) client.getWindow().getScaledWidth() / (double) client.getWindow().getWidth();
double mouseY = client.mouse.getY() * (double) client.getWindow().getScaledHeight() / (double) client.getWindow().getHeight();

View File

@@ -60,7 +60,7 @@ public class ControllerControlsScreen extends Screen
btn -> this.minecraft.openScreen(new ControlsOptionsScreen(this, this.minecraft.options))));
if (!this.hideSettings)
this.addButton(new SpruceButtonWidget(this.width / 2 - 155 + 160, 18, 150, 20, I18n.translate("menu.options"),
btn -> this.minecraft.openScreen(new LambdaControlsSettingsScreen(this, this.minecraft.options, true))));
btn -> this.minecraft.openScreen(new LambdaControlsSettingsScreen(this, true))));
this.bindingsListWidget = new ControlsListWidget(this, this.minecraft);
this.children.add(this.bindingsListWidget);
this.resetButton = this.addButton(new ButtonWidget(this.width / 2 - 155, this.height - 29, 150, 20, I18n.translate("controls.resetAll"),

View File

@@ -65,7 +65,7 @@ public class LambdaControlsSettingsScreen extends Screen
private ButtonListWidget list;
private SpruceLabelWidget gamepadToolUrlLabel;
public LambdaControlsSettingsScreen(Screen parent, @NotNull GameOptions options, boolean hideControls)
public LambdaControlsSettingsScreen(Screen parent, boolean hideControls)
{
super(new TranslatableText("lambdacontrols.title.settings"));
this.mod = LambdaControlsClient.get();

View File

@@ -19,7 +19,7 @@ import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.screen.ingame.InventoryScreen;
import net.minecraft.client.gui.widget.ButtonWidget;
import net.minecraft.client.gui.widget.TexturedButtonWidget;
import net.minecraft.server.network.packet.PlayerActionC2SPacket;
import net.minecraft.network.packet.c2s.play.PlayerActionC2SPacket;
import net.minecraft.text.LiteralText;
import net.minecraft.util.Arm;
import net.minecraft.util.Identifier;

View File

@@ -17,5 +17,5 @@ import org.spongepowered.asm.mixin.gen.Accessor;
public interface AbstractButtonWidgetAccessor
{
@Accessor("height")
int lambdacontrols_getHeight();
int getHeight();
}

View File

@@ -25,11 +25,11 @@ import java.util.Map;
public interface AdvancementsScreenAccessor
{
@Accessor("advancementHandler")
ClientAdvancementManager lambdacontrols_getAdvancementManager();
ClientAdvancementManager getAdvancementManager();
@Accessor("tabs")
Map<Advancement, AdvancementTab> lambdacontrols_getTabs();
Map<Advancement, AdvancementTab> getTabs();
@Accessor("selectedTab")
AdvancementTab lambdacontrols_getSelectedTab();
AdvancementTab getSelectedTab();
}

View File

@@ -13,7 +13,7 @@ import me.lambdaurora.lambdacontrols.LambdaControls;
import me.lambdaurora.lambdacontrols.client.LambdaControlsClient;
import net.fabricmc.fabric.api.network.ClientSidePacketRegistry;
import net.minecraft.client.network.ClientPlayNetworkHandler;
import net.minecraft.client.network.packet.GameJoinS2CPacket;
import net.minecraft.network.packet.s2c.play.GameJoinS2CPacket;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
@@ -23,7 +23,7 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
public class ClientPlayNetworkHandlerMixin
{
@Inject(method = "onGameJoin", at = @At(value = "TAIL"))
private void lambdacontrols_onConnect(GameJoinS2CPacket packet, CallbackInfo ci)
private void onGameJoin(GameJoinS2CPacket packet, CallbackInfo ci)
{
ClientSidePacketRegistry.INSTANCE.sendToServer(LambdaControls.HELLO_CHANNEL, LambdaControls.get().makeHello(LambdaControlsClient.get().config.getControlsMode()));
ClientSidePacketRegistry.INSTANCE.sendToServer(LambdaControls.CONTROLS_MODE_CHANNEL,

View File

@@ -52,7 +52,7 @@ public abstract class ClientPlayerEntityMixin extends AbstractClientPlayerEntity
}
@Inject(method = "move(Lnet/minecraft/entity/MovementType;Lnet/minecraft/util/math/Vec3d;)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/network/AbstractClientPlayerEntity;move(Lnet/minecraft/entity/MovementType;Lnet/minecraft/util/math/Vec3d;)V"))
public void lambdacontrols_move(MovementType type, Vec3d movement, CallbackInfo ci)
public void onMove(MovementType type, Vec3d movement, CallbackInfo ci)
{
LambdaControlsClient mod = LambdaControlsClient.get();
if (type == MovementType.SELF) {
@@ -70,7 +70,7 @@ public abstract class ClientPlayerEntityMixin extends AbstractClientPlayerEntity
}
@Inject(method = "tickMovement", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/network/ClientPlayerEntity;isCamera()Z"))
public void lambdacontrols_tickMovement(CallbackInfo ci)
public void onTickMovement(CallbackInfo ci)
{
if (this.abilities.flying && this.isCamera()) {
if (LambdaControlsClient.get().config.hasFlyVerticalDrifting())

View File

@@ -17,6 +17,7 @@ import net.minecraft.client.gui.screen.ingame.ContainerScreen;
import net.minecraft.container.Slot;
import org.lwjgl.glfw.GLFW;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
import org.spongepowered.asm.mixin.gen.Invoker;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
@@ -28,26 +29,17 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(ContainerScreen.class)
public abstract class ContainerScreenMixin implements ContainerScreenAccessor
{
protected int x;
protected int y;
@Accessor("x")
public abstract int getX();
@Override
public int lambdacontrols_getX()
{
return this.x;
}
@Override
public int lambdacontrols_getY()
{
return this.y;
}
@Accessor("y")
public abstract int getY();
@Invoker("getSlotAt")
public abstract Slot lambdacontrols_getSlotAt(double posX, double posY);
@Inject(method = "render", at = @At("RETURN"))
public void render(int mouseX, int mouseY, float delta, CallbackInfo ci)
public void onRender(int mouseX, int mouseY, float delta, CallbackInfo ci)
{
if (LambdaControlsClient.get().config.getControlsMode() == ControlsMode.CONTROLLER) {
MinecraftClient client = MinecraftClient.getInstance();

View File

@@ -35,12 +35,12 @@ public class ControlsOptionsScreenMixin extends GameOptionsScreen
}
@Redirect(method = "init", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screen/options/ControlsOptionsScreen;addButton(Lnet/minecraft/client/gui/widget/AbstractButtonWidget;)Lnet/minecraft/client/gui/widget/AbstractButtonWidget;", ordinal = 1))
private AbstractButtonWidget on_init(ControlsOptionsScreen screen, AbstractButtonWidget btn)
private AbstractButtonWidget onInit(ControlsOptionsScreen screen, AbstractButtonWidget btn)
{
if (this.parent instanceof ControllerControlsScreen)
return this.addButton(btn);
else
return this.addButton(new ButtonWidget(btn.x, btn.y, btn.getWidth(), ((AbstractButtonWidgetAccessor) btn).lambdacontrols_getHeight(), I18n.translate("menu.options"),
b -> this.minecraft.openScreen(new LambdaControlsSettingsScreen(this, this.gameOptions, true))));
return this.addButton(new ButtonWidget(btn.x, btn.y, btn.getWidth(), ((AbstractButtonWidgetAccessor) btn).getHeight(), I18n.translate("menu.options"),
b -> this.minecraft.openScreen(new LambdaControlsSettingsScreen(this, true))));
}
}

View File

@@ -30,7 +30,7 @@ public interface CreativeInventoryScreenAccessor
* @return The selected tab index.
*/
@Accessor("selectedTab")
int lambdacontrols_getSelectedTab();
int getSelectedTab();
/**
* Sets the selected tab.
@@ -48,4 +48,12 @@ public interface CreativeInventoryScreenAccessor
*/
@Invoker("isCreativeInventorySlot")
boolean lambdacontrols_isCreativeInventorySlot(@Nullable Slot slot);
/**
* Returns whether the current tab has a scrollbar or not.
*
* @return True if the current tab has a scrollbar, else false.
*/
@Invoker("hasScrollbar")
boolean lambdacontrols_hasScrollbar();
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright © 2020 LambdAurora <aurora42lambda@gmail.com>
*
* This file is part of LambdaControls.
*
* Licensed under the MIT license. For more information,
* see the LICENSE file.
*/
package me.lambdaurora.lambdacontrols.client.mixin;
import net.minecraft.client.options.GameOptions;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/**
* Represents a mixin to GameOptions.
* <p>
* Sets the default of the Auto-Jump option to false.
*/
@Mixin(GameOptions.class)
public class GameOptionsMixin
{
@Shadow
public boolean autoJump;
@Inject(method = "load", at = @At("HEAD"))
public void onInit(CallbackInfo ci)
{
// Set default value of the Auto-Jump option to false.
this.autoJump = false;
}
}

View File

@@ -18,9 +18,6 @@ import org.spongepowered.asm.mixin.Shadow;
@Mixin(KeyBinding.class)
public class KeyBindingMixin implements KeyBindingAccessor
{
@Shadow
private InputUtil.KeyCode keyCode;
@Shadow
private int timesPressed;

View File

@@ -61,25 +61,25 @@ public abstract class MinecraftClientMixin
public GameRenderer gameRenderer;
@Inject(method = "<init>", at = @At("RETURN"))
private void lambdacontrols_onInit(CallbackInfo ci)
private void onInit(CallbackInfo ci)
{
LambdaControlsClient.get().onMcInit((MinecraftClient) (Object) this);
}
@Inject(method = "render", at = @At("HEAD"))
private void lambdacontrols_onRender(boolean fullRender, CallbackInfo ci)
private void onRender(boolean fullRender, CallbackInfo ci)
{
LambdaControlsClient.get().onRender((MinecraftClient) (Object) (this));
}
@Inject(method = "disconnect(Lnet/minecraft/client/gui/screen/Screen;)V", at = @At("RETURN"))
private void lambdacontrols_onLeave(@Nullable Screen screen, CallbackInfo ci)
private void onLeave(@Nullable Screen screen, CallbackInfo ci)
{
LambdaControlsClient.get().onLeave();
}
@Inject(method = "doItemUse()V", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/hit/HitResult;getType()Lnet/minecraft/util/hit/HitResult$Type;"), locals = LocalCapture.CAPTURE_FAILEXCEPTION, cancellable = true)
private void lambdacontrols_onItemUse(CallbackInfo ci, Hand[] hands, int handCount, int handIndex, Hand hand, ItemStack stackInHand)
private void onItemUse(CallbackInfo ci, Hand[] hands, int handCount, int handIndex, Hand hand, ItemStack stackInHand)
{
if (!stackInHand.isEmpty() && this.player.pitch > 35.0F && LambdaControlsFeature.FRONT_BLOCK_PLACING.isAvailable()) {
if (this.crosshairTarget != null && this.crosshairTarget.getType() == HitResult.Type.MISS && this.player.onGround) {

View File

@@ -15,6 +15,7 @@ import me.lambdaurora.lambdacontrols.client.util.MouseAccessor;
import net.minecraft.client.Mouse;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.gen.Invoker;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@@ -25,22 +26,13 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(Mouse.class)
public abstract class MouseMixin implements MouseAccessor
{
@Shadow
protected abstract void onCursorPos(long window, double x, double y);
@Shadow
protected abstract void onMouseButton(long window, int button, int action, int mods);
@Invoker("onCursorPos")
public abstract void lambdacontrols_onCursorPos(long window, double x, double y);
@Inject(method = "lockCursor", at = @At("HEAD"), cancellable = true)
private void lambdacontrols_onMouseLocked(CallbackInfo ci)
private void onMouseLocked(CallbackInfo ci)
{
if (LambdaControlsClient.get().config.getControlsMode() == ControlsMode.TOUCHSCREEN)
ci.cancel();
}
@Override
public void lambdacontrols_onCursorPos(long window, double x, double y)
{
this.onCursorPos(window, x, y);
}
}

View File

@@ -36,7 +36,7 @@ public class SettingsScreenMixin extends Screen
private AbstractButtonWidget lambdacontrols_onInit(SettingsScreen screen, AbstractButtonWidget btn)
{
if (LambdaControlsClient.get().config.getControlsMode() == ControlsMode.CONTROLLER) {
return this.addButton(new ButtonWidget(btn.x, btn.y, btn.getWidth(), ((AbstractButtonWidgetAccessor) btn).lambdacontrols_getHeight(), btn.getMessage(),
return this.addButton(new ButtonWidget(btn.x, btn.y, btn.getWidth(), ((AbstractButtonWidgetAccessor) btn).getHeight(), btn.getMessage(),
b -> this.minecraft.openScreen(new ControllerControlsScreen(this, false))));
} else {
return this.addButton(btn);

View File

@@ -21,14 +21,14 @@ public interface ContainerScreenAccessor
*
* @return The left coordinate of the GUI.
*/
int lambdacontrols_getX();
int getX();
/**
* Gets the top coordinate of the GUI.
*
* @return The top coordinate of the GUI.
*/
int lambdacontrols_getY();
int getY();
/**
* Gets the slot at position.

View File

@@ -32,16 +32,19 @@
"depends": {
"fabricloader": ">=0.4.0",
"fabric": "*",
"minecraft": "1.15.x",
"minecraft": ">=1.15",
"spruceui": ">=1.3.4"
},
"recommends": {
"modmenu": ">=1.8.0+build.16",
"modmenu": ">=1.9.0",
"okzoomer": ">=1.0.4"
},
"suggests": {
"flamingo": "*"
},
"breaks": {
"modmenu": "<1.9.0"
},
"custom": {
"modmenu:clientsideOnly": true
}

View File

@@ -11,6 +11,7 @@
"ControlsOptionsScreenMixin",
"CreativeInventoryScreenAccessor",
"EntryListWidgetAccessor",
"GameOptionsMixin",
"GameRendererMixin",
"KeyBindingMixin",
"MinecraftClientMixin",

View File

@@ -4,7 +4,7 @@ org.gradle.jvmargs=-Xmx1G
# Fabric Properties
# check these on https://fabricmc.net/use
minecraft_version=1.15.2
yarn_mappings=1.15.2+build.9:v2
yarn_mappings=1.15.2+build.14:v2
loader_version=0.7.6+build.180
# Mod Properties
@@ -16,4 +16,4 @@ org.gradle.jvmargs=-Xmx1G
# currently not on the main fabric site, check on the maven: https://maven.fabricmc.net/net/fabricmc/fabric-api/fabric-api
fabric_version=0.4.29+build.290-1.15
spruceui_version=1.3.4
modmenu_version=1.10.1+build.30