DREAMFIRE Docs ← Back to site
Loading...
Searching...
No Matches
DreamPrompt.java
Go to the documentation of this file.
1/*
2 * MIT License
3 *
4 * Copyright (c) 2025 Dreamfire Studio
5 *
6 * Permission is hereby granted, free of charge, to any person obtaining a copy
7 * of this software and associated documentation files (the "Software"), to deal
8 * in the Software without restriction, including without limitation the rights
9 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 * copies of the Software, and to permit persons to whom the Software is
11 * furnished to do so, subject to the following conditions:
12 *
13 * The above copyright notice and this permission notice shall be included in all
14 * copies or substantial portions of the Software.
15 *
16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 * SOFTWARE.
23 */
24package com.dreamfirestudios.dreamcore.DreamPrompt;
25
26import com.dreamfirestudios.dreamcore.DreamChat.DreamMessageFormatter;
27import com.dreamfirestudios.dreamcore.DreamChat.DreamMessageSettings;
28import com.dreamfirestudios.dreamcore.DreamCore;
29import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
30import org.bukkit.Bukkit;
31import org.bukkit.conversations.*;
32import org.bukkit.entity.Player;
33import org.jetbrains.annotations.NotNull;
34
35import java.util.HashMap;
36import java.util.Map;
37import java.util.Objects;
38import java.util.function.Consumer;
39
60public class DreamPrompt extends StringPrompt {
61
63 public static final String END_KEY = "DreamfirePrompt::END";
64
65 private static final int CLEAR_LINES = 100;
66
67 private final String promptText;
68 private final boolean clearOnRestart;
69 private final boolean clearOnEnd;
70
71 private final Consumer<DreamPromptTriplet<Player, String, ConversationContext>> onResponse;
72 private final Consumer<DreamPromptTriplet<Player, String, ConversationContext>> onRestart;
73 private final Consumer<DreamPromptTriplet<Player, String, ConversationContext>> onEnd;
74
85 @NotNull String promptText,
86 boolean clearOnRestart,
87 boolean clearOnEnd,
88 @NotNull Consumer<DreamPromptTriplet<Player, String, ConversationContext>> onResponse,
89 @NotNull Consumer<DreamPromptTriplet<Player, String, ConversationContext>> onRestart,
90 @NotNull Consumer<DreamPromptTriplet<Player, String, ConversationContext>> onEnd
91 ) {
92 this.promptText = Objects.requireNonNull(promptText, "promptText");
93 this.clearOnRestart = clearOnRestart;
94 this.clearOnEnd = clearOnEnd;
95 this.onResponse = Objects.requireNonNull(onResponse, "onResponse");
96 this.onRestart = Objects.requireNonNull(onRestart, "onRestart");
97 this.onEnd = Objects.requireNonNull(onEnd, "onEnd");
98 }
99
105 @Override
106 public String getPromptText(ConversationContext context) {
107 return PlainTextComponentSerializer.plainText()
108 .serialize(DreamMessageFormatter.format(promptText, DreamMessageSettings.all()));
109 }
110
118 @Override
119 public Prompt acceptInput(ConversationContext context, String input) {
120 if (!(context.getForWhom() instanceof Player player)) return END_OF_CONVERSATION;
121
122 // Fire response callback + event
123 DreamPromptTriplet<Player, String, ConversationContext> payload =
124 new DreamPromptTriplet<>(player, input, context);
125 onResponse.accept(payload);
126 new PromptResponseEvent(player, input, context);
127
128 // Should we end?
129 boolean endNow = Boolean.TRUE.equals(getSessionFlag(context.getAllSessionData(), END_KEY));
130 if (endNow) {
131 if (clearOnEnd) clearChat(context);
132 onEnd.accept(payload);
133 new PromptEndedEvent(player, input, context, PromptEndedEvent.EndReason.NORMAL);
134 DreamCore.Conversations.remove(player.getUniqueId());
135 return END_OF_CONVERSATION;
136 }
137
138 // Continue same prompt (single-step loop)
139 if (clearOnRestart) clearChat(context);
140 onRestart.accept(payload);
141 new PromptRestartEvent(player, input, context);
142 return this;
143 }
144
148 private static Object getSessionFlag(Map<Object, Object> data, String key) {
149 return data == null ? null : data.get(key);
150 }
151
155 private static void clearChat(ConversationContext ctx) {
156 for (int i = 0; i < CLEAR_LINES; i++) {
157 ctx.getForWhom().sendRawMessage("");
158 }
159 }
160
161 /* -------------------------------------------------------------------------------------------------------------- */
162 /* Static convenience entrypoint for IDreamPrompt flows */
163 /* -------------------------------------------------------------------------------------------------------------- */
164
176 public static void start(Player player, IDreamPrompt prompt, boolean overrideExisting) {
177 Objects.requireNonNull(player, "player");
178 Objects.requireNonNull(prompt, "prompt");
179
180 Bukkit.getScheduler().runTask(DreamCore.DreamCore, () -> {
181 var existing = DreamCore.Conversations.get(player.getUniqueId());
182 if (existing != null && !overrideExisting) return;
183
184 PromptStartedEvent startEvent = new PromptStartedEvent(player);
185 if (startEvent.isCancelled()) return;
186
187 if (existing != null) existing.abandon();
188
189 if (prompt.clearPlayerChatOnStart(player)) {
190 for (int i = 0; i < CLEAR_LINES; i++) player.sendMessage("");
191 }
192
193 ConversationFactory factory = new ConversationFactory(DreamCore.DreamCore)
194 .withFirstPrompt(new DreamPrompt(
195 prompt.promptText(player),
196 prompt.clearPlayerChatOnRestart(player),
197 prompt.clearPlayerChatOnEnd(player),
198 prompt.onResponseCallback(),
201 ))
202 .withInitialSessionData(new HashMap<>(prompt.defaultData(player)))
203 .withModality(false)
204 .withLocalEcho(false)
205 .withEscapeSequence(null)
206 .addConversationAbandonedListener(abandonedEvent -> {
207 // Cleanup on any abandon path not caught by acceptInput()
208 if (abandonedEvent.getContext() != null
209 && abandonedEvent.getContext().getForWhom() instanceof Player p) {
210 DreamCore.Conversations.remove(p.getUniqueId());
211 // Distinguish between natural end and forced abandon
212 new PromptEndedEvent(p, "", abandonedEvent.getContext(),
213 abandonedEvent.gracefulExit() ? PromptEndedEvent.EndReason.NORMAL
214 : PromptEndedEvent.EndReason.ABANDONED);
215 }
216 });
217
218 Conversation conversation = factory.buildConversation(player);
219 conversation.begin();
220 DreamCore.Conversations.put(player.getUniqueId(), conversation);
221 });
222 }
223
224 /* -------------------------------------------------------------------------------------------------------------- */
225 /* Builder for ad-hoc prompts (not using IDreamPrompt) */
226 /* -------------------------------------------------------------------------------------------------------------- */
227
229 public static Builder builder() { return new Builder(); }
230
232 public static Builder PulsePromptBuilder() { return builder(); }
233
237 public static final class Builder {
238 private final HashMap<Object, Object> defaultData = new HashMap<>();
239 private Consumer<DreamPromptTriplet<Player, String, ConversationContext>> onResponse = t -> {};
240 private Consumer<DreamPromptTriplet<Player, String, ConversationContext>> onRestart = t -> {};
241 private Consumer<DreamPromptTriplet<Player, String, ConversationContext>> onEnd = t -> {};
242 private String promptText = "|Enter text: |";
243 private boolean clearOnStart = true;
244 private boolean clearOnRestart = true;
245 private boolean clearOnEnd = true;
246
248 public Builder addDefaultData(Object key, Object value) { defaultData.put(key, value); return this; }
250 public Builder promptText(String text) { this.promptText = text; return this; }
252 public Builder onResponse(Consumer<DreamPromptTriplet<Player, String, ConversationContext>> c) { this.onResponse = c; return this; }
254 public Builder onRestart(Consumer<DreamPromptTriplet<Player, String, ConversationContext>> c) { this.onRestart = c; return this; }
256 public Builder onEnd(Consumer<DreamPromptTriplet<Player, String, ConversationContext>> c) { this.onEnd = c; return this; }
258 public Builder clearOnStart(boolean v) { this.clearOnStart = v; return this; }
260 public Builder clearOnRestart(boolean v) { this.clearOnRestart = v; return this; }
262 public Builder clearOnEnd(boolean v) { this.clearOnEnd = v; return this; }
263
277 public void startConversation(@NotNull Player player, boolean overrideExisting) {
278 Objects.requireNonNull(player, "player");
279
280 Bukkit.getScheduler().runTask(DreamCore.DreamCore, () -> {
281 var existing = DreamCore.Conversations.get(player.getUniqueId());
282 if (existing != null && !overrideExisting) return;
283
284 PromptStartedEvent startEvent = new PromptStartedEvent(player);
285 if (startEvent.isCancelled()) return;
286
287 if (existing != null) existing.abandon();
288
289 if (clearOnStart) {
290 for (int i = 0; i < CLEAR_LINES; i++) player.sendMessage("");
291 }
292
293 ConversationFactory factory = new ConversationFactory(DreamCore.DreamCore)
294 .withFirstPrompt(new DreamPrompt(
295 promptText,
296 clearOnRestart,
297 clearOnEnd,
298 onResponse,
299 onRestart,
300 onEnd
301 ))
302 .withInitialSessionData(new HashMap<>(defaultData))
303 .withModality(false)
304 .withLocalEcho(false)
305 .withEscapeSequence(null)
306 .addConversationAbandonedListener(abandonedEvent -> {
307 if (abandonedEvent.getContext() != null
308 && abandonedEvent.getContext().getForWhom() instanceof Player p) {
309 DreamCore.Conversations.remove(p.getUniqueId());
310 new PromptEndedEvent(p, "", abandonedEvent.getContext(),
311 abandonedEvent.gracefulExit() ? PromptEndedEvent.EndReason.NORMAL
312 : PromptEndedEvent.EndReason.ABANDONED);
313 }
314 });
315
316 Conversation c = factory.buildConversation(player);
317 c.begin();
318 DreamCore.Conversations.put(player.getUniqueId(), c);
319 });
320 }
321
326 public void cancelConversation(@NotNull Player player) {
327 Objects.requireNonNull(player, "player");
328 var current = DreamCore.Conversations.remove(player.getUniqueId());
329 if (current != null) current.abandon();
330 }
331 }
332}
static final LinkedHashMap< UUID, Conversation > Conversations
Builder for ad-hoc prompts not using IDreamPrompt.
Builder onResponse(Consumer< DreamPromptTriplet< Player, String, ConversationContext > > c)
Sets response callback.
Builder clearOnEnd(boolean v)
Clears chat when ending.
Builder clearOnRestart(boolean v)
Clears chat when repeating.
void cancelConversation(@NotNull Player player)
Cancels a running conversation for a player (if any).
Builder promptText(String text)
Sets prompt text (MiniMessage supported).
void startConversation(@NotNull Player player, boolean overrideExisting)
Starts the conversation for a player.
Builder addDefaultData(Object key, Object value)
Adds default session data key/value.
Builder onEnd(Consumer< DreamPromptTriplet< Player, String, ConversationContext > > c)
Sets end callback.
Builder onRestart(Consumer< DreamPromptTriplet< Player, String, ConversationContext > > c)
Sets restart callback.
Builder clearOnStart(boolean v)
Clears chat when starting.
Single-step string prompt with start/response/restart/end events and safe cleanup.
DreamPrompt( @NotNull String promptText, boolean clearOnRestart, boolean clearOnEnd, @NotNull Consumer< DreamPromptTriplet< Player, String, ConversationContext > > onResponse, @NotNull Consumer< DreamPromptTriplet< Player, String, ConversationContext > > onRestart, @NotNull Consumer< DreamPromptTriplet< Player, String, ConversationContext > > onEnd)
Creates a prompt instance (normally constructed through the Builder or IDreamPrompt wrapper).
static Builder PulsePromptBuilder()
Backwards-compat alias (kept for older call sites).
String getPromptText(ConversationContext context)
Formats the visible prompt line (MiniMessage → Component → plain text).
static final String END_KEY
SessionData key that signals the prompt to end on next input cycle.
Prompt acceptInput(ConversationContext context, String input)
Handles user input for this step.
static void start(Player player, IDreamPrompt prompt, boolean overrideExisting)
Launches a conversation for the given player using an IDreamPrompt provider.
static Builder builder()
Creates a new Builder.
Fired when the conversation ends, either normally or via abandon.
Fired after a player's message is received and before restart/end logic is applied.
Fired when the prompt continues (chat may be cleared, same step repeats).
Adapters supply text and behavior for DreamPrompt.start(Player, IDreamPrompt, boolean) flows.
default boolean clearPlayerChatOnRestart(Player player)
Whether to clear the player's chat when the step repeats.
String promptText(Player player)
Returns the prompt text to display for the player.
default HashMap< Object, Object > defaultData(Player player)
Default session data inserted when the prompt begins.
Consumer< DreamPromptTriplet< Player, String, ConversationContext > > onEndConversationCallback()
Called when the prompt ends normally or via abandon.
Consumer< DreamPromptTriplet< Player, String, ConversationContext > > onResponseCallback()
Called when the player submits input for this prompt.
default boolean clearPlayerChatOnEnd(Player player)
Whether to clear the player's chat on end.
default Consumer< DreamPromptTriplet< Player, String, ConversationContext > > onConversationRestartCallback()
Called if the prompt continues (same step repeats).