diff options
| author | Anhgelus Morhtuuzh <william@herges.fr> | 2026-03-19 14:32:46 +0100 |
|---|---|---|
| committer | Anhgelus Morhtuuzh <william@herges.fr> | 2026-03-19 14:32:46 +0100 |
| commit | 095aa12d0cf8170014f59d1e1646311989aaca58 (patch) | |
| tree | 3af7b4e08aaae82a5bdcf6fbd0bb14b519d1cb27 /src/main/java | |
Copy Molehunt and rename
Diffstat (limited to 'src/main/java')
13 files changed, 1158 insertions, 0 deletions
diff --git a/src/main/java/world/anhgelus/floodhunt/Floodhunt.java b/src/main/java/world/anhgelus/floodhunt/Floodhunt.java new file mode 100644 index 0000000..95aa95c --- /dev/null +++ b/src/main/java/world/anhgelus/floodhunt/Floodhunt.java @@ -0,0 +1,225 @@ +package world.anhgelus.floodhunt; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; +import net.fabricmc.api.ModInitializer; +import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; +import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents; +import net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import net.fabricmc.fabric.api.gamerule.v1.GameRuleBuilder; +import net.fabricmc.fabric.api.gamerule.v1.GameRuleEvents; +import net.fabricmc.fabric.api.message.v1.ServerMessageEvents; +import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.minecraft.network.packet.s2c.play.OverlayMessageS2CPacket; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import net.minecraft.util.Identifier; +import net.minecraft.world.GameMode; +import net.minecraft.world.rule.GameRule; +import net.minecraft.world.rule.GameRuleCategory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import world.anhgelus.floodhunt.config.Config; +import world.anhgelus.floodhunt.config.ConfigPayload; +import world.anhgelus.floodhunt.config.SimpleConfig; +import world.anhgelus.floodhunt.game.Game; +import world.anhgelus.floodhunt.game.GamePayload; + +import java.util.HashMap; +import java.util.UUID; + +import static net.minecraft.server.command.CommandManager.literal; + + +public class Floodhunt implements ModInitializer { + + public static final String MOD_ID = "floodhunt"; + 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, "game_duration_minutes")); + 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, "mole_percentage")); + public static final GameRule<Integer> MOLE_COUNT = GameRuleBuilder + .forInteger(CONFIG_FILE.getOrDefault("mole_count", -1)) + .category(GameRuleCategory.MISC) + .buildAndRegister(Identifier.of(MOD_ID, "mole_count")); + public static final GameRule<Boolean> SHOW_NAMETAGS = GameRuleBuilder + .forBoolean(CONFIG_FILE.getOrDefault("show_nametags", false)) + .category(GameRuleCategory.MISC) + .buildAndRegister(Identifier.of(MOD_ID, "show_nametags")); + public static final GameRule<Boolean> SHOW_TAB = GameRuleBuilder + .forBoolean(CONFIG_FILE.getOrDefault("show_tab", false)) + .category(GameRuleCategory.MISC) + .buildAndRegister(Identifier.of(MOD_ID, "show_tab")); + public static final GameRule<Boolean> SHOW_SKINS = GameRuleBuilder + .forBoolean(CONFIG_FILE.getOrDefault("show_skins", false)) + .category(GameRuleCategory.MISC) + .buildAndRegister(Identifier.of(MOD_ID, "show_skins")); + 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, "initial_world_size")); + 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, "final_world_size")); + 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, "border_moving_starting_time_offset_minutes")); + public static final GameRule<Boolean> ENABLE_PORTALS = GameRuleBuilder + .forBoolean(CONFIG_FILE.getOrDefault("enable_portals", false)) + .category(GameRuleCategory.MISC) + .buildAndRegister(Identifier.of(MOD_ID, "enable_portals")); + 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, "food_on_start")); + public static Config CONFIG; + public static HashMap<UUID, Boolean> timerVisibility = new HashMap<>(); + + static { + GameRuleEvents.changeCallback(SHOW_NAMETAGS).register(Floodhunt::sendConfigPayload); + GameRuleEvents.changeCallback(SHOW_TAB).register(Floodhunt::sendConfigPayload); + GameRuleEvents.changeCallback(SHOW_SKINS).register(Floodhunt::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 Floodhunt"); + + final var command = literal("floodhunt"); + 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.floodhunt.timer.show"), false); + + if (game == null || !game.started()) { + player.networkHandler.sendPacket(new OverlayMessageS2CPacket( + Text.translatable("commands.floodhunt.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.floodhunt.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.floodhunt.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.floodhunt.role.mole") + .append("\n\n") + .append(Text.translatable("commands.floodhunt.role.mole.list", game.getMolesAsString())), + false); + } else if (player.isSpectator()) { + source.sendFeedback( + () -> Text.translatable("commands.floodhunt.role.survivor.mole_count", game.getMolesCount()), + false); + } else { + source.sendFeedback( + () -> Text.translatable("commands.floodhunt.role.survivor") + .append("\n\n") + .append(Text.translatable("commands.floodhunt.role.survivor.mole_count", game.getMolesCount())), + 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.floodhunt.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/floodhunt/config/Config.java b/src/main/java/world/anhgelus/floodhunt/config/Config.java new file mode 100644 index 0000000..4ec87fb --- /dev/null +++ b/src/main/java/world/anhgelus/floodhunt/config/Config.java @@ -0,0 +1,140 @@ +package world.anhgelus.floodhunt.config; + +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.minecraft.server.MinecraftServer; +import world.anhgelus.floodhunt.Floodhunt; + +public class Config { + + 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 """ + # Floodhunt mod configuration file + # To regenerate the default configuration, delete, move or rename this file. + + # Game settings + + # The duration of a floodhunt 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(Floodhunt.GAME_DURATION); + } + + public int getMolePercentage() { + return server.getOverworld().getGameRules().getValue(Floodhunt.MOLE_PERCENTAGE); + } + + public int getMoleCount() { + return server.getOverworld().getGameRules().getValue(Floodhunt.MOLE_COUNT); + } + + public boolean nametagsEnabled() { + return server.getOverworld().getGameRules().getValue(Floodhunt.SHOW_NAMETAGS); + } + + public boolean skinsEnabled() { + return server.getOverworld().getGameRules().getValue(Floodhunt.SHOW_SKINS); + } + + public boolean tabEnabled() { + return server.getOverworld().getGameRules().getValue(Floodhunt.SHOW_TAB); + } + + public int getInitialWorldSize() { + return server.getOverworld().getGameRules().getValue(Floodhunt.INITIAL_WORLD_SIZE); + } + + public int getFinalWorldSize() { + return server.getOverworld().getGameRules().getValue(Floodhunt.FINAL_WORLD_SIZE); + } + + public int getBorderShrinkingStartingTimeOffset() { + return server.getOverworld().getGameRules().getValue(Floodhunt.MOVING_STARTING_TIME_OFFSET); + } + + public boolean portalsEnabled() { + return server.getOverworld().getGameRules().getValue(Floodhunt.ENABLE_PORTALS); + } + + public boolean foodOnStart() { + return server.getOverworld().getGameRules().getValue(Floodhunt.FOOD_ON_START); + } +} diff --git a/src/main/java/world/anhgelus/floodhunt/config/ConfigPayload.java b/src/main/java/world/anhgelus/floodhunt/config/ConfigPayload.java new file mode 100644 index 0000000..2b6eb88 --- /dev/null +++ b/src/main/java/world/anhgelus/floodhunt/config/ConfigPayload.java @@ -0,0 +1,25 @@ +package world.anhgelus.floodhunt.config; + +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.codec.PacketCodecs; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.util.Identifier; +import world.anhgelus.floodhunt.Floodhunt; + +public record ConfigPayload(boolean showNametags, boolean showSkins, boolean showTab) implements CustomPayload { + public static final Identifier CONFIG_PACKET_ID = Identifier.of(Floodhunt.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 + ); + + @Override + public Id<? extends CustomPayload> getId() { + return ID; + } +} diff --git a/src/main/java/world/anhgelus/floodhunt/config/SimpleConfig.java b/src/main/java/world/anhgelus/floodhunt/config/SimpleConfig.java new file mode 100644 index 0000000..ed5f3ec --- /dev/null +++ b/src/main/java/world/anhgelus/floodhunt/config/SimpleConfig.java @@ -0,0 +1,253 @@ +package world.anhgelus.floodhunt.config; + +/* + * Copyright (c) 2021 magistermaks + * Slightly modified by Léo-21 and Anhgelus Morhtuuzh + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import net.fabricmc.loader.api.FabricLoader; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +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; + + 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"; + } + + } + +} diff --git a/src/main/java/world/anhgelus/floodhunt/game/Game.java b/src/main/java/world/anhgelus/floodhunt/game/Game.java new file mode 100644 index 0000000..b395a06 --- /dev/null +++ b/src/main/java/world/anhgelus/floodhunt/game/Game.java @@ -0,0 +1,204 @@ +package world.anhgelus.floodhunt.game; + +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.network.packet.s2c.play.OverlayMessageS2CPacket; +import net.minecraft.network.packet.s2c.play.SubtitleS2CPacket; +import net.minecraft.network.packet.s2c.play.TitleFadeS2CPacket; +import net.minecraft.network.packet.s2c.play.TitleS2CPacket; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; +import net.minecraft.world.GameMode; +import net.minecraft.world.rule.GameRules; +import world.anhgelus.floodhunt.Floodhunt; +import world.anhgelus.floodhunt.timer.TickTask; +import world.anhgelus.floodhunt.timer.TimerAccess; +import world.anhgelus.floodhunt.utils.TimeUtils; + +import java.util.*; +import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class Game { + + public final int defaultTime = Floodhunt.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 = Floodhunt.CONFIG.getMoleCount() < 0 + ? Math.floorDiv(server.getCurrentPlayerCount(), Math.floorDiv(100, Floodhunt.CONFIG.getMolePercentage())) + : Floodhunt.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(Floodhunt.CONFIG.getInitialWorldSize()); + if (Floodhunt.CONFIG.getBorderShrinkingStartingTimeOffset() < Floodhunt.CONFIG.getGameDuration()) { + timer.dds_runTask(new TickTask(() -> worldBorder.interpolateSize( + Floodhunt.CONFIG.getInitialWorldSize(), + Floodhunt.CONFIG.getFinalWorldSize(), + (Floodhunt.CONFIG.getGameDuration() - Floodhunt.CONFIG.getBorderShrinkingStartingTimeOffset()) * 60 * 20L, + 0L + ), Floodhunt.CONFIG.getBorderShrinkingStartingTimeOffset() * 60 * 20L)); + } + + final var title = new TitleS2CPacket(Text.translatable("floodhunt.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 (Floodhunt.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("floodhunt.game.start.mole.title"))); + p.networkHandler.sendPacket(new SubtitleS2CPacket(Text.translatable("floodhunt.game.start.mole.subtitle"))); + } else { + p.networkHandler.sendPacket(new TitleS2CPacket(Text.translatable("floodhunt.game.start.survivor.title"))); + p.networkHandler.sendPacket(new SubtitleS2CPacket(Text.translatable("floodhunt.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 (Floodhunt.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.floodhunt.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("floodhunt.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("floodhunt.game.end.winners.moles.title")); + } else { + winner = new TitleS2CPacket(Text.translatable("floodhunt.game.end.winners.survivors.title")); + } + pm.sendToAll(new SubtitleS2CPacket(Text.translatable("floodhunt.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)); + } + + private Stream<ServerPlayerEntity> getMoles() { + return moles.stream() + .map(uuid -> server.getPlayerManager().getPlayer(uuid)) + .filter(Objects::nonNull) + .filter(p -> !p.isSpectator() && !p.isCreative()); + } + + public int getMolesCount() { + return getMoles().toArray().length; + } + + public String getMolesAsString() { + return getMoles().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() { + final var moles = getMoles().map(PlayerEntity::getUuid).toList(); + return !moles.isEmpty() && 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().sendToAll(ServerPlayNetworking.createS2CPacket(payload)); + } +} diff --git a/src/main/java/world/anhgelus/floodhunt/game/GamePayload.java b/src/main/java/world/anhgelus/floodhunt/game/GamePayload.java new file mode 100644 index 0000000..76368df --- /dev/null +++ b/src/main/java/world/anhgelus/floodhunt/game/GamePayload.java @@ -0,0 +1,23 @@ +package world.anhgelus.floodhunt.game; + +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.codec.PacketCodecs; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.util.Identifier; +import world.anhgelus.floodhunt.Floodhunt; + +public record GamePayload(boolean gameLaunched) implements CustomPayload { + public static final Identifier GAME_PACKET_ID = Identifier.of(Floodhunt.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 + ); + + @Override + public Id<? extends CustomPayload> getId() { + return ID; + } +} diff --git a/src/main/java/world/anhgelus/floodhunt/mixin/NoJoinLeaveMessage.java b/src/main/java/world/anhgelus/floodhunt/mixin/NoJoinLeaveMessage.java new file mode 100644 index 0000000..376afba --- /dev/null +++ b/src/main/java/world/anhgelus/floodhunt/mixin/NoJoinLeaveMessage.java @@ -0,0 +1,18 @@ +package world.anhgelus.floodhunt.mixin; + +import net.minecraft.server.PlayerManager; +import net.minecraft.text.Text; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(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(); + } +} diff --git a/src/main/java/world/anhgelus/floodhunt/mixin/NoMsgCommand.java b/src/main/java/world/anhgelus/floodhunt/mixin/NoMsgCommand.java new file mode 100644 index 0000000..42ae152 --- /dev/null +++ b/src/main/java/world/anhgelus/floodhunt/mixin/NoMsgCommand.java @@ -0,0 +1,17 @@ +package world.anhgelus.floodhunt.mixin; + +import com.mojang.brigadier.CommandDispatcher; +import net.minecraft.server.command.MessageCommand; +import net.minecraft.server.command.ServerCommandSource; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(MessageCommand.class) +public class NoMsgCommand { + @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/floodhunt/mixin/NoPortals.java b/src/main/java/world/anhgelus/floodhunt/mixin/NoPortals.java new file mode 100644 index 0000000..59ca3b7 --- /dev/null +++ b/src/main/java/world/anhgelus/floodhunt/mixin/NoPortals.java @@ -0,0 +1,19 @@ +package world.anhgelus.floodhunt.mixin; + +import net.minecraft.entity.Entity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.world.dimension.PortalManager; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import world.anhgelus.floodhunt.Floodhunt; + +@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 (Floodhunt.CONFIG == null || Floodhunt.CONFIG.portalsEnabled()) return; + cir.setReturnValue(false); + } +} diff --git a/src/main/java/world/anhgelus/floodhunt/mixin/WorldTimerAccess.java b/src/main/java/world/anhgelus/floodhunt/mixin/WorldTimerAccess.java new file mode 100644 index 0000000..3b7d7fb --- /dev/null +++ b/src/main/java/world/anhgelus/floodhunt/mixin/WorldTimerAccess.java @@ -0,0 +1,45 @@ +package world.anhgelus.floodhunt.mixin; + +import net.minecraft.server.world.ServerWorld; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import world.anhgelus.floodhunt.timer.TimerAccess; + +import java.util.ArrayList; +import java.util.List; +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(); + } +} diff --git a/src/main/java/world/anhgelus/floodhunt/timer/TickTask.java b/src/main/java/world/anhgelus/floodhunt/timer/TickTask.java new file mode 100644 index 0000000..ca7f700 --- /dev/null +++ b/src/main/java/world/anhgelus/floodhunt/timer/TickTask.java @@ -0,0 +1,72 @@ +package world.anhgelus.floodhunt.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; + + /** + * 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; + } + + 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 boolean isRunning() { + return !cancelled; + } + + @Override + public long getTickingBeforeRun() { + if (cancelled) return -1; + return currentTicking; + } +} diff --git a/src/main/java/world/anhgelus/floodhunt/timer/TimerAccess.java b/src/main/java/world/anhgelus/floodhunt/timer/TimerAccess.java new file mode 100644 index 0000000..714ee59 --- /dev/null +++ b/src/main/java/world/anhgelus/floodhunt/timer/TimerAccess.java @@ -0,0 +1,65 @@ +package world.anhgelus.floodhunt.timer; + +import net.minecraft.server.MinecraftServer; +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; + } + + /** + * Run a task (called each tick ticked) + * + * @param task Task to run + */ + void dds_runTask(TimerAccess.TickTask task); + + void dds_cancel(); + + /** + * @return All non-cancelled tasks + */ + List<TickTask> dds_getTasks(); + + 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(); + + boolean isRunning(); + + /** + * @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(); + } +} diff --git a/src/main/java/world/anhgelus/floodhunt/utils/TimeUtils.java b/src/main/java/world/anhgelus/floodhunt/utils/TimeUtils.java new file mode 100644 index 0000000..756e0b0 --- /dev/null +++ b/src/main/java/world/anhgelus/floodhunt/utils/TimeUtils.java @@ -0,0 +1,52 @@ +package world.anhgelus.floodhunt.utils; + +public class TimeUtils { + + 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) { + } +} |
