diff --git a/gradle.properties b/gradle.properties index da42573..cec324b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,7 +9,7 @@ yarn_mappings=1.20.1+build.10 loader_version=0.16.7 # Mod Properties -mod_version=1.14.3 +mod_version=1.15.0 maven_group=cc.reconnected archives_base_name=rcc-server diff --git a/src/main/java/cc/reconnected/server/RccServer.java b/src/main/java/cc/reconnected/server/RccServer.java index 4aee8d1..6a1bd3c 100644 --- a/src/main/java/cc/reconnected/server/RccServer.java +++ b/src/main/java/cc/reconnected/server/RccServer.java @@ -99,6 +99,10 @@ public class RccServer implements ModInitializer { WarpCommand.register(dispatcher, registryAccess, environment); SetWarpCommand.register(dispatcher, registryAccess, environment); DeleteWarpCommand.register(dispatcher, registryAccess, environment); + + TimeBarCommand.register(dispatcher, registryAccess, environment); + + NearCommand.register(dispatcher, registryAccess, environment); }); AfkTracker.register(); @@ -106,6 +110,7 @@ public class RccServer implements ModInitializer { BackTracker.register(); TabList.register(); HttpApiServer.register(); + BossBarManager.register(); ServerLifecycleEvents.SERVER_STARTED.register(server -> { luckPerms = LuckPermsProvider.get(); diff --git a/src/main/java/cc/reconnected/server/RccServerConfigModel.java b/src/main/java/cc/reconnected/server/RccServerConfigModel.java index eb67d2c..9798bce 100644 --- a/src/main/java/cc/reconnected/server/RccServerConfigModel.java +++ b/src/main/java/cc/reconnected/server/RccServerConfigModel.java @@ -33,4 +33,13 @@ public class RccServerConfigModel { )); public String playerTabName = "%rcc-server:afk%%player:displayname_visual%"; + + public int nearCommandMaxRange = 48; + public int nearCommandDefaultRange = 32; + + public boolean enableAutoRestart = true; + public ArrayList restartAt = new ArrayList<>(List.of( + "06:00", + "18:00" + )); } diff --git a/src/main/java/cc/reconnected/server/api/events/BossBarEvents.java b/src/main/java/cc/reconnected/server/api/events/BossBarEvents.java new file mode 100644 index 0000000..4069105 --- /dev/null +++ b/src/main/java/cc/reconnected/server/api/events/BossBarEvents.java @@ -0,0 +1,56 @@ +package cc.reconnected.server.api.events; + +import cc.reconnected.server.core.BossBarManager; +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; +import net.minecraft.server.MinecraftServer; + +public class BossBarEvents { + public static final Event START = EventFactory.createArrayBacked(Start.class, callbacks -> + (timeBar, server) -> { + for (Start callback : callbacks) { + callback.onStart(timeBar, server); + } + }); + + public static final Event END = EventFactory.createArrayBacked(End.class, callbacks -> + (timeBar, server) -> { + for (End callback : callbacks) { + callback.onEnd(timeBar, server); + } + }); + + public static final Event CANCEL = EventFactory.createArrayBacked(Cancel.class, callbacks -> + (timeBar, server) -> { + for (Cancel callback : callbacks) { + callback.onCancel(timeBar, server); + } + }); + + public static final Event PROGRESS = EventFactory.createArrayBacked(Progress.class, callbacks -> + (timeBar, server) -> { + for (Progress callback : callbacks) { + callback.onProgress(timeBar, server); + } + }); + + @FunctionalInterface + public interface Start { + void onStart(BossBarManager.TimeBar timeBar, MinecraftServer server); + } + + @FunctionalInterface + public interface End { + void onEnd(BossBarManager.TimeBar timeBar, MinecraftServer server); + } + + @FunctionalInterface + public interface Cancel { + void onCancel(BossBarManager.TimeBar timeBar, MinecraftServer server); + } + + @FunctionalInterface + public interface Progress { + void onProgress(BossBarManager.TimeBar timeBar, MinecraftServer server); + } +} diff --git a/src/main/java/cc/reconnected/server/commands/misc/NearCommand.java b/src/main/java/cc/reconnected/server/commands/misc/NearCommand.java new file mode 100644 index 0000000..cfabbc3 --- /dev/null +++ b/src/main/java/cc/reconnected/server/commands/misc/NearCommand.java @@ -0,0 +1,82 @@ +package cc.reconnected.server.commands.misc; + +import cc.reconnected.server.RccServer; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.context.CommandContext; +import me.lucko.fabric.api.permissions.v0.Permissions; +import net.minecraft.command.CommandRegistryAccess; +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 java.util.ArrayList; +import java.util.Comparator; + +import static net.minecraft.server.command.CommandManager.*; + +public class NearCommand { + public static void register(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + var rootCommand = literal("near") + .requires(Permissions.require("rcc.command.near", 2)) + .executes(context -> { + if (!context.getSource().isExecutedByPlayer()) { + context.getSource().sendFeedback(() -> Text.of("This command can only be executed by players!"), false); + return 1; + } + return execute(context, RccServer.CONFIG.nearCommandDefaultRange(), context.getSource().getPlayer()); + }) + .then(argument("radius", IntegerArgumentType.integer(0, RccServer.CONFIG.nearCommandMaxRange())) + .executes(context -> { + if (!context.getSource().isExecutedByPlayer()) { + context.getSource().sendFeedback(() -> Text.of("This command can only be executed by players!"), false); + return 1; + } + return execute(context, IntegerArgumentType.getInteger(context, "radius"), context.getSource().getPlayer()); + })); + + dispatcher.register(rootCommand); + } + + private static int execute(CommandContext context, int range, ServerPlayerEntity sourcePlayer) { + var list = new ArrayList(); + + var sourcePos = sourcePlayer.getPos(); + sourcePlayer.getServerWorld().getPlayers().forEach(targetPlayer -> { + var targetPos = targetPlayer.getPos(); + if (!sourcePlayer.getUuid().equals(targetPlayer.getUuid()) && sourcePos.isInRange(targetPos, range)) { + var distance = sourcePos.distanceTo(targetPos); + list.add(new ClosePlayers(targetPlayer.getDisplayName(), distance)); + } + }); + + if(list.isEmpty()) { + context.getSource().sendFeedback(() -> Text.literal("There is no one near you.").formatted(Formatting.GOLD), false); + return 1; + } + + list.sort(Comparator.comparingDouble(ClosePlayers::distance)); + + var text = Text.empty().append(Text.literal("Nearest players: ").formatted(Formatting.GOLD)); + var comma = Text.literal(", ").formatted(Formatting.GOLD); + for (int i = 0; i < list.size(); i++) { + var player = list.get(i); + if (i > 0) { + text = text.append(comma); + } + text = text.append(player.displayName) + .append(" ") + .append(Text.literal(String.format("(%.1fm)", player.distance)).formatted(Formatting.GREEN)); + } + + final var finalText = text; + context.getSource().sendFeedback(() -> finalText, false); + + return 1; + } + + private record ClosePlayers(Text displayName, double distance) { + } +} diff --git a/src/main/java/cc/reconnected/server/commands/misc/TimeBarCommand.java b/src/main/java/cc/reconnected/server/commands/misc/TimeBarCommand.java new file mode 100644 index 0000000..a3eff3e --- /dev/null +++ b/src/main/java/cc/reconnected/server/commands/misc/TimeBarCommand.java @@ -0,0 +1,122 @@ +package cc.reconnected.server.commands.misc; + +import cc.reconnected.server.api.events.BossBarEvents; +import cc.reconnected.server.core.BossBarManager; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.BoolArgumentType; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import me.lucko.fabric.api.permissions.v0.Permissions; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.command.CommandSource; +import net.minecraft.command.argument.UuidArgumentType; +import net.minecraft.entity.boss.BossBar; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.text.ClickEvent; +import net.minecraft.text.HoverEvent; +import net.minecraft.text.Style; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; + +import java.util.Arrays; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +import static net.minecraft.server.command.CommandManager.*; + +public class TimeBarCommand { + private static final ConcurrentHashMap runningBars = new ConcurrentHashMap<>(); + + public static void register(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + var rootCommand = literal("timebar") + .requires(Permissions.require("rcc.command.timebar", 3)) + .then(literal("start") + .then(argument("seconds", IntegerArgumentType.integer(0)) + .then(argument("color", StringArgumentType.word()) + .suggests((context, builder) -> { + var colors = Arrays.stream(BossBar.Color.values()).map(Enum::toString).toList(); + return CommandSource.suggestMatching(colors, builder); + }) + .then(argument("style", StringArgumentType.word()) + .suggests((context, builder) -> { + var styles = Arrays.stream(BossBar.Style.values()).map(Enum::toString).toList(); + return CommandSource.suggestMatching(styles, builder); + }) + .then(argument("countdown", BoolArgumentType.bool()) + .then(argument("label", StringArgumentType.string()) + .then(argument("command", StringArgumentType.greedyString()) + .suggests((context, builder) -> dispatcher.getRoot().listSuggestions(context, builder)) + .executes(TimeBarCommand::execute)) + + ) + ) + ) + ) + ) + ) + .then(literal("cancel") + .then(argument("uuid", UuidArgumentType.uuid()) + .executes(TimeBarCommand::executeCancel))); + + dispatcher.register(rootCommand); + + BossBarEvents.END.register((timeBar, server) -> { + if (runningBars.containsKey(timeBar.getUuid())) { + var barCommand = runningBars.get(timeBar.getUuid()); + try { + dispatcher.execute(barCommand.command(), barCommand.source); + } catch (CommandSyntaxException e) { + barCommand.source.sendFeedback(() -> Text.literal(e.toString()).formatted(Formatting.RED), false); + } + runningBars.remove(timeBar.getUuid()); + } + }); + } + + private static int execute(CommandContext context) { + var seconds = IntegerArgumentType.getInteger(context, "seconds"); + var colorName = StringArgumentType.getString(context, "color"); + var styleName = StringArgumentType.getString(context, "style"); + var countdown = BoolArgumentType.getBool(context, "countdown"); + var label = StringArgumentType.getString(context, "label"); + var command = StringArgumentType.getString(context, "command"); + + var color = BossBar.Color.valueOf(colorName); + var style = BossBar.Style.valueOf(styleName); + + var bar = BossBarManager.getInstance().startTimeBar(label, seconds, color, style, countdown); + + runningBars.put(bar.getUuid(), new BarCommand(context.getSource(), command)); + + + context.getSource().sendFeedback(() -> Text + .literal("New time bar created with UUID ") + .append(Text.literal(bar.getUuid().toString()).setStyle(Style.EMPTY + .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Text.of("Click to copy"))) + .withClickEvent(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, bar.getUuid().toString())))), true); + + return 1; + } + + private static int executeCancel(CommandContext context) { + var uuid = UuidArgumentType.getUuid(context, "uuid"); + + if (!runningBars.containsKey(uuid)) { + context.getSource().sendFeedback(() -> Text.literal("Time bar not found!").formatted(Formatting.RED), false); + return 1; + } + + runningBars.remove(uuid); + BossBarManager.getInstance().cancelTimeBar(uuid); + + context.getSource().sendFeedback(() -> Text.literal("Time bar canceled"), true); + + return 1; + } + + private record BarCommand(ServerCommandSource source, String command) { + } +} diff --git a/src/main/java/cc/reconnected/server/core/AutoRestart.java b/src/main/java/cc/reconnected/server/core/AutoRestart.java new file mode 100644 index 0000000..f003091 --- /dev/null +++ b/src/main/java/cc/reconnected/server/core/AutoRestart.java @@ -0,0 +1,17 @@ +package cc.reconnected.server.core; + +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import net.minecraft.server.MinecraftServer; + +public class AutoRestart { + private static MinecraftServer server; + public static void register() { + ServerLifecycleEvents.SERVER_STARTING.register(s -> server = s); + + + } + + private static void schedule() { + + } +} diff --git a/src/main/java/cc/reconnected/server/core/BossBarManager.java b/src/main/java/cc/reconnected/server/core/BossBarManager.java new file mode 100644 index 0000000..52b6a89 --- /dev/null +++ b/src/main/java/cc/reconnected/server/core/BossBarManager.java @@ -0,0 +1,191 @@ +package cc.reconnected.server.core; + +import cc.reconnected.server.RccServer; +import cc.reconnected.server.api.events.BossBarEvents; +import cc.reconnected.server.util.Components; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; +import net.minecraft.entity.boss.BossBar; +import net.minecraft.entity.boss.CommandBossBar; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; + +import java.util.Collection; +import java.util.UUID; +import java.util.concurrent.ConcurrentLinkedDeque; + +public class BossBarManager { + private static BossBarManager instance; + + public static BossBarManager getInstance() { + return instance; + } + + private static MinecraftServer server; + private static ConcurrentLinkedDeque timeBars = new ConcurrentLinkedDeque<>(); + private static final MiniMessage miniMessage = MiniMessage.miniMessage(); + + public static void register() { + instance = new BossBarManager(); + + ServerLifecycleEvents.SERVER_STARTING.register(s -> server = s); + + ServerTickEvents.END_SERVER_TICK.register(server -> { + // every second + if (server.getTicks() % 20 == 0) { + for (var timeBar : timeBars) { + var remove = timeBar.elapse(); + BossBarEvents.PROGRESS.invoker().onProgress(timeBar, server); + + var players = server.getPlayerManager().getPlayerList(); + showBar(players, timeBar); + + if (remove) { + timeBars.remove(timeBar); + BossBarEvents.END.invoker().onEnd(timeBar, server); + hideBar(players, timeBar); + } + } + } + }); + } + + private static void showBar(Collection players, TimeBar timeBar) { + timeBar.getBossBar().addPlayers(players); + } + + private static void hideBar(Collection players, TimeBar timeBar) { + players.forEach(player -> { + timeBar.getBossBar().removePlayer(player); + + }); + } + + public TimeBar startTimeBar(String label, int seconds, BossBar.Color color, BossBar.Style style, boolean countdown) { + var countdownBar = new TimeBar(label, seconds, countdown, color, style); + + timeBars.add(countdownBar); + + var players = server.getPlayerManager().getPlayerList(); + showBar(players, countdownBar); + + BossBarEvents.START.invoker().onStart(countdownBar, server); + + return countdownBar; + } + + public boolean cancelTimeBar(TimeBar timeBar) { + var success = timeBars.remove(timeBar); + if (success) { + var players = server.getPlayerManager().getPlayerList(); + hideBar(players, timeBar); + BossBarEvents.CANCEL.invoker().onCancel(timeBar, server); + } + return success; + } + + public boolean cancelTimeBar(UUID uuid) { + var progressBar = timeBars.stream().filter(p -> p.uuid.equals(uuid)).findFirst().orElse(null); + if (progressBar == null) { + return false; + } + + return cancelTimeBar(progressBar); + } + + public static class TimeBar { + private final UUID uuid = UUID.randomUUID(); + private final CommandBossBar bossBar; + private final String label; + private final int time; + private int elapsedSeconds = 0; + private final boolean countdown; + + public TimeBar(String label, int time, boolean countdown, BossBar.Color color, BossBar.Style style) { + this.bossBar = new CommandBossBar(Identifier.of(RccServer.MOD_ID, uuid.toString()), Text.of(label)); + this.bossBar.setColor(color); + this.bossBar.setStyle(style); + this.label = label; + this.time = time; + this.countdown = countdown; + updateName(); + updateProgress(); + } + + public static String formatTime(int totalSeconds) { + var hours = totalSeconds / 3600; + var minutes = (totalSeconds / 60) % 60; + var seconds = totalSeconds % 60; + if(totalSeconds >= 3600) { + return String.format("%dh%dm%ds", hours, minutes, seconds); + } + return String.format("%dm%ds", minutes, seconds); + } + + public void updateName() { + var totalTime = formatTime(this.time); + var elapsedTime = formatTime(this.elapsedSeconds); + + var remaining = time - elapsedSeconds; + var remainingTime = formatTime(remaining); + var text = miniMessage.deserialize(label, TagResolver.resolver( + Placeholder.parsed("total_time", totalTime), + Placeholder.parsed("elapsed_time", elapsedTime), + Placeholder.parsed("remaining_time", remainingTime) + )); + + bossBar.setName(Components.toText(text)); + } + + public UUID getUuid() { + return uuid; + } + + public CommandBossBar getBossBar() { + return bossBar; + } + + public String getLabel() { + return label; + } + + public int getTime() { + return time; + } + + public int getElapsedSeconds() { + return elapsedSeconds; + } + + public boolean isCountdown() { + return countdown; + } + + public boolean elapse() { + this.elapsedSeconds++; + + updateProgress(); + updateName(); + + return this.elapsedSeconds >= this.time; + } + + private void updateProgress() { + float progress = (float) elapsedSeconds / (float) time; + if (countdown) { + progress = 1f - progress; + } + + bossBar.setPercent(Math.min( + Math.max( + progress, + 0f), + 1f)); + } + } +}