/* * This file is part of NucleusFramework for Bukkit, licensed under the MIT License (MIT). * * Copyright (c) JCThePants (www.jcwhatever.com) * * 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. */ package com.jcwhatever.nucleus.managed.resourcepacks.sounds.playlist; import com.jcwhatever.nucleus.Nucleus; import com.jcwhatever.nucleus.events.sounds.PlayListLoopEvent; import com.jcwhatever.nucleus.events.sounds.PlayListTrackChangeEvent; import com.jcwhatever.nucleus.managed.resourcepacks.IPlayerResourcePacks; import com.jcwhatever.nucleus.managed.resourcepacks.ResourcePackStatus; import com.jcwhatever.nucleus.managed.resourcepacks.ResourcePacks; import com.jcwhatever.nucleus.managed.scheduler.Scheduler; import com.jcwhatever.nucleus.managed.sounds.ISoundContext; import com.jcwhatever.nucleus.managed.sounds.SoundSettings; import com.jcwhatever.nucleus.managed.resourcepacks.sounds.types.IResourceSound; import com.jcwhatever.nucleus.mixins.IMeta; import com.jcwhatever.nucleus.mixins.IPluginOwned; import com.jcwhatever.nucleus.utils.MetaStore; import com.jcwhatever.nucleus.utils.PreCon; import com.jcwhatever.nucleus.utils.Rand; import com.jcwhatever.nucleus.utils.observer.future.FutureResultSubscriber; import com.jcwhatever.nucleus.utils.observer.future.Result; import org.bukkit.World; import org.bukkit.entity.Player; import org.bukkit.plugin.Plugin; import javax.annotation.Nullable; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.WeakHashMap; /** * Abstract implementation of a play list that plays a collection of * resource sounds to a player. */ public abstract class PlayList implements IPluginOwned { // static references for use by events private static Map<Player, Set<PlayList>> _instances = new WeakHashMap<>(100); /** * Remove player from all play lists. * * @param player The player to remove. */ public static void clearQueue(Player player) { Set<PlayList> playLists = _instances.get(player); if (playLists == null) return; for (PlayList playList : playLists) { playList.removePlayer(player); } } /** * Get all {@link PlayList}'s the player is currently listening to. * * @param player The player. * * @return A new {@link List} of {@link PlayList}. */ public static List<PlayList> getAll(Player player) { Set<PlayList> playLists = _instances.get(player); if (playLists == null) return new ArrayList<>(0); return new ArrayList<>(playLists); } private final Plugin _plugin; private final Map<Player, PlayerSoundQueue> _playerQueues = new WeakHashMap<>(25); private boolean _isLoop; private boolean _isRandom; /** * Constructor. * * @param plugin The owning plugin. */ public PlayList(Plugin plugin) { PreCon.notNull(plugin); _plugin = plugin; } /** * Get the owning plugin. */ @Override public Plugin getPlugin() { return _plugin; } /** * Determine if the play list is being * run in a loop. */ public boolean isLoop() { return _isLoop; } /** * Set the play lists looping mode. * * @param isLoop True to enable looping. */ public void setLoop(boolean isLoop) { _isLoop = isLoop; } /** * Determine if the playlist should be played in random order. */ public boolean isRandom() { return _isRandom; } /** * Set the playlist random order mode. * * @param isRandom True to randomize order, false to play in order. */ public void setRandom(boolean isRandom) { _isRandom = isRandom; } /** * Add a player to the playlist so they can listen to it. * * <p>The playlist is automatically started after 1 tick.</p> * * @param player The player to add. * @param settings The sound settings to use. * * @return The current or new {@link PlayerSoundQueue} for the player. */ public PlayerSoundQueue addPlayer(final Player player, final SoundSettings settings) { PreCon.notNull(player); PlayerSoundQueue current = _playerQueues.get(player); if (current != null) { current._isRemoved = false; return current; } final PlayerSoundQueue queue = new PlayerSoundQueue(player, settings); Set<PlayList> playLists = _instances.get(player); if (playLists == null) { playLists = new HashSet<>(10); _instances.put(player, playLists); } playLists.add(this); _playerQueues.put(player, queue); IPlayerResourcePacks packs = ResourcePacks.get(player); if (packs.getStatus() == ResourcePackStatus.SUCCESS) { play(player, queue, settings); } else { packs.getFinalStatus() .onSuccess(new FutureResultSubscriber<IPlayerResourcePacks>() { @Override public void on(Result<IPlayerResourcePacks> result) { assert result.getResult() != null; // play regardless of status in case player has a locally // installed resource pack. play(player, queue, settings); } }); } return queue; } private void play(final Player player, final PlayerSoundQueue queue, final SoundSettings settings) { // play later to give a chance to attach meta to PlayerSoundQueue // before any events are fired. Scheduler.runTaskLater(getPlugin(), new Runnable() { @Override public void run() { IResourceSound sound = queue.next(); if (sound == null) { queue.removeNow(); return; } Nucleus.getSoundManager().playSound(_plugin, player, sound, settings, null) .onSuccess(new TrackChanger(player, queue)); } }); } /** * Remove a player from the playlist. * * <p>Unless the player is in a different world than the playlist, the players * sound queue is marked for removal and removed when the currently playing * sound ends. Otherwise it is removed immediately.</p> * * @param player The player to remove. */ public boolean removePlayer(Player player) { return removePlayer(player, false); } /** * Remove a player from the playlist. * * <p>Unless forced or the player is in a different world, the players sound queue * is marked for removal and removed when the currently playing sound ends.</p> * * <p>Forcing the immediate removal of the players sound queue does not end the * sound on the client.</p> * * @param player The player to remove. * @param force True to force the immediate removal of the players sound queue. */ public boolean removePlayer(Player player, boolean force) { PreCon.notNull(player); PlayerSoundQueue queue = _playerQueues.get(player); if (queue == null) return false; if (force) queue.removeNow(); else queue.remove(); return true; } /** * Get the players current sound queue from the playlist, * if any. * * @param player The player to check. * * @return Null if the player is not listening to the playlist. */ @Nullable public PlayerSoundQueue getSoundQueue(Player player) { PreCon.notNull(player); return _playerQueues.get(player); } /** * Invoked to get a list of sounds to play in a {@link PlayerSoundQueue}. * * <p>If the playlist is loop enabled, this is invoked every time the * playlist needs to refill its sound queue.</p> * * @param queue The {@link PlayerSoundQueue} that will be refilled. * @param loopCount The number of times the sound queue has been refilled. */ protected abstract List<IResourceSound> getSounds(PlayerSoundQueue queue, int loopCount); /** * Invoked when the next sound is played from a playlist. * * <p>Allows the next sound to be changed.</p> * * <p>Calls the {@link PlayListTrackChangeEvent}.</p> * * <p>Intended for optional override by implementation.</p> * * @param queue The {@link PlayerSoundQueue} that will be refilled. * @param prev The previous sound that was playing, if any. * @param next The expected next sound to be played. * * @return The next sound to play. Null ends the queue playback. */ @Nullable protected IResourceSound onTrackChange(PlayerSoundQueue queue, @Nullable IResourceSound prev, IResourceSound next) { PlayListTrackChangeEvent event = new PlayListTrackChangeEvent(this, queue, prev, next); Nucleus.getEventManager().callBukkit(this, event); if (event.isCancelled()) return null; return event.getNextSound(); } /** * Invoked when a {@link PlayerSoundQueue} is finished and is preparing to refill so it * can loop. * * <p>Is also invoked on initial playback with a {@literal loopCount} of 0.</p> * * <p>Calls the {@link PlayListLoopEvent}.</p> * * <p>Intended for optional override by implementation.</p> * * @param queue The {@link PlayerSoundQueue} that is playing. * @param sounds The list of {@link IResourceSound}'s that will be played during the next loop. * @param loopCount The number of times the {@link PlayerSoundQueue} has already looped. */ protected void onLoop(PlayerSoundQueue queue, List<IResourceSound> sounds, int loopCount) { PlayListLoopEvent event = new PlayListLoopEvent(this, queue, sounds, loopCount); Nucleus.getEventManager().callBukkit(this, event); if (event.isCancelled()) sounds.clear(); } /** * Invoked by the track changer to play the next sound in the playlist. * * @param player The player the sound is to be played to. * @param sound The sound to play. * @param settings The sound settings to use. * @param trackChanger The track changer. */ protected void playNextSound(final Player player, final IResourceSound sound, final SoundSettings settings, final FutureResultSubscriber<ISoundContext> trackChanger) { if (settings.getTrackChangeDelay() == 0) { Nucleus.getSoundManager() .playSound(_plugin, player, sound, settings, null) .onSuccess(trackChanger); } else { Scheduler.runTaskLater(getPlugin(), (int)settings.getTrackChangeDelay(), new Runnable() { @Override public void run() { Nucleus.getSoundManager() .playSound(_plugin, player, sound, settings, null) .onSuccess(trackChanger); } }); } } /** * An active playlist for a specific player. */ public class PlayerSoundQueue implements IMeta { private final WeakReference<Player> _player; private final World _world; private final SoundSettings _settings; private final MetaStore _meta = new MetaStore(); private LinkedList<IResourceSound> _queue; private IResourceSound _current; private boolean _isRemoved; private int _loopCount; /** * Constructor. * * @param player The player the sound queue is for. */ PlayerSoundQueue(Player player, SoundSettings settings) { _player = new WeakReference<Player>(player); _settings = settings; _world = player.getWorld(); } /** * Get the player the sound queue is for. * * <p>Player is held by a weak reference, may return null.</p> */ @Nullable public Player getPlayer() { return _player.get(); } /** * Get the world the playlist is playing in. */ public World getWorld() { return _world; } /** * Get the current sound being played to the player. */ @Nullable public IResourceSound getCurrent() { return _current; } /** * Determine if the sound queue is marked for removal * after the current sound completes. */ public boolean isRemoved() { return _isRemoved; } /** * Get the sound settings to use. */ public SoundSettings getSettings() { return _settings; } @Override public MetaStore getMeta() { return _meta; } /** * Mark the sound queue for removal. * * <p>If the player is still in a world where the sound is playing, the player * is not removed from the queue until after the current sound ends since there * is no way to stop the sound. If the player moves to a different world, the sound * is ended on the client and the player is removed from the queue immediately.</p> * * <p>Remove operation is performed after a 1 tick delay to ensure the * {@link org.bukkit.World} reported by the {@link org.bukkit.entity.Player} * object is up-to-date.</p> */ void remove() { Scheduler.runTaskLater(getPlugin(), new Runnable() { @Override public void run() { // check if player should be removed from queue immediately or // wait for current song to end. Player player = getPlayer(); boolean removeNow = player == null; if (player != null) { World world = player.getWorld(); removeNow = world == null || !world.equals(_world); } if (removeNow) { removeNow(); } _isRemoved = true; } }); } /** * Remove the player sound queue. * * <p>Does not end sound on client.</p> */ void removeNow() { Player player = getPlayer(); if (player == null) return; if (_queue != null) _queue.clear(); _playerQueues.remove(player); Set<PlayList> playLists = _instances.get(player); if (playLists != null) { playLists.remove(PlayList.this); } _isRemoved = true; } /** * Get the next sound in the queue. * * @return Null if the playlist is finished. */ @Nullable IResourceSound next() { Player player = getPlayer(); IResourceSound prev = _current; // check for first time use of queue if (_queue == null) { _queue = new LinkedList<>(); } // check for end of queue or loop else if (_isRemoved || player == null || (_queue.isEmpty() && !_isLoop)) { return _current = null; } // refill queue if empty if (_queue.isEmpty()) { refill(); if (_queue.isEmpty()) return _current = null; } // check for linear or random playback if (_isRandom) { int index = Rand.getInt(0, _queue.size() - 1); _current = _queue.remove(index); } else { _current = _queue.pollFirst(); } return _current = onTrackChange(this, prev, _current); } // refill queue private void refill() { List<IResourceSound> sounds = getSounds(this, _loopCount); _queue.addAll(sounds); onLoop(this, _queue, _loopCount); _loopCount++; } } /** * Task to ensure the next song in the player queue is played. */ private class TrackChanger extends FutureResultSubscriber<ISoundContext> { private final WeakReference<Player> _player; private final PlayerSoundQueue _soundQueue; TrackChanger(Player player, PlayerSoundQueue queue) { _player = new WeakReference<Player>(player); _soundQueue = queue; } @Override public void on(Result<ISoundContext> result) { Player player = _player.get(); if (player == null) return; if (_soundQueue.isRemoved()) { removeNow(player); return; } IResourceSound sound = _soundQueue.next(); if (sound == null) { removeNow(player); return; } playNextSound(player, sound, _soundQueue.getSettings(), this); } private void removeNow(Player player) { PlayerSoundQueue current = _playerQueues.get(player); if (current != _soundQueue) return; current.removeNow(); } } }