aboutsummaryrefslogtreecommitdiff
path: root/src/main/java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java')
-rw-r--r--src/main/java/world/anhgelus/molehunt/Molehunt.java377
-rw-r--r--src/main/java/world/anhgelus/molehunt/config/Config.java262
-rw-r--r--src/main/java/world/anhgelus/molehunt/config/ConfigPayload.java24
-rw-r--r--src/main/java/world/anhgelus/molehunt/config/SimpleConfig.java422
-rw-r--r--src/main/java/world/anhgelus/molehunt/game/Game.java344
-rw-r--r--src/main/java/world/anhgelus/molehunt/game/GamePayload.java20
-rw-r--r--src/main/java/world/anhgelus/molehunt/mixin/NoJoinLeaveMessage.java12
-rw-r--r--src/main/java/world/anhgelus/molehunt/mixin/NoMsgCommand.java8
-rw-r--r--src/main/java/world/anhgelus/molehunt/mixin/NoPortals.java10
-rw-r--r--src/main/java/world/anhgelus/molehunt/mixin/WorldTimerAccess.java56
-rw-r--r--src/main/java/world/anhgelus/molehunt/timer/TickTask.java118
-rw-r--r--src/main/java/world/anhgelus/molehunt/timer/TimerAccess.java96
-rw-r--r--src/main/java/world/anhgelus/molehunt/utils/TimeUtils.java93
13 files changed, 915 insertions, 927 deletions
diff --git a/src/main/java/world/anhgelus/molehunt/Molehunt.java b/src/main/java/world/anhgelus/molehunt/Molehunt.java
index d5df320..6486700 100644
--- a/src/main/java/world/anhgelus/molehunt/Molehunt.java
+++ b/src/main/java/world/anhgelus/molehunt/Molehunt.java
@@ -40,199 +40,186 @@ import static net.minecraft.server.command.CommandManager.literal;
public class Molehunt implements ModInitializer {
- public static final String MOD_ID = "molehunt";
- public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
- public static Config CONFIG;
-
- public static final SimpleConfig CONFIG_FILE = Config.configFile(MOD_ID);
-
- public static final GameRule<Integer> GAME_DURATION = GameRuleBuilder
- .forInteger(CONFIG_FILE.getOrDefault("game_duration", 90))
- .category(GameRuleCategory.MISC)
- .buildAndRegister(Identifier.of(MOD_ID, "gameDurationMinutes"));
-
- public static final GameRule<Integer> MOLE_PERCENTAGE = GameRuleBuilder
- .forInteger(CONFIG_FILE.getOrDefault("mole_percentage", 25))
- .range(0, 100)
- .category(GameRuleCategory.MISC)
- .buildAndRegister(Identifier.of(MOD_ID, "molePercentage"));
-
- public static final GameRule<Integer> MOLE_COUNT = GameRuleBuilder
- .forInteger(CONFIG_FILE.getOrDefault("mole_count", -1))
- .category(GameRuleCategory.MISC)
- .buildAndRegister(Identifier.of(MOD_ID, "moleCount"));
-
- public static final GameRule<Boolean> SHOW_NAMETAGS = GameRuleBuilder
- .forBoolean(CONFIG_FILE.getOrDefault("show_nametags", false))
- .category(GameRuleCategory.MISC)
- .buildAndRegister(Identifier.of(MOD_ID, "showNametags"));
-
- public static final GameRule<Boolean> SHOW_TAB = GameRuleBuilder
- .forBoolean(CONFIG_FILE.getOrDefault("show_tab", false))
- .category(GameRuleCategory.MISC)
- .buildAndRegister(Identifier.of(MOD_ID, "showTab"));
-
- public static final GameRule<Boolean> SHOW_SKINS = GameRuleBuilder
- .forBoolean(CONFIG_FILE.getOrDefault("show_skins", false))
- .category(GameRuleCategory.MISC)
- .buildAndRegister(Identifier.of(MOD_ID, "showSkins"));
-
- public static final GameRule<Integer> INITIAL_WORLD_SIZE = GameRuleBuilder
- .forInteger(CONFIG_FILE.getOrDefault("initial_world_size", 600))
- .minValue(0)
- .category(GameRuleCategory.MISC)
- .buildAndRegister(Identifier.of(MOD_ID, "initialWorldSize"));
-
- public static final GameRule<Integer> FINAL_WORLD_SIZE = GameRuleBuilder
- .forInteger(CONFIG_FILE.getOrDefault("final_world_size", 100))
- .minValue(0)
- .category(GameRuleCategory.MISC)
- .buildAndRegister(Identifier.of(MOD_ID, "finalWorldSize"));
-
- public static final GameRule<Integer> MOVING_STARTING_TIME_OFFSET = GameRuleBuilder
- .forInteger(CONFIG_FILE.getOrDefault("border_moving_starting_time_offset", 30))
- .minValue(0)
- .category(GameRuleCategory.MISC)
- .buildAndRegister(Identifier.of(MOD_ID, "borderMovingStartingTimeOffsetMinutes"));
-
- public static final GameRule<Boolean> ENABLE_PORTALS = GameRuleBuilder
- .forBoolean(CONFIG_FILE.getOrDefault("enable_portals", false))
- .category(GameRuleCategory.MISC)
- .buildAndRegister(Identifier.of(MOD_ID, "enablePortals"));
-
- public static final GameRule<Boolean> FOOD_ON_START = GameRuleBuilder
- .forBoolean(CONFIG_FILE.getOrDefault("food_on_start", true))
- .category(GameRuleCategory.MISC)
- .buildAndRegister(Identifier.of(MOD_ID, "foodOnStart"));
-
- public Game game;
-
- public static HashMap<UUID, Boolean> timerVisibility = new HashMap<>();
-
- private static <T> void sendConfigPayload(T v, MinecraftServer server) {
- if (CONFIG == null) return;
- CONFIG.sendConfigPayload();
- }
-
- static {
- GameRuleEvents.changeCallback(SHOW_NAMETAGS).register(Molehunt::sendConfigPayload);
- GameRuleEvents.changeCallback(SHOW_TAB).register(Molehunt::sendConfigPayload);
- GameRuleEvents.changeCallback(SHOW_SKINS).register(Molehunt::sendConfigPayload);
- }
-
- @Override
- public void onInitialize() {
- LOGGER.info("Initializing Molehunt");
-
- final var command = literal("molehunt");
- command.then(literal("start")
- .requires(CommandManager.requirePermissionLevel(CommandManager.GAMEMASTERS_CHECK))
- .executes(context -> {
- game = new Game(context.getSource().getServer());
- game.start();
- return Command.SINGLE_SUCCESS;
- }));
- command.then(literal("timer").requires(ServerCommandSource::isExecutedByPlayer).then(
- literal("show").executes(context -> {
- var player = context.getSource().getPlayer();
- assert player != null;
-
- timerVisibility.put(player.getUuid(), true);
- context.getSource().sendFeedback(() -> Text.translatable("commands.molehunt.timer.show"), false);
-
- if (game == null || !game.started()) {
- player.networkHandler.sendPacket(new OverlayMessageS2CPacket(
- Text.translatable("commands.molehunt.error.game_not_started").formatted(Formatting.RED)
- ));
- } else {
- player.networkHandler.sendPacket(new OverlayMessageS2CPacket(Text.of(game.getRemainingText())));
- }
-
- return Command.SINGLE_SUCCESS;
- })
- ).then(
- literal("hide").executes(context -> {
- var player = context.getSource().getPlayer();
- assert player != null;
-
- timerVisibility.put(player.getUuid(), false);
- context.getSource().sendFeedback(() -> Text.translatable("commands.molehunt.timer.hide"), false);
- return Command.SINGLE_SUCCESS;
- })
- ));
- command.then(literal("role")
- .requires(ServerCommandSource::isExecutedByPlayer)
- .executes(context -> {
- if (game == null || !game.started()) {
- throw (new SimpleCommandExceptionType(Text.translatable("commands.molehunt.error.game_not_started"))).create();
- }
-
- final var source = context.getSource();
- final var player = source.getPlayer();
- assert player != null;
-
- if (game.isMole(player)) {
- source.sendFeedback(
- () -> Text.translatable("commands.molehunt.role.mole")
- .append("\n\n")
- .append(Text.translatable("commands.molehunt.role.mole.list", game.getMolesAsString())),
- false);
- } else if (player.isSpectator()) {
- source.sendFeedback(
- () -> Text.translatable("commands.molehunt.role.survivor.mole_count", game.getMoles().size()),
- false);
- } else {
- source.sendFeedback(
- () -> Text.translatable("commands.molehunt.role.survivor")
- .append("\n\n")
- .append(Text.translatable("commands.molehunt.role.survivor.mole_count", game.getMoles().size())),
- false);
- }
-
- return Command.SINGLE_SUCCESS;
- }));
- command.then(literal("stop")
- .requires(CommandManager.requirePermissionLevel(CommandManager.GAMEMASTERS_CHECK))
- .executes(context -> {
- if (game == null || !game.started()) {
- throw (new SimpleCommandExceptionType(Text.translatable("commands.molehunt.error.game_not_started"))).create();
- }
-
- game.stop();
-
- return Command.SINGLE_SUCCESS;
- }));
-
- ServerLifecycleEvents.SERVER_STARTED.register(server -> CONFIG = new Config(server));
-
- CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> dispatcher.register(command));
-
- ServerMessageEvents.ALLOW_CHAT_MESSAGE.register((message, sender, params) -> false);
-
- ServerLivingEntityEvents.AFTER_DEATH.register((entity, damageSource) -> {
- if (!(entity instanceof ServerPlayerEntity) || game == null) return;
- if (!game.started()) return;
- if (game.wonByMoles()) game.end();
- });
-
- ServerPlayerEvents.AFTER_RESPAWN.register((oldPlayer, newPlayer, alive) -> {
- if (game == null) return;
- if (!game.started()) return;
- newPlayer.changeGameMode(GameMode.SPECTATOR);
- });
-
- ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> {
- ServerPlayNetworking.send(
- handler.player,
- new ConfigPayload(CONFIG.nametagsEnabled(), CONFIG.skinsEnabled(), CONFIG.tabEnabled())
- );
- ServerPlayNetworking.send(
- handler.player,
- new GamePayload(game != null && game.started())
- );
- });
-
- PayloadTypeRegistry.playS2C().register(ConfigPayload.ID, ConfigPayload.CODEC);
- PayloadTypeRegistry.playS2C().register(GamePayload.ID, GamePayload.CODEC);
- }
+ public static final String MOD_ID = "molehunt";
+ public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
+ public static final SimpleConfig CONFIG_FILE = Config.configFile(MOD_ID);
+ public static final GameRule<Integer> GAME_DURATION = GameRuleBuilder
+ .forInteger(CONFIG_FILE.getOrDefault("game_duration", 90))
+ .category(GameRuleCategory.MISC)
+ .buildAndRegister(Identifier.of(MOD_ID, "gameDurationMinutes"));
+ public static final GameRule<Integer> MOLE_PERCENTAGE = GameRuleBuilder
+ .forInteger(CONFIG_FILE.getOrDefault("mole_percentage", 25))
+ .range(0, 100)
+ .category(GameRuleCategory.MISC)
+ .buildAndRegister(Identifier.of(MOD_ID, "molePercentage"));
+ public static final GameRule<Integer> MOLE_COUNT = GameRuleBuilder
+ .forInteger(CONFIG_FILE.getOrDefault("mole_count", -1))
+ .category(GameRuleCategory.MISC)
+ .buildAndRegister(Identifier.of(MOD_ID, "moleCount"));
+ public static final GameRule<Boolean> SHOW_NAMETAGS = GameRuleBuilder
+ .forBoolean(CONFIG_FILE.getOrDefault("show_nametags", false))
+ .category(GameRuleCategory.MISC)
+ .buildAndRegister(Identifier.of(MOD_ID, "showNametags"));
+ public static final GameRule<Boolean> SHOW_TAB = GameRuleBuilder
+ .forBoolean(CONFIG_FILE.getOrDefault("show_tab", false))
+ .category(GameRuleCategory.MISC)
+ .buildAndRegister(Identifier.of(MOD_ID, "showTab"));
+ public static final GameRule<Boolean> SHOW_SKINS = GameRuleBuilder
+ .forBoolean(CONFIG_FILE.getOrDefault("show_skins", false))
+ .category(GameRuleCategory.MISC)
+ .buildAndRegister(Identifier.of(MOD_ID, "showSkins"));
+ public static final GameRule<Integer> INITIAL_WORLD_SIZE = GameRuleBuilder
+ .forInteger(CONFIG_FILE.getOrDefault("initial_world_size", 600))
+ .minValue(0)
+ .category(GameRuleCategory.MISC)
+ .buildAndRegister(Identifier.of(MOD_ID, "initialWorldSize"));
+ public static final GameRule<Integer> FINAL_WORLD_SIZE = GameRuleBuilder
+ .forInteger(CONFIG_FILE.getOrDefault("final_world_size", 100))
+ .minValue(0)
+ .category(GameRuleCategory.MISC)
+ .buildAndRegister(Identifier.of(MOD_ID, "finalWorldSize"));
+ public static final GameRule<Integer> MOVING_STARTING_TIME_OFFSET = GameRuleBuilder
+ .forInteger(CONFIG_FILE.getOrDefault("border_moving_starting_time_offset", 30))
+ .minValue(0)
+ .category(GameRuleCategory.MISC)
+ .buildAndRegister(Identifier.of(MOD_ID, "borderMovingStartingTimeOffsetMinutes"));
+ public static final GameRule<Boolean> ENABLE_PORTALS = GameRuleBuilder
+ .forBoolean(CONFIG_FILE.getOrDefault("enable_portals", false))
+ .category(GameRuleCategory.MISC)
+ .buildAndRegister(Identifier.of(MOD_ID, "enablePortals"));
+ public static final GameRule<Boolean> FOOD_ON_START = GameRuleBuilder
+ .forBoolean(CONFIG_FILE.getOrDefault("food_on_start", true))
+ .category(GameRuleCategory.MISC)
+ .buildAndRegister(Identifier.of(MOD_ID, "foodOnStart"));
+ public static Config CONFIG;
+ public static HashMap<UUID, Boolean> timerVisibility = new HashMap<>();
+
+ static {
+ GameRuleEvents.changeCallback(SHOW_NAMETAGS).register(Molehunt::sendConfigPayload);
+ GameRuleEvents.changeCallback(SHOW_TAB).register(Molehunt::sendConfigPayload);
+ GameRuleEvents.changeCallback(SHOW_SKINS).register(Molehunt::sendConfigPayload);
+ }
+
+ public Game game;
+
+ private static <T> void sendConfigPayload(T v, MinecraftServer server) {
+ if (CONFIG == null) return;
+ CONFIG.sendConfigPayload();
+ }
+
+ @Override
+ public void onInitialize() {
+ LOGGER.info("Initializing Molehunt");
+
+ final var command = literal("molehunt");
+ command.then(literal("start")
+ .requires(CommandManager.requirePermissionLevel(CommandManager.GAMEMASTERS_CHECK))
+ .executes(context -> {
+ game = new Game(context.getSource().getServer());
+ game.start();
+ return Command.SINGLE_SUCCESS;
+ }));
+ command.then(literal("timer").requires(ServerCommandSource::isExecutedByPlayer).then(
+ literal("show").executes(context -> {
+ var player = context.getSource().getPlayer();
+ assert player != null;
+
+ timerVisibility.put(player.getUuid(), true);
+ context.getSource().sendFeedback(() -> Text.translatable("commands.molehunt.timer.show"), false);
+
+ if (game == null || !game.started()) {
+ player.networkHandler.sendPacket(new OverlayMessageS2CPacket(
+ Text.translatable("commands.molehunt.error.game_not_started").formatted(Formatting.RED)
+ ));
+ } else {
+ player.networkHandler.sendPacket(new OverlayMessageS2CPacket(Text.of(game.getRemainingText())));
+ }
+
+ return Command.SINGLE_SUCCESS;
+ })
+ ).then(
+ literal("hide").executes(context -> {
+ var player = context.getSource().getPlayer();
+ assert player != null;
+
+ timerVisibility.put(player.getUuid(), false);
+ context.getSource().sendFeedback(() -> Text.translatable("commands.molehunt.timer.hide"), false);
+ return Command.SINGLE_SUCCESS;
+ })
+ ));
+ command.then(literal("role")
+ .requires(ServerCommandSource::isExecutedByPlayer)
+ .executes(context -> {
+ if (game == null || !game.started()) {
+ throw (new SimpleCommandExceptionType(Text.translatable("commands.molehunt.error.game_not_started"))).create();
+ }
+
+ final var source = context.getSource();
+ final var player = source.getPlayer();
+ assert player != null;
+
+ if (game.isMole(player)) {
+ source.sendFeedback(
+ () -> Text.translatable("commands.molehunt.role.mole")
+ .append("\n\n")
+ .append(Text.translatable("commands.molehunt.role.mole.list", game.getMolesAsString())),
+ false);
+ } else if (player.isSpectator()) {
+ source.sendFeedback(
+ () -> Text.translatable("commands.molehunt.role.survivor.mole_count", game.getMoles().size()),
+ false);
+ } else {
+ source.sendFeedback(
+ () -> Text.translatable("commands.molehunt.role.survivor")
+ .append("\n\n")
+ .append(Text.translatable("commands.molehunt.role.survivor.mole_count", game.getMoles().size())),
+ false);
+ }
+
+ return Command.SINGLE_SUCCESS;
+ }));
+ command.then(literal("stop")
+ .requires(CommandManager.requirePermissionLevel(CommandManager.GAMEMASTERS_CHECK))
+ .executes(context -> {
+ if (game == null || !game.started()) {
+ throw (new SimpleCommandExceptionType(Text.translatable("commands.molehunt.error.game_not_started"))).create();
+ }
+
+ game.stop();
+
+ return Command.SINGLE_SUCCESS;
+ }));
+
+ ServerLifecycleEvents.SERVER_STARTED.register(server -> CONFIG = new Config(server));
+
+ CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> dispatcher.register(command));
+
+ ServerMessageEvents.ALLOW_CHAT_MESSAGE.register((message, sender, params) -> false);
+
+ ServerLivingEntityEvents.AFTER_DEATH.register((entity, damageSource) -> {
+ if (!(entity instanceof ServerPlayerEntity) || game == null) return;
+ if (!game.started()) return;
+ if (game.wonByMoles()) game.end();
+ });
+
+ ServerPlayerEvents.AFTER_RESPAWN.register((oldPlayer, newPlayer, alive) -> {
+ if (game == null) return;
+ if (!game.started()) return;
+ newPlayer.changeGameMode(GameMode.SPECTATOR);
+ });
+
+ ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> {
+ ServerPlayNetworking.send(
+ handler.player,
+ new ConfigPayload(CONFIG.nametagsEnabled(), CONFIG.skinsEnabled(), CONFIG.tabEnabled())
+ );
+ ServerPlayNetworking.send(
+ handler.player,
+ new GamePayload(game != null && game.started())
+ );
+ });
+
+ PayloadTypeRegistry.playS2C().register(ConfigPayload.ID, ConfigPayload.CODEC);
+ PayloadTypeRegistry.playS2C().register(GamePayload.ID, GamePayload.CODEC);
+ }
}
diff --git a/src/main/java/world/anhgelus/molehunt/config/Config.java b/src/main/java/world/anhgelus/molehunt/config/Config.java
index 08c3850..d65b569 100644
--- a/src/main/java/world/anhgelus/molehunt/config/Config.java
+++ b/src/main/java/world/anhgelus/molehunt/config/Config.java
@@ -6,135 +6,135 @@ import world.anhgelus.molehunt.Molehunt;
public class Config {
- private final MinecraftServer server;
-
- public Config(MinecraftServer server) {
- this.server = server;
-
- sendConfigPayload(nametagsEnabled(), skinsEnabled(), tabEnabled());
- }
-
- public void sendConfigPayload() {
- final var payload = new ConfigPayload(nametagsEnabled(), skinsEnabled(), tabEnabled());
- server.getPlayerManager().getPlayerList().forEach(p -> ServerPlayNetworking.send(p, payload));
- }
-
- public void sendConfigPayload(boolean showNametags, boolean showSkins, boolean showTab) {
- final var payload = new ConfigPayload(showNametags, showSkins, showTab);
- server.getPlayerManager().getPlayerList().forEach(p -> ServerPlayNetworking.send(p, payload));
- }
-
- public int getGameDuration() {
- return server.getOverworld().getGameRules().getValue(Molehunt.GAME_DURATION);
- }
-
- public int getMolePercentage() {
- return server.getOverworld().getGameRules().getValue(Molehunt.MOLE_PERCENTAGE);
- }
-
- public int getMoleCount() {
- return server.getOverworld().getGameRules().getValue(Molehunt.MOLE_COUNT);
- }
-
- public boolean nametagsEnabled() {
- return server.getOverworld().getGameRules().getValue(Molehunt.SHOW_NAMETAGS);
- }
-
- public boolean skinsEnabled() {
- return server.getOverworld().getGameRules().getValue(Molehunt.SHOW_SKINS);
- }
-
- public boolean tabEnabled() {
- return server.getOverworld().getGameRules().getValue(Molehunt.SHOW_TAB);
- }
-
- public int getInitialWorldSize() {
- return server.getOverworld().getGameRules().getValue(Molehunt.INITIAL_WORLD_SIZE);
- }
-
- public int getFinalWorldSize() {
- return server.getOverworld().getGameRules().getValue(Molehunt.FINAL_WORLD_SIZE);
- }
-
- public int getBorderShrinkingStartingTimeOffset() {
- return server.getOverworld().getGameRules().getValue(Molehunt.MOVING_STARTING_TIME_OFFSET);
- }
-
- public boolean portalsEnabled() {
- return server.getOverworld().getGameRules().getValue(Molehunt.ENABLE_PORTALS);
- }
-
- public boolean foodOnStart() {
- return server.getOverworld().getGameRules().getValue(Molehunt.FOOD_ON_START);
- }
-
- public static SimpleConfig configFile(String fileName) {
- return SimpleConfig.of(fileName).provider(Config::defaultConfig).request();
- }
-
- private static String defaultConfig(String s) {
- return """
- # Molehunt mod configuration file
- # To regenerate the default configuration, delete, move or rename this file.
-
- # Game settings
-
- # The duration of a molehunt game, in minutes.
- # Default: 90 minutes (1 hour 30 minutes).
- game_duration = 90
-
- # Mole percentage.
- # For example, a mole percentage of 25% will get 1 mole every 4 players.
- # Default: 25 %.
- mole_percentage = 25
-
- # Mole count (absolute).
- # This setting will overwrite the mole_percentage setting.
- # If set below 0, this setting is disabled.
- # Default: -1.
- mole_count = -1
-
- # Give food on start
- # Default: true
- food_on_start = true
-
-
- # Client-side settings (applies to all players)
-
- # Show nametags
- # Default: false
- show_nametags = false
-
- # Show skins
- # Default: false
- show_skins = false
-
- # Show tab
- # Default: false
- show_tab = false
-
-
- # World border settings
-
- # Initial world size (in blocks).
- # Default: 600 blocks.
- initial_world_size = 600
-
- # Final world size (in blocks).
- # Default: 100 blocks.
- final_world_size = 100
-
- # Moving starting time offset (in minutes)
- # The time before starting to move the world borders.
- # If this value is greater than the game duration, borders will never move.
- # Default: 30 minutes.
- border_moving_starting_time_offset = 30
-
- # Other
-
- # Enable portals (nether, end, end gateway).
- # Default: false.
- enable_portals = false
- """;
- }
+ private final MinecraftServer server;
+
+ public Config(MinecraftServer server) {
+ this.server = server;
+
+ sendConfigPayload(nametagsEnabled(), skinsEnabled(), tabEnabled());
+ }
+
+ public static SimpleConfig configFile(String fileName) {
+ return SimpleConfig.of(fileName).provider(Config::defaultConfig).request();
+ }
+
+ private static String defaultConfig(String s) {
+ return """
+ # Molehunt mod configuration file
+ # To regenerate the default configuration, delete, move or rename this file.
+
+ # Game settings
+
+ # The duration of a molehunt game, in minutes.
+ # Default: 90 minutes (1 hour 30 minutes).
+ game_duration = 90
+
+ # Mole percentage.
+ # For example, a mole percentage of 25% will get 1 mole every 4 players.
+ # Default: 25 %.
+ mole_percentage = 25
+
+ # Mole count (absolute).
+ # This setting will overwrite the mole_percentage setting.
+ # If set below 0, this setting is disabled.
+ # Default: -1.
+ mole_count = -1
+
+ # Give food on start
+ # Default: true
+ food_on_start = true
+
+
+ # Client-side settings (applies to all players)
+
+ # Show nametags
+ # Default: false
+ show_nametags = false
+
+ # Show skins
+ # Default: false
+ show_skins = false
+
+ # Show tab
+ # Default: false
+ show_tab = false
+
+
+ # World border settings
+
+ # Initial world size (in blocks).
+ # Default: 600 blocks.
+ initial_world_size = 600
+
+ # Final world size (in blocks).
+ # Default: 100 blocks.
+ final_world_size = 100
+
+ # Moving starting time offset (in minutes)
+ # The time before starting to move the world borders.
+ # If this value is greater than the game duration, borders will never move.
+ # Default: 30 minutes.
+ border_moving_starting_time_offset = 30
+
+ # Other
+
+ # Enable portals (nether, end, end gateway).
+ # Default: false.
+ enable_portals = false
+ """;
+ }
+
+ public void sendConfigPayload() {
+ final var payload = new ConfigPayload(nametagsEnabled(), skinsEnabled(), tabEnabled());
+ server.getPlayerManager().getPlayerList().forEach(p -> ServerPlayNetworking.send(p, payload));
+ }
+
+ public void sendConfigPayload(boolean showNametags, boolean showSkins, boolean showTab) {
+ final var payload = new ConfigPayload(showNametags, showSkins, showTab);
+ server.getPlayerManager().getPlayerList().forEach(p -> ServerPlayNetworking.send(p, payload));
+ }
+
+ public int getGameDuration() {
+ return server.getOverworld().getGameRules().getValue(Molehunt.GAME_DURATION);
+ }
+
+ public int getMolePercentage() {
+ return server.getOverworld().getGameRules().getValue(Molehunt.MOLE_PERCENTAGE);
+ }
+
+ public int getMoleCount() {
+ return server.getOverworld().getGameRules().getValue(Molehunt.MOLE_COUNT);
+ }
+
+ public boolean nametagsEnabled() {
+ return server.getOverworld().getGameRules().getValue(Molehunt.SHOW_NAMETAGS);
+ }
+
+ public boolean skinsEnabled() {
+ return server.getOverworld().getGameRules().getValue(Molehunt.SHOW_SKINS);
+ }
+
+ public boolean tabEnabled() {
+ return server.getOverworld().getGameRules().getValue(Molehunt.SHOW_TAB);
+ }
+
+ public int getInitialWorldSize() {
+ return server.getOverworld().getGameRules().getValue(Molehunt.INITIAL_WORLD_SIZE);
+ }
+
+ public int getFinalWorldSize() {
+ return server.getOverworld().getGameRules().getValue(Molehunt.FINAL_WORLD_SIZE);
+ }
+
+ public int getBorderShrinkingStartingTimeOffset() {
+ return server.getOverworld().getGameRules().getValue(Molehunt.MOVING_STARTING_TIME_OFFSET);
+ }
+
+ public boolean portalsEnabled() {
+ return server.getOverworld().getGameRules().getValue(Molehunt.ENABLE_PORTALS);
+ }
+
+ public boolean foodOnStart() {
+ return server.getOverworld().getGameRules().getValue(Molehunt.FOOD_ON_START);
+ }
}
diff --git a/src/main/java/world/anhgelus/molehunt/config/ConfigPayload.java b/src/main/java/world/anhgelus/molehunt/config/ConfigPayload.java
index b43bd1b..f33e2c5 100644
--- a/src/main/java/world/anhgelus/molehunt/config/ConfigPayload.java
+++ b/src/main/java/world/anhgelus/molehunt/config/ConfigPayload.java
@@ -8,18 +8,18 @@ import net.minecraft.util.Identifier;
import world.anhgelus.molehunt.Molehunt;
public record ConfigPayload(boolean showNametags, boolean showSkins, boolean showTab) implements CustomPayload {
- public static final Identifier CONFIG_PACKET_ID = Identifier.of(Molehunt.MOD_ID, "config");
+ public static final Identifier CONFIG_PACKET_ID = Identifier.of(Molehunt.MOD_ID, "config");
- public static final CustomPayload.Id<ConfigPayload> ID = new CustomPayload.Id<>(CONFIG_PACKET_ID);
- public static final PacketCodec<RegistryByteBuf, ConfigPayload> CODEC = PacketCodec.tuple(
- PacketCodecs.BOOLEAN, ConfigPayload::showNametags,
- PacketCodecs.BOOLEAN, ConfigPayload::showSkins,
- PacketCodecs.BOOLEAN, ConfigPayload::showTab,
- ConfigPayload::new
- );
+ public static final CustomPayload.Id<ConfigPayload> ID = new CustomPayload.Id<>(CONFIG_PACKET_ID);
+ public static final PacketCodec<RegistryByteBuf, ConfigPayload> CODEC = PacketCodec.tuple(
+ PacketCodecs.BOOLEAN, ConfigPayload::showNametags,
+ PacketCodecs.BOOLEAN, ConfigPayload::showSkins,
+ PacketCodecs.BOOLEAN, ConfigPayload::showTab,
+ ConfigPayload::new
+ );
- @Override
- public Id<? extends CustomPayload> getId() {
- return ID;
- }
+ @Override
+ public Id<? extends CustomPayload> getId() {
+ return ID;
+ }
}
diff --git a/src/main/java/world/anhgelus/molehunt/config/SimpleConfig.java b/src/main/java/world/anhgelus/molehunt/config/SimpleConfig.java
index 2b65829..6b7060a 100644
--- a/src/main/java/world/anhgelus/molehunt/config/SimpleConfig.java
+++ b/src/main/java/world/anhgelus/molehunt/config/SimpleConfig.java
@@ -38,216 +38,216 @@ import java.util.Scanner;
public class SimpleConfig {
- private static final Logger LOGGER = LogManager.getLogger("SimpleConfig");
- private final HashMap<String, String> config = new HashMap<>();
- private final ConfigRequest request;
- private boolean broken = false;
-
- public interface DefaultConfig {
- String get( String namespace );
-
- static String empty( String namespace ) {
- return "";
- }
- }
-
- public static class ConfigRequest {
-
- private final File file;
- private final String filename;
- private DefaultConfig provider;
-
- private ConfigRequest(File file, String filename ) {
- this.file = file;
- this.filename = filename;
- this.provider = DefaultConfig::empty;
- }
-
- /**
- * Sets the default config provider, used to generate the
- * config if it's missing.
- *
- * @param provider default config provider
- * @return current config request object
- * @see DefaultConfig
- */
- public ConfigRequest provider( DefaultConfig provider ) {
- this.provider = provider;
- return this;
- }
-
- /**
- * Loads the config from the filesystem.
- *
- * @return config object
- * @see SimpleConfig
- */
- public SimpleConfig request() {
- return new SimpleConfig( this );
- }
-
- private String getConfig() {
- return provider.get( filename ) + "\n";
- }
-
- }
-
- /**
- * Creates new config request object, ideally `namespace`
- * should be the name of the mod id of the requesting mod
- *
- * @param filename - name of the config file
- * @return new config request object
- */
- public static ConfigRequest of( String filename ) {
- Path path = FabricLoader.getInstance().getConfigDir();
- return new ConfigRequest( path.resolve( filename + ".properties" ).toFile(), filename );
- }
-
- private void createConfig() throws IOException {
-
- // try creating missing files
- request.file.getParentFile().mkdirs();
- Files.createFile( request.file.toPath() );
-
- // write default config data
- PrintWriter writer = new PrintWriter(request.file, StandardCharsets.UTF_8);
- writer.write( request.getConfig() );
- writer.close();
-
- }
-
- private void loadConfig() throws IOException {
- Scanner reader = new Scanner( request.file );
- for( int line = 1; reader.hasNextLine(); line ++ ) {
- parseConfigEntry( reader.nextLine(), line );
- }
- }
-
- private void parseConfigEntry( String entry, int line ) {
- if( !entry.isEmpty() && !entry.startsWith( "#" ) ) {
- String[] parts = entry.split("=", 2);
- if( parts.length == 2 ) {
- config.put( parts[0].stripTrailing(), parts[1].strip() );
- }else{
- throw new RuntimeException("Syntax error in config file on line " + line + "!");
- }
- }
- }
-
- private SimpleConfig( ConfigRequest request ) {
- this.request = request;
- String identifier = "Config '" + request.filename + "'";
-
- if( !request.file.exists() ) {
- LOGGER.info("{} is missing, generating default one...", identifier);
-
- try {
- createConfig();
- } catch (IOException e) {
- LOGGER.error("{} failed to generate!", identifier);
- LOGGER.trace( e );
- broken = true;
- }
- }
-
- if( !broken ) {
- try {
- loadConfig();
- } catch (Exception e) {
- LOGGER.error("{} failed to load!", identifier);
- LOGGER.trace( e );
- broken = true;
- }
- }
-
- }
-
- /**
- * Queries a value from config, returns `null` if the
- * key does not exist.
- *
- * @return value corresponding to the given key
- * @see SimpleConfig#getOrDefault
- */
- @Deprecated
- public String get( String key ) {
- return config.get( key );
- }
-
- /**
- * Returns string value from config corresponding to the given
- * key, or the default string if the key is missing.
- *
- * @return value corresponding to the given key, or the default value
- */
- public String getOrDefault( String key, String def ) {
- String val = get(key);
- return val == null ? def : val;
- }
-
- /**
- * Returns integer value from config corresponding to the given
- * key, or the default integer if the key is missing or invalid.
- *
- * @return value corresponding to the given key, or the default value
- */
- public int getOrDefault( String key, int def ) {
- try {
- return Integer.parseInt( get(key) );
- } catch (Exception e) {
- return def;
- }
- }
-
- /**
- * Returns boolean value from config corresponding to the given
- * key, or the default boolean if the key is missing.
- *
- * @return value corresponding to the given key, or the default value
- */
- public boolean getOrDefault( String key, boolean def ) {
- String val = get(key);
- if( val != null ) {
- return val.equalsIgnoreCase("true");
- }
-
- return def;
- }
-
- /**
- * Returns double value from config corresponding to the given
- * key, or the default string if the key is missing or invalid.
- *
- * @return value corresponding to the given key, or the default value
- */
- public double getOrDefault( String key, double def ) {
- try {
- return Double.parseDouble( get(key) );
- } catch (Exception e) {
- return def;
- }
- }
-
- /**
- * If any error occurred during loading or reading from the config
- * a 'broken' flag is set, indicating that the config's state
- * is undefined and should be discarded using `delete()`
- *
- * @return the 'broken' flag of the configuration
- */
- public boolean isBroken() {
- return broken;
- }
-
- /**
- * deletes the config file from the filesystem
- *
- * @return true if the operation was successful
- */
- public boolean delete() {
- LOGGER.warn("Config '{}' was removed from existence! Restart the game to regenerate it.", request.filename);
- return request.file.delete();
- }
+ private static final Logger LOGGER = LogManager.getLogger("SimpleConfig");
+ private final HashMap<String, String> config = new HashMap<>();
+ private final ConfigRequest request;
+ private boolean broken = false;
+
+ private SimpleConfig(ConfigRequest request) {
+ this.request = request;
+ String identifier = "Config '" + request.filename + "'";
+
+ if (!request.file.exists()) {
+ LOGGER.info("{} is missing, generating default one...", identifier);
+
+ try {
+ createConfig();
+ } catch (IOException e) {
+ LOGGER.error("{} failed to generate!", identifier);
+ LOGGER.trace(e);
+ broken = true;
+ }
+ }
+
+ if (!broken) {
+ try {
+ loadConfig();
+ } catch (Exception e) {
+ LOGGER.error("{} failed to load!", identifier);
+ LOGGER.trace(e);
+ broken = true;
+ }
+ }
+
+ }
+
+ /**
+ * Creates new config request object, ideally `namespace`
+ * should be the name of the mod id of the requesting mod
+ *
+ * @param filename - name of the config file
+ * @return new config request object
+ */
+ public static ConfigRequest of(String filename) {
+ Path path = FabricLoader.getInstance().getConfigDir();
+ return new ConfigRequest(path.resolve(filename + ".properties").toFile(), filename);
+ }
+
+ private void createConfig() throws IOException {
+
+ // try creating missing files
+ request.file.getParentFile().mkdirs();
+ Files.createFile(request.file.toPath());
+
+ // write default config data
+ PrintWriter writer = new PrintWriter(request.file, StandardCharsets.UTF_8);
+ writer.write(request.getConfig());
+ writer.close();
+
+ }
+
+ private void loadConfig() throws IOException {
+ Scanner reader = new Scanner(request.file);
+ for (int line = 1; reader.hasNextLine(); line++) {
+ parseConfigEntry(reader.nextLine(), line);
+ }
+ }
+
+ private void parseConfigEntry(String entry, int line) {
+ if (!entry.isEmpty() && !entry.startsWith("#")) {
+ String[] parts = entry.split("=", 2);
+ if (parts.length == 2) {
+ config.put(parts[0].stripTrailing(), parts[1].strip());
+ } else {
+ throw new RuntimeException("Syntax error in config file on line " + line + "!");
+ }
+ }
+ }
+
+ /**
+ * Queries a value from config, returns `null` if the
+ * key does not exist.
+ *
+ * @return value corresponding to the given key
+ * @see SimpleConfig#getOrDefault
+ */
+ @Deprecated
+ public String get(String key) {
+ return config.get(key);
+ }
+
+ /**
+ * Returns string value from config corresponding to the given
+ * key, or the default string if the key is missing.
+ *
+ * @return value corresponding to the given key, or the default value
+ */
+ public String getOrDefault(String key, String def) {
+ String val = get(key);
+ return val == null ? def : val;
+ }
+
+ /**
+ * Returns integer value from config corresponding to the given
+ * key, or the default integer if the key is missing or invalid.
+ *
+ * @return value corresponding to the given key, or the default value
+ */
+ public int getOrDefault(String key, int def) {
+ try {
+ return Integer.parseInt(get(key));
+ } catch (Exception e) {
+ return def;
+ }
+ }
+
+ /**
+ * Returns boolean value from config corresponding to the given
+ * key, or the default boolean if the key is missing.
+ *
+ * @return value corresponding to the given key, or the default value
+ */
+ public boolean getOrDefault(String key, boolean def) {
+ String val = get(key);
+ if (val != null) {
+ return val.equalsIgnoreCase("true");
+ }
+
+ return def;
+ }
+
+ /**
+ * Returns double value from config corresponding to the given
+ * key, or the default string if the key is missing or invalid.
+ *
+ * @return value corresponding to the given key, or the default value
+ */
+ public double getOrDefault(String key, double def) {
+ try {
+ return Double.parseDouble(get(key));
+ } catch (Exception e) {
+ return def;
+ }
+ }
+
+ /**
+ * If any error occurred during loading or reading from the config
+ * a 'broken' flag is set, indicating that the config's state
+ * is undefined and should be discarded using `delete()`
+ *
+ * @return the 'broken' flag of the configuration
+ */
+ public boolean isBroken() {
+ return broken;
+ }
+
+ /**
+ * deletes the config file from the filesystem
+ *
+ * @return true if the operation was successful
+ */
+ public boolean delete() {
+ LOGGER.warn("Config '{}' was removed from existence! Restart the game to regenerate it.", request.filename);
+ return request.file.delete();
+ }
+
+ public interface DefaultConfig {
+ static String empty(String namespace) {
+ return "";
+ }
+
+ String get(String namespace);
+ }
+
+ public static class ConfigRequest {
+
+ private final File file;
+ private final String filename;
+ private DefaultConfig provider;
+
+ private ConfigRequest(File file, String filename) {
+ this.file = file;
+ this.filename = filename;
+ this.provider = DefaultConfig::empty;
+ }
+
+ /**
+ * Sets the default config provider, used to generate the
+ * config if it's missing.
+ *
+ * @param provider default config provider
+ * @return current config request object
+ * @see DefaultConfig
+ */
+ public ConfigRequest provider(DefaultConfig provider) {
+ this.provider = provider;
+ return this;
+ }
+
+ /**
+ * Loads the config from the filesystem.
+ *
+ * @return config object
+ * @see SimpleConfig
+ */
+ public SimpleConfig request() {
+ return new SimpleConfig(this);
+ }
+
+ private String getConfig() {
+ return provider.get(filename) + "\n";
+ }
+
+ }
} \ No newline at end of file
diff --git a/src/main/java/world/anhgelus/molehunt/game/Game.java b/src/main/java/world/anhgelus/molehunt/game/Game.java
index 4f3234c..35c45fa 100644
--- a/src/main/java/world/anhgelus/molehunt/game/Game.java
+++ b/src/main/java/world/anhgelus/molehunt/game/Game.java
@@ -25,176 +25,176 @@ import java.util.stream.Collectors;
public class Game {
- public final int defaultTime = Molehunt.CONFIG.getGameDuration() * 60;
- private final MinecraftServer server;
- private final List<UUID> moles = new ArrayList<>();
- private final TitleFadeS2CPacket timing = new TitleFadeS2CPacket(20, 40, 20);
- private int remaining = defaultTime;
- private boolean started = false;
-
- public Game(MinecraftServer server) {
- this.server = server;
- }
-
- public void start() {
- final int n = Molehunt.CONFIG.getMoleCount() < 0
- ? Math.floorDiv(server.getCurrentPlayerCount(), Math.floorDiv(100, Molehunt.CONFIG.getMolePercentage()))
- : Molehunt.CONFIG.getMoleCount();
-
- final var playerManager = server.getPlayerManager();
-
- final var players = new ArrayList<>(playerManager.getPlayerList());
- for (int i = 0; i < n && !players.isEmpty(); i++) {
- final var r = ThreadLocalRandom.current().nextInt(0, players.size());
- final var mole = players.get(r);
- if (mole == null) throw new IllegalStateException("Mole is null!");
- moles.add(mole.getUuid());
- players.remove(r);
- }
-
- final var gamerules = server.getOverworld().getGameRules();
- // immutable gamerules
- gamerules.setValue(GameRules.SHOW_DEATH_MESSAGES, false, server);
- gamerules.setValue(GameRules.ANNOUNCE_ADVANCEMENTS, false, server);
- // gamerules for the start
- gamerules.setValue(GameRules.DO_IMMEDIATE_RESPAWN, true, server);
-
- final var timer = TimerAccess.getTimerFromOverworld(server);
-
- final var worldBorder = server.getOverworld().getWorldBorder();
- worldBorder.setSize(Molehunt.CONFIG.getInitialWorldSize());
- if (Molehunt.CONFIG.getBorderShrinkingStartingTimeOffset() < Molehunt.CONFIG.getGameDuration()) {
- timer.dds_runTask(new TickTask(() -> worldBorder.interpolateSize(
- Molehunt.CONFIG.getInitialWorldSize(),
- Molehunt.CONFIG.getFinalWorldSize(),
- (Molehunt.CONFIG.getGameDuration() - Molehunt.CONFIG.getBorderShrinkingStartingTimeOffset()) * 60 * 20L,
- 0L
- ), Molehunt.CONFIG.getBorderShrinkingStartingTimeOffset() * 60 * 20L));
- }
-
- final var title = new TitleS2CPacket(Text.translatable("molehunt.game.start.suspense"));
- playerManager.getPlayerList().forEach(p -> {
- p.getInventory().clear();
- p.kill(p.getEntityWorld());
- p.networkHandler.sendPacket(timing);
- p.networkHandler.sendPacket(title);
- p.changeGameMode(GameMode.SURVIVAL);
- if (Molehunt.CONFIG.foodOnStart()) p.giveItemStack(new ItemStack(Items.COOKED_BEEF, 64));
- });
-
- server.setDefaultGameMode(GameMode.SPECTATOR);
-
- timer.dds_runTask(new TickTask(() -> {
- playerManager.getPlayerList().forEach(p -> {
- p.networkHandler.sendPacket(timing);
- if (moles.contains(p.getUuid())) {
- p.networkHandler.sendPacket(new TitleS2CPacket(Text.translatable("molehunt.game.start.mole.title")));
- p.networkHandler.sendPacket(new SubtitleS2CPacket(Text.translatable("molehunt.game.start.mole.subtitle")));
- } else {
- p.networkHandler.sendPacket(new TitleS2CPacket(Text.translatable("molehunt.game.start.survivor.title")));
- p.networkHandler.sendPacket(new SubtitleS2CPacket(Text.translatable("molehunt.game.start.survivor.subtitle")));
- }
- // reset health and food level
- p.setHealth(p.getMaxHealth());
- p.getHungerManager().setFoodLevel(20);
- p.getHungerManager().setSaturationLevel(5.0f);
- });
- // reset gamerules after the start
- gamerules.setValue(GameRules.DO_IMMEDIATE_RESPAWN, false, server);
- // reset time and weather
- server.getOverworld().setTimeOfDay(0);
- server.getOverworld().resetWeather();
- changeState(true);
- timer.dds_runTask(new TickTask(() -> {
- remaining--;
- playerManager.getPlayerList().forEach(player -> {
- if (Molehunt.timerVisibility.getOrDefault(player.getUuid(), true)) {
- player.networkHandler.sendPacket(new OverlayMessageS2CPacket(Text.of(getRemainingText())));
- }
- });
- playerManager.sendToAll(timing);
- if (remaining == 0) end();
- }, 5 * 20, 20));
- }, 4 * 20));
- }
-
- public void stop() {
- server.getPlayerManager().broadcast(Text.translatable("commands.molehunt.stop.success"), false);
- end();
- }
-
- public void end() {
- final var timer = TimerAccess.getTimerFromOverworld(server);
- timer.dds_cancel();
-
- final var worldBorder = server.getOverworld().getWorldBorder();
- // Stops the border shrinking.
- worldBorder.setSize(worldBorder.getSize());
-
- changeState(false);
- final var pm = server.getPlayerManager();
- final var winnerSuspense = new TitleS2CPacket(Text.translatable("molehunt.game.end.suspense.title"));
- pm.getPlayerList().forEach(p -> {
- p.networkHandler.sendPacket(timing);
- p.networkHandler.sendPacket(winnerSuspense);
- p.changeGameMode(GameMode.CREATIVE);
- });
- timer.dds_runTask(new TickTask(() -> {
- TitleS2CPacket winner;
- if (wonByMoles()) {
- winner = new TitleS2CPacket(Text.translatable("molehunt.game.end.winners.moles.title"));
- } else {
- winner = new TitleS2CPacket(Text.translatable("molehunt.game.end.winners.survivors.title"));
- }
- pm.sendToAll(new SubtitleS2CPacket(Text.translatable("molehunt.game.end.winners.subtitle", getMolesAsString())));
- pm.sendToAll(winner);
- pm.sendToAll(timing);
- moles.clear();
- }, 4 * 20));
- }
-
- public Text getRemainingText() {
- return Text.of("§c" + TimeUtils.generateShortString(remaining));
- }
-
- public List<ServerPlayerEntity> getMoles() {
- return moles.stream()
- .map(uuid -> server.getPlayerManager().getPlayer(uuid))
- .filter(Objects::nonNull)
- .filter(p -> !p.isSpectator())
- .toList();
- }
-
- public String getMolesAsString() {
- return getMoles().stream()
- .map(PlayerEntity::getDisplayName)
- .filter(Objects::nonNull)
- .map(Object::toString)
- .collect(Collectors.joining(", "));
- }
-
- public boolean isMole(ServerPlayerEntity player) {
- return moles.contains(player.getUuid());
- }
-
- public boolean wonByMoles() {
- return new HashSet<>(moles).containsAll(
- server.getPlayerManager()
- .getPlayerList()
- .stream()
- .filter(p -> !p.isSpectator() && !p.isCreative())
- .map(Entity::getUuid)
- .toList()
- );
- }
-
- public boolean started() {
- return started;
- }
-
- private void changeState(boolean hasStarted) {
- started = hasStarted;
- final var payload = new GamePayload(hasStarted);
- server.getPlayerManager().getPlayerList().forEach(p -> ServerPlayNetworking.send(p, payload));
- }
+ public final int defaultTime = Molehunt.CONFIG.getGameDuration() * 60;
+ private final MinecraftServer server;
+ private final List<UUID> moles = new ArrayList<>();
+ private final TitleFadeS2CPacket timing = new TitleFadeS2CPacket(20, 40, 20);
+ private int remaining = defaultTime;
+ private boolean started = false;
+
+ public Game(MinecraftServer server) {
+ this.server = server;
+ }
+
+ public void start() {
+ final int n = Molehunt.CONFIG.getMoleCount() < 0
+ ? Math.floorDiv(server.getCurrentPlayerCount(), Math.floorDiv(100, Molehunt.CONFIG.getMolePercentage()))
+ : Molehunt.CONFIG.getMoleCount();
+
+ final var playerManager = server.getPlayerManager();
+
+ final var players = new ArrayList<>(playerManager.getPlayerList());
+ for (int i = 0; i < n && !players.isEmpty(); i++) {
+ final var r = ThreadLocalRandom.current().nextInt(0, players.size());
+ final var mole = players.get(r);
+ if (mole == null) throw new IllegalStateException("Mole is null!");
+ moles.add(mole.getUuid());
+ players.remove(r);
+ }
+
+ final var gamerules = server.getOverworld().getGameRules();
+ // immutable gamerules
+ gamerules.setValue(GameRules.SHOW_DEATH_MESSAGES, false, server);
+ gamerules.setValue(GameRules.ANNOUNCE_ADVANCEMENTS, false, server);
+ // gamerules for the start
+ gamerules.setValue(GameRules.DO_IMMEDIATE_RESPAWN, true, server);
+
+ final var timer = TimerAccess.getTimerFromOverworld(server);
+
+ final var worldBorder = server.getOverworld().getWorldBorder();
+ worldBorder.setSize(Molehunt.CONFIG.getInitialWorldSize());
+ if (Molehunt.CONFIG.getBorderShrinkingStartingTimeOffset() < Molehunt.CONFIG.getGameDuration()) {
+ timer.dds_runTask(new TickTask(() -> worldBorder.interpolateSize(
+ Molehunt.CONFIG.getInitialWorldSize(),
+ Molehunt.CONFIG.getFinalWorldSize(),
+ (Molehunt.CONFIG.getGameDuration() - Molehunt.CONFIG.getBorderShrinkingStartingTimeOffset()) * 60 * 20L,
+ 0L
+ ), Molehunt.CONFIG.getBorderShrinkingStartingTimeOffset() * 60 * 20L));
+ }
+
+ final var title = new TitleS2CPacket(Text.translatable("molehunt.game.start.suspense"));
+ playerManager.getPlayerList().forEach(p -> {
+ p.getInventory().clear();
+ p.kill(p.getEntityWorld());
+ p.networkHandler.sendPacket(timing);
+ p.networkHandler.sendPacket(title);
+ p.changeGameMode(GameMode.SURVIVAL);
+ if (Molehunt.CONFIG.foodOnStart()) p.giveItemStack(new ItemStack(Items.COOKED_BEEF, 64));
+ });
+
+ server.setDefaultGameMode(GameMode.SPECTATOR);
+
+ timer.dds_runTask(new TickTask(() -> {
+ playerManager.getPlayerList().forEach(p -> {
+ p.networkHandler.sendPacket(timing);
+ if (moles.contains(p.getUuid())) {
+ p.networkHandler.sendPacket(new TitleS2CPacket(Text.translatable("molehunt.game.start.mole.title")));
+ p.networkHandler.sendPacket(new SubtitleS2CPacket(Text.translatable("molehunt.game.start.mole.subtitle")));
+ } else {
+ p.networkHandler.sendPacket(new TitleS2CPacket(Text.translatable("molehunt.game.start.survivor.title")));
+ p.networkHandler.sendPacket(new SubtitleS2CPacket(Text.translatable("molehunt.game.start.survivor.subtitle")));
+ }
+ // reset health and food level
+ p.setHealth(p.getMaxHealth());
+ p.getHungerManager().setFoodLevel(20);
+ p.getHungerManager().setSaturationLevel(5.0f);
+ });
+ // reset gamerules after the start
+ gamerules.setValue(GameRules.DO_IMMEDIATE_RESPAWN, false, server);
+ // reset time and weather
+ server.getOverworld().setTimeOfDay(0);
+ server.getOverworld().resetWeather();
+ changeState(true);
+ timer.dds_runTask(new TickTask(() -> {
+ remaining--;
+ playerManager.getPlayerList().forEach(player -> {
+ if (Molehunt.timerVisibility.getOrDefault(player.getUuid(), true)) {
+ player.networkHandler.sendPacket(new OverlayMessageS2CPacket(Text.of(getRemainingText())));
+ }
+ });
+ playerManager.sendToAll(timing);
+ if (remaining == 0) end();
+ }, 5 * 20, 20));
+ }, 4 * 20));
+ }
+
+ public void stop() {
+ server.getPlayerManager().broadcast(Text.translatable("commands.molehunt.stop.success"), false);
+ end();
+ }
+
+ public void end() {
+ final var timer = TimerAccess.getTimerFromOverworld(server);
+ timer.dds_cancel();
+
+ final var worldBorder = server.getOverworld().getWorldBorder();
+ // Stops the border shrinking.
+ worldBorder.setSize(worldBorder.getSize());
+
+ changeState(false);
+ final var pm = server.getPlayerManager();
+ final var winnerSuspense = new TitleS2CPacket(Text.translatable("molehunt.game.end.suspense.title"));
+ pm.getPlayerList().forEach(p -> {
+ p.networkHandler.sendPacket(timing);
+ p.networkHandler.sendPacket(winnerSuspense);
+ p.changeGameMode(GameMode.CREATIVE);
+ });
+ timer.dds_runTask(new TickTask(() -> {
+ TitleS2CPacket winner;
+ if (wonByMoles()) {
+ winner = new TitleS2CPacket(Text.translatable("molehunt.game.end.winners.moles.title"));
+ } else {
+ winner = new TitleS2CPacket(Text.translatable("molehunt.game.end.winners.survivors.title"));
+ }
+ pm.sendToAll(new SubtitleS2CPacket(Text.translatable("molehunt.game.end.winners.subtitle", getMolesAsString())));
+ pm.sendToAll(winner);
+ pm.sendToAll(timing);
+ moles.clear();
+ }, 4 * 20));
+ }
+
+ public Text getRemainingText() {
+ return Text.of("§c" + TimeUtils.generateShortString(remaining));
+ }
+
+ public List<ServerPlayerEntity> getMoles() {
+ return moles.stream()
+ .map(uuid -> server.getPlayerManager().getPlayer(uuid))
+ .filter(Objects::nonNull)
+ .filter(p -> !p.isSpectator())
+ .toList();
+ }
+
+ public String getMolesAsString() {
+ return getMoles().stream()
+ .map(PlayerEntity::getDisplayName)
+ .filter(Objects::nonNull)
+ .map(Object::toString)
+ .collect(Collectors.joining(", "));
+ }
+
+ public boolean isMole(ServerPlayerEntity player) {
+ return moles.contains(player.getUuid());
+ }
+
+ public boolean wonByMoles() {
+ return new HashSet<>(moles).containsAll(
+ server.getPlayerManager()
+ .getPlayerList()
+ .stream()
+ .filter(p -> !p.isSpectator() && !p.isCreative())
+ .map(Entity::getUuid)
+ .toList()
+ );
+ }
+
+ public boolean started() {
+ return started;
+ }
+
+ private void changeState(boolean hasStarted) {
+ started = hasStarted;
+ final var payload = new GamePayload(hasStarted);
+ server.getPlayerManager().getPlayerList().forEach(p -> ServerPlayNetworking.send(p, payload));
+ }
}
diff --git a/src/main/java/world/anhgelus/molehunt/game/GamePayload.java b/src/main/java/world/anhgelus/molehunt/game/GamePayload.java
index 66e3209..1532fa9 100644
--- a/src/main/java/world/anhgelus/molehunt/game/GamePayload.java
+++ b/src/main/java/world/anhgelus/molehunt/game/GamePayload.java
@@ -8,16 +8,16 @@ import net.minecraft.util.Identifier;
import world.anhgelus.molehunt.Molehunt;
public record GamePayload(boolean gameLaunched) implements CustomPayload {
- public static final Identifier GAME_PACKET_ID = Identifier.of(Molehunt.MOD_ID, "game");
+ public static final Identifier GAME_PACKET_ID = Identifier.of(Molehunt.MOD_ID, "game");
- public static final CustomPayload.Id<GamePayload> ID = new CustomPayload.Id<>(GAME_PACKET_ID);
- public static final PacketCodec<RegistryByteBuf, GamePayload> CODEC = PacketCodec.tuple(
- PacketCodecs.BOOLEAN, GamePayload::gameLaunched,
- GamePayload::new
- );
+ public static final CustomPayload.Id<GamePayload> ID = new CustomPayload.Id<>(GAME_PACKET_ID);
+ public static final PacketCodec<RegistryByteBuf, GamePayload> CODEC = PacketCodec.tuple(
+ PacketCodecs.BOOLEAN, GamePayload::gameLaunched,
+ GamePayload::new
+ );
- @Override
- public Id<? extends CustomPayload> getId() {
- return ID;
- }
+ @Override
+ public Id<? extends CustomPayload> getId() {
+ return ID;
+ }
}
diff --git a/src/main/java/world/anhgelus/molehunt/mixin/NoJoinLeaveMessage.java b/src/main/java/world/anhgelus/molehunt/mixin/NoJoinLeaveMessage.java
index a82a21e..0dfd624 100644
--- a/src/main/java/world/anhgelus/molehunt/mixin/NoJoinLeaveMessage.java
+++ b/src/main/java/world/anhgelus/molehunt/mixin/NoJoinLeaveMessage.java
@@ -9,10 +9,10 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(PlayerManager.class)
public class NoJoinLeaveMessage {
- @Inject(at = @At("HEAD"), method = "broadcast*", cancellable = true)
- public void broadcastNoJoinLeaveMessage(Text message, boolean overlay, CallbackInfo ci) {
- final var content = message.getContent().toString();
- if (content.startsWith("translation{key='multiplayer.player.joined")) ci.cancel();
- else if (content.startsWith("translation{key='multiplayer.player.left")) ci.cancel();
- }
+ @Inject(at = @At("HEAD"), method = "broadcast*", cancellable = true)
+ public void broadcastNoJoinLeaveMessage(Text message, boolean overlay, CallbackInfo ci) {
+ final var content = message.getContent().toString();
+ if (content.startsWith("translation{key='multiplayer.player.joined")) ci.cancel();
+ else if (content.startsWith("translation{key='multiplayer.player.left")) ci.cancel();
+ }
}
diff --git a/src/main/java/world/anhgelus/molehunt/mixin/NoMsgCommand.java b/src/main/java/world/anhgelus/molehunt/mixin/NoMsgCommand.java
index 2727143..f698f1c 100644
--- a/src/main/java/world/anhgelus/molehunt/mixin/NoMsgCommand.java
+++ b/src/main/java/world/anhgelus/molehunt/mixin/NoMsgCommand.java
@@ -10,8 +10,8 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(MessageCommand.class)
public class NoMsgCommand {
- @Inject(at = @At("HEAD"), method = "register", cancellable = true)
- private static void register(CommandDispatcher<ServerCommandSource> dispatcher, CallbackInfo ci) {
- ci.cancel();
- }
+ @Inject(at = @At("HEAD"), method = "register", cancellable = true)
+ private static void register(CommandDispatcher<ServerCommandSource> dispatcher, CallbackInfo ci) {
+ ci.cancel();
+ }
}
diff --git a/src/main/java/world/anhgelus/molehunt/mixin/NoPortals.java b/src/main/java/world/anhgelus/molehunt/mixin/NoPortals.java
index a71e6d1..2648f25 100644
--- a/src/main/java/world/anhgelus/molehunt/mixin/NoPortals.java
+++ b/src/main/java/world/anhgelus/molehunt/mixin/NoPortals.java
@@ -11,9 +11,9 @@ import world.anhgelus.molehunt.Molehunt;
@Mixin(PortalManager.class)
public class NoPortals {
- @Inject(at = @At("HEAD"), method = "tick", cancellable = true)
- public void disableTick(ServerWorld world, Entity entity, boolean canUsePortals, CallbackInfoReturnable<Boolean> cir) {
- if (Molehunt.CONFIG == null || Molehunt.CONFIG.portalsEnabled()) return;
- cir.setReturnValue(false);
- }
+ @Inject(at = @At("HEAD"), method = "tick", cancellable = true)
+ public void disableTick(ServerWorld world, Entity entity, boolean canUsePortals, CallbackInfoReturnable<Boolean> cir) {
+ if (Molehunt.CONFIG == null || Molehunt.CONFIG.portalsEnabled()) return;
+ cir.setReturnValue(false);
+ }
}
diff --git a/src/main/java/world/anhgelus/molehunt/mixin/WorldTimerAccess.java b/src/main/java/world/anhgelus/molehunt/mixin/WorldTimerAccess.java
index cd8a1b4..1e0ba12 100644
--- a/src/main/java/world/anhgelus/molehunt/mixin/WorldTimerAccess.java
+++ b/src/main/java/world/anhgelus/molehunt/mixin/WorldTimerAccess.java
@@ -14,32 +14,32 @@ import java.util.function.BooleanSupplier;
@Mixin(ServerWorld.class)
public class WorldTimerAccess implements TimerAccess {
- @Unique
- private final List<TickTask> tasks = new ArrayList<>();
-
- @Unique
- private final List<TimerAccess.TickTask> tasksToAdd = new ArrayList<>();
-
- @Inject(method = "tick", at = @At("TAIL"))
- private void onTick(BooleanSupplier shouldKeepTicking, CallbackInfo ci) {
- tasks.stream().filter(TickTask::isRunning).forEach(TickTask::tick);
- tasks.addAll(tasksToAdd);
- tasksToAdd.clear();
- }
-
- @Override
- public void dds_runTask(TimerAccess.TickTask task) {
- tasksToAdd.add(task);
- }
-
- @Override
- public void dds_cancel() {
- tasks.stream().filter(TickTask::isRunning).forEach(TickTask::cancel);
- tasks.clear();
- }
-
- @Override
- public List<TickTask> dds_getTasks() {
- return tasks.stream().filter(TickTask::isRunning).toList();
- }
+ @Unique
+ private final List<TickTask> tasks = new ArrayList<>();
+
+ @Unique
+ private final List<TimerAccess.TickTask> tasksToAdd = new ArrayList<>();
+
+ @Inject(method = "tick", at = @At("TAIL"))
+ private void onTick(BooleanSupplier shouldKeepTicking, CallbackInfo ci) {
+ tasks.stream().filter(TickTask::isRunning).forEach(TickTask::tick);
+ tasks.addAll(tasksToAdd);
+ tasksToAdd.clear();
+ }
+
+ @Override
+ public void dds_runTask(TimerAccess.TickTask task) {
+ tasksToAdd.add(task);
+ }
+
+ @Override
+ public void dds_cancel() {
+ tasks.stream().filter(TickTask::isRunning).forEach(TickTask::cancel);
+ tasks.clear();
+ }
+
+ @Override
+ public List<TickTask> dds_getTasks() {
+ return tasks.stream().filter(TickTask::isRunning).toList();
+ }
} \ No newline at end of file
diff --git a/src/main/java/world/anhgelus/molehunt/timer/TickTask.java b/src/main/java/world/anhgelus/molehunt/timer/TickTask.java
index 8c74b89..1cc83e2 100644
--- a/src/main/java/world/anhgelus/molehunt/timer/TickTask.java
+++ b/src/main/java/world/anhgelus/molehunt/timer/TickTask.java
@@ -4,69 +4,69 @@ package world.anhgelus.molehunt.timer;
* Represents a complete task called each tick
*/
public class TickTask implements TimerAccess.TickTask {
- public final long ticksDelay;
- public final long ticksRepeat;
- public final boolean repeating;
- public final TimerAccess.Task task;
- private boolean cancelled = false;
- private long currentTicking;
+ public final long ticksDelay;
+ public final long ticksRepeat;
+ public final boolean repeating;
+ public final TimerAccess.Task task;
+ private boolean cancelled = false;
+ private long currentTicking;
- /**
- * Create a new repeating TickTask
- *
- * @param task Task to run after the delay or the repeat time
- * @param ticksDelay Delay before the first task's run
- * @param ticksRepeat Repeat each tick (if the repeat is 0, it will repeat each tick, if it is below 0, it will not repeat)
- * @throws IllegalArgumentException if ticksDelay is below 0
- */
- public TickTask(TimerAccess.Task task, long ticksDelay, long ticksRepeat) {
- if (ticksDelay < 0) throw new IllegalArgumentException("Ticks delay must be non-negative");
- this.ticksDelay = ticksDelay;
- this.ticksRepeat = ticksRepeat;
- this.task = task;
- repeating = ticksRepeat >= 0;
- currentTicking = ticksDelay;
- }
+ /**
+ * Create a new repeating TickTask
+ *
+ * @param task Task to run after the delay or the repeat time
+ * @param ticksDelay Delay before the first task's run
+ * @param ticksRepeat Repeat each tick (if the repeat is 0, it will repeat each tick, if it is below 0, it will not repeat)
+ * @throws IllegalArgumentException if ticksDelay is below 0
+ */
+ public TickTask(TimerAccess.Task task, long ticksDelay, long ticksRepeat) {
+ if (ticksDelay < 0) throw new IllegalArgumentException("Ticks delay must be non-negative");
+ this.ticksDelay = ticksDelay;
+ this.ticksRepeat = ticksRepeat;
+ this.task = task;
+ repeating = ticksRepeat >= 0;
+ currentTicking = ticksDelay;
+ }
- /**
- * Create a new delayed TickTask
- *
- * @param task Task to run after the delay or the repeat time
- * @param ticksDelay Delay before the first task's run
- * @throws IllegalArgumentException if ticksDelay or if ticksRepeat is below 0
- */
- public TickTask(TimerAccess.Task task, long ticksDelay) {
- if (ticksDelay < 0) throw new IllegalArgumentException("Ticks delay must be non-negative");
- this.ticksDelay = ticksDelay;
- this.ticksRepeat = -1;
- this.task = task;
- repeating = false;
- currentTicking = ticksDelay;
- }
+ /**
+ * Create a new delayed TickTask
+ *
+ * @param task Task to run after the delay or the repeat time
+ * @param ticksDelay Delay before the first task's run
+ * @throws IllegalArgumentException if ticksDelay or if ticksRepeat is below 0
+ */
+ public TickTask(TimerAccess.Task task, long ticksDelay) {
+ if (ticksDelay < 0) throw new IllegalArgumentException("Ticks delay must be non-negative");
+ this.ticksDelay = ticksDelay;
+ this.ticksRepeat = -1;
+ this.task = task;
+ repeating = false;
+ currentTicking = ticksDelay;
+ }
- public void tick() {
- if (--currentTicking > 0) return;
- task.run();
- if (repeating) {
- currentTicking = ticksRepeat;
- } else {
- cancel();
- }
- }
+ public void tick() {
+ if (--currentTicking > 0) return;
+ task.run();
+ if (repeating) {
+ currentTicking = ticksRepeat;
+ } else {
+ cancel();
+ }
+ }
- public long cancel() {
- if (cancelled) throw new IllegalStateException("Task already cancelled");
- cancelled = true;
- return currentTicking;
- }
+ public long cancel() {
+ if (cancelled) throw new IllegalStateException("Task already cancelled");
+ cancelled = true;
+ return currentTicking;
+ }
- public boolean isRunning() {
- return !cancelled;
- }
+ public boolean isRunning() {
+ return !cancelled;
+ }
- @Override
- public long getTickingBeforeRun() {
- if (cancelled) return -1;
- return currentTicking;
- }
+ @Override
+ public long getTickingBeforeRun() {
+ if (cancelled) return -1;
+ return currentTicking;
+ }
} \ No newline at end of file
diff --git a/src/main/java/world/anhgelus/molehunt/timer/TimerAccess.java b/src/main/java/world/anhgelus/molehunt/timer/TimerAccess.java
index 2e02b12..32ee611 100644
--- a/src/main/java/world/anhgelus/molehunt/timer/TimerAccess.java
+++ b/src/main/java/world/anhgelus/molehunt/timer/TimerAccess.java
@@ -6,60 +6,60 @@ import net.minecraft.world.World;
import java.util.List;
public interface TimerAccess {
- /**
- * Get the timer linked to the overworld
- *
- * @param server Current server
- * @return TimerAccess linked to the overworld
- */
- static TimerAccess getTimerFromOverworld(MinecraftServer server) {
- final var timer = (TimerAccess) server.getWorld(World.OVERWORLD);
- if (timer == null)
- throw new NullPointerException("Impossible to get TimerAccess from the overworld (it is null)");
- return timer;
- }
+ /**
+ * Get the timer linked to the overworld
+ *
+ * @param server Current server
+ * @return TimerAccess linked to the overworld
+ */
+ static TimerAccess getTimerFromOverworld(MinecraftServer server) {
+ final var timer = (TimerAccess) server.getWorld(World.OVERWORLD);
+ if (timer == null)
+ throw new NullPointerException("Impossible to get TimerAccess from the overworld (it is null)");
+ return timer;
+ }
- /**
- * Run a task (called each tick ticked)
- *
- * @param task Task to run
- */
- void dds_runTask(TimerAccess.TickTask task);
+ /**
+ * Run a task (called each tick ticked)
+ *
+ * @param task Task to run
+ */
+ void dds_runTask(TimerAccess.TickTask task);
- void dds_cancel();
+ void dds_cancel();
- /**
- * @return All non-cancelled tasks
- */
- List<TickTask> dds_getTasks();
+ /**
+ * @return All non-cancelled tasks
+ */
+ List<TickTask> dds_getTasks();
- interface TickTask {
- /**
- * Tick the task
- */
- void tick();
+ interface TickTask {
+ /**
+ * Tick the task
+ */
+ void tick();
- /**
- * Cancel the task
- *
- * @return the remaining ticks before the run of the Task
- * @throws IllegalStateException if the task is already cancelled
- */
- long cancel();
+ /**
+ * Cancel the task
+ *
+ * @return the remaining ticks before the run of the Task
+ * @throws IllegalStateException if the task is already cancelled
+ */
+ long cancel();
- boolean isRunning();
+ boolean isRunning();
- /**
- * @return the number of ticks before run of the task (if the task is cancelled, returns -1)
- */
- long getTickingBeforeRun();
- }
+ /**
+ * @return the number of ticks before run of the task (if the task is cancelled, returns -1)
+ */
+ long getTickingBeforeRun();
+ }
- /**
- * Represents a task to run after ticking
- */
- @FunctionalInterface
- interface Task {
- void run();
- }
+ /**
+ * Represents a task to run after ticking
+ */
+ @FunctionalInterface
+ interface Task {
+ void run();
+ }
}
diff --git a/src/main/java/world/anhgelus/molehunt/utils/TimeUtils.java b/src/main/java/world/anhgelus/molehunt/utils/TimeUtils.java
index 2b96606..952f2d2 100644
--- a/src/main/java/world/anhgelus/molehunt/utils/TimeUtils.java
+++ b/src/main/java/world/anhgelus/molehunt/utils/TimeUtils.java
@@ -2,50 +2,51 @@ package world.anhgelus.molehunt.utils;
public class TimeUtils {
- private record Time(long hours, long minutes, long seconds) {}
-
- public static String generateString(long time) {
- final var pt = generateTime(time);
-
- StringBuilder sb = new StringBuilder();
- if (pt.hours != 0) {
- sb.append(pt.hours).append(" hours ");
- }
- if (pt.minutes != 0 || pt.hours != 0) {
- sb.append(pt.minutes).append(" minutes ");
- }
- sb.append(pt.seconds).append(" seconds");
-
- return sb.toString();
- }
-
- public static String generateShortString(long time) {
- final var pt = generateTime(time);
-
- return padLeft(pt.hours) + ":" +
- padLeft(pt.minutes) + ":" +
- padLeft(pt.seconds);
- }
-
- private static Time generateTime(long time) {
- long hours = 0;
- if (time > 3600) {
- hours = Math.floorDiv(time, 3600);
- }
- long minutes = 0;
- if (hours != 0 || time > 60) {
- minutes = Math.floorDiv(time - hours * 3600, 60);
- }
- long seconds = (long) Math.floor(time - hours * 3600 - minutes * 60);
- return new Time(hours, minutes, seconds);
- }
-
- private static String padLeft(long n) {
- if (n < 10 && n != 0) {
- return "0" + Math.round(n);
- } else if (n == 0) {
- return "00";
- }
- return Long.toString(Math.round(n));
- }
+ public static String generateString(long time) {
+ final var pt = generateTime(time);
+
+ StringBuilder sb = new StringBuilder();
+ if (pt.hours != 0) {
+ sb.append(pt.hours).append(" hours ");
+ }
+ if (pt.minutes != 0 || pt.hours != 0) {
+ sb.append(pt.minutes).append(" minutes ");
+ }
+ sb.append(pt.seconds).append(" seconds");
+
+ return sb.toString();
+ }
+
+ public static String generateShortString(long time) {
+ final var pt = generateTime(time);
+
+ return padLeft(pt.hours) + ":" +
+ padLeft(pt.minutes) + ":" +
+ padLeft(pt.seconds);
+ }
+
+ private static Time generateTime(long time) {
+ long hours = 0;
+ if (time > 3600) {
+ hours = Math.floorDiv(time, 3600);
+ }
+ long minutes = 0;
+ if (hours != 0 || time > 60) {
+ minutes = Math.floorDiv(time - hours * 3600, 60);
+ }
+ long seconds = (long) Math.floor(time - hours * 3600 - minutes * 60);
+ return new Time(hours, minutes, seconds);
+ }
+
+ private static String padLeft(long n) {
+ if (n < 10 && n != 0) {
+ return "0" + Math.round(n);
+ } else if (n == 0) {
+ return "00";
+ }
+ return Long.toString(Math.round(n));
+ }
+
+ private record Time(long hours, long minutes, long seconds) {
+ }
}