Skip to content

Commit

Permalink
open/close voting mechanics, vote reminder
Browse files Browse the repository at this point in the history
  • Loading branch information
sisby-folk committed Jul 23, 2024
1 parent 34177ae commit c2875cb
Show file tree
Hide file tree
Showing 12 changed files with 176 additions and 42 deletions.
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ org.gradle.configureondemand=true
# Enable advanced multi-module optimizations (share tiny-remaper instance between projects)
fabric.loom.multiProjectOptimisation=true
# Mod Properties
baseVersion = 0.3.1
baseVersion = 0.4.0
defaultBranch = 1.21
branch = 1.21
53 changes: 50 additions & 3 deletions src/main/java/net/modfest/ballotbox/BallotBox.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,62 @@
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerWorldEvents;
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.World;
import net.modfest.ballotbox.data.VotingCategory;
import net.modfest.ballotbox.data.VotingSelections;
import net.modfest.ballotbox.packet.S2CGameJoin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.DateTimeException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.concurrent.TimeUnit;

public class BallotBox implements ModInitializer {
public static final String ID = "ballotbox";
public static final Logger LOGGER = LoggerFactory.getLogger(ID);
public static final BallotBoxConfig CONFIG = BallotBoxConfig.createToml(FabricLoader.getInstance().getConfigDir(), "", ID, BallotBoxConfig.class);
public static final String STATE_KEY = "ballotbox_ballots";
public static BallotState STATE = null;
public static Instant closingTime = null;

public static String relativeTime(Instant then) {
Instant now = Instant.now();
long offset = now.toEpochMilli() - then.toEpochMilli();
long days = TimeUnit.MILLISECONDS.toDays(Math.abs(offset));
if (days > 0) return (offset > 0 ? "%s days ago" : "in %s days").formatted(days);
long hours = TimeUnit.MILLISECONDS.toHours(Math.abs(offset));
if (hours > 0) return (offset > 0 ? "%s hours ago" : "in %s hours").formatted(days);
long minutes = TimeUnit.MILLISECONDS.toMinutes(Math.abs(offset));
if (minutes > 0) return (offset > 0 ? "%s minutes ago" : "in %s minutes").formatted(days);
return (offset > 0 ? "%s seconds ago" : "in %s seconds").formatted(TimeUnit.MILLISECONDS.toSeconds(offset));
}

public static boolean isEnabled(MinecraftServer server) {
return !server.isSingleplayer();
}

public static boolean isOpen() {
return closingTime == null || closingTime.isAfter(Instant.now());
}

public static Instant parseClosingTime(String value) {
try {
if (!value.isEmpty()) return LocalDateTime.parse(value).toInstant(ZoneOffset.UTC);
} catch (DateTimeException e) {
LOGGER.error("Failed to parse configured closing time '{}', ignoring...", value, e);
}
return null;
}

@Override
public void onInitialize() {
LOGGER.info("[BallotBox] Initialized!");
closingTime = parseClosingTime(CONFIG.closingTime.value());
BallotBoxNetworking.init();
CommandRegistrationCallback.EVENT.register(BallotBoxCommands::register);
ServerWorldEvents.LOAD.register(((server, world) -> {
Expand All @@ -28,12 +68,19 @@ public void onInitialize() {
}
}));
ServerLifecycleEvents.SERVER_STARTED.register((server -> {
if (server.isSingleplayer()) return;
if (!isEnabled(server)) return;
BallotBoxPlatformClient.init(server.getResourceManager());
}));
ServerLifecycleEvents.END_DATA_PACK_RELOAD.register(((server, resourceManager, success) -> {
if (server.isSingleplayer()) return;
if (!isEnabled(server)) return;
BallotBoxPlatformClient.init(resourceManager);
}));
ServerPlayConnectionEvents.JOIN.register(((handler, sender, server) -> {
VotingSelections selections = STATE.selections().get(handler.getPlayer().getUuid());
int totalVotes = BallotBoxPlatformClient.categories.values().stream().mapToInt(VotingCategory::limit).sum();
int remainingVotes = totalVotes - (selections == null ? 0 : selections.votes().size());
sender.sendPacket(new S2CGameJoin(CONFIG.closingTime.value(), remainingVotes));
}));
LOGGER.info("[BallotBox] Initialized!");
}
}
33 changes: 17 additions & 16 deletions src/main/java/net/modfest/ballotbox/BallotBoxCommands.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import com.google.common.collect.Multisets;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
import net.minecraft.command.CommandRegistryAccess;
import net.minecraft.server.command.CommandManager;
Expand All @@ -15,7 +14,7 @@
import net.minecraft.util.Formatting;
import net.modfest.ballotbox.data.VotingCategory;
import net.modfest.ballotbox.data.VotingOption;
import net.modfest.ballotbox.packet.OpenVoteScreenPacket;
import net.modfest.ballotbox.packet.OpenVoteScreen;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
Expand All @@ -27,14 +26,7 @@ public interface BallotBoxCommandExecutor {
}

public static int execute(CommandContext<ServerCommandSource> context, String arg, BallotBoxCommandExecutor executor) {
ServerPlayerEntity player;
try {
player = context.getSource().getPlayerOrThrow();
} catch (CommandSyntaxException e) {
BallotBox.LOGGER.error("[BallotBox] Commands cannot be invoked by a non-player");
return 0;
}

ServerPlayerEntity player = context.getSource().getPlayer();
try {
return executor.execute(player, arg != null ? context.getArgument(arg, String.class) : null, t -> context.getSource().sendFeedback(() -> t, false));
} catch (Exception e) {
Expand All @@ -47,23 +39,24 @@ public static int execute(CommandContext<ServerCommandSource> context, String ar
public static void register(CommandDispatcher<ServerCommandSource> dispatcher, CommandRegistryAccess context, CommandManager.RegistrationEnvironment environment) {
dispatcher.register(
CommandManager.literal("vote")
.executes(c -> execute(c, null, BallotBoxCommands::vote))
.executes(c -> execute(c, null, (p, a1, f) -> BallotBoxCommands.vote(p, f)))
);
dispatcher.register(
CommandManager.literal("votes")
.requires(s -> s.hasPermissionLevel(4))
.executes(c -> execute(c, null, BallotBoxCommands::votes))
.executes(c -> execute(c, null, (p, a1, f) -> BallotBoxCommands.votes(f)))
);
}

private static int votes(ServerPlayerEntity player, String ignored, Consumer<Text> feedback) {
private static int votes(Consumer<Text> feedback) {
Map<VotingCategory, Multiset<VotingOption>> votes = new ConcurrentHashMap<>();
BallotBox.STATE.selections().forEach((uuid, selections) -> selections.votes().forEach((category, option) -> {
if (BallotBoxPlatformClient.categories.containsKey(category) && BallotBoxPlatformClient.options.containsKey(option)) {
votes.computeIfAbsent(BallotBoxPlatformClient.categories.get(category), k -> HashMultiset.create()).add(BallotBoxPlatformClient.options.get(option));
}
}));
feedback.accept(Text.literal("[BallotBox] ").formatted(Formatting.GREEN).append(Text.literal("%d players have submitted %d votes so far!".formatted(BallotBox.STATE.selections().size(), votes.values().stream().mapToInt(Multiset::size).sum())).formatted(Formatting.AQUA)));
if (BallotBox.closingTime != null) feedback.accept(Text.literal("[BallotBox] ").formatted(Formatting.GREEN).append(Text.literal((BallotBox.isOpen() ? "Voting closes %s." : "Voting closed %s.").formatted(BallotBox.relativeTime(BallotBox.closingTime))).formatted(Formatting.AQUA)));
feedback.accept(Text.literal("[BallotBox] ").formatted(Formatting.GREEN).append(Text.literal("%d players have submitted %d votes!".formatted(BallotBox.STATE.selections().size(), votes.values().stream().mapToInt(Multiset::size).sum())).formatted(Formatting.AQUA)));
votes.forEach((category, options) -> {
feedback.accept(Text.literal("[BallotBox] ").formatted(Formatting.GREEN).append(Text.literal("--- Top %d for %s ---".formatted(BallotBox.CONFIG.awardLimit.value(), category.name())).formatted(Formatting.LIGHT_PURPLE)));
int i = 0;
Expand All @@ -77,8 +70,16 @@ private static int votes(ServerPlayerEntity player, String ignored, Consumer<Tex
return 0;
}

private static int vote(ServerPlayerEntity player, String ignored, Consumer<Text> feedback) {
ServerPlayNetworking.send(player, new OpenVoteScreenPacket());
private static int vote(ServerPlayerEntity player, Consumer<Text> feedback) {
if (player == null) {
feedback.accept(Text.literal("[BallotBox] ").formatted(Formatting.GREEN).append(Text.literal("Vote cannot be invoked by a non-player").formatted(Formatting.RED)));
return 0;
}
if (!BallotBox.isOpen()) {
feedback.accept(Text.literal("[BallotBox] ").formatted(Formatting.GREEN).append(Text.literal("Voting is unavailable! Voting closed %s.".formatted(BallotBox.relativeTime(BallotBox.closingTime))).formatted(Formatting.RED)));
return 0;
}
ServerPlayNetworking.send(player, new OpenVoteScreen());
BallotBoxNetworking.sendVoteScreenData(player);
return 1;
}
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/net/modfest/ballotbox/BallotBoxConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ public class BallotBoxConfig extends ReflectiveConfig {
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");
}
14 changes: 9 additions & 5 deletions src/main/java/net/modfest/ballotbox/BallotBoxNetworking.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,34 @@
import net.minecraft.util.Formatting;
import net.modfest.ballotbox.packet.C2SUpdateVote;
import net.modfest.ballotbox.data.VotingCategory;
import net.modfest.ballotbox.packet.OpenVoteScreenPacket;
import net.modfest.ballotbox.packet.OpenVoteScreen;
import net.modfest.ballotbox.packet.S2CGameJoin;
import net.modfest.ballotbox.packet.S2CVoteScreenData;

import java.util.ArrayList;

public class BallotBoxNetworking {
public static void init() {
PayloadTypeRegistry.playC2S().register(C2SUpdateVote.ID, C2SUpdateVote.CODEC);
PayloadTypeRegistry.playC2S().register(OpenVoteScreenPacket.ID, OpenVoteScreenPacket.CODEC);
PayloadTypeRegistry.playS2C().register(OpenVoteScreenPacket.ID, OpenVoteScreenPacket.CODEC);
PayloadTypeRegistry.playC2S().register(OpenVoteScreen.ID, OpenVoteScreen.CODEC);
PayloadTypeRegistry.playS2C().register(S2CGameJoin.ID, S2CGameJoin.CODEC);
PayloadTypeRegistry.playS2C().register(OpenVoteScreen.ID, OpenVoteScreen.CODEC);
PayloadTypeRegistry.playS2C().register(S2CVoteScreenData.ID, S2CVoteScreenData.CODEC);
ServerPlayNetworking.registerGlobalReceiver(C2SUpdateVote.ID, BallotBoxNetworking::handleUpdateVote);
ServerPlayNetworking.registerGlobalReceiver(OpenVoteScreenPacket.ID, BallotBoxNetworking::handleOpenVoteScreen);
ServerPlayNetworking.registerGlobalReceiver(OpenVoteScreen.ID, BallotBoxNetworking::handleOpenVoteScreen);
}

public static void sendVoteScreenData(ServerPlayerEntity player) {
if (!BallotBox.isOpen()) return;
BallotBoxPlatformClient.getSelections(player.getUuid()).thenAccept(selections -> ServerPlayNetworking.send(player, new S2CVoteScreenData(new ArrayList<>(BallotBoxPlatformClient.categories.values()), new ArrayList<>(BallotBoxPlatformClient.options.values()), selections)));
}

private static void handleOpenVoteScreen(OpenVoteScreenPacket packet, ServerPlayNetworking.Context context) {
private static void handleOpenVoteScreen(OpenVoteScreen packet, ServerPlayNetworking.Context context) {
sendVoteScreenData(context.player());
}

private static void handleUpdateVote(C2SUpdateVote packet, ServerPlayNetworking.Context context) {
if (!BallotBox.isOpen()) return;
BallotBoxPlatformClient.putSelections(context.player().getUuid(), packet.selections()).thenAccept(success -> {
if (success) {
context.player().sendMessage(Text.literal("[BallotBox] ").formatted(Formatting.AQUA).append(Text.literal("Votes Saved! You assigned %s/%s votes over %s/%s categories.".formatted(packet.selections().votes().size(), BallotBoxPlatformClient.categories.values().stream().mapToInt(VotingCategory::limit).sum(), packet.selections().votes().keySet().size(), BallotBoxPlatformClient.categories.size())).formatted(Formatting.GREEN)), true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,24 @@

import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
import net.minecraft.client.MinecraftClient;
import net.modfest.ballotbox.packet.OpenVoteScreenPacket;
import net.modfest.ballotbox.BallotBox;
import net.modfest.ballotbox.packet.OpenVoteScreen;
import net.modfest.ballotbox.packet.S2CGameJoin;
import net.modfest.ballotbox.packet.S2CVoteScreenData;

public class BallotBoxClientNetworking {
public static void init() {
ClientPlayNetworking.registerGlobalReceiver(S2CGameJoin.ID, BallotBoxClientNetworking::handleGameJoin);
ClientPlayNetworking.registerGlobalReceiver(S2CVoteScreenData.ID, BallotBoxClientNetworking::handleVoteScreenData);
ClientPlayNetworking.registerGlobalReceiver(OpenVoteScreenPacket.ID, BallotBoxClientNetworking::handleOpenVoteScreen);
ClientPlayNetworking.registerGlobalReceiver(OpenVoteScreen.ID, BallotBoxClientNetworking::handleOpenVoteScreen);
}

private static void handleOpenVoteScreen(OpenVoteScreenPacket packet, ClientPlayNetworking.Context context) {
private static void handleGameJoin(S2CGameJoin packet, ClientPlayNetworking.Context context) {
NotBallotBoxClient.closingTime = BallotBox.parseClosingTime(packet.closingTime());
NotBallotBoxClient.remainingVotes = packet.remainingVotes();
}

private static void handleOpenVoteScreen(OpenVoteScreen packet, ClientPlayNetworking.Context context) {
MinecraftClient.getInstance().setScreen(new VotingScreen());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.option.KeyBinding;
import net.minecraft.client.util.InputUtil;
import net.modfest.ballotbox.packet.OpenVoteScreenPacket;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
import net.modfest.ballotbox.BallotBox;
import net.modfest.ballotbox.packet.OpenVoteScreen;

public class BallotBoxKeybinds {
public static final KeyBinding OPEN_VOTING_SCREEN = new KeyBinding("key.ballotbox.open", InputUtil.Type.KEYSYM, InputUtil.GLFW_KEY_APOSTROPHE, "key.ballotbox.category");
Expand All @@ -17,10 +20,12 @@ public static void init() {
}

private static void tick(MinecraftClient client) {
while (OPEN_VOTING_SCREEN.wasPressed()) {
if (client.currentScreen == null) {
while (OPEN_VOTING_SCREEN.wasPressed() && NotBallotBoxClient.isEnabled(client)) {
if (!NotBallotBoxClient.isOpen()) {
client.inGameHud.setOverlayMessage(Text.literal("[BallotBox] ").formatted(Formatting.GREEN).append(Text.literal("Voting is unavailable! Voting closed %s.".formatted(BallotBox.relativeTime(NotBallotBoxClient.closingTime))).formatted(Formatting.RED)), false);
} else if (client.currentScreen == null) {
client.setScreen(new VotingScreen());
ClientPlayNetworking.send(new OpenVoteScreenPacket());
ClientPlayNetworking.send(new OpenVoteScreen());
}
}
}
Expand Down
20 changes: 20 additions & 0 deletions src/main/java/net/modfest/ballotbox/client/NotBallotBoxClient.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,36 @@
package net.modfest.ballotbox.client;

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.minecraft.client.MinecraftClient;
import net.modfest.ballotbox.BallotBox;
import net.modfest.ballotbox.packet.OpenVoteScreen;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.Instant;

public class NotBallotBoxClient implements ClientModInitializer {
public static final Logger LOGGER = LoggerFactory.getLogger("%s-client".formatted(BallotBox.ID));
public static Instant closingTime = null;
public static int remainingVotes = 0;

public static boolean isEnabled(MinecraftClient client) {
return !client.isIntegratedServerRunning() && ClientPlayNetworking.canSend(OpenVoteScreen.ID);
}

public static boolean isOpen() {
return closingTime == null || closingTime.isAfter(Instant.now());
}

@Override
public void onInitializeClient() {
LOGGER.info("[BallotBox Client] Initialized!");
ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> {
remainingVotes = 0;
closingTime = null;
});
BallotBoxClientNetworking.init();
BallotBoxKeybinds.init();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -137,6 +136,7 @@ public void render(DrawContext context, int mouseX, int mouseY, float delta) {
@Override
public void removed() {
if (!previousSelections.equals(selections)) {
NotBallotBoxClient.remainingVotes = categories.stream().mapToInt(VotingCategory::limit).sum() - selections.size();
ClientPlayNetworking.send(new C2SUpdateVote(new VotingSelections(selections)));
}
super.removed();
Expand Down
Loading

0 comments on commit c2875cb

Please sign in to comment.