Skip to content

Commit

Permalink
data driven voting options, configurable pause replacement, modmenu s…
Browse files Browse the repository at this point in the history
…tyle icon getting
  • Loading branch information
sisby-folk committed Nov 26, 2024
1 parent b032d29 commit ae2f902
Show file tree
Hide file tree
Showing 13 changed files with 1,253 additions and 58 deletions.
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ Players can vote for options (usually mods) across multiple award categories usi

**Features:**

- Server-driven voting UI - the server sends voting options, categories, and any previous vote selections.
- Data-driven vote categories.
- API-driven vote options intended for the ModFest platform API.
- Selections saved to persistent world state.
- Use `/votes` (op 4) to tally the top options for each category.
- Tracking of vote closing date (via config), and automatic vote closing.
- Server-driven voting UI - the server sends voting options, categories, and any previous vote selections
- Data-driven vote categories
- Data-driven vote options
- Vote options show mod icons when using mod IDs
- Selections saved to persistent world state
- Use `/votes` (op 4) to tally the top options for each category
- Tracking of vote closing date (via config), and automatic vote closing

---

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.4.3
baseVersion=0.5.0
# Branch Metadata
branch=1.21
tagBranch=1.21
Expand Down
8 changes: 4 additions & 4 deletions libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ minotaur = "2.+"

kaleidoConfig = "0.3.1+1.3.1"

mc = "1.21"
fl = "0.15.11"
yarn = "1.21+build.9"
fapi = "0.100.7+1.21"
mc = "1.21.1"
fl = "0.16.7"
yarn = "1.21.1+build.3"
fapi = "0.104.0+1.21.1"

spruceui = "5.1.0+1.21"

Expand Down
10 changes: 5 additions & 5 deletions src/main/java/net/modfest/ballotbox/BallotBoxConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@
import folk.sisby.kaleido.lib.quiltconfig.api.values.TrackedValue;

public class BallotBoxConfig extends ReflectiveConfig {
@Comment("The event ID to use when making requests to the API")
public final TrackedValue<String> eventId = value("carnival");
@Comment("The URL to request vote options from, with an optional event ID %s placeholder")
public final TrackedValue<String> options_url = value("https://platform.modfest.net/event/%s/submissions");
@Comment("Whether to replace the send feedback button with a voting button")
public final TrackedValue<Boolean> replace_feedback = value(true);
@Comment("Whether to replace the bug report button with another link")
public final TrackedValue<Boolean> replace_bugs = value(true);
@Comment("The text to use to replace the bug report button")
public final TrackedValue<String> bug_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("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-07-28T12:00:00");
public final TrackedValue<String> closingTime = value("2024-12-16T12:00:00");
}
18 changes: 3 additions & 15 deletions src/main/java/net/modfest/ballotbox/BallotBoxPlatformClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,41 +13,29 @@
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;

public class BallotBoxPlatformClient {
public static final Identifier CATEGORIES_DATA = Identifier.of(BallotBox.ID, "ballot/categories.json");
public static final Identifier OPTIONS_DATA = Identifier.of(BallotBox.ID, "ballot/options.json");
public final static Gson GSON = new Gson();
public static Map<String, VotingOption> options = new ConcurrentHashMap<>();
public static Map<String, VotingCategory> categories = new ConcurrentHashMap<>();

public static void init(ResourceManager resourceManager) {
if (options.isEmpty()) options = getOptions(BallotBox.CONFIG.eventId.value());
try {
categories.clear();
GSON.fromJson(new BufferedReader(new InputStreamReader(resourceManager.getResourceOrThrow(CATEGORIES_DATA).getInputStream())), JsonArray.class).asList().stream().map(e -> VotingCategory.CODEC.decode(JsonOps.INSTANCE, e).getOrThrow().getFirst()).forEach(category -> categories.put(category.id(), category));
options.clear();
GSON.fromJson(new BufferedReader(new InputStreamReader(resourceManager.getResourceOrThrow(OPTIONS_DATA).getInputStream())), JsonArray.class).asList().stream().map(e -> VotingOption.CODEC.decode(JsonOps.INSTANCE, e).getOrThrow().getFirst()).forEach(option -> options.put(option.id(), option));
} catch (IOException e) {
throw new RuntimeException(e);
}
}

private static Map<String, VotingOption> getOptions(String eventId) {
String uri = BallotBox.CONFIG.options_url.value().formatted(eventId);
BallotBox.LOGGER.info("[BallotBox] Retrieving vote options from %s!".formatted(uri));
Map<String, VotingOption> options = new ConcurrentHashMap<>();
try {
GSON.fromJson(new BufferedReader(new InputStreamReader((new URI(uri)).toURL().openStream())), JsonArray.class).asList().stream().map(e -> VotingOption.CODEC.decode(JsonOps.INSTANCE, e).getOrThrow().getFirst()).forEach(option -> options.put(option.id(), option));
} catch (IOException | URISyntaxException e) {
BallotBox.LOGGER.error("[BallotBox] Failed to retrieve ballotbox options from specified url", e);
}
return options;
}

public static CompletableFuture<VotingSelections> getSelections(UUID playerId) {
return CompletableFuture.completedFuture(BallotBox.STATE.selections().getOrDefault(playerId, new VotingSelections(HashMultimap.create())));
}
Expand Down
31 changes: 10 additions & 21 deletions src/main/java/net/modfest/ballotbox/client/VotingScreen.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
import dev.lambdaurora.spruceui.widget.container.tabbed.SpruceTabbedWidget;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
import net.fabricmc.loader.api.FabricLoader;
import net.fabricmc.loader.api.ModContainer;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.texture.NativeImage;
import net.minecraft.client.texture.NativeImageBackedTexture;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
Expand All @@ -28,18 +28,14 @@
import net.modfest.ballotbox.mixin.client.OptionEntryAccessor;
import net.modfest.ballotbox.packet.C2SUpdateVote;
import net.modfest.ballotbox.packet.S2CVoteScreenData;
import net.modfest.ballotbox.util.ModMetaUtil;
import org.lwjgl.glfw.GLFW;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

Expand All @@ -52,7 +48,7 @@ public class VotingScreen extends SpruceScreen {
);

public static final Identifier LOCKUP_TEXTURE = Identifier.of("modfest", "textures/art/graphics/lockup-transparent.png");
public static final int LOCKUP_TEXTURE_WIDTH = 878;
public static final int LOCKUP_TEXTURE_WIDTH = 1101;
public static final int LOCKUP_TEXTURE_HEIGHT = 256;

protected final Multimap<String, String> previousSelections = HashMultimap.create();
Expand All @@ -65,7 +61,6 @@ public class VotingScreen extends SpruceScreen {
protected int sidePanelVerticalPadding;
protected Map<String, CategoryContainerWidget> categoryWidgets = new ConcurrentHashMap<>();

private final Set<String> modIconChecked = new HashSet<>();
private final Map<String, Identifier> modIconCache = new ConcurrentHashMap<>();

public VotingScreen() {
Expand Down Expand Up @@ -184,19 +179,13 @@ public VotingOptionButtonWidget(Position position, int width, int height, Voting
this.parent = parent;
selected = selections.containsEntry(category.id(), option.id());
this.prohibited = prohibited;
if (!modIconChecked.contains(option.id())) {
FabricLoader.getInstance().getModContainer(option.id())
.or(() -> FabricLoader.getInstance().getModContainer(option.id().replace('_', '-')))
.or(() -> FabricLoader.getInstance().getModContainer(option.id().replace("_", "")))
.ifPresent(mod -> mod.getMetadata().getIconPath(16).flatMap(mod::findPath).ifPresent(path -> {
try (InputStream inputStream = Files.newInputStream(path)) {
Identifier textureId = Identifier.of(BallotBox.ID, mod.getMetadata().getId() + "_icon");
modIconCache.put(option.id(), textureId);
this.client.getTextureManager().registerTexture(textureId, new NativeImageBackedTexture(NativeImage.read(inputStream)));
} catch (IOException ignored) {
}
}));
modIconChecked.add(option.id());
if (!modIconCache.containsKey(option.id())) {
modIconCache.put(option.id(), Identifier.of(BallotBox.ID, option.id() + "_icon"));
Optional<ModContainer> mod = FabricLoader.getInstance().getModContainer(option.id())
.or(() -> FabricLoader.getInstance().getModContainer(option.id().replace("_", "-")))
.or(() -> FabricLoader.getInstance().getModContainer(option.id().replace("_", "")));
NativeImageBackedTexture icon = mod.isPresent() ? ModMetaUtil.getIcon(mod.get(), 64 * this.client.options.getGuiScale().getValue()) : ModMetaUtil.getMissingIcon();
this.client.getTextureManager().registerTexture(modIconCache.get(option.id()), icon);
}
texture = modIconCache.get(option.id());
if (option.platform().type().equals("modrinth")) url = "https://modrinth.com/mod/%s".formatted(option.platform().project_id()); // Use project ID later
Expand Down
5 changes: 4 additions & 1 deletion src/main/java/net/modfest/ballotbox/data/VotingOption.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;

public record VotingOption(String id, String name, String description, Platform platform) {
import java.util.Optional;

public record VotingOption(String id, Optional<String> mod_id, String name, String description, Platform platform) {
public static final Codec<VotingOption> CODEC = RecordCodecBuilder.create(instance -> instance.group(
Codec.STRING.fieldOf("id").forGetter(VotingOption::id),
Codec.STRING.optionalFieldOf("mod_id").forGetter(VotingOption::mod_id),
Codec.STRING.fieldOf("name").forGetter(VotingOption::name),
Codec.STRING.fieldOf("description").forGetter(VotingOption::description),
Platform.CODEC.fieldOf("platform").forGetter(VotingOption::platform)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public class GameMenuScreenMixin {

@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, GridWidget.Adder gridAdder) {
if (!BallotBoxClient.isEnabled(MinecraftClient.getInstance())) return original.call(instance, widget);
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());
Expand All @@ -40,7 +40,7 @@ private static Widget replaceSendFeedback(GridWidget.Adder instance, Widget widg

@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, GridWidget.Adder gridAdder) {
if (!BallotBoxClient.isEnabled(MinecraftClient.getInstance())) return original.call(instance, widget);
if (!BallotBox.CONFIG.replace_bugs.value() || !BallotBoxClient.isEnabled(MinecraftClient.getInstance())) return original.call(instance, widget);
return gridAdder.add(ButtonWidget.builder(Text.of(BallotBox.CONFIG.bug_text.value()), ConfirmLinkScreen.opening(parentScreen, BallotBox.CONFIG.bug_url.value())).width(98).build());
}

Expand Down
82 changes: 82 additions & 0 deletions src/main/java/net/modfest/ballotbox/util/ModMetaUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package net.modfest.ballotbox.util;

import net.fabricmc.loader.api.FabricLoader;
import net.fabricmc.loader.api.ModContainer;
import net.fabricmc.loader.api.metadata.ModMetadata;
import net.minecraft.client.texture.NativeImage;
import net.minecraft.client.texture.NativeImageBackedTexture;
import net.modfest.ballotbox.BallotBox;
import org.apache.commons.lang3.Validate;

import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
* Liberally stolen from ModMenu. Thanks ModMenu!
*/
public class ModMetaUtil {
private static final Map<Path, NativeImageBackedTexture> modIconCache = new HashMap<>();

public static NativeImageBackedTexture createIcon(ModContainer iconSource, String iconPath) {
try {
Path path = iconSource.getPath(iconPath);
NativeImageBackedTexture cachedIcon = modIconCache.get(path);
if (cachedIcon != null) {
return cachedIcon;
}
cachedIcon = modIconCache.get(path);
if (cachedIcon != null) {
return cachedIcon;
}
try (InputStream inputStream = Files.newInputStream(path)) {
NativeImage image = NativeImage.read(Objects.requireNonNull(inputStream));
Validate.validState(image.getHeight() == image.getWidth(), "Must be square icon");
NativeImageBackedTexture tex = new NativeImageBackedTexture(image);
modIconCache.put(path, tex);
return tex;
}

} catch (IllegalStateException e) {
if (e.getMessage().equals("Must be square icon")) {
BallotBox.LOGGER.error("Mod icon must be a square for icon source {}: {}",
iconSource.getMetadata().getId(),
iconPath,
e
);
}

return null;
} catch (Throwable t) {
if (!iconPath.equals("assets/" + iconSource.getMetadata().getId() + "/icon.png")) {
BallotBox.LOGGER.error("Invalid mod icon for icon source {}: {}", iconSource.getMetadata().getId(), iconPath, t);
}
return null;
}
}

public static NativeImageBackedTexture getMissingIcon() {
return createIcon(
FabricLoader.getInstance()
.getModContainer(BallotBox.ID)
.orElseThrow(() -> new RuntimeException("Cannot get ModContainer for Fabric mod with id " + BallotBox.ID)),
"assets/" + BallotBox.ID + "/unknown_icon.png"
);
}

public static NativeImageBackedTexture getIcon(ModContainer mod, int preferredSize) {
ModMetadata meta = mod.getMetadata();
String modId = meta.getId();
String iconPath = meta.getIconPath(preferredSize).orElse("assets/" + modId + "/icon.png");
final String finalIconSourceId = modId;
ModContainer iconSource = FabricLoader.getInstance()
.getModContainer(modId)
.orElseThrow(() -> new RuntimeException("Cannot get ModContainer for Fabric mod with id " + finalIconSourceId));
NativeImageBackedTexture icon = createIcon(iconSource, iconPath);
if (icon == null) return getMissingIcon();
return icon;
}
}
Binary file added src/main/resources/assets/ballotbox/unknown_icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 17 additions & 3 deletions src/main/resources/data/ballotbox/ballot/categories.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
[
{
"id": "best_carnival_themed",
"name": "Best Carnival-Themed",
"id": "best_time_themed",
"name": "Best Time-Themed",
"description": "The best of the best!",
"type": "theme",
"limit": 5
},
{
"id": "best_technology_themed",
"name": "Best Technology-Themed",
"description": "The best of the best!",
"type": "theme",
"limit": 5
},
{
"id": "best_throwbacks_themed",
"name": "Best Throwbacks-Themed",
"description": "The best of the best!",
"type": "theme",
"limit": 5
Expand All @@ -27,4 +41,4 @@
"type": "community",
"limit": 3
}
]
]
Loading

0 comments on commit ae2f902

Please sign in to comment.