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
Original file line number Diff line number Diff line change
Expand Up @@ -205,11 +205,13 @@ public List<String> onTabComplete(CommandSender sender, Command command, String
if (args.length == 1) {
List<String> subcommands = new ArrayList<>();
if (sender.hasPermission("instruments.use")) {
subcommands.add("play");
subcommands.add("keybinds");
if (sender instanceof Player) {
subcommands.add("play");
subcommands.add("keybinds");
}
subcommands.add("list");
}
if (sender.hasPermission("instruments.give")) {
if (sender instanceof Player && sender.hasPermission("instruments.give")) {
subcommands.add("give");
}
if (sender.hasPermission("instruments.reload")) {
Expand All @@ -219,7 +221,9 @@ public List<String> onTabComplete(CommandSender sender, Command command, String
return filterPrefix(subcommands, args[0]);
}

if (args.length == 2 && (args[0].equalsIgnoreCase("give") || args[0].equalsIgnoreCase("play"))
if (sender instanceof Player && args.length == 2
&& (args[0].equalsIgnoreCase("give")
|| args[0].equalsIgnoreCase("play") && sender.hasPermission("instruments.use"))
&& sender.hasPermission("instruments.give")) {
return filterPrefix(new ArrayList<>(manager.getAllInstruments()), args[1]);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ private float gain(String value) {

@Override
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
if (!(sender instanceof Player player) || args.length == 0) return List.of();
List<String> options = List.of();
if (args.length == 1) {
var actions = new java.util.ArrayList<String>();
Expand All @@ -206,8 +207,10 @@ public List<String> onTabComplete(CommandSender sender, Command command, String
options = actions;
} else if (args.length == 2 && sender.hasPermission("instruments.record")) {
options = switch (args[0].toLowerCase(Locale.ROOT)) {
case "record", "volume", "mute", "remove" -> java.util.stream.IntStream.rangeClosed(1, studio.settings().tracks())
case "record" -> java.util.stream.IntStream.rangeClosed(1, studio.settings().tracks())
.mapToObj(Integer::toString).toList();
case "open" -> studio.completionProjectIds(player);
case "volume", "mute", "remove" -> studio.completionTracks(player);
case "preview" -> List.of("take");
case "metronome" -> List.of("on", "off");
case "reset" -> List.of("confirm");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ public final class StudioService {
private final StudioStation station;
private final DiscItems discs;
private final Map<UUID, Project> projects = new HashMap<>();
private final Map<UUID, List<UUID>> completionIds = new HashMap<>();
private final Set<UUID> completionLoaded = new HashSet<>();
private final Map<UUID, Long> completionRetryAt = new HashMap<>();
private final Map<UUID, Capture> recordings = new HashMap<>();
private final Map<UUID, Preview> previews = new HashMap<>();
private final Map<UUID, Set<String>> privateSounds = new HashMap<>();
Expand Down Expand Up @@ -188,7 +191,11 @@ boolean quiescent(UUID character) {
&& recordings.values().stream().noneMatch(capture -> capture.token.owner().character().equals(character));
}

void transferred(UUID character) { projects.remove(character); owners.remove(character); }
void transferred(UUID character) {
projects.remove(character); owners.remove(character);
completionIds.remove(character); completionLoaded.remove(character);
completionRetryAt.remove(character);
}

public record Activity(int track, int seconds, int notes, int countdown, boolean previewing) {
public boolean recording() { return track > 0; }
Expand Down Expand Up @@ -274,6 +281,7 @@ public void deleteProject(Player player, Project expected) throws IOException {
throw new IllegalArgumentException("The project changed. Open the delete confirmation again");
}
store.delete(owner, id);
completionIds.computeIfPresent(owner, (key, ids) -> ids.stream().filter(candidate -> !candidate.equals(id)).toList());
Project current = projects.get(owner);
if (current != null && current.song().id().equals(id)) {
projects.remove(owner);
Expand All @@ -292,7 +300,46 @@ private List<ProjectSummary> listProjects(StudioIdentity.Token token) throws IOE
// An uncached project matches its files; a cached one may be newer than them.
Project active = projects.get(owner);
if (active != null) all.put(active.song().id(), ProjectSummary.of(active));
return all.values().stream().sorted(ProjectSummary.ORDER).toList();
List<ProjectSummary> summaries = all.values().stream().sorted(ProjectSummary.ORDER).toList();
completionIds.put(owner, summaries.stream().map(ProjectSummary::id).toList());
return summaries;
}

// Completion reads memory only. Coalesce a cold owner lookup into one later server task.
// Failed loads retry only on explicit requests after the same cooldown used for failed saves.
private StudioIdentity.Token completionToken(Player player) {
StudioIdentity.Token token = identity.current(player).orElse(null);
if (closed || token == null || !current(token, player)) return null;
UUID owner = token.owner().character();
if (clock >= completionRetryAt.getOrDefault(owner, 0L) && completionLoaded.add(owner)) {
plugin.getServer().getScheduler().runTask(plugin, () -> {
if (closed || !current(token, player)) return;
try {
listProjects(player);
project(player);
completionRetryAt.remove(owner);
} catch (IOException | IllegalArgumentException ex) {
completionIds.remove(owner);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
completionLoaded.remove(owner);
completionRetryAt.put(owner, clock + RETRY_TICKS);
plugin.getLogger().log(Level.FINE, "Studio completion data unavailable", ex);
}
});
}
return token;
}

List<String> completionProjectIds(Player player) {
StudioIdentity.Token token = completionToken(player);
return token == null ? List.of() : completionIds.getOrDefault(token.owner().character(), List.of())
.stream().map(UUID::toString).toList();
}

List<String> completionTracks(Player player) {
StudioIdentity.Token token = completionToken(player);
Project project = token == null ? null : projects.get(token.owner().character());
return project == null ? List.of() : project.song().tracks().stream()
.map(track -> Integer.toString(track.slot())).toList();
}

public void selectProject(Player player, UUID id) throws IOException {
Expand All @@ -318,6 +365,11 @@ private void save(Player player, StudioIdentity.Token token, Project project) th
store.saveOwned(token.owner(), project); // Persist successfully before replacing the in-memory version.
projects.put(token.owner().character(), project);
unsaved.remove(token.owner().character());
completionIds.computeIfPresent(token.owner().character(), (owner, ids) -> {
var updated = new java.util.LinkedHashSet<>(ids);
updated.add(project.song().id());
return List.copyOf(updated);
});
}

/**
Expand Down Expand Up @@ -903,6 +955,8 @@ public void quit(Player player) {
StudioIdentity.Token token = identity.current(player).orElse(null);
if (token == null) return;
UUID owner = token.owner().character();
completionIds.remove(owner); completionLoaded.remove(owner);
completionRetryAt.remove(owner);
if (pendingSaves.containsKey(owner)) return; // completeSave evicts the project once it is written.
if (unsaved.contains(owner)) queueSave(owners.get(owner), projects.get(owner)); // Retry a write that failed.
else projects.remove(owner); // Its files are current; the next visit reloads it.
Expand All @@ -913,6 +967,8 @@ public void end(Player player, StudioIdentity.Token ending) {
if (capture != null && capture.token.equals(ending)) finish(player, capture, "Recording stopped when the character session ended");
stopPreview(player);
UUID character = ending.owner().character();
completionIds.remove(character); completionLoaded.remove(character);
completionRetryAt.remove(character);
if (!pendingSaves.containsKey(character)) {
if (unsaved.contains(character)) queueSave(ending.owner(), projects.get(character));
else projects.remove(character);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -265,4 +265,16 @@ void completesInstrumentNamesForPlay() {
assertEquals(List.of("celtic_harp"), complete(operator, "play", "c"));
assertEquals(List.of(), complete(player, "play", ""));
}
@Test void consoleAndUseDeniedPlayersOnlySeeUsableInstrumentCommands() {
loadInstruments("lute");
assertEquals(List.of("list", "reload"), complete(console, ""));
assertEquals(List.of(), complete(console, "play", ""));
assertEquals(List.of(), complete(console, "give", ""));
player.addAttachment(MockBukkit.createMockPlugin(), "instruments.give", true);
player.addAttachment(MockBukkit.createMockPlugin(), "instruments.use", false);
assertFalse(complete(player, "").contains("play"));
assertEquals(List.of(), complete(player, "play", "L"));
assertEquals(List.of("lute"), complete(player, "give", "L"));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ class StudioBridgeTest {
Plugin plugin = mock(Plugin.class); var server = mock(org.bukkit.Server.class); var manager = mock(org.bukkit.plugin.PluginManager.class);
when(plugin.getServer()).thenReturn(server); when(server.getPluginManager()).thenReturn(manager);
StudioService studio = mock(StudioService.class); StudioMenu menu = mock(StudioMenu.class);
bukkit.when(Bukkit::getOnlinePlayers).thenReturn(java.util.List.of(player));
CharacterStudioBridge.start(plugin, studio, menu);
verifyNoInteractions(studio, menu);
var listener = ArgumentCaptor.forClass(Listener.class); verify(manager).registerEvents(listener.capture(), eq(plugin));
characters.when(() -> Characters.isCurrent(session)).thenReturn(false);
listener.getValue().getClass().getMethod("end", CharacterSessionEndEvent.class).invoke(listener.getValue(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,10 @@ void tabCompletionReflectsPermissionsAndMatchesTheLastArgument() {
when(player.hasPermission("instruments.studio")).thenReturn(true);
when(player.hasPermission("instruments.copy")).thenReturn(true);
assertTrue(commands.onTabComplete(player, cmd, "music", new String[]{""}).containsAll(List.of("copy", "studio")));
for (String action : List.of("record", "volume", "mute", "remove"))
assertEquals(List.of("1", "2", "3", "4"), commands.onTabComplete(player, cmd, "music", new String[]{action, ""}));
assertEquals(List.of("1", "2", "3", "4"), commands.onTabComplete(player, cmd, "music", new String[]{"record", ""}));
when(studio.completionTracks(player)).thenReturn(List.of("1", "2"));
for (String action : List.of("volume", "mute", "remove"))
assertEquals(List.of("1", "2"), commands.onTabComplete(player, cmd, "music", new String[]{action, ""}));
assertEquals(List.of("take"), commands.onTabComplete(player, cmd, "music", new String[]{"preview", "T"}));
assertEquals(List.of("on", "off"), commands.onTabComplete(player, cmd, "music", new String[]{"metronome", ""}));
assertEquals(List.of("confirm"), commands.onTabComplete(player, cmd, "music", new String[]{"reset", ""}));
Expand Down
Loading
Loading