1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ dependencies {
modImplementation libs.fapi

modImplementation libs.spruceui
modRuntimeOnly libs.modmenu
include libs.spruceui
}

Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ authors=ModFest
contributors=Prospector, Sisby folk, acikek
license=MIT
# Mod Version
baseVersion=0.5.4
baseVersion=0.6.0
# Branch Metadata
branch=1.21
tagBranch=1.21
Expand Down
2 changes: 2 additions & 0 deletions libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ yarn = "1.21.1+build.3"
fapi = "0.104.0+1.21.1"

spruceui = "5.1.0+1.21"
modmenu = "11.0.3"

[plugins]
loom = { id = "fabric-loom", version.ref = "loom" }
Expand All @@ -24,3 +25,4 @@ yarn = { group = "net.fabricmc", name = "yarn", version.ref = "yarn" }
fapi = { group = "net.fabricmc.fabric-api", name = "fabric-api", version.ref = "fapi" }

spruceui = { group = "dev.lambdaurora", name = "spruceui", version.ref = "spruceui" }
modmenu = { group = "com.terraformersmc", name = "modmenu", version.ref = "modmenu" }
40 changes: 31 additions & 9 deletions src/main/java/net/modfest/ballotbox/BallotBoxConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,46 @@
import folk.sisby.kaleido.api.ReflectiveConfig;
import folk.sisby.kaleido.lib.quiltconfig.api.annotations.Comment;
import folk.sisby.kaleido.lib.quiltconfig.api.values.TrackedValue;
import folk.sisby.kaleido.lib.quiltconfig.api.values.ValueList;

import java.util.List;

public class BallotBoxConfig extends ReflectiveConfig {
@Comment("Whether to replace the send feedback button with a voting button")
public final TrackedValue<Boolean> replace_feedback = value(true);
@Comment("Whether to add a voting button")
public final ButtonSettings voting_button = new ButtonSettings(List.of("menu.feedback", "menu.sendFeedback"), false, true);
@Comment("Whether to replace the bug report button with another link")
public final TrackedValue<Boolean> replace_bugs = value(true);
public final ButtonSettings custom_link_button = new ButtonSettings(List.of("menu.reportBugs"), false, true);
@Comment("The text to use to replace the bug report button")
public final TrackedValue<String> bug_text = value("ModFest Discord");
public final TrackedValue<String> custom_link_text = value("ModFest Discord");
@Comment("The link to use to replace the bug report button")
public final TrackedValue<String> bug_url = value("https://discord.gg/gn543Ee");
@Comment("Whether to replace the realms button on the title screen with a credits button")
public final TrackedValue<Boolean> replace_realms_credits = value(true);
@Comment("Whether to replace the player reporting button on the pause screen with a credits button")
public final TrackedValue<Boolean> replace_reporting_credits = value(true);
public final TrackedValue<String> custom_link_url = value("https://discord.gg/gn543Ee");
@Comment("Whether to add a credits button")
public final ButtonSettings credits_button = new ButtonSettings(List.of("menu.online", "menu.playerReporting"), true, true);
@Comment("The text to use for replacement credits but tons button")
public final TrackedValue<String> credits_text = value("Modpack Credits");
@Comment("The number of top results to show when displaying voting results")
public final TrackedValue<Integer> awardLimit = value(8);
@Comment("The closing date, as an ISO local date time - or an empty string for none")
public final TrackedValue<String> closingTime = value("2024-12-16T12:00:00");


public static class ButtonSettings extends Section {

public final TrackedValue<ButtonActionType> action_type = value(ButtonActionType.REPLACE);
public final TrackedValue<ValueList<String>> target_button;

public final TrackedValue<Boolean> apply_in_main_menu;
public final TrackedValue<Boolean> apply_in_pause_screen;
public ButtonSettings() {
this.target_button = list("");
this.apply_in_main_menu = value(false);
this.apply_in_pause_screen = value(false);
}

public ButtonSettings(List<String> target, boolean mainMenu, boolean pauseScreen) {
this.target_button = list("", target.toArray(new String[0]));
this.apply_in_main_menu = value(mainMenu);
this.apply_in_pause_screen = value(pauseScreen);
}
}
}
27 changes: 27 additions & 0 deletions src/main/java/net/modfest/ballotbox/ButtonActionType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package net.modfest.ballotbox;

import folk.sisby.kaleido.lib.quiltconfig.api.values.ComplexConfigValue;
import folk.sisby.kaleido.lib.quiltconfig.api.values.ConfigSerializableObject;

import java.util.Locale;

public enum ButtonActionType implements ConfigSerializableObject<String> {
INSERT_AFTER,
INSERT_BEFORE,
REPLACE;

@Override
public ConfigSerializableObject<String> convertFrom(String string) {
return ButtonActionType.valueOf(string.toUpperCase(Locale.ROOT));
}

@Override
public String getRepresentation() {
return this.name();
}

@Override
public ComplexConfigValue copy() {
return this;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package net.modfest.ballotbox.client;

public interface ApplyModifications {
void ballotbox$applyModifications();
}
57 changes: 57 additions & 0 deletions src/main/java/net/modfest/ballotbox/client/BallotBoxButtons.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package net.modfest.ballotbox.client;

import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.screen.ConfirmLinkScreen;
import net.minecraft.client.gui.screen.CreditsScreen;
import net.minecraft.client.gui.screen.GameMenuScreen;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.tooltip.Tooltip;
import net.minecraft.client.gui.widget.ButtonWidget;
import net.minecraft.client.gui.widget.ClickableWidget;
import net.minecraft.client.gui.widget.Widget;
import net.minecraft.sound.MusicType;
import net.minecraft.text.Text;
import net.minecraft.text.TranslatableTextContent;
import net.minecraft.util.Formatting;
import net.minecraft.util.Pair;
import net.modfest.ballotbox.BallotBox;
import net.modfest.ballotbox.BallotBoxConfig;
import net.modfest.ballotbox.packet.OpenVoteScreen;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;

public class BallotBoxButtons {
public static List<Pair<BallotBoxConfig.ButtonSettings, Function<Screen, ButtonWidget.Builder>>> createButtons() {
var list = new ArrayList<Pair<BallotBoxConfig.ButtonSettings, Function<Screen, ButtonWidget.Builder>>>();

list.add(new Pair<>(BallotBox.CONFIG.voting_button, (screen) -> ButtonWidget.builder(Text.of("Submission Voting"), b -> {
MinecraftClient.getInstance().setScreen(new VotingScreen());
ClientPlayNetworking.send(new OpenVoteScreen());
}).tooltip(BallotBoxClient.isOpen() ? null : Tooltip.of(Text.literal("Closed %s.".formatted(BallotBox.relativeTime(BallotBoxClient.closingTime))).formatted(Formatting.GRAY)))));

list.add(new Pair<>(BallotBox.CONFIG.custom_link_button,
(screen) -> ButtonWidget.builder(Text.of(BallotBox.CONFIG.custom_link_text.value()), ConfirmLinkScreen.opening(screen, BallotBox.CONFIG.custom_link_url.value()))));

list.add(new Pair<>(BallotBox.CONFIG.credits_button, (screen) -> ButtonWidget.builder(Text.of(BallotBox.CONFIG.credits_text.value()), b -> {
MinecraftClient.getInstance().setScreen(new CreditsScreen(false, () -> MinecraftClient.getInstance().setScreen(screen)));
MinecraftClient.getInstance().getMusicTracker().stop();
MinecraftClient.getInstance().getMusicTracker().play(MusicType.CREDITS);
})));

return list;
}

public static boolean match(Widget child, BallotBoxConfig.ButtonSettings settings) {
if (child instanceof ClickableWidget widget) {
if (widget.getMessage().getContent() instanceof TranslatableTextContent textContent && settings.target_button.value().contains(textContent.getKey())) {
return true;
}

return settings.target_button.value().contains(widget.getMessage().getString());
}
return false;
}
}
12 changes: 11 additions & 1 deletion src/main/java/net/modfest/ballotbox/client/BallotBoxClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents;
import net.fabricmc.fabric.api.event.Event;
import net.minecraft.client.MinecraftClient;
import net.minecraft.util.Identifier;
import net.modfest.ballotbox.BallotBox;
import net.modfest.ballotbox.packet.OpenVoteScreen;
import org.slf4j.Logger;
Expand Down Expand Up @@ -33,5 +36,12 @@ public void onInitializeClient() {
});
BallotBoxClientNetworking.init();
BallotBoxKeybinds.init();


var lateModify = Identifier.of("ballotbox", "late");
ScreenEvents.AFTER_INIT.addPhaseOrdering(Event.DEFAULT_PHASE, lateModify);
ScreenEvents.AFTER_INIT.register(lateModify, (client, screen, scaledWidth, scaledHeight) -> {
if (screen instanceof ApplyModifications applyModifications) applyModifications.ballotbox$applyModifications();
});
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package net.modfest.ballotbox.mixin.client;

import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Mutable;
import org.spongepowered.asm.mixin.gen.Accessor;

@Mixin(targets = "net.minecraft.client.gui.widget.GridWidget$Element")
public interface ElementAccessor {
@Accessor
int getRow();

@Mutable
@Accessor
void setRow(int row);

@Accessor
int getColumn();

@Mutable
@Accessor
void setColumn(int column);
}
Original file line number Diff line number Diff line change
@@ -1,59 +1,127 @@
package net.modfest.ballotbox.mixin.client;

import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
import com.llamalad7.mixinextras.injector.ModifyReceiver;
import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.screen.ConfirmLinkScreen;
import net.minecraft.client.gui.screen.CreditsScreen;
import net.minecraft.client.gui.screen.GameMenuScreen;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.tooltip.Tooltip;
import net.minecraft.client.gui.widget.ButtonWidget;
import net.minecraft.client.gui.widget.GridWidget;
import net.minecraft.client.gui.widget.Widget;
import net.minecraft.sound.MusicType;
import net.minecraft.client.gui.widget.SimplePositioningWidget;
import net.minecraft.client.gui.widget.TexturedButtonWidget;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
import net.modfest.ballotbox.BallotBox;
import net.modfest.ballotbox.ButtonActionType;
import net.modfest.ballotbox.client.BallotBoxButtons;
import net.modfest.ballotbox.client.BallotBoxClient;
import net.modfest.ballotbox.client.VotingScreen;
import net.modfest.ballotbox.packet.OpenVoteScreen;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;

@Mixin(GameMenuScreen.class)
public class GameMenuScreenMixin {
import java.util.function.Consumer;

@Mixin(value = GameMenuScreen.class, priority = 1200)
public abstract class GameMenuScreenMixin extends Screen {
private static ButtonWidget ballotbox$voteButton = null;

@WrapOperation(method = "addFeedbackAndBugsButtons", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/widget/GridWidget$Adder;add(Lnet/minecraft/client/gui/widget/Widget;)Lnet/minecraft/client/gui/widget/Widget;", ordinal = 0))
private static Widget replaceSendFeedback(GridWidget.Adder instance, Widget widget, Operation<Widget> original, Screen parentScreen) {
if (!BallotBox.CONFIG.replace_feedback.value() || !BallotBoxClient.isEnabled(MinecraftClient.getInstance())) return original.call(instance, widget);
ballotbox$voteButton = ButtonWidget.builder(Text.of("Submission Voting"), b -> {
MinecraftClient.getInstance().setScreen(new VotingScreen());
ClientPlayNetworking.send(new OpenVoteScreen());
}).width(98).tooltip(BallotBoxClient.isOpen() ? null : Tooltip.of(Text.literal("Closed %s.".formatted(BallotBox.relativeTime(BallotBoxClient.closingTime))).formatted(Formatting.GRAY))).build();
ballotbox$voteButton.active = BallotBoxClient.isOpen();
return instance.add(ballotbox$voteButton);
protected GameMenuScreenMixin(Text title) {
super(title);
}

@WrapOperation(method = "addFeedbackAndBugsButtons", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/widget/GridWidget$Adder;add(Lnet/minecraft/client/gui/widget/Widget;)Lnet/minecraft/client/gui/widget/Widget;", ordinal = 1))
private static Widget replaceReportBugs(GridWidget.Adder instance, Widget widget, Operation<Widget> original, Screen parentScreen) {
if (!BallotBox.CONFIG.replace_bugs.value() || !BallotBoxClient.isEnabled(MinecraftClient.getInstance())) return original.call(instance, widget);
return instance.add(ButtonWidget.builder(Text.of(BallotBox.CONFIG.bug_text.value()), ConfirmLinkScreen.opening(parentScreen, BallotBox.CONFIG.bug_url.value())).width(98).build());
}
@Inject(method = "initWidgets", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/widget/GridWidget;forEachChild(Ljava/util/function/Consumer;)V"))
private void onInitWidgets(CallbackInfo ci, @Local GridWidget instance) {
var reorganize = false;
var children = ((GridWidgetAccessor) instance).getChildren();
var grids = ((GridWidgetAccessor) instance).getGrids();
for (var pair : BallotBoxButtons.createButtons()) {
var settings = pair.getLeft();
if (!settings.apply_in_pause_screen.value()) {
continue;
}

if (settings.action_type.value() == ButtonActionType.REPLACE) {
for (int i = 0; i < children.size(); i++) {
var child = children.get(i);
if (BallotBoxButtons.match(child, settings)) {
var button = pair.getRight().apply(this).width(child.getWidth()).position(child.getX(), child.getY()).build();
if (settings == BallotBox.CONFIG.voting_button) {
ballotbox$voteButton = button;
}
children.set(i, button);
break;
}
}
} else {
reorganize = true;
if (children.size() != grids.size()) {
// State is broken! Assume it's modmenu breaking it.
for (int i = 1; i < children.size(); i++) {
var child = children.get(i);
if (child.getClass().getName().equals("com.terraformersmc.modmenu.gui.widget.ModMenuButtonWidget")) {
children.remove(i);
instance.add(child, ((ElementAccessor) grids.get(i - 1)).getRow() + 1, 0, 1, 2);
i++;
var newChild = children.removeLast();
var grid = grids.removeLast();
children.add(i, newChild);
grids.add(i, grid);

for (i++; i < grids.size(); i++) {
grid = grids.get(i);
((ElementAccessor) grid).setRow(((ElementAccessor) grid).getRow() + 2);
}

@WrapOperation(method = "initWidgets", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/widget/GridWidget$Adder;add(Lnet/minecraft/client/gui/widget/Widget;)Lnet/minecraft/client/gui/widget/Widget;", ordinal = 6))
private Widget replacePlayerReporting(GridWidget.Adder instance, Widget widget, Operation<Widget> original) {
if (!BallotBox.CONFIG.replace_reporting_credits.value()) return original.call(instance, widget);
return instance.add(ButtonWidget.builder(Text.of(BallotBox.CONFIG.credits_text.value()), b -> {
MinecraftClient.getInstance().setScreen(new CreditsScreen(false, () -> MinecraftClient.getInstance().setScreen((GameMenuScreen) (Object) this)));
MinecraftClient.getInstance().getMusicTracker().stop();
MinecraftClient.getInstance().getMusicTracker().play(MusicType.CREDITS);
}).width(98).build());
break;
} else if (child.getClass().getName().equals("com.terraformersmc.modmenu.gui.widget.UpdateCheckerTexturedButtonWidget")) {
children.remove(i);
this.addDrawableChild((TexturedButtonWidget) child);
break;
}
}
}

for (int i = 0; i < children.size(); i++) {
var child = children.get(i);
if (BallotBoxButtons.match(child, settings)) {
var after = settings.action_type.value() == ButtonActionType.INSERT_AFTER;
var currentGrid = grids.get(i);
while (after && i < children.size() - 1 && ((ElementAccessor) currentGrid).getRow() == ((ElementAccessor) grids.get(i)).getRow()) {
i++;
}
while (!after && i > 1 && ((ElementAccessor) currentGrid).getRow() == ((ElementAccessor) grids.get(i - 1)).getRow()) {
--i;
}

var button = pair.getRight().apply(this).width(204).build();
if (settings == BallotBox.CONFIG.voting_button) {
ballotbox$voteButton = button;
}
var isLast = i == children.size() - 1;
instance.add(button, ((ElementAccessor) grids.get(i)).getRow() + (after ? 1 : 0), 0, 1, 2);
if (isLast) {
break;
}
var newChild = children.removeLast();
var grid = grids.removeLast();
children.add(i, newChild);
grids.add(i, grid);
i++;
for (; i < grids.size(); i++) {
grid = grids.get(i);
((ElementAccessor) grid).setRow(((ElementAccessor) grid).getRow() + 2);
}
break;
}
}
}
}
if (reorganize) {
instance.refreshPositions();
SimplePositioningWidget.setPos(instance, 0, 0, this.width, this.height, 0.5F, 0.25F);
}
}

@Inject(method = "render", at = @At("TAIL"))
Expand Down
Loading