Merge branch 'ProjectSWGCore:master' into master

This commit is contained in:
Undercova
2023-12-06 01:35:33 +01:00
committed by GitHub
2 changed files with 411 additions and 507 deletions
@@ -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 <http://www.gnu.org/licenses/>. *
***********************************************************************************/
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<QuestLoader.QuestTaskInfo> 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<QuestLoader.QuestTaskInfo> 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<Map.Entry<CRC, Quest>> entries = playerObject.getQuests().entrySet();
for (Map.Entry<CRC, Quest> entry : entries) {
CRC key = entry.getKey();
Quest quest = entry.getValue();
if (quest.isComplete()) {
continue;
}
String questName = key.getString();
List<QuestLoader.QuestTaskInfo> 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<QuestLoader.QuestTaskInfo> 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<QuestLoader.QuestTaskInfo> taskListInfos = questLoader.getTaskListInfos(questName);
Collection<Integer> nextTasksOnComplete = currentTask.getNextTasksOnComplete();
List<QuestLoader.QuestTaskInfo> 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<QuestLoader.QuestTaskInfo> taskListInfos = questLoader.getTaskListInfos(questName);
Collection<Integer> nextTasksOnComplete = currentTask.getNextTasksOnComplete();
List<QuestLoader.QuestTaskInfo> 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<QuestLoader.QuestTaskInfo> taskListInfos = questLoader.getTaskListInfos(questName);
Collection<Integer> nextTasksOnComplete = currentTask.getNextTasksOnComplete();
List<QuestLoader.QuestTaskInfo> 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<QuestLoader.QuestTaskInfo> taskListInfos = questLoader.getTaskListInfos(questName);
Collection<Integer> nextTasksOnComplete = currentTask.getNextTasksOnComplete();
List<QuestLoader.QuestTaskInfo> nextTasks = mapActiveTasks(nextTasksOnComplete, taskListInfos);
handleTaskEvents(player, questName, nextTasks);
});
}
private void advanceQuest(String questName, Player player, List<QuestLoader.QuestTaskInfo> currentTasks) {
PlayerObject playerObject = player.getPlayerObject();
for (QuestLoader.QuestTaskInfo currentTask : currentTasks) {
playerObject.removeActiveQuestTask(questName, currentTask.getIndex());
playerObject.addCompleteQuestTask(questName, currentTask.getIndex());
Collection<Integer> nextTasksOnComplete = currentTask.getNextTasksOnComplete();
for (Integer nextTaskOnComplete : nextTasksOnComplete) {
playerObject.addActiveQuestTask(questName, nextTaskOnComplete);
StandardLog.onPlayerTrace(this, player, "advanced quest %s, activated task %d", questName, nextTaskOnComplete);
}
}
List<QuestLoader.QuestTaskInfo> 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<QuestLoader.QuestTaskInfo> getActiveTaskInfos(String questName, PlayerObject playerObject) {
List<QuestLoader.QuestTaskInfo> taskListInfos = questLoader.getTaskListInfos(questName);
Collection<Integer> questActiveTasks = playerObject.getQuestActiveTasks(questName);
return mapActiveTasks(questActiveTasks, taskListInfos);
}
private List<QuestLoader.QuestTaskInfo> mapActiveTasks(Collection<Integer> activeTaskIndices, List<QuestLoader.QuestTaskInfo> taskListInfos) {
return activeTaskIndices.stream()
.map(taskListInfos::get)
.collect(Collectors.toList());
}
}
@@ -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 <http://www.gnu.org/licenses/>. *
***********************************************************************************/
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<QuestTaskInfo>) {
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<QuestTaskInfo>) {
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<QuestTaskInfo> {
val taskListInfos = questLoader.getTaskListInfos(questName)
val questActiveTasks = playerObject.getQuestActiveTasks(questName)
return mapActiveTasks(questActiveTasks, taskListInfos)
}
private fun mapActiveTasks(activeTaskIndices: Collection<Int>, taskListInfos: List<QuestTaskInfo>): List<QuestTaskInfo> {
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.
}
}