Skip to content

Commit efd8d77

Browse files
committed
Basic quest tracker and conversation builder
1 parent 8b7ea28 commit efd8d77

14 files changed

Lines changed: 567 additions & 0 deletions

File tree

api/build.gradle

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,14 @@ bukkit {
309309
permissionMessage = 'You do not have permission.'
310310
permission = 'parallelutils.hat.*'
311311
}
312+
questtracker {
313+
description = 'Opens the quest tracker to view your active quests'
314+
usage = '/questtracker'
315+
}
316+
exampleconversation {
317+
description = 'Runs a test conversation'
318+
usage = '/exampleconversation'
319+
}
312320
}
313321

314322
permissions {
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
package parallelmc.parallelutils.modules.parallelquests;
2+
3+
import org.bukkit.Bukkit;
4+
import org.bukkit.entity.Player;
5+
import org.bukkit.plugin.Plugin;
6+
import org.bukkit.plugin.PluginManager;
7+
import org.checkerframework.checker.units.qual.C;
8+
import org.jetbrains.annotations.NotNull;
9+
import org.jetbrains.annotations.Nullable;
10+
import parallelmc.parallelutils.Constants;
11+
import parallelmc.parallelutils.ParallelClassLoader;
12+
import parallelmc.parallelutils.ParallelModule;
13+
import parallelmc.parallelutils.ParallelUtils;
14+
import parallelmc.parallelutils.modules.parallelquests.commands.ExampleConversation;
15+
import parallelmc.parallelutils.modules.parallelquests.commands.QuestTracker;
16+
import parallelmc.parallelutils.modules.parallelquests.dialogue.Conversation;
17+
import parallelmc.parallelutils.modules.parallelquests.dialogue.Dialogue;
18+
import parallelmc.parallelutils.modules.parallelquests.events.OnJoinLeave;
19+
import parallelmc.parallelutils.modules.parallelquests.events.OnSlotUpdated;
20+
21+
import java.sql.*;
22+
import java.util.*;
23+
import java.util.concurrent.ConcurrentHashMap;
24+
import java.util.logging.Level;
25+
26+
public class ParallelQuests extends ParallelModule {
27+
private ParallelUtils puPlugin;
28+
29+
public ParallelQuests(ParallelClassLoader classLoader, List<String> dependents) { super(classLoader, dependents); }
30+
31+
// TODO: populate
32+
private final HashSet<String> ValidQuests = new HashSet<>();
33+
34+
private final ConcurrentHashMap<UUID, List<QuestStatus>> PlayerQuestStatuses = new ConcurrentHashMap<>();
35+
private final HashMap<UUID, Conversation> ActiveConversations = new HashMap<>();
36+
37+
private static ParallelQuests Instance;
38+
39+
@Override
40+
public void onLoad() {}
41+
42+
@Override
43+
public void onEnable() {
44+
PluginManager manager = Bukkit.getPluginManager();
45+
Plugin plugin = manager.getPlugin(Constants.PLUGIN_NAME);
46+
47+
if (plugin == null) {
48+
ParallelUtils.log(Level.SEVERE, "Unable to enable ParallelQuests. Plugin " + Constants.PLUGIN_NAME
49+
+ " does not exist!");
50+
return;
51+
}
52+
53+
this.puPlugin = (ParallelUtils) plugin;
54+
55+
if (!puPlugin.registerModule(this)) {
56+
ParallelUtils.log(Level.SEVERE, "Unable to register module ParallelQuests! " +
57+
"Module may already be registered. Quitting...");
58+
return;
59+
}
60+
61+
manager.registerEvents(new OnJoinLeave(), puPlugin);
62+
manager.registerEvents(new OnSlotUpdated(), puPlugin);
63+
64+
puPlugin.getCommand("questtracker").setExecutor(new QuestTracker());
65+
puPlugin.getCommand("exampleconversation").setExecutor(new ExampleConversation());
66+
67+
try (Connection conn = puPlugin.getDbConn()) {
68+
if (conn == null) throw new SQLException("Unable to establish connection!");
69+
Statement statement = conn.createStatement();
70+
statement.setQueryTimeout(15);
71+
statement.execute("""
72+
create table if not exists Quests
73+
(
74+
Id int not null auto_increment,
75+
UUID varchar(36) not null,
76+
QuestId varchar(128) not null,
77+
Completed tinyint not null,
78+
constraint Quests_Id_uindex
79+
unique (Id),
80+
PRIMARY KEY (Id)
81+
);
82+
""");
83+
conn.commit();
84+
statement.close();
85+
} catch (SQLException e) {
86+
e.printStackTrace();
87+
}
88+
89+
Bukkit.getScheduler().runTaskTimer(puPlugin, () -> {
90+
PlayerQuestStatuses.keySet().forEach(this::savePlayerQuestStatus);
91+
}, 6000L, 6000L); // 5 minutes
92+
93+
Instance = this;
94+
}
95+
96+
@Override
97+
public void onDisable() {}
98+
99+
@Override
100+
public void onUnload() {}
101+
102+
@Override
103+
public @NotNull String getName() {
104+
return "ParallelQuests";
105+
}
106+
107+
public static ParallelQuests get() {
108+
return Instance;
109+
}
110+
111+
public void startConversation(Player player, Dialogue dialogue) {
112+
if (ActiveConversations.containsKey(player.getUniqueId())) {
113+
ParallelUtils.log(Level.WARNING, "Tried to add " + player.getName() + " to a conversation when they are already in one!");
114+
return;
115+
}
116+
Conversation c = new Conversation(dialogue);
117+
c.enter(player);
118+
ActiveConversations.put(player.getUniqueId(), c);
119+
}
120+
121+
public void endConversation(UUID uuid) {
122+
ActiveConversations.remove(uuid);
123+
}
124+
125+
public @Nullable Conversation getActiveConversation(UUID uuid) {
126+
return ActiveConversations.getOrDefault(uuid, null);
127+
}
128+
129+
/**
130+
* Returns a list of quests a player currently has active, as well as if they are completed.
131+
* If a quest ID is not in this list, the player has not accepted it yet.
132+
* @param uuid The player UUID to search
133+
*/
134+
public List<QuestStatus> getQuestStatus(UUID uuid) {
135+
return PlayerQuestStatuses.getOrDefault(uuid, List.of());
136+
}
137+
138+
public boolean markQuestCompleted(UUID uuid, String questId) {
139+
if (!ValidQuests.contains(questId)) {
140+
ParallelUtils.log(Level.SEVERE, "Invalid Quest ID " + questId + " provided for quest completion!");
141+
return false;
142+
}
143+
144+
Optional<QuestStatus> status = getQuestStatus(uuid).stream().filter(x -> x.getQuestId().equals(questId)).findFirst();
145+
if (status.isEmpty()) {
146+
return false;
147+
}
148+
149+
status.get().markCompleted();
150+
return false;
151+
}
152+
153+
/**
154+
* Asynchronously loads a player's quest data into the cache.
155+
* If a player's data already exists in the cache, the load will be ignored.
156+
* @param uuid The Player UUID to load
157+
*/
158+
public void loadPlayerQuestStatus(UUID uuid) {
159+
Bukkit.getScheduler().runTaskAsynchronously(puPlugin, () -> {
160+
List<QuestStatus> result = new ArrayList<>();
161+
try (Connection conn = puPlugin.getDbConn()) {
162+
if (conn == null) throw new SQLException("Unable to establish connection!");
163+
Statement statement = conn.createStatement();
164+
statement.setQueryTimeout(10);
165+
ResultSet results = statement.executeQuery("select * from Quests where UUID = '" + uuid + "'");
166+
while (results.next()) {
167+
String questId = results.getString("QuestId");
168+
boolean completed = results.getBoolean("Completed");
169+
result.add(new QuestStatus(questId, completed));
170+
}
171+
conn.commit();
172+
statement.close();
173+
} catch (SQLException e) {
174+
e.printStackTrace();
175+
}
176+
177+
if (PlayerQuestStatuses.putIfAbsent(uuid, result) != null) {
178+
ParallelUtils.log(Level.WARNING, "UUID " + uuid + " already has an entry in PlayerQuestStatuses, ignoring!");
179+
}
180+
});
181+
}
182+
183+
/**
184+
* Asynchronously saves a player's quest data to the database.
185+
* The player's data WILL be removed from the cache, see savePlayerQuestStatus to avoid this.
186+
* @param uuid The Player UUID to save
187+
*/
188+
public void saveAndRemovePlayerQuestStatus(UUID uuid) {
189+
savePlayerQuestStatus(uuid);
190+
PlayerQuestStatuses.remove(uuid);
191+
}
192+
193+
/**
194+
* Asynchronously saves a player's quest data to the database.
195+
* The player's data will NOT be removed from the cache, see saveAndRemovePlayerQuestStatus
196+
* @param uuid The Player UUID to save
197+
*/
198+
public void savePlayerQuestStatus(UUID uuid) {
199+
Bukkit.getScheduler().runTaskAsynchronously(puPlugin, () -> {
200+
List<QuestStatus> status = getQuestStatus(uuid);
201+
try (Connection conn = puPlugin.getDbConn()) {
202+
if (conn == null) throw new SQLException("Unable to establish connection!");
203+
Statement statement = conn.createStatement();
204+
statement.setQueryTimeout(10);
205+
statement.execute("delete from Quests where UUID = '" + uuid + "'");
206+
PreparedStatement prepared = conn.prepareStatement("insert into Quests (UUID, QuestId, Completed) values (?, ?, ?)");
207+
prepared.setQueryTimeout(30);
208+
status.forEach(s -> {
209+
try {
210+
prepared.setString(1, uuid.toString());
211+
prepared.setString(2, s.getQuestId());
212+
prepared.setBoolean(3, s.isCompleted());
213+
prepared.addBatch();
214+
}
215+
catch (SQLException e){
216+
e.printStackTrace();
217+
}
218+
});
219+
prepared.executeBatch();
220+
conn.commit();
221+
statement.close();
222+
prepared.close();
223+
} catch (SQLException e) {
224+
e.printStackTrace();
225+
}
226+
});
227+
}
228+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package parallelmc.parallelutils.modules.parallelquests;
2+
3+
public class QuestStatus {
4+
private final String questId;
5+
private boolean completed;
6+
7+
public QuestStatus(String questId, boolean completed) {
8+
this.questId = questId;
9+
this.completed = completed;
10+
}
11+
12+
public String getQuestId() {
13+
return questId;
14+
}
15+
16+
public boolean isCompleted() {
17+
return completed;
18+
}
19+
20+
public void markCompleted() {
21+
completed = true;
22+
}
23+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package parallelmc.parallelutils.modules.parallelquests.commands;
2+
3+
import org.bukkit.Material;
4+
import org.bukkit.command.Command;
5+
import org.bukkit.command.CommandExecutor;
6+
import org.bukkit.command.CommandSender;
7+
import org.bukkit.entity.Player;
8+
import org.bukkit.inventory.ItemStack;
9+
import org.jetbrains.annotations.NotNull;
10+
import parallelmc.parallelutils.modules.parallelquests.ParallelQuests;
11+
import parallelmc.parallelutils.modules.parallelquests.dialogue.Dialogue;
12+
import parallelmc.parallelutils.modules.parallelquests.dialogue.DialogueBuilder;
13+
14+
public class ExampleConversation implements CommandExecutor {
15+
@Override
16+
public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String label, String[] args) {
17+
if (commandSender instanceof Player player) {
18+
Dialogue dialogue = Dialogue.withSpeaker("Villager",
19+
DialogueBuilder.node()
20+
.text("Hello there, can I help you with something?")
21+
.option("Can you spare me an emerald?", DialogueBuilder.node()
22+
.text("I guess. Here, I have one for you.")
23+
.action(p -> p.getInventory().addItem(new ItemStack(Material.EMERALD, 1)))
24+
)
25+
.option("Nothing, goodbye.", DialogueBuilder.node()
26+
.text("Alright. See you around.")
27+
)
28+
.build()
29+
);
30+
ParallelQuests.get().startConversation(player, dialogue);
31+
return true;
32+
}
33+
return false;
34+
}
35+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package parallelmc.parallelutils.modules.parallelquests.commands;
2+
3+
import org.bukkit.command.Command;
4+
import org.bukkit.command.CommandExecutor;
5+
import org.bukkit.command.CommandSender;
6+
import org.bukkit.entity.Player;
7+
import org.jetbrains.annotations.NotNull;
8+
import parallelmc.parallelutils.modules.parallelquests.gui.QuestTrackerInventory;
9+
import parallelmc.parallelutils.util.GUIManager;
10+
11+
public class QuestTracker implements CommandExecutor {
12+
@Override
13+
public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String label, String[] args) {
14+
if (commandSender instanceof Player player) {
15+
GUIManager.get().openInventoryForPlayer(player, new QuestTrackerInventory());
16+
return true;
17+
}
18+
return false;
19+
}
20+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package parallelmc.parallelutils.modules.parallelquests.dialogue;
2+
3+
import net.kyori.adventure.text.Component;
4+
import net.kyori.adventure.text.format.NamedTextColor;
5+
import org.bukkit.entity.Player;
6+
7+
public class Conversation {
8+
private final Dialogue dialogue;
9+
private DialogueNode current;
10+
11+
public Conversation(Dialogue dialogue) {
12+
this.dialogue = dialogue;
13+
this.current = dialogue.getRoot();
14+
}
15+
16+
public void enter(Player player) {
17+
current.onEnter(player);
18+
display(player);
19+
}
20+
21+
public void choose(Player player, int index) {
22+
current = current.getOptions().get(index).getNext();
23+
enter(player);
24+
}
25+
26+
private void display(Player player) {
27+
player.sendMessage(Component.text(dialogue.getSpeaker() + ": " + current.getText()));
28+
for (int i = 0; i < current.getOptions().size(); i++) {
29+
player.sendMessage(Component.text("[" + current.getOptions().get(i).getText() + "]", NamedTextColor.AQUA));
30+
}
31+
}
32+
33+
public boolean isFinished() { return !current.hasNext(); }
34+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package parallelmc.parallelutils.modules.parallelquests.dialogue;
2+
3+
public class Dialogue {
4+
private final String speaker;
5+
private final DialogueNode root;
6+
7+
private Dialogue(String speaker, DialogueNode root) {
8+
this.speaker = speaker;
9+
this.root = root;
10+
}
11+
12+
public static Dialogue withSpeaker(String speaker, DialogueNode root) {
13+
return new Dialogue(speaker, root);
14+
}
15+
16+
public String getSpeaker() { return speaker; }
17+
public DialogueNode getRoot() { return root; }
18+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package parallelmc.parallelutils.modules.parallelquests.dialogue;
2+
3+
import org.bukkit.entity.Player;
4+
5+
public interface DialogueAction {
6+
void execute(Player player);
7+
}

0 commit comments

Comments
 (0)