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. + } +}