diff --git a/src/main/java/com/projectswg/holocore/services/gameplay/player/quest/QuestService.java b/src/main/java/com/projectswg/holocore/services/gameplay/player/quest/QuestService.java
deleted file mode 100644
index 4b3393658..000000000
--- a/src/main/java/com/projectswg/holocore/services/gameplay/player/quest/QuestService.java
+++ /dev/null
@@ -1,507 +0,0 @@
-/***********************************************************************************
- * Copyright (c) 2023 /// Project SWG /// www.projectswg.com *
- * *
- * ProjectSWG is the first NGE emulator for Star Wars Galaxies founded on *
- * July 7th, 2011 after SOE announced the official shutdown of Star Wars Galaxies. *
- * Our goal is to create an emulator which will provide a server for players to *
- * continue playing a game similar to the one they used to play. We are basing *
- * it on the final publish of the game prior to end-game events. *
- * *
- * This file is part of Holocore. *
- * *
- * --------------------------------------------------------------------------------*
- * *
- * Holocore is free software: you can redistribute it and/or modify *
- * it under the terms of the GNU Affero General Public License as *
- * published by the Free Software Foundation, either version 3 of the *
- * License, or (at your option) any later version. *
- * *
- * Holocore is distributed in the hope that it will be useful, *
- * but WITHOUT ANY WARRANTY; without even the implied warranty of *
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
- * GNU Affero General Public License for more details. *
- * *
- * You should have received a copy of the GNU Affero General Public License *
- * along with Holocore. If not, see . *
- ***********************************************************************************/
-package com.projectswg.holocore.services.gameplay.player.quest;
-
-import com.projectswg.common.data.CRC;
-import com.projectswg.common.data.encodables.oob.OutOfBandPackage;
-import com.projectswg.common.data.encodables.oob.ProsePackage;
-import com.projectswg.common.data.encodables.oob.StringId;
-import com.projectswg.common.data.swgfile.ClientFactory;
-import com.projectswg.common.network.packets.swg.zone.CommPlayerMessage;
-import com.projectswg.common.network.packets.swg.zone.PlayMusicMessage;
-import com.projectswg.common.network.packets.swg.zone.chat.ChatSystemMessage;
-import com.projectswg.common.network.packets.swg.zone.object_controller.quest.QuestCompletedMessage;
-import com.projectswg.common.network.packets.swg.zone.object_controller.quest.QuestTaskCounterMessage;
-import com.projectswg.holocore.intents.gameplay.combat.CreatureKilledIntent;
-import com.projectswg.holocore.intents.gameplay.player.experience.ExperienceIntent;
-import com.projectswg.holocore.intents.gameplay.player.quest.AbandonQuestIntent;
-import com.projectswg.holocore.intents.gameplay.player.quest.AdvanceQuestIntent;
-import com.projectswg.holocore.intents.gameplay.player.quest.CompleteQuestIntent;
-import com.projectswg.holocore.intents.gameplay.player.quest.GrantQuestIntent;
-import com.projectswg.holocore.intents.support.global.chat.SystemMessageIntent;
-import com.projectswg.holocore.intents.support.objects.swg.ObjectCreatedIntent;
-import com.projectswg.holocore.resources.support.data.server_info.StandardLog;
-import com.projectswg.holocore.resources.support.data.server_info.loader.QuestLoader;
-import com.projectswg.holocore.resources.support.data.server_info.loader.ServerData;
-import com.projectswg.holocore.resources.support.global.player.Player;
-import com.projectswg.holocore.resources.support.global.zone.sui.SuiButtons;
-import com.projectswg.holocore.resources.support.global.zone.sui.SuiMessageBox;
-import com.projectswg.holocore.resources.support.npc.spawn.Spawner;
-import com.projectswg.holocore.resources.support.objects.ObjectCreator;
-import com.projectswg.holocore.resources.support.objects.StaticItemCreator;
-import com.projectswg.holocore.resources.support.objects.swg.SWGObject;
-import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureObject;
-import com.projectswg.holocore.resources.support.objects.swg.custom.AIObject;
-import com.projectswg.holocore.resources.support.objects.swg.player.PlayerObject;
-import com.projectswg.holocore.resources.support.objects.swg.player.Quest;
-import me.joshlarson.jlcommon.concurrency.ScheduledThreadPool;
-import me.joshlarson.jlcommon.control.IntentHandler;
-import me.joshlarson.jlcommon.control.Service;
-import org.jetbrains.annotations.NotNull;
-
-import java.util.*;
-import java.util.concurrent.ThreadLocalRandom;
-import java.util.stream.Collectors;
-
-public class QuestService extends Service {
-
- private static final StringId PLACED_IN_INVENTORY = new StringId("quest/ground/system_message", "placed_in_inventory");
- private final ScheduledThreadPool executor;
- private final QuestLoader questLoader;
-
- public QuestService() {
- this.executor = new ScheduledThreadPool(1, "quest-service-%d");
- this.questLoader = ServerData.INSTANCE.getQuestLoader();
- }
-
- @Override
- public boolean initialize() {
- executor.start();
- return true;
- }
-
- @Override
- public boolean terminate() {
- executor.stop();
- return executor.awaitTermination(1000);
- }
-
- @IntentHandler
- private void handleGrantQuestIntent(GrantQuestIntent intent) {
- Player player = intent.getPlayer();
- String questName = intent.getQuestName();
- PlayerObject playerObject = player.getPlayerObject();
-
- QuestLoader.QuestListInfo questListInfo = questLoader.getQuestListInfo(questName);
-
- boolean repeatable = Boolean.TRUE.equals(questListInfo.isRepeatable());
-
- if (!repeatable && playerObject.isQuestInJournal(questName)) {
- StandardLog.onPlayerError(this, player, "already had non-repeatable quest %s", questName);
- return;
- }
-
- playerObject.addQuest(questName);
- playerObject.addActiveQuestTask(questName, 0);
-
- StandardLog.onPlayerTrace(this, player, "received quest %s", questName);
-
- List currentTasks = getActiveTaskInfos(questName, playerObject);
-
- handleTaskEvents(player, questName, currentTasks);
-
- ProsePackage prose = new ProsePackage(new StringId("quest/ground/system_message", "quest_received"), "TO", questListInfo.getJournalEntryTitle());
- SystemMessageIntent.broadcastPersonal(player, prose, ChatSystemMessage.SystemChatType.QUEST);
- }
-
- @IntentHandler
- private void handleAbandonQuestIntent(AbandonQuestIntent intent) {
- Player player = intent.getPlayer();
- String questName = intent.getQuestName();
- PlayerObject playerObject = player.getPlayerObject();
-
- if (playerObject.isQuestComplete(questName)) {
- StandardLog.onPlayerTrace(this, player, "attempted to abandon quest %s which they have completed", questName);
- return;
- }
-
- playerObject.removeQuest(questName);
- }
-
- @IntentHandler
- private void handleCompleteQuestIntent(CompleteQuestIntent intent) {
- Player player = intent.getPlayer();
- PlayerObject playerObject = player.getPlayerObject();
- String questName = intent.getQuestName();
-
- if (playerObject.isQuestRewardReceived(questName)) {
- StandardLog.onPlayerTrace(this, player, "attempted to claim reward for quest %s but they have already received it", questName);
- return;
- }
-
- // TODO award XP, load from tier+level matrix: datatables/quest/quest_experience.iff
-
- playerObject.setQuestRewardReceived(questName, true);
- }
-
- @IntentHandler
- private void handleAdvanceQuestIntent(AdvanceQuestIntent intent) {
- Player player = intent.getPlayer();
- String questName = intent.getQuestName();
- PlayerObject playerObject = player.getPlayerObject();
-
- if (!playerObject.isQuestInJournal(questName)) {
- StandardLog.onPlayerError(this, player, "advanced quest %s that was not in their quest journal", questName);
- return;
- }
-
- List currentTasks = getActiveTaskInfos(questName, playerObject);
-
- advanceQuest(questName, player, currentTasks);
- }
-
- @IntentHandler
- private void handleCreatureKilledIntent(CreatureKilledIntent intent) {
- CreatureObject corpse = intent.getCorpse();
-
- if (!(corpse instanceof AIObject)) {
- return;
- }
-
- AIObject npcCorpse = (AIObject) corpse;
- CreatureObject killer = intent.getKiller();
- Player owner = killer.getOwner();
-
- if (owner == null) {
- return;
- }
-
- PlayerObject playerObject = killer.getPlayerObject();
-
- Set> entries = playerObject.getQuests().entrySet();
-
- for (Map.Entry entry : entries) {
- CRC key = entry.getKey();
- Quest quest = entry.getValue();
-
- if (quest.isComplete()) {
- continue;
- }
-
- String questName = key.getString();
- List activeTaskListInfos = getActiveTaskInfos(questName, playerObject);
-
- for (QuestLoader.QuestTaskInfo activeTaskListInfo : activeTaskListInfos) {
- String type = activeTaskListInfo.getType();
-
- if ("quest.task.ground.destroy_multi".equals(type)) {
- if (isKillPartOfTask(activeTaskListInfo, npcCorpse)) {
- int max = activeTaskListInfo.getCount();
- int counter = playerObject.incrementQuestCounter(questName);
- int remaining = max - counter;
- int task = activeTaskListInfo.getIndex();
- StandardLog.onPlayerTrace(this, owner, "%d remaining kills required on quest %s", remaining, questName);
-
- incrementKillCount(questName, task, owner, counter, max);
-
- if (remaining <= 0) {
- advanceQuest(questName, owner, activeTaskListInfos);
- }
- }
- }
- }
- }
- }
-
- private boolean isKillPartOfTask(QuestLoader.QuestTaskInfo swgQuestTask, AIObject npcCorpse) {
- String targetServerTemplate = swgQuestTask.getTargetServerTemplate();
- Spawner spawner = npcCorpse.getSpawner();
- String stfName = spawner.getStfName();
- String questSocialGroup = swgQuestTask.getSocialGroup();
- String npcSocialGroup = npcCorpse.getSpawner().getSocialGroup();
-
- return isMatchingSocialGroup(questSocialGroup, npcSocialGroup) || isMatchingServerTemplate(targetServerTemplate, stfName);
- }
-
- private static boolean isMatchingServerTemplate(String targetServerTemplate, String stfName) {
- return targetServerTemplate != null && Objects.equals(targetServerTemplate, stfName);
- }
-
- private static boolean isMatchingSocialGroup(String questSocialGroup, String npcSocialGroup) {
- return questSocialGroup != null && questSocialGroup.equalsIgnoreCase(npcSocialGroup);
- }
-
- private void incrementKillCount(String questName, int task, Player player, int counter, int max) {
- player.sendPacket(new QuestTaskCounterMessage(player.getCreatureObject().getObjectId(), questName, task, "@quest/groundquests:destroy_counter", counter, max));
- int remaining = max - counter;
- ProsePackage prose = new ProsePackage(new StringId("quest/groundquests", "destroy_multiple_success"), "DI", remaining);
- SystemMessageIntent.broadcastPersonal(player, prose);
- }
-
- private void handleTaskEvents(Player player, String questName, Collection currentTasks) {
- PlayerObject playerObject = player.getPlayerObject();
-
- for (QuestLoader.QuestTaskInfo currentTask : currentTasks) {
- String type = currentTask.getType();
-
- if (type == null) {
- continue;
- }
-
- if (currentTask.isVisible()) {
- player.sendPacket(new PlayMusicMessage(0, "sound/ui_journal_updated.snd", 1, false));
- }
-
- switch (type) {
- case "quest.task.ground.comm_player": {
- handleCommPlayer(player, questName, playerObject, currentTask);
- break;
- }
- case "quest.task.ground.complete_quest": {
- completeQuest(player, questName);
- break;
- }
- case "quest.task.ground.timer": {
- handleTimer(player, questName, playerObject, currentTask);
- break;
- }
- case "quest.task.ground.show_message_box": {
- handleShowMessageBox(player, questName, playerObject, currentTask);
- break;
- }
- case "quest.task.ground.destroy_multi": {
- handleDestroyMulti(player, questName, currentTask);
- break;
- }
- case "quest.task.ground.reward": {
- handleReward(player, questName, playerObject, currentTask);
- break;
- }
- }
- }
- }
-
- private void handleReward(Player player, String questName, PlayerObject playerObject, QuestLoader.QuestTaskInfo currentTask) {
- grantXPReward(player, currentTask);
- grantFactionPointsReward(playerObject, currentTask);
- grantCreditsReward(player, currentTask);
- grantLootRewards(player, currentTask);
- grantItemRewards(player, currentTask);
- // weapon stuff: weapon count_weapon speed damage efficiency elemental_value
- // armor stuff: armor count_armor quality
- // speed etc. are percentages, the absolute values we must define somewhere else before we can hand out weapons and armor rewards
-
- playerObject.removeActiveQuestTask(questName, currentTask.getIndex());
- playerObject.addCompleteQuestTask(questName, currentTask.getIndex());
-
- for (Integer taskIndex : currentTask.getNextTasksOnComplete()) {
- playerObject.addActiveQuestTask(questName, taskIndex);
- }
-
- List taskListInfos = questLoader.getTaskListInfos(questName);
- Collection nextTasksOnComplete = currentTask.getNextTasksOnComplete();
- List nextTasks = mapActiveTasks(nextTasksOnComplete, taskListInfos);
-
- handleTaskEvents(player, questName, nextTasks);
- }
-
- private static void grantXPReward(Player player, QuestLoader.QuestTaskInfo currentTask) {
- String experienceType = currentTask.getExperienceType();
- if (experienceType != null) {
- new ExperienceIntent(player.getCreatureObject(), experienceType, currentTask.getExperienceAmount()).broadcast();
- }
- }
-
- private static void grantFactionPointsReward(PlayerObject playerObject, QuestLoader.QuestTaskInfo currentTask) {
- String factionName = currentTask.getFactionName();
- if (factionName != null) {
- playerObject.adjustFactionPoints(factionName, currentTask.getFactionAmount());
- }
- }
-
- private static void grantCreditsReward(Player player, QuestLoader.QuestTaskInfo currentTask) {
- int bankCredits = currentTask.getBankCredits();
- if (bankCredits != 0) {
- player.getCreatureObject().addToBank(bankCredits);
- }
- }
-
- private static void grantLootRewards(Player player, QuestLoader.QuestTaskInfo currentTask) {
- int lootCount = currentTask.getLootCount();
- if (lootCount > 0) {
- for (int i = 0; i < lootCount; i++) {
- String lootName = currentTask.getLootName();
- if (lootName != null && !lootName.isBlank()) {
- SWGObject item = StaticItemCreator.INSTANCE.createItem(lootName);
- if (item != null) {
- transferItemToInventory(player, item);
- }
- }
- }
- }
- }
-
- private static void grantItemRewards(Player player, QuestLoader.QuestTaskInfo currentTask) {
- int itemCount = currentTask.getItemCount();
- if (itemCount > 0) {
- for (int i = 0; i < itemCount; i++) {
- String itemTemplate = currentTask.getItemTemplate();
- if (itemTemplate != null && !itemTemplate.isBlank()) {
- SWGObject item = ObjectCreator.createObjectFromTemplate(itemTemplate);
- transferItemToInventory(player, item);
- }
- }
- }
- }
-
- private static void transferItemToInventory(Player player, SWGObject item) {
- SWGObject inventory = player.getCreatureObject().getInventory();
- item.moveToContainer(inventory);
- ObjectCreatedIntent.broadcast(item);
- SystemMessageIntent.broadcastPersonal(player, new ProsePackage(PLACED_IN_INVENTORY, "TO", item.getStringId()));
- }
-
- private static void handleDestroyMulti(Player player, String questName, QuestLoader.QuestTaskInfo currentTask) {
- int task = currentTask.getIndex();
- int max = currentTask.getCount();
- int counter = 0;
- player.sendPacket(new QuestTaskCounterMessage(player.getCreatureObject().getObjectId(), questName, task, "@quest/groundquests:destroy_counter", counter, max));
- }
-
- private void handleShowMessageBox(Player player, String questName, PlayerObject playerObject, QuestLoader.QuestTaskInfo currentTask) {
- String messageBoxTitle = currentTask.getMessageBoxTitle();
- String messageBoxText = currentTask.getMessageBoxText();
- SuiMessageBox sui = new SuiMessageBox(SuiButtons.OK, messageBoxTitle, messageBoxText);
- sui.setSize(384, 256);
- sui.setLocation(320, 256);
- sui.display(player);
-
- playerObject.removeActiveQuestTask(questName, currentTask.getIndex());
- playerObject.addCompleteQuestTask(questName, currentTask.getIndex());
-
- for (Integer taskIndex : currentTask.getNextTasksOnComplete()) {
- playerObject.addActiveQuestTask(questName, taskIndex);
- }
-
- List taskListInfos = questLoader.getTaskListInfos(questName);
- Collection nextTasksOnComplete = currentTask.getNextTasksOnComplete();
- List nextTasks = mapActiveTasks(nextTasksOnComplete, taskListInfos);
-
- handleTaskEvents(player, questName, nextTasks);
- }
-
- private void handleCommPlayer(Player player, String questName, PlayerObject playerObject, QuestLoader.QuestTaskInfo currentTask) {
- String commMessageText = currentTask.getCommMessageText();
- OutOfBandPackage message = new OutOfBandPackage(new ProsePackage(new StringId(commMessageText)));
- long objectId = player.getCreatureObject().getObjectId();
- CRC modelCrc = getModelCrc(currentTask);
-
- player.sendPacket(new CommPlayerMessage(objectId, message, modelCrc, "", 10));
-
- playerObject.removeActiveQuestTask(questName, currentTask.getIndex());
- playerObject.addCompleteQuestTask(questName, currentTask.getIndex());
-
- for (Integer taskIndex : currentTask.getNextTasksOnComplete()) {
- playerObject.addActiveQuestTask(questName, taskIndex);
- }
-
- List taskListInfos = questLoader.getTaskListInfos(questName);
- Collection nextTasksOnComplete = currentTask.getNextTasksOnComplete();
- List nextTasks = mapActiveTasks(nextTasksOnComplete, taskListInfos);
-
- handleTaskEvents(player, questName, nextTasks);
- }
-
- @NotNull
- private static CRC getModelCrc(QuestLoader.QuestTaskInfo currentTask) {
- String npcAppearanceServerTemplate = currentTask.getNpcAppearanceServerTemplate();
- if (npcAppearanceServerTemplate != null && !npcAppearanceServerTemplate.isBlank()) {
- String sharedTemplate = ClientFactory.formatToSharedFile(npcAppearanceServerTemplate);
- return new CRC(sharedTemplate);
- }
-
- return new CRC(0); // Fallback case, as some tasks don't have an appearance set. The player sees their own character in the comm window.
- }
-
- private void handleTimer(Player player, String questName, PlayerObject playerObject, QuestLoader.QuestTaskInfo currentTask) {
- int minTime = currentTask.getMinTime();
- int maxTime = currentTask.getMaxTime();
- ThreadLocalRandom random = ThreadLocalRandom.current();
- int delay = random.nextInt(minTime, maxTime) * 1000;
-
- executor.execute(delay, () -> {
- playerObject.removeActiveQuestTask(questName, currentTask.getIndex());
- playerObject.addCompleteQuestTask(questName, currentTask.getIndex());
-
- List taskListInfos = questLoader.getTaskListInfos(questName);
- Collection nextTasksOnComplete = currentTask.getNextTasksOnComplete();
- List nextTasks = mapActiveTasks(nextTasksOnComplete, taskListInfos);
-
- handleTaskEvents(player, questName, nextTasks);
- });
- }
-
- private void advanceQuest(String questName, Player player, List currentTasks) {
- PlayerObject playerObject = player.getPlayerObject();
-
- for (QuestLoader.QuestTaskInfo currentTask : currentTasks) {
- playerObject.removeActiveQuestTask(questName, currentTask.getIndex());
- playerObject.addCompleteQuestTask(questName, currentTask.getIndex());
-
- Collection nextTasksOnComplete = currentTask.getNextTasksOnComplete();
-
- for (Integer nextTaskOnComplete : nextTasksOnComplete) {
- playerObject.addActiveQuestTask(questName, nextTaskOnComplete);
- StandardLog.onPlayerTrace(this, player, "advanced quest %s, activated task %d", questName, nextTaskOnComplete);
- }
- }
-
- List nextTasks = getActiveTaskInfos(questName, playerObject);
-
- if (nextTasks.isEmpty()) {
- QuestLoader.QuestListInfo questListInfo = questLoader.getQuestListInfo(questName);
- boolean completeWhenTasksComplete = questListInfo.isCompleteWhenTasksComplete();
-
- if (completeWhenTasksComplete) {
- completeQuest(player, questName);
-
- nextTasks = currentTasks;
- }
- }
-
- handleTaskEvents(player, questName, nextTasks);
-
- for (QuestLoader.QuestTaskInfo nextTask : nextTasks) {
- String grantQuestOnComplete = nextTask.getGrantQuestOnComplete();
-
- if (grantQuestOnComplete != null && !grantQuestOnComplete.isBlank()) {
- GrantQuestIntent.broadcast(player, grantQuestOnComplete);
- }
- }
- }
-
- private void completeQuest(Player player, String questName) {
- PlayerObject playerObject = player.getPlayerObject();
- playerObject.completeQuest(questName);
- player.sendPacket(new QuestCompletedMessage(player.getCreatureObject().getObjectId(), new CRC(questName)));
- StandardLog.onPlayerTrace(this, player, "completed quest %s", questName);
- }
-
- private List getActiveTaskInfos(String questName, PlayerObject playerObject) {
- List taskListInfos = questLoader.getTaskListInfos(questName);
- Collection questActiveTasks = playerObject.getQuestActiveTasks(questName);
-
- return mapActiveTasks(questActiveTasks, taskListInfos);
- }
-
- private List mapActiveTasks(Collection activeTaskIndices, List taskListInfos) {
- return activeTaskIndices.stream()
- .map(taskListInfos::get)
- .collect(Collectors.toList());
- }
-
-}
diff --git a/src/main/java/com/projectswg/holocore/services/gameplay/player/quest/QuestService.kt b/src/main/java/com/projectswg/holocore/services/gameplay/player/quest/QuestService.kt
new file mode 100644
index 000000000..a10691ddb
--- /dev/null
+++ b/src/main/java/com/projectswg/holocore/services/gameplay/player/quest/QuestService.kt
@@ -0,0 +1,411 @@
+/***********************************************************************************
+ * Copyright (c) 2023 /// Project SWG /// www.projectswg.com *
+ * *
+ * ProjectSWG is the first NGE emulator for Star Wars Galaxies founded on *
+ * July 7th, 2011 after SOE announced the official shutdown of Star Wars Galaxies. *
+ * Our goal is to create an emulator which will provide a server for players to *
+ * continue playing a game similar to the one they used to play. We are basing *
+ * it on the final publish of the game prior to end-game events. *
+ * *
+ * This file is part of Holocore. *
+ * *
+ * --------------------------------------------------------------------------------*
+ * *
+ * Holocore is free software: you can redistribute it and/or modify *
+ * it under the terms of the GNU Affero General Public License as *
+ * published by the Free Software Foundation, either version 3 of the *
+ * License, or (at your option) any later version. *
+ * *
+ * Holocore is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU Affero General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU Affero General Public License *
+ * along with Holocore. If not, see . *
+ ***********************************************************************************/
+package com.projectswg.holocore.services.gameplay.player.quest
+
+import com.projectswg.common.data.CRC
+import com.projectswg.common.data.encodables.oob.OutOfBandPackage
+import com.projectswg.common.data.encodables.oob.ProsePackage
+import com.projectswg.common.data.encodables.oob.StringId
+import com.projectswg.common.data.swgfile.ClientFactory
+import com.projectswg.common.network.packets.swg.zone.CommPlayerMessage
+import com.projectswg.common.network.packets.swg.zone.PlayMusicMessage
+import com.projectswg.common.network.packets.swg.zone.chat.ChatSystemMessage
+import com.projectswg.common.network.packets.swg.zone.object_controller.quest.QuestCompletedMessage
+import com.projectswg.common.network.packets.swg.zone.object_controller.quest.QuestTaskCounterMessage
+import com.projectswg.holocore.intents.gameplay.combat.CreatureKilledIntent
+import com.projectswg.holocore.intents.gameplay.player.experience.ExperienceIntent
+import com.projectswg.holocore.intents.gameplay.player.quest.AbandonQuestIntent
+import com.projectswg.holocore.intents.gameplay.player.quest.AdvanceQuestIntent
+import com.projectswg.holocore.intents.gameplay.player.quest.CompleteQuestIntent
+import com.projectswg.holocore.intents.gameplay.player.quest.GrantQuestIntent
+import com.projectswg.holocore.intents.gameplay.player.quest.GrantQuestIntent.Companion.broadcast
+import com.projectswg.holocore.intents.support.global.chat.SystemMessageIntent
+import com.projectswg.holocore.intents.support.objects.swg.ObjectCreatedIntent
+import com.projectswg.holocore.resources.support.data.server_info.StandardLog
+import com.projectswg.holocore.resources.support.data.server_info.loader.QuestLoader.QuestTaskInfo
+import com.projectswg.holocore.resources.support.data.server_info.loader.ServerData
+import com.projectswg.holocore.resources.support.global.player.Player
+import com.projectswg.holocore.resources.support.global.zone.sui.SuiButtons
+import com.projectswg.holocore.resources.support.global.zone.sui.SuiMessageBox
+import com.projectswg.holocore.resources.support.objects.ObjectCreator
+import com.projectswg.holocore.resources.support.objects.StaticItemCreator.createItem
+import com.projectswg.holocore.resources.support.objects.swg.SWGObject
+import com.projectswg.holocore.resources.support.objects.swg.custom.AIObject
+import com.projectswg.holocore.resources.support.objects.swg.player.PlayerObject
+import me.joshlarson.jlcommon.concurrency.ScheduledThreadPool
+import me.joshlarson.jlcommon.control.IntentHandler
+import me.joshlarson.jlcommon.control.Service
+import java.util.concurrent.ThreadLocalRandom
+
+class QuestService : Service() {
+ private val executor = ScheduledThreadPool(1, "quest-service-%d")
+ private val questLoader = ServerData.questLoader
+
+ override fun initialize(): Boolean {
+ executor.start()
+ return true
+ }
+
+ override fun terminate(): Boolean {
+ executor.stop()
+ return executor.awaitTermination(1000)
+ }
+
+ @IntentHandler
+ private fun handleGrantQuestIntent(intent: GrantQuestIntent) {
+ val player = intent.player
+ val questName = intent.questName
+ val playerObject = player.getPlayerObject()
+ val questListInfo = questLoader.getQuestListInfo(questName)
+ val repeatable = java.lang.Boolean.TRUE == questListInfo.isRepeatable
+ if (!repeatable && playerObject.isQuestInJournal(questName)) {
+ StandardLog.onPlayerError(this, player, "already had non-repeatable quest %s", questName)
+ return
+ }
+ playerObject.addQuest(questName)
+ playerObject.addActiveQuestTask(questName, 0)
+ StandardLog.onPlayerTrace(this, player, "received quest %s", questName)
+ val currentTasks = getActiveTaskInfos(questName, playerObject)
+ handleTaskEvents(player, questName, currentTasks)
+ val prose = ProsePackage(StringId("quest/ground/system_message", "quest_received"), "TO", questListInfo.journalEntryTitle)
+ SystemMessageIntent.broadcastPersonal(player, prose, ChatSystemMessage.SystemChatType.QUEST)
+ }
+
+ @IntentHandler
+ private fun handleAbandonQuestIntent(intent: AbandonQuestIntent) {
+ val player = intent.player
+ val questName = intent.questName
+ val playerObject = player.getPlayerObject()
+ if (playerObject.isQuestComplete(questName)) {
+ StandardLog.onPlayerTrace(this, player, "attempted to abandon quest %s which they have completed", questName)
+ return
+ }
+ playerObject.removeQuest(questName)
+ }
+
+ @IntentHandler
+ private fun handleCompleteQuestIntent(intent: CompleteQuestIntent) {
+ val player = intent.player
+ val playerObject = player.getPlayerObject()
+ val questName = intent.questName
+ if (playerObject.isQuestRewardReceived(questName)) {
+ StandardLog.onPlayerTrace(this, player, "attempted to claim reward for quest %s but they have already received it", questName)
+ return
+ }
+
+ // TODO award XP, load from tier+level matrix: datatables/quest/quest_experience.iff
+ playerObject.setQuestRewardReceived(questName, true)
+ }
+
+ @IntentHandler
+ private fun handleAdvanceQuestIntent(intent: AdvanceQuestIntent) {
+ val player = intent.player
+ val questName = intent.questName
+ val playerObject = player.getPlayerObject()
+ if (!playerObject.isQuestInJournal(questName)) {
+ StandardLog.onPlayerError(this, player, "advanced quest %s that was not in their quest journal", questName)
+ return
+ }
+ val currentTasks = getActiveTaskInfos(questName, playerObject)
+ advanceQuest(questName, player, currentTasks)
+ }
+
+ @IntentHandler
+ private fun handleCreatureKilledIntent(intent: CreatureKilledIntent) {
+ val corpse = intent.corpse as? AIObject ?: return
+ val killer = intent.killer
+ val owner = killer.owner ?: return
+ val playerObject = killer.playerObject
+ val entries = playerObject.quests.entries
+ for ((key, quest) in entries) {
+ if (quest.isComplete) {
+ continue
+ }
+ val questName = key.string
+ val activeTaskListInfos = getActiveTaskInfos(questName, playerObject)
+ for (activeTaskListInfo in activeTaskListInfos) {
+ val type = activeTaskListInfo.type
+ if ("quest.task.ground.destroy_multi" == type) {
+ if (isKillPartOfTask(activeTaskListInfo, corpse)) {
+ val max = activeTaskListInfo.count
+ val counter = playerObject.incrementQuestCounter(questName)
+ val remaining = max - counter
+ val task = activeTaskListInfo.index
+ StandardLog.onPlayerTrace(this, owner, "%d remaining kills required on quest %s", remaining, questName)
+ incrementKillCount(questName, task, owner, counter, max)
+ if (remaining <= 0) {
+ advanceQuest(questName, owner, activeTaskListInfos)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private fun isKillPartOfTask(swgQuestTask: QuestTaskInfo, npcCorpse: AIObject): Boolean {
+ val targetServerTemplate = swgQuestTask.targetServerTemplate
+ val spawner = npcCorpse.spawner
+ val stfName = spawner.stfName
+ val questSocialGroup = swgQuestTask.socialGroup
+ val npcSocialGroup = npcCorpse.spawner.socialGroup
+ return isMatchingSocialGroup(questSocialGroup, npcSocialGroup) || isMatchingServerTemplate(targetServerTemplate, stfName)
+ }
+
+ private fun incrementKillCount(questName: String, task: Int, player: Player, counter: Int, max: Int) {
+ player.sendPacket(
+ QuestTaskCounterMessage(
+ player.creatureObject.objectId,
+ questName,
+ task,
+ "@quest/groundquests:destroy_counter",
+ counter,
+ max
+ )
+ )
+ val remaining = max - counter
+ val prose = ProsePackage(StringId("quest/groundquests", "destroy_multiple_success"), "DI", remaining)
+ SystemMessageIntent.broadcastPersonal(player, prose)
+ }
+
+ private fun handleTaskEvents(player: Player, questName: String, currentTasks: Collection) {
+ val playerObject = player.getPlayerObject()
+ for (currentTask in currentTasks) {
+ val type = currentTask.type ?: continue
+ if (currentTask.isVisible) {
+ player.sendPacket(PlayMusicMessage(0, "sound/ui_journal_updated.snd", 1, false))
+ }
+ when (type) {
+ "quest.task.ground.comm_player" -> handleCommPlayer(player, questName, playerObject, currentTask)
+ "quest.task.ground.complete_quest" -> completeQuest(player, questName)
+ "quest.task.ground.timer" -> handleTimer(player, questName, playerObject, currentTask)
+ "quest.task.ground.show_message_box" -> handleShowMessageBox(player, questName, playerObject, currentTask)
+ "quest.task.ground.destroy_multi" -> handleDestroyMulti(player, questName, currentTask)
+ "quest.task.ground.reward" -> handleReward(player, questName, playerObject, currentTask)
+ }
+ }
+ }
+
+ private fun handleReward(player: Player, questName: String, playerObject: PlayerObject, currentTask: QuestTaskInfo) {
+ grantXPReward(player, currentTask)
+ grantFactionPointsReward(playerObject, currentTask)
+ grantCreditsReward(player, currentTask)
+ grantLootRewards(player, currentTask)
+ grantItemRewards(player, currentTask)
+ // weapon stuff: weapon count_weapon speed damage efficiency elemental_value
+ // armor stuff: armor count_armor quality
+ // speed etc. are percentages, the absolute values we must define somewhere else before we can hand out weapons and armor rewards
+ playerObject.removeActiveQuestTask(questName, currentTask.index)
+ playerObject.addCompleteQuestTask(questName, currentTask.index)
+ currentTask.nextTasksOnComplete.forEach { playerObject.addActiveQuestTask(questName, it) }
+ val taskListInfos = questLoader.getTaskListInfos(questName)
+ val nextTasksOnComplete = currentTask.nextTasksOnComplete
+ val nextTasks = mapActiveTasks(nextTasksOnComplete, taskListInfos)
+ handleTaskEvents(player, questName, nextTasks)
+ }
+
+ private fun handleShowMessageBox(player: Player, questName: String, playerObject: PlayerObject, currentTask: QuestTaskInfo) {
+ val messageBoxTitle = currentTask.messageBoxTitle
+ val messageBoxText = currentTask.messageBoxText
+ val sui = SuiMessageBox(SuiButtons.OK, messageBoxTitle, messageBoxText)
+ sui.setSize(384, 256)
+ sui.setLocation(320, 256)
+ sui.display(player)
+ playerObject.removeActiveQuestTask(questName, currentTask.index)
+ playerObject.addCompleteQuestTask(questName, currentTask.index)
+ currentTask.nextTasksOnComplete.forEach { playerObject.addActiveQuestTask(questName, it) }
+ val taskListInfos = questLoader.getTaskListInfos(questName)
+ val nextTasksOnComplete = currentTask.nextTasksOnComplete
+ val nextTasks = mapActiveTasks(nextTasksOnComplete, taskListInfos)
+ handleTaskEvents(player, questName, nextTasks)
+ }
+
+ private fun handleCommPlayer(player: Player, questName: String, playerObject: PlayerObject, currentTask: QuestTaskInfo) {
+ val commMessageText = currentTask.commMessageText
+ val message = OutOfBandPackage(ProsePackage(StringId(commMessageText)))
+ val objectId = player.creatureObject.objectId
+ val modelCrc = getModelCrc(currentTask)
+ player.sendPacket(CommPlayerMessage(objectId, message, modelCrc, "", 10f))
+ playerObject.removeActiveQuestTask(questName, currentTask.index)
+ playerObject.addCompleteQuestTask(questName, currentTask.index)
+ currentTask.nextTasksOnComplete.forEach { playerObject.addActiveQuestTask(questName, it) }
+ val taskListInfos = questLoader.getTaskListInfos(questName)
+ val nextTasksOnComplete = currentTask.nextTasksOnComplete
+ val nextTasks = mapActiveTasks(nextTasksOnComplete, taskListInfos)
+ handleTaskEvents(player, questName, nextTasks)
+ }
+
+ private fun handleTimer(player: Player, questName: String, playerObject: PlayerObject, currentTask: QuestTaskInfo) {
+ val minTime = currentTask.minTime
+ val maxTime = currentTask.maxTime
+ val random = ThreadLocalRandom.current()
+ val delay = random.nextInt(minTime, maxTime) * 1000
+ executor.execute(delay.toLong()) {
+ playerObject.removeActiveQuestTask(questName, currentTask.index)
+ playerObject.addCompleteQuestTask(questName, currentTask.index)
+ val taskListInfos = questLoader.getTaskListInfos(questName)
+ val nextTasksOnComplete = currentTask.nextTasksOnComplete
+ val nextTasks = mapActiveTasks(nextTasksOnComplete, taskListInfos)
+ handleTaskEvents(player, questName, nextTasks)
+ }
+ }
+
+ private fun advanceQuest(questName: String, player: Player, currentTasks: List) {
+ val playerObject = player.getPlayerObject()
+ for (currentTask in currentTasks) {
+ playerObject.removeActiveQuestTask(questName, currentTask.index)
+ playerObject.addCompleteQuestTask(questName, currentTask.index)
+ val nextTasksOnComplete = currentTask.nextTasksOnComplete
+ for (nextTaskOnComplete in nextTasksOnComplete) {
+ playerObject.addActiveQuestTask(questName, nextTaskOnComplete)
+ StandardLog.onPlayerTrace(this, player, "advanced quest %s, activated task %d", questName, nextTaskOnComplete)
+ }
+ }
+ var nextTasks = getActiveTaskInfos(questName, playerObject)
+ if (nextTasks.isEmpty()) {
+ val questListInfo = questLoader.getQuestListInfo(questName)
+ val completeWhenTasksComplete = questListInfo.isCompleteWhenTasksComplete
+ if (completeWhenTasksComplete) {
+ completeQuest(player, questName)
+ nextTasks = currentTasks
+ }
+ }
+ handleTaskEvents(player, questName, nextTasks)
+ for (nextTask in nextTasks) {
+ val grantQuestOnComplete = nextTask.grantQuestOnComplete
+ if (grantQuestOnComplete != null && grantQuestOnComplete.isNotBlank()) {
+ broadcast(player, grantQuestOnComplete)
+ }
+ }
+ }
+
+ private fun completeQuest(player: Player, questName: String) {
+ val playerObject = player.getPlayerObject()
+ playerObject.completeQuest(questName)
+ player.sendPacket(QuestCompletedMessage(player.creatureObject.objectId, CRC(questName)))
+ StandardLog.onPlayerTrace(this, player, "completed quest %s", questName)
+ }
+
+ private fun getActiveTaskInfos(questName: String, playerObject: PlayerObject): List {
+ val taskListInfos = questLoader.getTaskListInfos(questName)
+ val questActiveTasks = playerObject.getQuestActiveTasks(questName)
+ return mapActiveTasks(questActiveTasks, taskListInfos)
+ }
+
+ private fun mapActiveTasks(activeTaskIndices: Collection, taskListInfos: List): List {
+ return activeTaskIndices.map { taskListInfos[it] }
+ }
+
+ private fun isMatchingServerTemplate(targetServerTemplate: String?, stfName: String): Boolean {
+ return targetServerTemplate != null && targetServerTemplate == stfName
+ }
+
+ private fun isMatchingSocialGroup(questSocialGroup: String?, npcSocialGroup: String?): Boolean {
+ return questSocialGroup != null && questSocialGroup.equals(npcSocialGroup, ignoreCase = true)
+ }
+
+ private fun grantXPReward(player: Player, currentTask: QuestTaskInfo) {
+ val experienceType = currentTask.experienceType
+ if (experienceType != null) {
+ ExperienceIntent(player.creatureObject, experienceType, currentTask.experienceAmount).broadcast()
+ }
+ }
+
+ private fun grantFactionPointsReward(playerObject: PlayerObject, currentTask: QuestTaskInfo) {
+ val factionName = currentTask.factionName
+ if (factionName != null) {
+ playerObject.adjustFactionPoints(factionName, currentTask.factionAmount)
+ }
+ }
+
+ private fun grantCreditsReward(player: Player, currentTask: QuestTaskInfo) {
+ val bankCredits = currentTask.bankCredits
+ if (bankCredits != 0) {
+ player.creatureObject.addToBank(bankCredits.toLong())
+ }
+ }
+
+ private fun grantLootRewards(player: Player, currentTask: QuestTaskInfo) {
+ val lootCount = currentTask.lootCount
+ if (lootCount > 0) {
+ for (i in 0 until lootCount) {
+ val lootName = currentTask.lootName
+ if (lootName != null && lootName.isNotBlank()) {
+ val item = createItem(lootName)
+ if (item != null) {
+ transferItemToInventory(player, item)
+ }
+ }
+ }
+ }
+ }
+
+ private fun grantItemRewards(player: Player, currentTask: QuestTaskInfo) {
+ val itemCount = currentTask.itemCount
+ if (itemCount > 0) {
+ for (i in 0 until itemCount) {
+ val itemTemplate = currentTask.itemTemplate
+ if (itemTemplate != null && itemTemplate.isNotBlank()) {
+ val item = ObjectCreator.createObjectFromTemplate(itemTemplate)
+ transferItemToInventory(player, item)
+ }
+ }
+ }
+ }
+
+ private fun transferItemToInventory(player: Player, item: SWGObject) {
+ val inventory = player.creatureObject.getInventory()
+ item.moveToContainer(inventory)
+ ObjectCreatedIntent.broadcast(item)
+ SystemMessageIntent.broadcastPersonal(player,
+ ProsePackage(StringId("quest/ground/system_message", "placed_in_inventory"), "TO", item.stringId))
+ }
+
+ private fun handleDestroyMulti(player: Player, questName: String, currentTask: QuestTaskInfo) {
+ val task = currentTask.index
+ val max = currentTask.count
+ val counter = 0
+ player.sendPacket(
+ QuestTaskCounterMessage(
+ player.creatureObject.objectId,
+ questName,
+ task,
+ "@quest/groundquests:destroy_counter",
+ counter,
+ max
+ )
+ )
+ }
+
+ private fun getModelCrc(currentTask: QuestTaskInfo): CRC {
+ val npcAppearanceServerTemplate = currentTask.npcAppearanceServerTemplate
+ if (npcAppearanceServerTemplate != null && npcAppearanceServerTemplate.isNotBlank()) {
+ val sharedTemplate = ClientFactory.formatToSharedFile(npcAppearanceServerTemplate)
+ return CRC(sharedTemplate)
+ }
+ return CRC(0) // Fallback case, as some tasks don't have an appearance set. The player sees their own character in the comm window.
+ }
+}