Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion src/main/java/net/tfminecraft/tfmccore/TFMCCore.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
import java.util.Set;
import java.util.Random;

import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.plugin.java.JavaPlugin;

import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;
Expand Down Expand Up @@ -57,6 +60,7 @@ public class TFMCCore extends JavaPlugin{
private final SkillsStatConfig skillsStatConfig = new SkillsStatConfig();
private final FactionsStatConfig factionsStatConfig = new FactionsStatConfig();
private final TfmcConfig tfmcConfig = new TfmcConfig();
private TfmcCommand tfmcCommand;

private final CoreManager coreManager = new CoreManager();
private final StationManager stationManager = new StationManager();
Expand Down Expand Up @@ -140,14 +144,21 @@ private void registerTfmcCommand() {
}
cooldowns.save();
}
TfmcCommand tfmc = new TfmcCommand(tfmcConfig, cooldowns, new BukkitTfmcActions(this),
TfmcCommand tfmc = tfmcCommand = new TfmcCommand(tfmcConfig, cooldowns, new BukkitTfmcActions(this),
Clock.systemDefaultZone(), new Random(), task -> getServer().getScheduler().runTask(this, task));
getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS,
event -> event.registrar().register(tfmc.build(), "TFMC player commands"));
getServer().getPluginManager().registerEvents(new Listener() {
@EventHandler
public void onQuit(PlayerQuitEvent event) {
tfmc.onQuit(event.getPlayer());
}
}, this);
}

@Override
public void onDisable() {
if (tfmcCommand != null) tfmcCommand.shutdown();
if (multipartPacks != null) multipartPacks.close();
if (stoneListener != null) {
stoneListener.refundAll();
Expand Down
27 changes: 27 additions & 0 deletions src/main/java/net/tfminecraft/tfmccore/tfmc/BukkitTfmcActions.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.RegisteredServiceProvider;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
import org.bukkit.scheduler.BukkitTask;

import net.kyori.adventure.text.Component;
import net.luckperms.api.LuckPerms;
Expand Down Expand Up @@ -106,6 +109,30 @@ public CompletableFuture<Optional<String>> stepTrack(Player player, String track
});
}

@Override
public FlightState startFlight(Player player, float speed) {
FlightState previous = new FlightState(player.getAllowFlight(), player.isFlying(), player.getFlySpeed());
player.setAllowFlight(true);
player.setFlySpeed(Math.max(-1f, Math.min(1f, speed)));
return previous;
}

@Override
public void endFlight(Player player, FlightState previous) {
player.setFlySpeed(previous.flySpeed());
player.setAllowFlight(previous.allowFlight());
player.setFlying(previous.allowFlight() && previous.flying());
if (!player.getAllowFlight() && !player.isOnGround()) {
player.addPotionEffect(new PotionEffect(PotionEffectType.SLOW_FALLING, 100, 0, false, false, true));
}
}

@Override
public Runnable later(long ticks, Runnable task) {
BukkitTask scheduled = Bukkit.getScheduler().runTaskLater(plugin, task, ticks);
return scheduled::cancel;
}

private static CompletableFuture<User> withUser(LuckPerms api, Player player) {
User loaded = api.getUserManager().getUser(player.getUniqueId());
return loaded != null
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/net/tfminecraft/tfmccore/tfmc/TfmcActions.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
/** Side effects of the /tfmc subcommands, separated from the command tree for testing. */
public interface TfmcActions {

/** A player's flight settings before a temporary change, so they can be put back. */
record FlightState(boolean allowFlight, boolean flying, float flySpeed) {}

void consoleCommand(String command);

void playerCommand(Player player, String command);
Expand All @@ -28,4 +31,13 @@ public interface TfmcActions {

/** Moves the player one step along a LuckPerms track; completes with the new group. */
CompletableFuture<Optional<String>> stepTrack(Player player, String track, boolean promote);

/** Lets the player fly at {@code speed} and returns their previous flight settings. */
FlightState startFlight(Player player, float speed);

/** Restores flight settings, easing the player down if the change leaves them airborne. */
void endFlight(Player player, FlightState previous);

/** Runs {@code task} on the main thread after {@code ticks}; the returned action cancels it. */
Runnable later(long ticks, Runnable task);
}
115 changes: 112 additions & 3 deletions src/main/java/net/tfminecraft/tfmccore/tfmc/TfmcCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
import java.util.function.Predicate;
Expand Down Expand Up @@ -43,6 +46,8 @@ public final class TfmcCommand {
public static final String NAME = "tfmc";
public static final String BOOSTER_COOLDOWN = "booster";

private record ParrotFlight(TfmcActions.FlightState previous, Runnable cancelExpiry) {}

private static final Logger LOGGER = Logger.getLogger("TFMCCore");
private static final Pattern PLAYER_NAME = Pattern.compile("[A-Za-z0-9_]{1,16}");
private static final MiniMessage MINI = MiniMessage.miniMessage();
Expand All @@ -53,6 +58,7 @@ public final class TfmcCommand {
private final Clock clock;
private final RandomGenerator random;
private final Executor mainThread;
private final Map<UUID, ParrotFlight> parrots = new ConcurrentHashMap<>();

/** {@code mainThread} runs LuckPerms completions, which arrive on LuckPerms worker threads. */
public TfmcCommand(TfmcConfig config, TfmcCooldowns cooldowns, TfmcActions actions, Clock clock,
Expand Down Expand Up @@ -106,6 +112,34 @@ public LiteralCommandNode<CommandSourceStack> build() {
.executes(player(p -> step(p, name, false)))));
}

// Content still being built on dev; each section stays hidden until tfmc.yml enables it
root.then(Commands.literal("tutorial").requires(enabled("tutorials"))
.then(Commands.argument("name", StringArgumentType.word())
.suggests((context, builder) -> {
String typed = builder.getRemainingLowerCase();
config.keys("tutorials.list").stream()
.filter(tutorial -> tutorial.toLowerCase(Locale.ROOT).startsWith(typed))
.forEach(builder::suggest);
return builder.buildFuture();
})
.then(Commands.literal("clear").executes(context -> {
String name = StringArgumentType.getString(context, "name");
return player(p -> clearTutorial(p, name)).run(context);
}))));

root.then(Commands.literal("parrot")
.requires(enabled("parrot").and(anyPermission("parrot.permissions")))
.executes(player(this::parrot)));
root.then(Commands.literal("unparrot").requires(enabled("parrot"))
.executes(player(p -> {
if (!endParrot(p, true)) {
send(p, "parrot.not-a-parrot");
}
})));

root.then(Commands.literal("worldboss").requires(enabled("worldboss"))
.then(Commands.literal("info").executes(player(p -> send(p, "worldboss.info")))));

root.then(Commands.literal("poster")
.then(Commands.argument("name", StringArgumentType.word())
.suggests((context, builder) -> {
Expand Down Expand Up @@ -244,6 +278,65 @@ private void step(Player player, String track, boolean promote) {
}, mainThread);
}

private void clearTutorial(Player player, String name) {
Optional<String> tutorial = config.keys("tutorials.list").stream()
.filter(key -> key.equalsIgnoreCase(name))
.findFirst();
if (tutorial.isEmpty()) {
send(player, "tutorials.unknown", Placeholder.unparsed("name", name));
return;
}
String base = "tutorials.list." + tutorial.get();
if (onCooldown(player, "tutorial." + tutorial.get(), config.seconds("tutorials.cooldown-seconds"), "messages.cooldown")) {
return;
}
runConsole(player, config.lines(base + ".clear"));
send(player, "tutorials.cleared", Placeholder.unparsed("name", config.string(base + ".name", tutorial.get())));
}

private void parrot(Player player) {
if (parrots.containsKey(player.getUniqueId())) {
send(player, "parrot.already");
return;
}
if (onCooldown(player, "parrot", config.seconds("parrot.cooldown-seconds"), "parrot.cooldown")) {
return;
}
long seconds = Math.max(1L, config.seconds("parrot.duration-seconds"));
actions.consoleCommand(config.string("parrot.disguise")
.replace("<player>", player.getName()).replace("<seconds>", Long.toString(seconds)));
TfmcActions.FlightState previous = actions.startFlight(player, (float) config.decimal("parrot.fly-speed", 0.01));
Runnable cancel = actions.later(seconds * 20L, () -> endParrot(player, false));
parrots.put(player.getUniqueId(), new ParrotFlight(previous, cancel));
send(player, "parrot.started");
}

/** Ends a parrot flight early or on expiry; false when the player is not a parrot. */
private boolean endParrot(Player player, boolean undisguise) {
ParrotFlight flight = parrots.remove(player.getUniqueId());
if (flight == null) {
return false;
}
flight.cancelExpiry().run();
if (undisguise) {
actions.consoleCommand(config.string("parrot.undisguise").replace("<player>", player.getName()));
}
actions.endFlight(player, flight.previous());
send(player, "parrot.ended");
return true;
}

/** Puts a leaving player's flight settings back so the temporary flight is not saved with them. */
public void onQuit(Player player) {
endParrot(player, true);
}

public void shutdown() {
for (Player player : Bukkit.getOnlinePlayers()) {
endParrot(player, true);
}
}

private void poster(Player player, String name) {
config.entries("posters").entrySet().stream()
.filter(poster -> poster.getKey().equalsIgnoreCase(name))
Expand Down Expand Up @@ -271,12 +364,16 @@ private void runConsole(Player player, List<String> commands) {
}

private boolean onCooldown(Player player, String subcommand) {
long left = cooldowns.remaining(subcommand, player.getUniqueId(), config.seconds(subcommand + ".cooldown-seconds") * 1000L);
return onCooldown(player, subcommand, config.seconds(subcommand + ".cooldown-seconds"), "messages.cooldown");
}

private boolean onCooldown(Player player, String key, long seconds, String messagePath) {
long left = cooldowns.remaining(key, player.getUniqueId(), seconds * 1000L);
if (left > 0) {
send(player, "messages.cooldown", Placeholder.unparsed("time", TfmcCooldowns.format(left)));
send(player, messagePath, Placeholder.unparsed("time", TfmcCooldowns.format(left)));
return true;
}
cooldowns.markUsed(subcommand, player.getUniqueId());
cooldowns.markUsed(key, player.getUniqueId());
return false;
}

Expand Down Expand Up @@ -305,6 +402,18 @@ private String trackPermission(String track, boolean promote) {
return promote ? steps.promotePermission() : steps.demotePermission();
}

private Predicate<CommandSourceStack> enabled(String section) {
return source -> config.enabled(section);
}

/** Any of the listed permissions; an empty list means everyone. */
private Predicate<CommandSourceStack> anyPermission(String path) {
return source -> {
List<String> nodes = config.lines(path);
return nodes.isEmpty() || nodes.stream().anyMatch(source.getSender()::hasPermission);
};
}

// Restricted nodes fail closed: a missing or blank permission hides the command from everyone
private static Predicate<CommandSourceStack> permission(Supplier<String> node) {
return source -> {
Expand Down
8 changes: 8 additions & 0 deletions src/main/java/net/tfminecraft/tfmccore/tfmc/TfmcConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ public String string(String path, String fallback) {
return value == null || value.isBlank() ? fallback : value;
}

public boolean enabled(String section) {
return config.getBoolean(section + ".enabled", false);
}

public double decimal(String path, double fallback) {
return config.getDouble(path, fallback);
}

public long seconds(String path) {
return Math.max(0L, config.getLong(path, 0L));
}
Expand Down
Loading