Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
95c2e94
feat(actionbar): modernize HUD system with static facade, dynamic ale…
Trixmas Sep 30, 2026
9490f1c
feat(sign): modernize sign input API with prompt presets and separate…
Trixmas Sep 30, 2026
3c4b8c6
fix(actionbar): adjust burst interval and handle empty content gracef…
Trixmas Sep 30, 2026
beffccc
feat(sign): revert sign handling to previous fixed commit
Trixmas Sep 30, 2026
750e59e
feat(book): redesign book UI with structured page composition and pix…
Trixmas Sep 30, 2026
4fc74c0
test(book): add unit tests for BookPage, BookPaginator, and BookMenu
Trixmas Sep 30, 2026
2ef3d94
refactor(menu): modernize Menu and PaginatedMenu API with framed layo…
Trixmas Sep 30, 2026
eff9bf9
feat(menu): unify menu click sound handling and silence static elements
Trixmas Sep 30, 2026
850524d
perf(sound): bypass scheduler for zero-delay SoundCue and improve Plu…
Trixmas Sep 30, 2026
7585e92
feat(commands): add sender context, branch path matching, and explici…
Trixmas Sep 30, 2026
40e6028
feat(title): introduce Title API and shared TextAnimation engine
Trixmas Sep 30, 2026
25ef818
refactor(animation): deduplicate collection dispatch and fix
Trixmas Sep 30, 2026
8a1d9e2
feat(utils): enhance ItemBuilder
Trixmas Sep 30, 2026
bf129cc
refactor: update code to use new ItemBuilder methods
Trixmas Sep 30, 2026
79f80a4
refactor(utils): simplify ItemBuilder meta handling with `asMetaFor`
Trixmas Sep 30, 2026
a223c7b
feat(actionbar): stop service and clear state when owning plugin unre…
Trixmas Sep 30, 2026
f905cc0
fix(sign): re-register listener on owner unregistration and modernize…
Trixmas Oct 1, 2026
568fbbe
fix(sign): re-register listener on owner unregistration and modernize…
Trixmas Oct 1, 2026
686787f
fix(book): duplicate components during link resolution and colorize t…
Trixmas Oct 1, 2026
1b8b9e2
fix(title): ensure per-player alert sounds, snapshot countdown recipi…
Trixmas Oct 1, 2026
2135adc
refactor(animation): track playbacks per channel and manage cancellat…
Trixmas Oct 1, 2026
fd17e9f
test: update test suites
Trixmas Oct 1, 2026
fe66a5c
fix(animation): invalidate composer cache and reset cancelled title a…
Trixmas Oct 1, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ public abstract class PlayerCommand implements CommandExecutor {
/**
* {@inheritDoc}
*
* <p>If the sender is not a player, the command is rejected and
* {@code false} is returned.
* <p>If the sender is not a player, a warning is sent to the sender and
* {@code true} is returned.
*/
@Override
public final boolean onCommand(
Expand All @@ -33,7 +33,8 @@ public final boolean onCommand(
String @NonNull [] arguments
) {
if (!(sender instanceof Player player)) {
return false;
warn(sender, "This command can only be executed by in-game players.");
return true;
}

return execute(player, command, label, arguments);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,7 @@
import org.bukkit.util.StringUtil;
import org.jspecify.annotations.NonNull;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.*;

/**
* Produces positional tab completions for commands from {@link CommandArgument} declarations.
Expand Down Expand Up @@ -98,7 +92,7 @@ public List<String> onTabComplete(
}

if (entry.rule().matches(args)) {
aggregatedCandidates.addAll(resolveCandidates(entry.suggestions()));
aggregatedCandidates.addAll(resolveCandidates(sender, entry.suggestions()));
}
}

Expand All @@ -112,7 +106,7 @@ public List<String> onTabComplete(
return matchedResults;
}

private List<String> resolveCandidates(List<String> suggestions) {
private List<String> resolveCandidates(@NonNull CommandSender sender, List<String> suggestions) {
// ponytail: fast-path check avoiding collection allocation when no dynamic tokens are present
boolean hasDynamicKey = false;
for (String suggestion : suggestions) {
Expand All @@ -134,7 +128,7 @@ private List<String> resolveCandidates(List<String> suggestions) {
}

if (suggestion.startsWith("$")) {
List<String> provided = suggestionRegistry.resolve(suggestion);
List<String> provided = suggestionRegistry.resolve(sender, suggestion);
if (provided != null) {
resolvedValues.addAll(provided);
}
Expand Down Expand Up @@ -163,15 +157,26 @@ private boolean matches(String[] input) {
return true;
}

// ponytail: match ancestor chain sequentially leading up to position
int offset = position - path.size();
for (int index = 0; index < path.size(); index++) {
if (!input[offset + index].equalsIgnoreCase(path.get(index))) {
if (!matchesStep(input[offset + index], path.get(index))) {
return false;
}
}

return true;
}

private static boolean matchesStep(String actual, String expected) {
if (expected.contains("|")) {
for (String branch : expected.split("\\|")) {
if (actual.equalsIgnoreCase(branch.strip())) {
return true;
}
}
return false;
}
return actual.equalsIgnoreCase(expected);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package fr.moussax.bedrock.commands;

import org.bukkit.command.CommandSender;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;

import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.function.Supplier;

/**
Expand All @@ -16,41 +19,63 @@
*/
public final class TabSuggestionRegistry {

private final Map<String, Supplier<List<String>>> providers = new HashMap<>();
private final Map<String, Function<@Nullable CommandSender, List<String>>> providers = new HashMap<>();

/**
* Registers a dynamic tab completion provider under a symbolic key.
* Registers a sender-aware dynamic tab completion provider under a symbolic key.
*
* @param key symbolic suggestion key (such as {@code $players})
* @param provider supplier evaluated whenever completion is requested
* @param provider function evaluated with the command sender whenever completion is requested
*/
public void register(String key, Supplier<List<String>> provider) {
public void register(@NonNull String key, @NonNull Function<@Nullable CommandSender, List<String>> provider) {
Objects.requireNonNull(key, "key");
Objects.requireNonNull(provider, "provider");
providers.put(normalizeKey(key), provider);
}

/**
* Resolves dynamic suggestions registered under a symbolic key.
* Registers a context-free dynamic tab completion provider under a symbolic key.
*
* @param key symbolic suggestion key
* @param key symbolic suggestion key (such as {@code $players})
* @param provider supplier evaluated whenever completion is requested
*/
public void register(@NonNull String key, @NonNull Supplier<List<String>> provider) {
Objects.requireNonNull(provider, "provider");
register(key, _ -> provider.get());
}

/**
* Resolves dynamic suggestions registered under a symbolic key for a specific command sender.
*
* @param sender command sender requesting completion, or {@code null}
* @param key symbolic suggestion key
* @return list of suggestions provided by the registered key, or {@code null} when unregistered
*/
public @Nullable List<String> resolve(String key) {
Supplier<List<String>> provider = providers.get(normalizeKey(key));
public @Nullable List<String> resolve(@Nullable CommandSender sender, @NonNull String key) {
Function<@Nullable CommandSender, List<String>> provider = providers.get(normalizeKey(key));

if (provider == null) {
return null;
}

List<String> suggestions = provider.get();
List<String> suggestions = provider.apply(sender);
if (suggestions == null) {
return List.of();
}

return suggestions.stream().filter(Objects::nonNull).toList();
}

/**
* Resolves dynamic suggestions registered under a symbolic key without sender context.
*
* @param key symbolic suggestion key
* @return list of suggestions provided by the registered key, or {@code null} when unregistered
*/
public @Nullable List<String> resolve(@NonNull String key) {
return resolve(null, key);
}

// ponytail: kept — centralized case-insensitive key normalization
private static String normalizeKey(String key) {
return key.toLowerCase(Locale.ROOT);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@

import org.bukkit.Bukkit;
import org.bukkit.plugin.Plugin;
import org.bukkit.scheduler.BukkitTask;
import org.jspecify.annotations.NonNull;

/**
* Utility for accessing the main plugin instance and scheduling delayed tasks.
* Utility for accessing the main plugin instance and scheduling synchronous tasks.
*/
public final class PluginContext {

private static Plugin plugin;
private static volatile Plugin plugin;

private PluginContext() {
}
Expand All @@ -23,6 +24,13 @@ public static void bind(@NonNull Plugin pluginInstance) {
plugin = pluginInstance;
}

/**
* Unbinds the current plugin instance from this context.
*/
public static void unbind() {
plugin = null;
}

/**
* Retrieves the bound plugin instance.
*
Expand All @@ -36,13 +44,24 @@ public static Plugin get() {
return plugin;
}

/**
* Schedules a task to run synchronously on the next server tick.
*
* @param runnable the task to execute
* @return the scheduled task handle
*/
public static BukkitTask run(@NonNull Runnable runnable) {
return Bukkit.getScheduler().runTask(get(), runnable);
}

/**
* Schedules a task to run after a specified number of server ticks.
*
* @param runnable the task to execute
* @param ticks number of server ticks to wait before execution
* @return the scheduled task handle
*/
public static void delay(@NonNull Runnable runnable, long ticks) {
Bukkit.getScheduler().runTaskLater(get(), runnable, ticks);
public static BukkitTask delay(@NonNull Runnable runnable, long ticks) {
return Bukkit.getScheduler().runTaskLater(get(), runnable, ticks);
}
}
24 changes: 24 additions & 0 deletions bedrock/src/main/java/fr/moussax/bedrock/sound/SoundCue.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import fr.moussax.bedrock.scheduling.PluginContext;
import org.bukkit.Location;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NonNull;

import java.util.Objects;
Expand All @@ -24,10 +25,33 @@ public record SoundCue(Sound sound, float volume, float pitch, long delay) {
/**
* Plays this sound at the specified location after the configured delay.
*
* <p>If {@code delay <= 0}, plays immediately on the current thread.</p>
*
* @param location the location where the sound should be played
*/
public void play(@NonNull Location location) {
if (delay <= 0) {
Objects.requireNonNull(location.getWorld()).playSound(location, sound, volume, pitch);
return;
}
PluginContext.delay(() -> Objects.requireNonNull(location.getWorld())
.playSound(location, sound, volume, pitch), delay);
}

/**
* Plays this sound directly to the specified player after the configured delay.
*
* <p>If {@code delay <= 0}, plays immediately on the current thread.</p>
*
* @param player the player who should hear the sound
*/
public void play(@NonNull Player player) {
if (delay <= 0) {
player.playSound(player.getLocation(), sound, volume, pitch);
return;
}
PluginContext.delay(() -> player.playSound(player.getLocation(), sound, volume, pitch), delay);
}
}


Loading
Loading