#include "StarQuests.hpp" #include "StarJsonExtra.hpp" #include "StarFile.hpp" #include "StarRoot.hpp" #include "StarAssets.hpp" #include "StarTime.hpp" #include "StarRandom.hpp" #include "StarItemDatabase.hpp" #include "StarItemDrop.hpp" #include "StarMonster.hpp" #include "StarNpc.hpp" #include "StarObjectDatabase.hpp" #include "StarObject.hpp" #include "StarPlayer.hpp" #include "StarPlayerInventory.hpp" #include "StarPlayerTech.hpp" #include "StarConfigLuaBindings.hpp" #include "StarEntityLuaBindings.hpp" #include "StarPlayerLuaBindings.hpp" #include "StarStatusControllerLuaBindings.hpp" #include "StarQuestManager.hpp" #include "StarClientContext.hpp" #include "StarUuid.hpp" #include "StarCelestialLuaBindings.hpp" namespace Star { EnumMap const QuestStateNames { {QuestState::New, "New"}, {QuestState::Offer, "Offer"}, {QuestState::Active, "Active"}, {QuestState::Complete, "Complete"}, {QuestState::Failed, "Failed"} }; Quest::Quest(QuestArcDescriptor const& questArc, size_t arcPos, Player* player) { m_trackedIndicator = Root::singleton().assets()->json("/quests/quests.config:trackedCustomIndicator").toString(); m_untrackedIndicator = Root::singleton().assets()->json("/quests/quests.config:untrackedCustomIndicator").toString(); m_money = 0; m_unread = true; m_canTurnIn = false; m_lastUpdatedOn = Time::monotonicMilliseconds(); m_arc = questArc; m_arcPos = arcPos; auto itemDatabase = Root::singleton().itemDatabase(); auto templateDatabase = Root::singleton().questTemplateDatabase(); auto questTemplate = templateDatabase->questTemplate(templateId()); m_parameters = questDescriptor().parameters; m_displayParameters = DisplayParameters { questTemplate->ephemeral, questTemplate->showInLog, questTemplate->showAcceptDialog, questTemplate->showCompleteDialog, questTemplate->showFailDialog, questTemplate->mainQuest, questTemplate->hideCrossServer }; setEntityParameter("player", player); m_money = Random::randUInt(questTemplate->moneyRange[0], questTemplate->moneyRange[1]); m_rewards = Random::randValueFrom(questTemplate->rewards, {}).transformed( [&itemDatabase](ItemDescriptor const& item) -> ItemConstPtr { return itemDatabase->item(item); }); for (String const& rewardParamName : questTemplate->rewardParameters) { if (!m_parameters.contains(rewardParamName)) continue; QuestParam const& rewardParam = m_parameters[rewardParamName]; if (rewardParam.detail.is()) { addReward(rewardParam.detail.get().descriptor()); } else { if (!rewardParam.detail.is()) throw StarException::format("Quest parameter {} cannot be used as a reward parameter", rewardParamName); for (auto item : rewardParam.detail.get()) addReward(item); } } m_title = questTemplate->title; m_text = questTemplate->text; m_completionText = questTemplate->completionText; m_failureText = questTemplate->failureText; m_state = QuestState::New; m_showDialog = false; m_player = nullptr; m_world = nullptr; m_inited = false; } Quest::Quest(Json const& spec) { m_trackedIndicator = Root::singleton().assets()->json("/quests/quests.config:trackedCustomIndicator").toString(); m_untrackedIndicator = Root::singleton().assets()->json("/quests/quests.config:untrackedCustomIndicator").toString(); auto versioningDatabase = Root::singleton().versioningDatabase(); Json diskStore = versioningDatabase->loadVersionedJson(VersionedJson::fromJson(spec), "Quest"); m_state = QuestStateNames.getLeft(diskStore.getString("state")); m_arc = QuestArcDescriptor::diskLoad(diskStore.get("arc")); m_arcPos = diskStore.getUInt("arcPos"); m_parameters = questParamsDiskLoad(diskStore.get("parameters")); m_worldId = diskStore.optString("worldId").apply(parseWorldId); m_location = diskStore.opt("location").apply([](Json const& json) { return make_pair(jsonToVec3I(json.get("system")), jsonToSystemLocation(json.get("location"))); }); m_serverUuid = diskStore.optString("serverUuid").apply(construct()); m_money = diskStore.getUInt("money"); auto itemDatabase = Root::singleton().itemDatabase(); m_rewards = diskStore.getArray("rewards").transformed( [&itemDatabase](Json const& json) -> ItemConstPtr { return itemDatabase->diskLoad(json); }); m_lastUpdatedOn = diskStore.getInt("lastUpdatedOn"); m_unread = diskStore.getBool("unread", true); m_canTurnIn = diskStore.getBool("canTurnIn", false); m_indicators = jsonToStringSet(diskStore.get("indicators", JsonArray{})); m_scriptComponent.setScriptStorage(diskStore.getObject("scriptStorage", JsonObject{})); auto templateDatabase = Root::singleton().questTemplateDatabase(); auto questTemplate = templateDatabase->questTemplate(templateId()); m_displayParameters = DisplayParameters{ questTemplate->ephemeral, questTemplate->showInLog, questTemplate->showAcceptDialog, questTemplate->showCompleteDialog, questTemplate->showFailDialog, questTemplate->mainQuest, questTemplate->hideCrossServer }; m_title = diskStore.getString("title", questTemplate->title); m_text = diskStore.getString("text", questTemplate->text); m_completionText = diskStore.getString("completionText", questTemplate->completionText); m_failureText = diskStore.getString("failureText", questTemplate->failureText); m_portraits = jsonToMapV>>(diskStore.get("portraits", JsonObject{}), [](Json const& portrait) { return portrait.toArray().transformed(construct()); }); m_portraitTitles = jsonToMapV>(diskStore.get("portraitTitles", JsonObject{}), mem_fn(&Json::toString)); m_showDialog = diskStore.getBool("showDialog", false); m_player = nullptr; m_world = nullptr; m_inited = false; } Json Quest::diskStore() const { auto versioningDatabase = Root::singleton().versioningDatabase(); JsonObject result; result["state"] = QuestStateNames.getRight(m_state); result["arc"] = m_arc.diskStore(); result["arcPos"] = m_arcPos; result["parameters"] = questParamsDiskStore(m_parameters); result["money"] = m_money; result["worldId"] = jsonFromMaybe(m_worldId.apply(printWorldId)); result["location"] = jsonFromMaybe(m_location.apply([](pair const& location) { return JsonObject{{"system", jsonFromVec3I(location.first)}, {"location", jsonFromSystemLocation(location.second)}}; })); result["serverUuid"] = jsonFromMaybe(m_serverUuid.apply(mem_fn(&Uuid::hex))); auto itemDatabase = Root::singleton().itemDatabase(); result["rewards"] = m_rewards.transformed([&itemDatabase](ItemConstPtr const& item) { return itemDatabase->diskStore(item); }); result["lastUpdatedOn"] = m_lastUpdatedOn; result["unread"] = m_unread; result["canTurnIn"] = m_canTurnIn; result["indicators"] = jsonFromStringSet(m_indicators); result["scriptStorage"] = m_scriptComponent.getScriptStorage(); result["title"] = m_title; result["text"] = m_text; result["completionText"] = m_completionText; result["failureText"] = m_failureText; result["portraits"] = jsonFromMapV(m_portraits, [](List const& portrait) { return portrait.transformed(mem_fn(&Drawable::toJson)); }); result["portraitTitles"] = jsonFromMap(m_portraitTitles); result["showDialog"] = m_showDialog; return versioningDatabase->makeCurrentVersionedJson("Quest", result).toJson(); } QuestTemplatePtr Quest::getTemplate() const { return Root::singleton().questTemplateDatabase()->questTemplate(templateId()); } void Quest::init(Player* player, World* world, UniverseClient* client) { m_player = player; m_world = world; m_client = client; if (m_state == QuestState::Offer || m_state == QuestState::Active) initScript(); } void Quest::uninit() { if (m_inited) uninitScript(); m_player = nullptr; m_world = nullptr; } Maybe Quest::receiveMessage(String const& message, bool localMessage, JsonArray const& args) { if (!m_inited) return {}; return m_scriptComponent.handleMessage(message, localMessage, args); } void Quest::update(float dt) { if (!m_inited) return; m_scriptComponent.update(m_scriptComponent.updateDt(dt)); } void Quest::offer() { starAssert(m_player && m_world); if (!showAcceptDialog()) { start(); } else { setState(QuestState::Offer); initScript(); m_scriptComponent.invoke("questOffer"); } } void Quest::declineOffer() { setState(QuestState::New); m_scriptComponent.invoke("questDecline"); uninitScript(); } void Quest::cancelOffer() { setState(QuestState::New); m_scriptComponent.invoke("questCancel"); uninitScript(); } void Quest::start() { setState(QuestState::Active); initScript(); auto current = m_player->questManager()->trackedQuest(); if (mainQuest() || !current) m_player->questManager()->setAsTracked(questId()); m_scriptComponent.invoke("questStart"); } void Quest::complete(Maybe followupIndex) { setState(QuestState::Complete); m_showDialog = showCompleteDialog(); m_scriptComponent.invoke("questComplete"); uninitScript(); // Grant reward items and money for (auto const& item : rewards()) m_player->giveItem(item->clone()); m_player->inventory()->addCurrency("money", money()); // Offer follow-up quests bool trackNewQuest = m_player->questManager()->isTracked(questId()); size_t nextArcPos = followupIndex.value(questArcPosition() + 1); if (nextArcPos < m_arc.quests.size()) { auto followUp = make_shared(m_arc, nextArcPos, m_player); followUp->setWorldId(worldId()); followUp->setLocation(location()); followUp->setServerUuid(serverUuid()); m_player->questManager()->offer(followUp); if (trackNewQuest) m_player->questManager()->setAsTracked(followUp->questId()); } else if (trackNewQuest) { // no followup, track another main quest or clear quest tracker if (auto main = m_player->questManager()->getFirstMainQuest()) m_player->questManager()->setAsTracked(main.value()->questId()); else m_player->questManager()->setAsTracked({}); } } void Quest::fail() { setState(QuestState::Failed); m_showDialog = showFailDialog(); m_scriptComponent.invoke("questFail", false); uninitScript(); } void Quest::abandon() { setState(QuestState::Failed); m_showDialog = false; m_scriptComponent.invoke("questFail", true); uninitScript(); } bool Quest::interactWithEntity(EntityId entity) { Maybe result = m_scriptComponent.invoke("questInteract", entity); return result && result.value(); } String Quest::questId() const { return questDescriptor().questId; } String Quest::templateId() const { return questDescriptor().templateId; } StringMap const& Quest::parameters() const { return m_parameters; } QuestState Quest::state() const { return m_state; } bool Quest::showDialog() const { return m_showDialog; } void Quest::setDialogShown() { m_showDialog = false; } void Quest::setEntityParameter(String const& paramName, EntityConstPtr const& entity) { setEntityParameter(paramName, entity.get()); } void Quest::setParameter(String const& paramName, QuestParam const& paramValue) { m_parameters[paramName] = paramValue; } Maybe> Quest::portrait(String const& portraitName) const { return m_portraits.maybe(portraitName); } Maybe Quest::portraitTitle(String const& portraitName) const { return m_portraitTitles.maybe(portraitName); } QuestDescriptor Quest::questDescriptor() const { return m_arc.quests[m_arcPos]; } QuestArcDescriptor Quest::questArcDescriptor() const { return m_arc; } size_t Quest::questArcPosition() const { return m_arcPos; } Maybe Quest::worldId() const { return m_worldId; } Maybe> Quest::location() const { return m_location; } Maybe Quest::serverUuid() const { return m_serverUuid; } void Quest::setWorldId(Maybe worldId) { m_worldId = worldId; } void Quest::setLocation(Maybe> location) { m_location = std::move(location); } void Quest::setServerUuid(Maybe serverUuid) { m_serverUuid = serverUuid; } String Quest::title() const { return m_title; } String Quest::text() const { return m_text; } String Quest::completionText() const { return m_completionText; } String Quest::failureText() const { return m_failureText; } size_t Quest::money() const { return m_money; } List Quest::rewards() const { return m_rewards; } int64_t Quest::lastUpdatedOn() const { return m_lastUpdatedOn; } bool Quest::unread() const { return m_unread; } void Quest::markAsRead() { m_unread = false; } bool Quest::canTurnIn() const { return m_canTurnIn; } String Quest::questGiverIndicator() const { return getTemplate()->questGiverIndicator; } String Quest::questReceiverIndicator() const { return getTemplate()->questReceiverIndicator; } bool hasItemIndicator(EntityPtr const& entity, List indicatedItems) { if (auto itemDrop = as(entity)) { for (auto const& itemDesc : indicatedItems) { if (itemDrop->item()->matches(itemDesc, true)) { return true; } } } else if (auto object = as(entity)) { ObjectConfigPtr objectConfig = Root::singleton().objectDatabase()->getConfig(object->name()); if (!objectConfig->hasObjectItem) return false; for (auto const& itemDesc : indicatedItems) { if (object->name() == itemDesc.name()) return true; } } return false; } Maybe Quest::customIndicator(EntityPtr const& entity) const { if (!m_inited) return {}; for (String const& indicator : m_indicators) { auto param = parameters().get(indicator); if (param.detail.is()) { QuestEntity questEntity = param.detail.get(); if (questEntity.uniqueId && entity->uniqueId() == questEntity.uniqueId) { return param.indicator.value(defaultCustomIndicator()); } } else if (param.detail.is()) { QuestItem questItem = param.detail.get(); if (hasItemIndicator(entity, {questItem.descriptor()})) return param.indicator.value(defaultCustomIndicator()); } else if (param.detail.is()) { String questItemTag = param.detail.get(); if (auto itemDrop = as(entity)) { if (itemDrop->item()->itemTags().contains(questItemTag)) return param.indicator.value(defaultCustomIndicator()); } } else if (param.detail.is()) { QuestItemList questItemList = param.detail.get(); if (hasItemIndicator(entity, questItemList)) return param.indicator.value(defaultCustomIndicator()); } else if (param.detail.is()) { QuestMonsterType questMonsterType = param.detail.get(); if (auto monster = as(entity)) { if (monster->typeName() == questMonsterType.typeName) { TeamType team = monster->getTeam().type; if (team == TeamType::Enemy || team == TeamType::Passive) return param.indicator.value(defaultCustomIndicator()); } } } } return {}; } Maybe Quest::objectiveList() const { return m_objectiveList; } Maybe Quest::progress() const { return m_progress; } Maybe Quest::compassDirection() const { return m_compassDirection; } void Quest::setObjectiveList(Maybe const& objectiveList) { m_objectiveList = objectiveList; } void Quest::setProgress(Maybe const& progress) { m_progress = progress; } void Quest::setCompassDirection(Maybe const& compassDirection) { m_compassDirection = compassDirection; } Maybe Quest::completionCinema() const { return getTemplate()->completionCinema; } bool Quest::canBeAbandoned() const { return getTemplate()->canBeAbandoned; } bool Quest::ephemeral() const { return m_displayParameters.ephemeral; } bool Quest::showInLog() const { return m_displayParameters.showInLog; } bool Quest::showAcceptDialog() const { return m_displayParameters.showAcceptDialog; } bool Quest::showCompleteDialog() const { return m_displayParameters.showCompleteDialog; } bool Quest::showFailDialog() const { return m_displayParameters.showFailDialog; } bool Quest::mainQuest() const { return m_displayParameters.mainQuest; } bool Quest::hideCrossServer() const { return m_displayParameters.hideCrossServer; } void Quest::setState(QuestState state) { m_state = state; m_lastUpdatedOn = Time::monotonicMilliseconds(); } void Quest::initScript() { if (!m_player || !m_world || m_inited) return; auto questTemplate = getTemplate(); if (questTemplate->script.isValid()) m_scriptComponent.setScript(*questTemplate->script); else m_scriptComponent.setScripts(StringList{}); m_scriptComponent.setUpdateDelta(questTemplate->updateDelta); m_scriptComponent.addCallbacks("quest", makeQuestCallbacks(m_player)); m_scriptComponent.addCallbacks("celestial", LuaBindings::makeCelestialCallbacks(m_client)); m_scriptComponent.addCallbacks("player", LuaBindings::makePlayerCallbacks(m_player)); m_scriptComponent.addCallbacks("config", LuaBindings::makeConfigCallbacks([this](String const& name, Json const& def) { return Json(getTemplate()->scriptConfig).query(name, def); })); m_scriptComponent.addCallbacks("entity", LuaBindings::makeEntityCallbacks(m_player)); m_scriptComponent.addCallbacks("status", LuaBindings::makeStatusControllerCallbacks(m_player->statusController())); m_scriptComponent.addActorMovementCallbacks(m_player->movementController()); m_inited = true; m_scriptComponent.init(m_world); } void Quest::uninitScript() { m_scriptComponent.uninit(); m_scriptComponent.removeCallbacks("quest"); m_scriptComponent.removeCallbacks("celestial"); m_scriptComponent.removeCallbacks("player"); m_scriptComponent.removeCallbacks("config"); m_scriptComponent.removeCallbacks("entity"); m_scriptComponent.removeCallbacks("status"); m_scriptComponent.removeActorMovementCallbacks(); m_inited = false; } LuaCallbacks Quest::makeQuestCallbacks(Player* player) { LuaCallbacks callbacks; callbacks.registerCallback("state", [this]() { return QuestStateNames.getRight(state()); }); callbacks.registerCallback("complete", [this](Maybe followup) { complete(followup); }); callbacks.registerCallback("fail", [this]() { fail(); }); callbacks.registerCallback("setCanTurnIn", [this](bool value) { this->m_canTurnIn = value; }); callbacks.registerCallback("questId", [this]() { return questId(); }); callbacks.registerCallback("templateId", [this]() { return templateId(); }); callbacks.registerCallback("seed", [this]() { return questDescriptor().seed; }); callbacks.registerCallback("questDescriptor", [this]() { return questDescriptor().toJson(); }); callbacks.registerCallback("questArcDescriptor", [this]() { return questArcDescriptor().toJson(); }); callbacks.registerCallback("questArcPosition", [this]() { return m_arcPos; }); callbacks.registerCallback("worldId", [this]() { return worldId().apply(printWorldId); }); callbacks.registerCallback("setWorldId", [this](Maybe const& worldId) { setWorldId(worldId.apply(parseWorldId)); }); callbacks.registerCallback("serverUuid", [this]() { return serverUuid().apply(mem_fn(&Uuid::hex)); }); callbacks.registerCallback("setServerUuid", [this](String const& serverUuid) { setServerUuid(Uuid(serverUuid)); }); callbacks.registerCallback("isCurrent", [this, player]() -> bool { return player->questManager()->isCurrent(questId()); }); callbacks.registerCallback("location", [this]() -> Json { if (auto loc = location()) { return JsonObject{ {"system", jsonFromVec3I(loc.get().first)}, {"location", jsonFromSystemLocation(loc.get().second)} }; } return {}; }); callbacks.registerCallback("setLocation", [this](Json const& json) { if (json.isNull()) { setLocation({}); } else { Vec3I system = jsonToVec3I(json.get("system")); SystemLocation location = jsonToSystemLocation(json.opt("location").value({})); setLocation(make_pair(system, location)); } }); callbacks.registerCallback("parameters", [this]() { return questParamsToJson(parameters()); }); callbacks.registerCallback("setParameter", [this](String const& name, Json paramJson) { m_parameters[name] = QuestParam::fromJson(paramJson); }); callbacks.registerCallback( "setIndicators", [this](StringList indicators) { m_indicators = StringSet::from(indicators); }); callbacks.registerCallbackWithSignature>("setObjectiveList", bind(&Quest::setObjectiveList, this, _1)); callbacks.registerCallbackWithSignature>("setProgress", bind(&Quest::setProgress, this, _1)); callbacks.registerCallbackWithSignature>("setCompassDirection", bind(&Quest::setCompassDirection, this, _1)); callbacks.registerCallbackWithSignature("setTitle", [this](String const& title) { m_title = title; }); callbacks.registerCallbackWithSignature("setText", [this](String const& text) { m_text = text; }); callbacks.registerCallbackWithSignature("setCompletionText", [this](String const& completionText) { m_completionText = completionText; }); callbacks.registerCallbackWithSignature("setFailureText", [this](String const& failureText) { m_failureText = failureText; }); callbacks.registerCallbackWithSignature>("setPortrait", [this](String const& portraitName, Maybe const& portrait) { if (portrait) { m_portraits[portraitName] = portrait->transformed(construct()); } else { m_portraits.remove(portraitName); } }); callbacks.registerCallbackWithSignature>("setPortraitTitle", [this](String const& portraitName, Maybe const& portrait) { if (portrait) { m_portraitTitles[portraitName] = *portrait; } else { m_portraitTitles.remove(portraitName); } }); callbacks.registerCallbackWithSignature("addReward", [this](Json const& reward) { addReward(ItemDescriptor(reward)); }); return callbacks; } void Quest::setEntityParameter(String const& paramName, Entity const* entity) { Maybe portrait = {}; Maybe name = {}; Maybe species = {}; Maybe gender = {}; if (auto portraitEntity = as(entity)) { portrait = Json{portraitEntity->portrait(PortraitMode::Full).transformed(mem_fn(&Drawable::toJson))}; name = portraitEntity->name(); } if (auto npc = as(entity)) { species = npc->species(); gender = npc->gender(); } else if (auto player = as(entity)) { species = player->species(); gender = player->gender(); } m_parameters[paramName] = QuestParam{QuestEntity{entity->uniqueId(), species, gender}, name, portrait, {}}; } void Quest::addReward(ItemDescriptor const& reward) { if (reward.name() == "money") { m_money += reward.count(); return; } auto itemDatabase = Root::singleton().itemDatabase(); m_rewards.append(itemDatabase->item(reward)); } String const& Quest::defaultCustomIndicator() const { return m_player->questManager()->isCurrent(questId()) ? m_trackedIndicator : m_untrackedIndicator; } QuestPtr createPreviewQuest( String const& templateId, String const& position, String const& questGiverSpecies, Player* player) { auto questTemplates = Root::singleton().questTemplateDatabase(); auto questTemplate = questTemplates->questTemplate(templateId); if (!questTemplate) return {}; Json portrait = player->portrait(PortraitMode::Full).transformed(mem_fn(&Drawable::toJson)); JsonObject paramJson = questTemplate->parameterExamples; for (auto const& paramName : paramJson.keys()) { paramJson[paramName] = paramJson[paramName].set("type", questTemplate->parameterTypes[paramName]); paramJson[paramName] = paramJson[paramName].set("portrait", portrait); } StringMap parameters = questParamsFromJson(paramJson); QuestDescriptor questDesc = QuestDescriptor{"preview", templateId, parameters, Random::randu64()}; List quests; if (!position.equalsIgnoreCase("next") && !position.equalsIgnoreCase("last") && !position.equalsIgnoreCase("first")) { quests = {questDesc}; } else { quests = {questDesc, questDesc, questDesc}; } QuestArcDescriptor arc = QuestArcDescriptor{quests, {}}; size_t arcPos = 0; if (position.equalsIgnoreCase("next")) { arcPos = 1; } else if (position.equalsIgnoreCase("last")) { arcPos = 2; } auto quest = make_shared(arc, arcPos, player); quest->setParameter("questGiver", QuestParam{QuestEntity{{}, questGiverSpecies, {}}, {"Quest Giver"}, portrait, {}}); return quest; } }