/* * 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.views; import com.jcwhatever.nucleus.Nucleus; import com.jcwhatever.nucleus.collections.players.PlayerMap; import com.jcwhatever.nucleus.managed.scheduler.Scheduler; import com.jcwhatever.nucleus.mixins.IDisposable; import com.jcwhatever.nucleus.mixins.IMeta; import com.jcwhatever.nucleus.mixins.IPlayerReference; import com.jcwhatever.nucleus.utils.MetaStore; import com.jcwhatever.nucleus.utils.PreCon; import com.jcwhatever.nucleus.utils.observer.future.FutureAgent; import com.jcwhatever.nucleus.utils.observer.future.IFuture; import org.bukkit.block.Block; import org.bukkit.entity.Player; import javax.annotation.Nullable; import java.util.Iterator; import java.util.Map; import java.util.UUID; /** * A session that tracks and provides session context data to view instances. * * <p>Not thread safe. {@link ViewSession} should always be invoked from the * main thread.</p> */ public final class ViewSession implements IMeta, Iterable<View>, IPlayerReference, IDisposable { private static final Map<UUID, ViewSession> _sessionMap = new PlayerMap<>(Nucleus.getPlugin()); /** * Get a players current view session. * * @param player The player to check. * * @return The view session or null if the player does not have one. */ @Nullable public static ViewSession getCurrent(Player player) { PreCon.notNull(player); ViewSession session = _sessionMap.get(player.getUniqueId()); if (session == null) return null; if (session.isDisposed()) { _sessionMap.remove(player.getUniqueId()); return null; } return session; } /** * Get a players current view session or create a new one. * * @param player The player. * @param sessionBlock The session block to use if a new session is created. */ public static ViewSession get(Player player, @Nullable Block sessionBlock) { PreCon.notNull(player); ViewSession session = getCurrent(player); if (session == null || session.isDisposed()) { session = new ViewSession(player, sessionBlock); _sessionMap.put(player.getUniqueId(), session); } return session; } private final Player _player; private final MetaStore _meta = new MetaStore(); private final Block _sessionBlock; private ViewContainer _first; private ViewContainer _current; private boolean _isDisposed; /** * Constructor. * * @param player The player to create the session for. * @param sessionBlock The optional session block, a block that represents the view. */ private ViewSession(Player player, @Nullable Block sessionBlock) { _player = player; _sessionBlock = sessionBlock; ViewEventListener.register(this); _sessionMap.put(player.getUniqueId(), this); } /** * Get the player the view session is for. */ @Override public final Player getPlayer() { return _player; } /** * Get the block that is the source of the view session. This is normally * the block that a player clicks in order to open the view. * * @return The {@link Block} or null if a block did not start the session. */ @Nullable public Block getSessionBlock() { return _sessionBlock; } /** * Determine if the view session contains the specified {@link View}. * * @param view The view to check. */ public boolean contains(View view) { PreCon.notNull(view); ViewContainer container = _first; while (container != null) { if (container.view.equals(view)) return true; container = container.next; } return false; } /** * Get the view instance the player is currently looking at. * * @return The current {@link View} or null if the player is not looking at * any views in the session. */ @Nullable public View getCurrent() { if (_current == null) return null; return _current.view; } /** * Get the previous view, if any. * * @return The previous {@link View} or null if the current view is the first * view or there is no current view. */ @Nullable public View getPrev() { if (_current == null || _current.prev == null) return null; return _current.prev.view; } /** * Get the next view, if any. * * @return The next {@link View} or null if the current view is the last view or * there is no current view. */ @Nullable public View getNext() { if (_current == null || _current.next == null) return null; return _current.next.view; } /** * Get the first view, if any. * * @return The first {@link View} or null if there are no views. */ @Nullable public View getFirst() { if (_current == null) return null; ViewContainer current = _current; while (current.prev != null) { current = current.prev; } return current.view; } /** * Get the last view, if any. * * @return The last {@link View} or null if there are no views. */ @Nullable public View getLast() { if (_current == null) return null; ViewContainer current = _current; while (current.next != null) { current = current.next; } return current.view; } /** * Close the current view and go to the previous view. * * <p>If there is no previous view, the session is ended.</p> * * <p>There is a 1 tick delay before the previous view is shown. The * state of the view will reflect the previous state until then.</p> * * @return A future that will return the result once it has completed. Possible * outcomes are success or cancel. * * @throws java.lang.IllegalStateException if there is no current view or the view * session is disposed. */ public IFuture previous() { if (_current == null) throw new IllegalStateException("No previous view."); if (_isDisposed) throw new IllegalStateException("Cannot use a disposed ViewSession."); final FutureAgent agent = new FutureAgent(); Scheduler.runTaskLater(_current.view.getPlugin(), new Runnable() { @Override public void run() { if (_current == null) return; if (_current.view.close(ViewCloseReason.PREV)) { _current = _current.prev; if (_current == null) { dispose(); } agent.success(); } else { agent.cancel(); } } }); return agent.getFuture(); } /** * Invoked to indicate a menu was escaped. * * <p>The same as invoking {@link #previous} except the {@link View} is not * invoked to close.</p> */ @Nullable void escaped() { if (_current == null) return; _current.view.close(ViewCloseReason.ESCAPE); _current = _current.prev; if (_current == null) { dispose(); } } /** * Show the next view. * * <p>There is a 1 tick delay before the view is actually* opened. The state of * the view may be inaccurate until then.</p> * * @param view The view instance to display next. * * @return A future that will return the result of the operation once * it has completed. Possible outcomes are success or cancel. * * @throws IllegalStateException if the view session is disposed. */ public IFuture next(final View view) { PreCon.notNull(view); if (_isDisposed) throw new IllegalStateException("Cannot use a disposed ViewSession."); final FutureAgent agent = new FutureAgent(); view.setViewSession(this); if (_current == null) { _first = new ViewContainer(view, null, null); _current = _first; Scheduler.runTaskLater(view.getPlugin(), new Runnable() { @Override public void run() { if (_current == null || _current.view == null) { agent.cancel(); return; } if (_current.view.open(ViewOpenReason.FIRST)) { agent.success(); } else { _first = null; _current = null; agent.cancel(null); } } }); } else { Scheduler.runTaskLater(view.getPlugin(), new Runnable() { @Override public void run() { if (_current == null) { agent.cancel(); return; } ViewContainer prev = _current; ViewContainer origNext = prev.next; ViewContainer current = new ViewContainer(view, prev, null); prev.next = current; // close current view, ViewEventListener will open next view if (_current.view.close(ViewCloseReason.NEXT)) { _current = current; agent.success(); } else { prev.next = origNext; agent.cancel(); } } }); } return agent.getFuture(); } /** * Close and re-open the current view. * * <p>There is a 1-2 tick delay before the view refresh is complete. The state * of the view will be inaccurate until then.</p> * * @return A future that will return the result of the operation once * it has completed. Possible outcomes are success, error or cancel. * * @throws IllegalStateException if the view session is disposed. */ public IFuture refresh() { if (_isDisposed) throw new IllegalStateException("Cannot use a disposed ViewSession."); final FutureAgent agent = new FutureAgent(); final View view = getCurrent(); if (view == null) { return agent.cancel("No current view to refresh."); } if (!view.close(ViewCloseReason.REFRESH)) { return agent.cancel(); } Scheduler.runTaskLater(view.getPlugin(), new Runnable() { @Override public void run() { if (view.open(ViewOpenReason.REFRESH)) { agent.success(); } else { // error: refresh is not an appropriate time to not be able to re-open the view. agent.error(); } } }); return agent.getFuture(); } @Override public boolean isDisposed() { return _isDisposed; } @Override public void dispose() { if (_isDisposed) return; ViewEventListener.unregister(this); _sessionMap.remove(_player.getUniqueId()); if (_current != null) { ViewContainer current = _current; while (current != null) { current.view.onDispose(); current = current.prev; } } _player.closeInventory(); _isDisposed = true; } @Override public MetaStore getMeta() { return _meta; } @Override public Iterator<View> iterator() { return new Iterator<View>() { ViewContainer _current = _first; @Override public boolean hasNext() { return _current.next != null; } @Override public View next() { _current = _current.next; return _current.view; } @Override public void remove() { throw new UnsupportedOperationException(); } }; } private static class ViewContainer { final View view; final ViewContainer prev; ViewContainer next; ViewContainer(View view, @Nullable ViewContainer prev, @Nullable ViewContainer next) { this.view = view; this.prev = prev; this.next = next; } } }