RuneHive-Game
Loading...
Searching...
No Matches
AIDialogueHandler.java
Go to the documentation of this file.
1package com.runehive.content.ai;
2
3import com.runehive.content.dialogue.Dialogue;
4import com.runehive.content.dialogue.DialogueFactory;
5import com.runehive.content.dialogue.Expression;
6import com.runehive.game.task.Task;
7import com.runehive.game.world.World;
8import com.runehive.game.world.entity.mob.npc.Npc;
9import com.runehive.game.world.entity.mob.player.Player;
10import com.runehive.game.world.object.CustomGameObject;
11import com.runehive.game.world.object.ObjectDirection;
12import com.runehive.game.world.position.Position;
13import com.runehive.net.packet.out.SendInputMessage;
14import org.slf4j.Logger;
15import org.slf4j.LoggerFactory;
16
17public final class AIDialogueHandler extends Dialogue {
18
19 private static final Logger logger = LoggerFactory.getLogger(AIDialogueHandler.class);
20 private static final int GANDALF_AI_ID = 2108;
21
22 private final Npc npc;
23
25 this.npc = npc;
26 }
27
28 @Override
29 public void sendDialogues(DialogueFactory factory) {
30 Player player = factory.getPlayer();
31 player.locking.lock();
32
33 // Ensure director knows the NPC and start with delay to prevent coordinate misalignment
34 player.dialogueCamNpc = npc; // 2108
35 player.dialogueCamMode = com.runehive.game.world.entity.mob.player.camera.DialogueCameraDirector.Mode.OFF;
36 player.dialogueCamDelayTicks = 2; // delay 2 ticks before camera activates
37 com.runehive.game.world.entity.mob.player.camera.DialogueCameraDirector.boostSwap(player, 8);
38
39 factory.sendNpcChat(npc.id, Expression.HAPPY,
40 "What would you like to ask?",
41 "Type in chat: ::gpt <your message>")
42 .onAction(() -> {
43 player.locking.unlock();
44 player.interfaceManager.close();
45 com.runehive.game.world.entity.mob.player.camera.DialogueCameraDirector.requestReset(player);
46 })
47 .execute();
48 }
49
50 /**
51 * Process AI command input (called from command handler)
52 */
53 public static void handleAICommand(Player player, String message) {
54 int npcSpawnX = 1684;
55 int npcSpawnY = 3746;
56 int maxDistance = 1;
57
58 if (Math.abs(player.getPosition().getX() - npcSpawnX) > maxDistance ||
59 Math.abs(player.getPosition().getY() - npcSpawnY) > maxDistance) {
60 player.message("You must be near the Wise Old Man to use AI chat.");
61 return;
62 }
63
65 player.message("AI services are not initialized. Click the Wise Old Man first.");
66 return;
67 }
68
69 // Find the actual spawned Wise Old Man NPC near the spawn coordinates
70 Npc gandalf = findNearbyNpc(player, GANDALF_AI_ID, npcSpawnX, npcSpawnY, maxDistance);
71
72 if (gandalf == null) {
73 player.message("Could not find the Wise Old Man NPC. Please stand closer.");
74 return;
75 }
76
77 // ::gpt must use the exact same choreography as Talk-to on NPC 2108
78 // Teleport P to exact position facing the NPC; LOCK P.
80 com.runehive.game.world.position.Position playerTarget = new com.runehive.game.world.position.Position(npcPos.getX() - 1, npcPos.getY(), npcPos.getHeight());
81 player.move(playerTarget);
82 player.face(gandalf);
83 player.locking.lock();
84
85 // Initialize dialogue camera - player speaks first (with delay to prevent misalignment)
86 player.dialogueCamNpc = gandalf;
87 player.dialogueCamMode = com.runehive.game.world.entity.mob.player.camera.DialogueCameraDirector.Mode.OFF;
88 player.dialogueCamDelayTicks = 2; // delay 2 ticks before camera activates
89 com.runehive.game.world.entity.mob.player.camera.DialogueCameraDirector.boostSwap(player, 8);
90
91 // Mark that player has received instructions and is now using the command
93
94 String sanitizedInput = new AIDialogueHandler(gandalf).sanitizeInput(message);
95 new AIDialogueHandler(gandalf).processGandalfAIMessage(player, gandalf, sanitizedInput);
96 }
97
98 /**
99 * Find an NPC by ID near specific coordinates
100 */
101 private static Npc findNearbyNpc(Player player, int npcId, int targetX, int targetY, int maxDistance) {
102 for (Npc npc : World.getNpcs()) {
103 if (npc.id == npcId && npc.getPosition().getHeight() == player.getPosition().getHeight()) {
104 int distX = Math.abs(npc.getPosition().getX() - targetX);
105 int distY = Math.abs(npc.getPosition().getY() - targetY);
106 if (distX <= maxDistance && distY <= maxDistance) {
107 return npc;
108 }
109 }
110 }
111 return null;
112 }
113
114 private String sanitizeInput(String input) {
115 if (input == null) {
116 return "";
117 }
118 String cleaned = input.trim().replaceAll("[<>]", "");
119 return cleaned.substring(0, Math.min(cleaned.length(), 500));
120 }
121
122 private void processGandalfAIMessage(Player player, Npc npc, String message) {
123 // Spawn chair at NPC position (facing WEST)
124 Position chairPos = npc.getPosition().copy();
125 CustomGameObject chair = new CustomGameObject(1096, chairPos, ObjectDirection.WEST, com.runehive.game.world.object.ObjectType.GENERAL_PROP);
126 chair.register();
127
128 // Start thinking animation and speak "Hmmmmm" for 3 ticks
129 // Treat as advancing into NPC turn (flip camera behind NPC and speed up the turn slightly).
130 player.dialogueCamMode = com.runehive.game.world.entity.mob.player.camera.DialogueCameraDirector.Mode.BEHIND_NPC;
131 com.runehive.game.world.entity.mob.player.camera.DialogueCameraDirector.boostSwap(player, 8);
132 npc.animate(new com.runehive.game.Animation(4079));
133 npc.speak("Hmmmmm");
134
135 logger.info("Processing Gandalf AI request from {} (length: {})",
136 player.getUsername(), message.length());
137
139 .processPlayerMessage(player.getUsername(), message, npc.id)
140 .thenAccept(response -> {
141 World.schedule(new Task(3) {
142 @Override
143 public void execute() {
144 if (player == null || !player.isRegistered()) {
145 logger.warn("Player {} is no longer online, skipping response",
146 player != null ? player.getUsername() : "null");
147 chair.unregister();
148 cancel();
149 return;
150 }
151
152 try {
153 // Remove chair and reset animation after 3 ticks
154 chair.unregister();
155 npc.animate(new com.runehive.game.Animation(-1));
156 player.locking.lock();
157
158 if (response != null && !response.isEmpty()) {
159 streamGandalfResponse(player, npc, response);
160 } else {
161 player.locking.unlock();
162 player.dialogueFactory
163 .sendNpcChat(npc.id, Expression.SAD,
164 "The mystical connection faltered.",
165 "Perhaps try again with a different query.")
166 .onAction(() -> {
167 player.locking.unlock();
168 player.interfaceManager.close();
169 })
170 .execute();
171 }
172 } catch (Exception e) {
173 logger.error("Exception while processing AI response for {}", player.getUsername(), e);
174 player.locking.unlock();
175 }
176 cancel();
177 }
178 });
179 })
180 .exceptionally(throwable -> {
181 logger.error("Error in Gandalf AI processing", throwable);
182 World.schedule(new Task(1) {
183 @Override
184 public void execute() {
185 if (player == null || !player.isRegistered()) {
186 cancel();
187 return;
188 }
189
190 try {
191 player.locking.unlock();
192 player.dialogueFactory
193 .sendNpcChat(npc.id, Expression.ANGRY,
194 "The AI spirits are restless.",
195 "Please try again in a moment.")
196 .onAction(() -> {
197 player.interfaceManager.close();
198 })
199 .execute();
200 } catch (Exception e) {
201 logger.error("Exception in error handler for {}", player.getUsername(), e);
202 player.locking.unlock();
203 }
204 cancel();
205 }
206 });
207 return null;
208 });
209 }
210
211 private void streamGandalfResponse(Player player, Npc npc, String response) {
212 logger.info("[TTS-SERVER] ========================================");
213 logger.info("[TTS-SERVER] Sending Gandalf response to player: {}", player.getUsername());
214 logger.info("[TTS-SERVER] Full response: {}", response);
215
216 String[] lines = wrapTextToLines(response, 50);
217 logger.info("[TTS-SERVER] Wrapped into {} lines", lines.length);
218 for (int i = 0; i < lines.length; i++) {
219 logger.info("[TTS-SERVER] Line {}: {}", i+1, lines[i]);
220 }
221
222 DialogueFactory factory = player.dialogueFactory;
223
224 // Set camera to behind NPC for AI response
225 player.dialogueCamMode = com.runehive.game.world.entity.mob.player.camera.DialogueCameraDirector.Mode.BEHIND_NPC;
226
227 int linesPerPage = 3;
228 for (int i = 0; i < lines.length; i += linesPerPage) {
229 int endIndex = Math.min(i + linesPerPage, lines.length);
230 String[] pageLines = java.util.Arrays.copyOfRange(lines, i, endIndex);
231
232 logger.info("[TTS-SERVER] Sending dialogue page {}: {} lines", (i/linesPerPage)+1, pageLines.length);
233
234 factory.sendNpcChat(npc.id, Expression.HAPPY, pageLines)
235 .onAction(() -> {
236 String combinedText = String.join(" ", pageLines);
237 npc.speak(combinedText);
238 });
239 }
240 logger.info("[TTS-SERVER] ========================================");
241
242 factory.sendOption(
243 "Ask another question", () -> {
244 // ASK ANOTHER QUESTION: Immediately reset camera & unlock
245 com.runehive.game.world.entity.mob.player.camera.DialogueCameraDirector.requestReset(player);
246 player.locking.unlock();
248 },
249 "No further questions", () -> {
250 // OPT OUT: Rewind to BEHIND_PLAYER (buttery swap), wait 4 ticks, then overhead line + wave, then unlock and reset
251 player.interfaceManager.close(); // Close interface first
252 player.dialogueCamMode = com.runehive.game.world.entity.mob.player.camera.DialogueCameraDirector.Mode.BEHIND_PLAYER;
253 com.runehive.game.world.entity.mob.player.camera.DialogueCameraDirector.boostSwap(player, 6);
254
255 // Wait 5 ticks for camera to reposition behind player, then player speaks
256 World.schedule(new Task(5) {
257 @Override
258 public void execute() {
259 // Player overhead text
260 player.speak("No, that's all, thank you.");
261
262 // 1 tick later, NPC responds with overhead + wave
263 World.schedule(new Task(1) {
264 @Override
265 public void execute() {
266 npc.speak("Come back again, anytime!");
267 npc.animate(863);
268
269 // After overhead line + animation, unlock & reset camera
270 World.schedule(new Task(3) {
271 @Override
272 public void execute() {
274 player.locking.unlock();
275 com.runehive.game.world.entity.mob.player.camera.DialogueCameraDirector.requestReset(player);
276 cancel();
277 }
278 });
279 cancel();
280 }
281 });
282 cancel();
283 }
284 });
285 }
286 );
287
288 factory.execute();
289 }
290
291 private String[] wrapTextToLines(String text, int maxCharsPerLine) {
292 String[] words = text.split(" ");
293 StringBuilder current = new StringBuilder();
294 java.util.List<String> lines = new java.util.ArrayList<>();
295
296 for (String word : words) {
297 if (current.length() + word.length() + 1 > maxCharsPerLine) {
298 if (current.length() > 0) {
299 lines.add(current.toString().trim());
300 current = new StringBuilder(word);
301 } else {
302 lines.add(word);
303 }
304 } else {
305 if (current.length() > 0) {
306 current.append(" ");
307 }
308 current.append(word);
309 }
310 }
311
312 if (current.length() > 0) {
313 lines.add(current.toString().trim());
314 }
315
316 return lines.toArray(new String[0]);
317 }
318}
static Npc findNearbyNpc(Player player, int npcId, int targetX, int targetY, int maxDistance)
Find an NPC by ID near specific coordinates.
void sendDialogues(DialogueFactory factory)
Sends a player a dialogue.
static void handleAICommand(Player player, String message)
Process AI command input (called from command handler)
void processGandalfAIMessage(Player player, Npc npc, String message)
String[] wrapTextToLines(String text, int maxCharsPerLine)
void streamGandalfResponse(Player player, Npc npc, String response)
static void clearPlayerConsent(String username)
static void markInstructionsReceived(String username)
CompletableFuture< String > processPlayerMessage(String username, String message, int npcId)
Represents a factory class that contains important functions for building dialogues.
final DialogueFactory sendDialogue(Dialogue dialogue)
Sends a player a dialogue.
final DialogueFactory execute()
Retrieves the next dialogue in the chain and executes it.
final DialogueFactory onAction(Runnable action)
Sets an action so this action can be executed after dialogues are done.
final Player player
The player who owns this factory.
Player getPlayer()
The player that owns this factory.
final DialogueFactory sendNpcChat(int id, String... lines)
Appends an NpcDialogue to the current dialogue chain.
final DialogueFactory sendOption(String option1, Runnable action1, String option2, Runnable action2)
Appends the OptionDialogue onto the current dialogue chain.
Represents an abstract dialogue, in which extending classes will be able to construct and send dialog...
Definition Dialogue.java:11
A game representing a cyclic unit of work.
Definition Task.java:11
Represents the game world.
Definition World.java:46
static void schedule(Task task)
Submits a new event.
Definition World.java:247
static MobList< Npc > getNpcs()
Definition World.java:548
void speak(String forceChat)
Sets the mob's forced chat.
Definition Mob.java:127
void move(Position position)
Moves the mob to a set position.
Definition Mob.java:340
void face(GameObject object)
Sets the client update flag to face a certain direction.
Definition Mob.java:289
Represents a non-player character in the in-game world.
Definition Npc.java:29
This class represents a character controlled by a player.
Definition Player.java:125
Represents a static game object loaded from the map fs.
void unregister()
Unregisters an entity from the World.
void register()
Registers an entity to the World.
Represents a single tile on the game world.
Definition Position.java:14
int getHeight()
Gets the height coordinate, or height.
Definition Position.java:51
int getY()
Gets the absolute y coordinate.
Definition Position.java:46
Position(int x, int y)
Creates a location with a default height of 0.
Definition Position.java:29
int getX()
Gets the absolute x coordinate.
Definition Position.java:41
Represents the expressions of entities for dialogue.
The enumerated type whose elements represent the directions for objects.