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 }