001    package org.bukkit.conversations;
002    
003    import org.bukkit.plugin.Plugin;
004    
005    import java.util.ArrayList;
006    import java.util.HashMap;
007    import java.util.List;
008    import java.util.Map;
009    
010    /**
011     * The Conversation class is responsible for tracking the current state of a
012     * conversation, displaying prompts to the user, and dispatching the user's
013     * response to the appropriate place. Conversation objects are not typically
014     * instantiated directly. Instead a {@link ConversationFactory} is used to
015     * construct identical conversations on demand.
016     * <p>
017     * Conversation flow consists of a directed graph of {@link Prompt} objects.
018     * Each time a prompt gets input from the user, it must return the next prompt
019     * in the graph. Since each Prompt chooses the next Prompt, complex
020     * conversation trees can be implemented where the nature of the player's
021     * response directs the flow of the conversation.
022     * <p>
023     * Each conversation has a {@link ConversationPrefix} that prepends all output
024     * from the conversation to the player. The ConversationPrefix can be used to
025     * display the plugin name or conversation status as the conversation evolves.
026     * <p>
027     * Each conversation has a timeout measured in the number of inactive seconds
028     * to wait before abandoning the conversation. If the inactivity timeout is
029     * reached, the conversation is abandoned and the user's incoming and outgoing
030     * chat is returned to normal.
031     * <p>
032     * You should not construct a conversation manually. Instead, use the {@link
033     * ConversationFactory} for access to all available options.
034     */
035    public class Conversation {
036    
037        private Prompt firstPrompt;
038        private boolean abandoned;
039        protected Prompt currentPrompt;
040        protected ConversationContext context;
041        protected boolean modal;
042        protected boolean localEchoEnabled;
043        protected ConversationPrefix prefix;
044        protected List<ConversationCanceller> cancellers;
045        protected List<ConversationAbandonedListener> abandonedListeners;
046    
047        /**
048         * Initializes a new Conversation.
049         *
050         * @param plugin The plugin that owns this conversation.
051         * @param forWhom The entity for whom this conversation is mediating.
052         * @param firstPrompt The first prompt in the conversation graph.
053         */
054        public Conversation(Plugin plugin, Conversable forWhom, Prompt firstPrompt) {
055            this(plugin, forWhom, firstPrompt, new HashMap<Object, Object>());
056        }
057    
058        /**
059         * Initializes a new Conversation.
060         *
061         * @param plugin The plugin that owns this conversation.
062         * @param forWhom The entity for whom this conversation is mediating.
063         * @param firstPrompt The first prompt in the conversation graph.
064         * @param initialSessionData Any initial values to put in the conversation
065         *     context sessionData map.
066         */
067        public Conversation(Plugin plugin, Conversable forWhom, Prompt firstPrompt, Map<Object, Object> initialSessionData) {
068            this.firstPrompt = firstPrompt;
069            this.context = new ConversationContext(plugin, forWhom, initialSessionData);
070            this.modal = true;
071            this.localEchoEnabled = true;
072            this.prefix = new NullConversationPrefix();
073            this.cancellers = new ArrayList<ConversationCanceller>();
074            this.abandonedListeners = new ArrayList<ConversationAbandonedListener>();
075        }
076    
077        /**
078         * Gets the entity for whom this conversation is mediating.
079         *
080         * @return The entity.
081         */
082        public Conversable getForWhom() {
083            return context.getForWhom();
084        }
085    
086        /**
087         * Gets the modality of this conversation. If a conversation is modal, all
088         * messages directed to the player are suppressed for the duration of the
089         * conversation.
090         *
091         * @return The conversation modality.
092         */
093        public boolean isModal() {
094            return modal;
095        }
096    
097        /**
098         * Sets the modality of this conversation.  If a conversation is modal,
099         * all messages directed to the player are suppressed for the duration of
100         * the conversation.
101         *
102         * @param modal The new conversation modality.
103         */
104        void setModal(boolean modal) {
105            this.modal = modal;
106        }
107    
108        /**
109         * Gets the status of local echo for this conversation. If local echo is
110         * enabled, any text submitted to a conversation gets echoed back into the
111         * submitter's chat window.
112         *
113         * @return The status of local echo.
114         */
115        public boolean isLocalEchoEnabled() {
116            return localEchoEnabled;
117        }
118    
119        /**
120         * Sets the status of local echo for this conversation. If local echo is
121         * enabled, any text submitted to a conversation gets echoed back into the
122         * submitter's chat window.
123         *
124         * @param localEchoEnabled The status of local echo.
125         */
126        public void setLocalEchoEnabled(boolean localEchoEnabled) {
127            this.localEchoEnabled = localEchoEnabled;
128        }
129    
130        /**
131         * Gets the {@link ConversationPrefix} that prepends all output from this
132         * conversation.
133         *
134         * @return The ConversationPrefix in use.
135         */
136        public ConversationPrefix getPrefix() {
137            return prefix;
138        }
139    
140        /**
141         * Sets the {@link ConversationPrefix} that prepends all output from this
142         * conversation.
143         *
144         * @param prefix The ConversationPrefix to use.
145         */
146        void setPrefix(ConversationPrefix prefix) {
147            this.prefix = prefix;
148        }
149    
150        /**
151         * Adds a {@link ConversationCanceller} to the cancellers collection.
152         *
153         * @param canceller The {@link ConversationCanceller} to add.
154         */
155        void addConversationCanceller(ConversationCanceller canceller) {
156            canceller.setConversation(this);
157            this.cancellers.add(canceller);
158        }
159    
160        /**
161         * Gets the list of {@link ConversationCanceller}s
162         *
163         * @return The list.
164         */
165        public List<ConversationCanceller> getCancellers() {
166            return cancellers;
167        }
168    
169        /**
170         * Returns the Conversation's {@link ConversationContext}.
171         *
172         * @return The ConversationContext.
173         */
174        public ConversationContext getContext() {
175            return context;
176        }
177    
178        /**
179         * Displays the first prompt of this conversation and begins redirecting
180         * the user's chat responses.
181         */
182        public void begin() {
183            if (currentPrompt == null) {
184                abandoned = false;
185                currentPrompt = firstPrompt;
186                context.getForWhom().beginConversation(this);
187            }
188        }
189    
190        /**
191         * Returns Returns the current state of the conversation.
192         *
193         * @return The current state of the conversation.
194         */
195        public ConversationState getState() {
196            if (currentPrompt != null) {
197                return ConversationState.STARTED;
198            } else if (abandoned) {
199                return ConversationState.ABANDONED;
200            } else {
201                return ConversationState.UNSTARTED;
202            }
203        }
204    
205        /**
206         * Passes player input into the current prompt. The next prompt (as
207         * determined by the current prompt) is then displayed to the user.
208         *
209         * @param input The user's chat text.
210         */
211        public void acceptInput(String input) {
212            if (currentPrompt != null) {
213    
214                // Echo the user's input
215                if (localEchoEnabled) {
216                    context.getForWhom().sendRawMessage(prefix.getPrefix(context) + input);
217                }
218    
219                // Test for conversation abandonment based on input
220                for(ConversationCanceller canceller : cancellers) {
221                    if (canceller.cancelBasedOnInput(context, input)) {
222                        abandon(new ConversationAbandonedEvent(this, canceller));
223                        return;
224                    }
225                }
226    
227                // Not abandoned, output the next prompt
228                currentPrompt = currentPrompt.acceptInput(context, input);
229                outputNextPrompt();
230            }
231        }
232    
233        /**
234         * Adds a {@link ConversationAbandonedListener}.
235         *
236         * @param listener The listener to add.
237         */
238        public synchronized void addConversationAbandonedListener(ConversationAbandonedListener listener) {
239            abandonedListeners.add(listener);
240        }
241    
242        /**
243         * Removes a {@link ConversationAbandonedListener}.
244         *
245         * @param listener The listener to remove.
246         */
247        public synchronized void removeConversationAbandonedListener(ConversationAbandonedListener listener) {
248            abandonedListeners.remove(listener);
249        }
250    
251        /**
252         * Abandons and resets the current conversation. Restores the user's
253         * normal chat behavior.
254         */
255        public void abandon() {
256            abandon(new ConversationAbandonedEvent(this, new ManuallyAbandonedConversationCanceller()));
257        }
258    
259        /**
260         * Abandons and resets the current conversation. Restores the user's
261         * normal chat behavior.
262         *
263         * @param details Details about why the conversation was abandoned
264         */
265        public synchronized void abandon(ConversationAbandonedEvent details) {
266            if (!abandoned) {
267                abandoned = true;
268                currentPrompt = null;
269                context.getForWhom().abandonConversation(this);
270                for (ConversationAbandonedListener listener : abandonedListeners) {
271                    listener.conversationAbandoned(details);
272                }
273            }
274        }
275    
276        /**
277         * Displays the next user prompt and abandons the conversation if the next
278         * prompt is null.
279         */
280        public void outputNextPrompt() {
281            if (currentPrompt == null) {
282                abandon(new ConversationAbandonedEvent(this));
283            } else {
284                context.getForWhom().sendRawMessage(prefix.getPrefix(context) + currentPrompt.getPromptText(context));
285                if (!currentPrompt.blocksForInput(context)) {
286                    currentPrompt = currentPrompt.acceptInput(context, null);
287                    outputNextPrompt();
288                }
289            }
290        }
291    
292        public enum ConversationState {
293            UNSTARTED,
294            STARTED,
295            ABANDONED
296        }
297    }