2021-03-17 19:07:40 +00:00
|
|
|
#include "servers/CNShardServer.hpp"
|
2021-03-16 22:29:13 +00:00
|
|
|
#include "Missions.hpp"
|
2020-08-24 21:04:56 +00:00
|
|
|
#include "PlayerManager.hpp"
|
2021-03-16 22:29:13 +00:00
|
|
|
#include "Nanos.hpp"
|
|
|
|
#include "Items.hpp"
|
2021-05-06 01:41:17 +00:00
|
|
|
#include "Transport.hpp"
|
2020-09-09 19:09:01 +00:00
|
|
|
|
2020-09-10 13:01:35 +00:00
|
|
|
#include "string.h"
|
|
|
|
|
2021-03-16 22:29:13 +00:00
|
|
|
using namespace Missions;
|
2021-03-16 21:06:10 +00:00
|
|
|
|
2021-03-16 22:29:13 +00:00
|
|
|
std::map<int32_t, Reward*> Missions::Rewards;
|
|
|
|
std::map<int32_t, TaskData*> Missions::Tasks;
|
|
|
|
nlohmann::json Missions::AvatarGrowth[37];
|
2020-08-24 21:04:56 +00:00
|
|
|
|
2021-03-16 21:06:10 +00:00
|
|
|
static void saveMission(Player* player, int missionId) {
|
|
|
|
// sanity check missionID so we don't get exceptions
|
|
|
|
if (missionId < 0 || missionId > 1023) {
|
|
|
|
std::cout << "[WARN] Client submitted invalid missionId: " <<missionId<< std::endl;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Missions are stored in int64_t array
|
|
|
|
int row = missionId / 64;
|
|
|
|
int column = missionId % 64;
|
|
|
|
player->aQuestFlag[row] |= (1ULL << column);
|
|
|
|
}
|
|
|
|
|
2021-04-20 20:38:51 +00:00
|
|
|
static bool isMissionCompleted(Player* player, int missionId) {
|
2022-02-11 17:27:00 +00:00
|
|
|
int row = missionId / 64;
|
|
|
|
int column = missionId % 64;
|
|
|
|
return player->aQuestFlag[row] & (1ULL << column);
|
2021-04-20 20:38:51 +00:00
|
|
|
}
|
|
|
|
|
2021-05-14 19:24:08 +00:00
|
|
|
int Missions::findQSlot(Player *plr, int id) {
|
2021-03-16 21:06:10 +00:00
|
|
|
int i;
|
|
|
|
|
|
|
|
// two passes. we mustn't fail to find an existing stack.
|
|
|
|
for (i = 0; i < AQINVEN_COUNT; i++)
|
|
|
|
if (plr->QInven[i].iID == id)
|
|
|
|
return i;
|
|
|
|
|
|
|
|
// no stack. start a new one.
|
|
|
|
for (i = 0; i < AQINVEN_COUNT; i++)
|
|
|
|
if (plr->QInven[i].iOpt == 0)
|
|
|
|
return i;
|
|
|
|
|
|
|
|
// not found
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool isQuestItemFull(CNSocket* sock, int itemId, int itemCount) {
|
|
|
|
Player* plr = PlayerManager::getPlayer(sock);
|
|
|
|
|
2021-05-14 19:24:08 +00:00
|
|
|
int slot = Missions::findQSlot(plr, itemId);
|
2021-03-16 21:06:10 +00:00
|
|
|
if (slot == -1) {
|
|
|
|
// this should never happen
|
|
|
|
std::cout << "[WARN] Player has no room for quest item!?" << std::endl;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return (itemCount == plr->QInven[slot].iOpt);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void dropQuestItem(CNSocket *sock, int task, int count, int id, int mobid) {
|
|
|
|
std::cout << "Altered item id " << id << " by " << count << " for task id " << task << std::endl;
|
|
|
|
const size_t resplen = sizeof(sP_FE2CL_REP_REWARD_ITEM) + sizeof(sItemReward);
|
|
|
|
assert(resplen < CN_PACKET_BUFFER_SIZE);
|
|
|
|
// we know it's only one trailing struct, so we can skip full validation
|
|
|
|
|
|
|
|
Player *plr = PlayerManager::getPlayer(sock);
|
|
|
|
|
|
|
|
uint8_t respbuf[resplen]; // not a variable length array, don't worry
|
|
|
|
sP_FE2CL_REP_REWARD_ITEM *reward = (sP_FE2CL_REP_REWARD_ITEM *)respbuf;
|
|
|
|
sItemReward *item = (sItemReward *)(respbuf + sizeof(sP_FE2CL_REP_REWARD_ITEM));
|
|
|
|
|
|
|
|
// don't forget to zero the buffer!
|
|
|
|
memset(respbuf, 0, resplen);
|
|
|
|
|
|
|
|
// find free quest item slot
|
2021-05-14 19:24:08 +00:00
|
|
|
int slot = Missions::findQSlot(plr, id);
|
2021-03-16 21:06:10 +00:00
|
|
|
if (slot == -1) {
|
|
|
|
// this should never happen
|
|
|
|
std::cout << "[WARN] Player has no room for quest item!?" << std::endl;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (id != 0)
|
|
|
|
std::cout << "new qitem in slot " << slot << std::endl;
|
|
|
|
|
|
|
|
// update player
|
|
|
|
if (id != 0) {
|
|
|
|
plr->QInven[slot].iType = 8;
|
|
|
|
plr->QInven[slot].iID = id;
|
|
|
|
plr->QInven[slot].iOpt += count; // stacking
|
|
|
|
}
|
|
|
|
|
|
|
|
// fully destory deleted items, for good measure
|
|
|
|
if (plr->QInven[slot].iOpt <= 0)
|
|
|
|
memset(&plr->QInven[slot], 0, sizeof(sItemBase));
|
|
|
|
|
|
|
|
// preserve stats
|
|
|
|
reward->m_iCandy = plr->money;
|
|
|
|
reward->m_iFusionMatter = plr->fusionmatter;
|
|
|
|
reward->iFatigue = 100; // prevents warning message
|
|
|
|
reward->iFatigue_Level = 1;
|
|
|
|
reward->m_iBatteryN = plr->batteryN;
|
|
|
|
reward->m_iBatteryW = plr->batteryW;
|
|
|
|
|
|
|
|
reward->iItemCnt = 1; // remember to update resplen if you change this
|
|
|
|
reward->iTaskID = task;
|
|
|
|
reward->iNPC_TypeID = mobid;
|
|
|
|
|
|
|
|
item->sItem = plr->QInven[slot];
|
|
|
|
item->iSlotNum = slot;
|
|
|
|
item->eIL = 2;
|
|
|
|
|
|
|
|
sock->sendPacket((void*)respbuf, P_FE2CL_REP_REWARD_ITEM, resplen);
|
|
|
|
}
|
|
|
|
|
|
|
|
static int giveMissionReward(CNSocket *sock, int task, int choice=0) {
|
|
|
|
Reward *reward = Rewards[task];
|
|
|
|
Player *plr = PlayerManager::getPlayer(sock);
|
|
|
|
|
|
|
|
int nrewards = 0;
|
|
|
|
for (int i = 0; i < 4; i++) {
|
|
|
|
if (reward->itemIds[i] != 0)
|
|
|
|
nrewards++;
|
|
|
|
}
|
|
|
|
|
|
|
|
// this handles multiple choice rewards in the Academy's Mt. Neverest missions
|
|
|
|
if (choice != 0)
|
|
|
|
nrewards = 1;
|
|
|
|
|
|
|
|
int slots[4];
|
|
|
|
for (int i = 0; i < nrewards; i++) {
|
2021-03-16 22:29:13 +00:00
|
|
|
slots[i] = Items::findFreeSlot(plr);
|
2021-03-16 21:06:10 +00:00
|
|
|
if (slots[i] == -1) {
|
|
|
|
std::cout << "Not enough room to complete task" << std::endl;
|
|
|
|
INITSTRUCT(sP_FE2CL_REP_PC_TASK_END_FAIL, fail);
|
|
|
|
|
|
|
|
fail.iTaskNum = task;
|
|
|
|
fail.iErrorCode = 13; // inventory full
|
|
|
|
|
|
|
|
sock->sendPacket((void*)&fail, P_FE2CL_REP_PC_TASK_END_FAIL, sizeof(sP_FE2CL_REP_PC_TASK_END_FAIL));
|
|
|
|
|
|
|
|
// delete any temp items we might have set
|
|
|
|
for (int j = 0; j < i; j++) {
|
|
|
|
plr->Inven[slots[j]] = { 0, 0, 0, 0 }; // empty
|
|
|
|
}
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
plr->Inven[slots[i]] = { 999, 999, 999, 0 }; // temp item; overwritten later
|
|
|
|
}
|
|
|
|
|
|
|
|
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
|
|
|
|
size_t resplen = sizeof(sP_FE2CL_REP_REWARD_ITEM) + nrewards * sizeof(sItemReward);
|
|
|
|
assert(resplen < CN_PACKET_BUFFER_SIZE);
|
|
|
|
sP_FE2CL_REP_REWARD_ITEM *resp = (sP_FE2CL_REP_REWARD_ITEM *)respbuf;
|
|
|
|
sItemReward *item = (sItemReward *)(respbuf + sizeof(sP_FE2CL_REP_REWARD_ITEM));
|
|
|
|
|
|
|
|
// don't forget to zero the buffer!
|
|
|
|
memset(respbuf, 0, resplen);
|
|
|
|
|
|
|
|
// update player
|
|
|
|
plr->money += reward->money;
|
|
|
|
if (plr->iConditionBitFlag & CSB_BIT_REWARD_CASH) { // nano boost for taros
|
|
|
|
int boost = 0;
|
2021-03-16 22:29:13 +00:00
|
|
|
if (Nanos::getNanoBoost(plr)) // for gumballs
|
2021-03-16 21:06:10 +00:00
|
|
|
boost = 1;
|
|
|
|
plr->money += reward->money * (5 + boost) / 25;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (plr->iConditionBitFlag & CSB_BIT_REWARD_BLOB) { // nano boost for fm
|
|
|
|
int boost = 0;
|
2021-03-16 22:29:13 +00:00
|
|
|
if (Nanos::getNanoBoost(plr)) // for gumballs
|
2021-03-16 21:06:10 +00:00
|
|
|
boost = 1;
|
|
|
|
updateFusionMatter(sock, reward->fusionmatter * (30 + boost) / 25);
|
|
|
|
} else
|
|
|
|
updateFusionMatter(sock, reward->fusionmatter);
|
|
|
|
|
|
|
|
// simple rewards
|
|
|
|
resp->m_iCandy = plr->money;
|
|
|
|
resp->m_iFusionMatter = plr->fusionmatter;
|
|
|
|
resp->iFatigue = 100; // prevents warning message
|
|
|
|
resp->iFatigue_Level = 1;
|
|
|
|
resp->iItemCnt = nrewards;
|
|
|
|
resp->m_iBatteryN = plr->batteryN;
|
|
|
|
resp->m_iBatteryW = plr->batteryW;
|
|
|
|
|
|
|
|
int offset = 0;
|
|
|
|
|
|
|
|
// choice is actually a bitfield
|
|
|
|
if (choice != 0)
|
|
|
|
offset = (int)log2((int)choice);
|
|
|
|
|
|
|
|
for (int i = 0; i < nrewards; i++) {
|
|
|
|
item[i].sItem.iType = reward->itemTypes[offset+i];
|
|
|
|
item[i].sItem.iID = reward->itemIds[offset+i];
|
|
|
|
item[i].sItem.iOpt = 1;
|
|
|
|
item[i].iSlotNum = slots[i];
|
|
|
|
item[i].eIL = 1;
|
|
|
|
|
|
|
|
// update player inventory, overwriting temporary item
|
|
|
|
plr->Inven[slots[i]] = item[i].sItem;
|
|
|
|
}
|
|
|
|
|
|
|
|
sock->sendPacket((void*)respbuf, P_FE2CL_REP_REWARD_ITEM, resplen);
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool endTask(CNSocket *sock, int32_t taskNum, int choice=0) {
|
|
|
|
Player *plr = PlayerManager::getPlayer(sock);
|
|
|
|
|
|
|
|
if (Tasks.find(taskNum) == Tasks.end())
|
|
|
|
return false;
|
|
|
|
|
|
|
|
// ugly pointer/reference juggling for the sake of operator overloading...
|
|
|
|
TaskData& task = *Tasks[taskNum];
|
|
|
|
|
2022-02-11 16:45:08 +00:00
|
|
|
// sanity check
|
2021-03-16 21:06:10 +00:00
|
|
|
int i;
|
2021-04-20 20:38:51 +00:00
|
|
|
bool found = false;
|
2021-03-16 21:06:10 +00:00
|
|
|
for (i = 0; i < ACTIVE_MISSION_COUNT; i++) {
|
|
|
|
if (plr->tasks[i] == taskNum) {
|
2021-04-20 20:38:51 +00:00
|
|
|
found = true;
|
2022-02-11 16:45:08 +00:00
|
|
|
break;
|
2021-03-16 21:06:10 +00:00
|
|
|
}
|
|
|
|
}
|
2021-04-20 20:38:51 +00:00
|
|
|
|
2022-02-12 19:59:00 +00:00
|
|
|
if (!found)
|
2022-02-11 17:27:00 +00:00
|
|
|
return false;
|
2021-04-20 20:38:51 +00:00
|
|
|
|
2021-03-16 21:06:10 +00:00
|
|
|
// mission rewards
|
|
|
|
if (Rewards.find(taskNum) != Rewards.end()) {
|
|
|
|
if (giveMissionReward(sock, taskNum, choice) == -1)
|
|
|
|
return false; // we don't want to send anything
|
|
|
|
}
|
|
|
|
// don't take away quest items if we haven't finished the quest
|
|
|
|
|
2022-02-11 16:45:08 +00:00
|
|
|
/*
|
|
|
|
* Update player's active mission data.
|
|
|
|
*
|
|
|
|
* This must be done after all early returns have passed, otherwise we
|
|
|
|
* risk introducing non-atomic changes. For example, failing to finish
|
|
|
|
* a mission due to not having any inventory space could delete the
|
|
|
|
* mission server-side; leading to a desync.
|
|
|
|
*/
|
|
|
|
for (i = 0; i < ACTIVE_MISSION_COUNT; i++) {
|
|
|
|
if (plr->tasks[i] == taskNum) {
|
|
|
|
plr->tasks[i] = 0;
|
|
|
|
for (int j = 0; j < 3; j++) {
|
|
|
|
plr->RemainingNPCCount[i][j] = 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-16 21:06:10 +00:00
|
|
|
/*
|
|
|
|
* Give (or take away) quest items
|
|
|
|
*
|
|
|
|
* Some mission tasks give the player a quest item upon completion.
|
|
|
|
* This is distinct from quest item mob drops.
|
|
|
|
* They can be identified by a counter in the task indicator (ie. 1/1 Gravity Decelerator).
|
|
|
|
* The server is responsible for dropping the correct item.
|
|
|
|
* Yes, this is pretty stupid.
|
|
|
|
*
|
|
|
|
* iSUInstancename is the number of items to give. It is usually negative at the end of
|
|
|
|
* a mission, to clean up its quest items.
|
|
|
|
*/
|
|
|
|
|
|
|
|
for (int i = 0; i < 3; i++)
|
|
|
|
if (task["m_iSUItem"][i] != 0)
|
|
|
|
dropQuestItem(sock, taskNum, task["m_iSUInstancename"][i], task["m_iSUItem"][i], 0);
|
|
|
|
|
|
|
|
// if it's the last task
|
|
|
|
if (task["m_iSUOutgoingTask"] == 0) {
|
|
|
|
// save completed mission on player
|
|
|
|
saveMission(plr, (int)(task["m_iHMissionID"])-1);
|
|
|
|
|
|
|
|
// if it's a nano mission, reward the nano.
|
|
|
|
if (task["m_iSTNanoID"] != 0)
|
2021-03-16 22:29:13 +00:00
|
|
|
Nanos::addNano(sock, task["m_iSTNanoID"], 0, true);
|
2021-03-16 21:06:10 +00:00
|
|
|
|
|
|
|
// remove current mission
|
|
|
|
plr->CurrentMissionID = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
2020-08-24 21:04:56 +00:00
|
|
|
}
|
|
|
|
|
2021-03-16 22:29:13 +00:00
|
|
|
bool Missions::startTask(Player* plr, int TaskID) {
|
|
|
|
if (Missions::Tasks.find(TaskID) == Missions::Tasks.end()) {
|
2020-09-10 16:40:38 +00:00
|
|
|
std::cout << "[WARN] Player submitted unknown task!?" << std::endl;
|
2020-09-23 19:44:27 +00:00
|
|
|
return false;
|
2020-09-10 16:40:38 +00:00
|
|
|
}
|
2020-10-05 00:03:13 +00:00
|
|
|
|
2021-03-16 22:29:13 +00:00
|
|
|
TaskData& task = *Missions::Tasks[TaskID];
|
2020-12-22 21:18:46 +00:00
|
|
|
|
2021-04-20 20:38:51 +00:00
|
|
|
if (task["m_iCTRReqLvMin"] > plr->level) {
|
2022-02-12 20:05:14 +00:00
|
|
|
std::cout << "[WARN] Player tried to start a task above their level" << std::endl;
|
2022-02-11 17:27:00 +00:00
|
|
|
return false;
|
2021-04-20 20:38:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (isMissionCompleted(plr, (int)(task["m_iHMissionID"]) - 1)) {
|
2022-02-11 17:27:00 +00:00
|
|
|
std::cout << "[WARN] Player tried to start an already completed mission" << std::endl;
|
|
|
|
return false;
|
2021-04-20 20:38:51 +00:00
|
|
|
}
|
|
|
|
|
2020-12-22 21:18:46 +00:00
|
|
|
// client freaks out if nano mission isn't sent first after relogging, so it's easiest to set it here
|
|
|
|
if (task["m_iSTNanoID"] != 0 && plr->tasks[0] != 0) {
|
2020-09-25 09:30:59 +00:00
|
|
|
// lets move task0 to different spot
|
|
|
|
int moveToSlot = 1;
|
|
|
|
for (; moveToSlot < ACTIVE_MISSION_COUNT; moveToSlot++)
|
|
|
|
if (plr->tasks[moveToSlot] == 0)
|
|
|
|
break;
|
|
|
|
|
|
|
|
plr->tasks[moveToSlot] = plr->tasks[0];
|
|
|
|
plr->tasks[0] = 0;
|
|
|
|
for (int i = 0; i < 3; i++) {
|
|
|
|
plr->RemainingNPCCount[moveToSlot][i] = plr->RemainingNPCCount[0][i];
|
|
|
|
plr->RemainingNPCCount[0][i] = 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-10 16:40:38 +00:00
|
|
|
int i;
|
|
|
|
for (i = 0; i < ACTIVE_MISSION_COUNT; i++) {
|
|
|
|
if (plr->tasks[i] == 0) {
|
2020-09-23 19:44:27 +00:00
|
|
|
plr->tasks[i] = TaskID;
|
2020-09-21 19:43:53 +00:00
|
|
|
for (int j = 0; j < 3; j++) {
|
|
|
|
plr->RemainingNPCCount[i][j] = (int)task["m_iCSUNumToKill"][j];
|
|
|
|
}
|
2020-09-10 16:40:38 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2020-09-21 19:43:53 +00:00
|
|
|
|
2020-09-23 19:44:27 +00:00
|
|
|
if (i == ACTIVE_MISSION_COUNT - 1 && plr->tasks[i] != TaskID) {
|
2020-09-10 16:40:38 +00:00
|
|
|
std::cout << "[WARN] Player has more than 6 active missions!?" << std::endl;
|
2021-03-09 16:57:33 +00:00
|
|
|
return false;
|
2020-09-10 16:40:38 +00:00
|
|
|
}
|
2020-08-24 21:04:56 +00:00
|
|
|
|
2020-09-23 19:44:27 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2021-03-16 21:06:10 +00:00
|
|
|
static void taskStart(CNSocket* sock, CNPacketData* data) {
|
2020-09-23 19:44:27 +00:00
|
|
|
sP_CL2FE_REQ_PC_TASK_START* missionData = (sP_CL2FE_REQ_PC_TASK_START*)data->buf;
|
|
|
|
INITSTRUCT(sP_FE2CL_REP_PC_TASK_START_SUCC, response);
|
|
|
|
Player *plr = PlayerManager::getPlayer(sock);
|
|
|
|
|
2020-12-22 21:18:46 +00:00
|
|
|
if (!startTask(plr, missionData->iTaskNum)) {
|
2021-04-20 20:38:51 +00:00
|
|
|
INITSTRUCT(sP_FE2CL_REP_PC_TASK_START_FAIL, failresp);
|
|
|
|
failresp.iTaskNum = missionData->iTaskNum;
|
|
|
|
failresp.iErrorCode = 1; // unused in the client
|
|
|
|
sock->sendPacket(failresp, P_FE2CL_REP_PC_TASK_START_FAIL);
|
2020-09-23 19:44:27 +00:00
|
|
|
return;
|
|
|
|
}
|
2020-10-05 00:03:13 +00:00
|
|
|
|
2020-10-21 05:24:51 +00:00
|
|
|
TaskData& task = *Tasks[missionData->iTaskNum];
|
|
|
|
|
2020-12-16 02:25:52 +00:00
|
|
|
// Give player their delivery items at the start, or reset them to 0 at the start.
|
|
|
|
for (int i = 0; i < 3; i++)
|
|
|
|
if (task["m_iSTItemID"][i] != 0)
|
|
|
|
dropQuestItem(sock, missionData->iTaskNum, task["m_iSTItemNumNeeded"][i], task["m_iSTItemID"][i], 0);
|
|
|
|
std::cout << "Mission requested task: " << missionData->iTaskNum << std::endl;
|
2020-08-24 21:04:56 +00:00
|
|
|
response.iTaskNum = missionData->iTaskNum;
|
2020-10-21 05:24:51 +00:00
|
|
|
response.iRemainTime = task["m_iSTGrantTimer"];
|
2020-08-24 21:04:56 +00:00
|
|
|
sock->sendPacket((void*)&response, P_FE2CL_REP_PC_TASK_START_SUCC, sizeof(sP_FE2CL_REP_PC_TASK_START_SUCC));
|
2020-09-25 21:05:36 +00:00
|
|
|
|
2021-05-06 01:41:17 +00:00
|
|
|
// if escort task, assign matching paths to all nearby NPCs
|
2021-10-19 22:40:40 +00:00
|
|
|
if (task["m_iHTaskType"] == (int)eTaskTypeProperty::EscortDefence) {
|
2021-05-09 12:36:17 +00:00
|
|
|
for (ChunkPos& chunkPos : Chunking::getChunksInMap(plr->instanceID)) { // check all NPCs in the instance
|
2021-05-06 15:41:24 +00:00
|
|
|
Chunk* chunk = Chunking::chunks[chunkPos];
|
2021-05-06 01:41:17 +00:00
|
|
|
for (EntityRef ref : chunk->entities) {
|
2022-04-13 19:56:12 +00:00
|
|
|
if (ref.kind != EntityKind::PLAYER) {
|
2021-05-06 01:41:17 +00:00
|
|
|
BaseNPC* npc = (BaseNPC*)ref.getEntity();
|
2021-06-20 18:37:37 +00:00
|
|
|
NPCPath* path = Transport::findApplicablePath(npc->id, npc->type, missionData->iTaskNum);
|
2021-05-06 01:41:17 +00:00
|
|
|
if (path != nullptr) {
|
2021-06-20 18:37:37 +00:00
|
|
|
Transport::constructPathNPC(npc->id, path);
|
2021-05-06 01:41:17 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-05-06 15:41:24 +00:00
|
|
|
}
|
2020-09-25 21:05:36 +00:00
|
|
|
}
|
2020-08-24 21:04:56 +00:00
|
|
|
}
|
|
|
|
|
2021-03-16 21:06:10 +00:00
|
|
|
static void taskEnd(CNSocket* sock, CNPacketData* data) {
|
2020-08-24 21:04:56 +00:00
|
|
|
sP_CL2FE_REQ_PC_TASK_END* missionData = (sP_CL2FE_REQ_PC_TASK_END*)data->buf;
|
2020-10-21 05:24:51 +00:00
|
|
|
|
2022-02-12 20:05:14 +00:00
|
|
|
TaskData* task = Missions::Tasks[missionData->iTaskNum];
|
|
|
|
|
|
|
|
// handle timed mission failure
|
|
|
|
if (task->task["m_iSTGrantTimer"] > 0 && missionData->iNPC_ID == 0) {
|
|
|
|
Player* plr = PlayerManager::getPlayer(sock);
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Enemy killing missions
|
|
|
|
* this is gross and should be cleaned up later
|
|
|
|
* once we comb over mission logic more throughly
|
|
|
|
*/
|
|
|
|
bool mobsAreKilled = false;
|
2021-10-19 22:40:40 +00:00
|
|
|
if (task->task["m_iHTaskType"] == (int)eTaskTypeProperty::Defeat) {
|
2022-02-12 20:05:14 +00:00
|
|
|
mobsAreKilled = true;
|
|
|
|
for (int i = 0; i < ACTIVE_MISSION_COUNT; i++) {
|
|
|
|
if (plr->tasks[i] == missionData->iTaskNum) {
|
|
|
|
for (int j = 0; j < 3; j++) {
|
|
|
|
if (plr->RemainingNPCCount[i][j] > 0) {
|
|
|
|
mobsAreKilled = false;
|
|
|
|
break;
|
2020-12-12 22:22:22 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-02-12 20:05:14 +00:00
|
|
|
}
|
2020-12-12 22:22:22 +00:00
|
|
|
|
2022-02-12 20:05:14 +00:00
|
|
|
if (!mobsAreKilled) {
|
|
|
|
int failTaskID = task->task["m_iFOutgoingTask"];
|
|
|
|
if (failTaskID != 0) {
|
|
|
|
Missions::quitTask(sock, missionData->iTaskNum, false);
|
|
|
|
|
|
|
|
for (int i = 0; i < 6; i++)
|
|
|
|
if (plr->tasks[i] == missionData->iTaskNum)
|
|
|
|
plr->tasks[i] = failTaskID;
|
|
|
|
return;
|
2020-10-21 05:24:51 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-24 21:04:56 +00:00
|
|
|
INITSTRUCT(sP_FE2CL_REP_PC_TASK_END_SUCC, response);
|
|
|
|
|
|
|
|
response.iTaskNum = missionData->iTaskNum;
|
2020-09-09 22:31:09 +00:00
|
|
|
|
2020-12-13 00:32:19 +00:00
|
|
|
if (!endTask(sock, missionData->iTaskNum, missionData->iBox1Choice)) {
|
2020-09-10 16:40:38 +00:00
|
|
|
return;
|
2020-09-10 13:01:35 +00:00
|
|
|
}
|
|
|
|
|
2020-09-25 21:05:36 +00:00
|
|
|
sock->sendPacket((void*)&response, P_FE2CL_REP_PC_TASK_END_SUCC, sizeof(sP_FE2CL_REP_PC_TASK_END_SUCC));
|
|
|
|
}
|
|
|
|
|
2021-03-16 21:06:10 +00:00
|
|
|
static void setMission(CNSocket* sock, CNPacketData* data) {
|
2020-09-28 18:11:13 +00:00
|
|
|
Player* plr = PlayerManager::getPlayer(sock);
|
2020-10-05 00:03:13 +00:00
|
|
|
|
2020-09-10 13:01:35 +00:00
|
|
|
sP_CL2FE_REQ_PC_SET_CURRENT_MISSION_ID* missionData = (sP_CL2FE_REQ_PC_SET_CURRENT_MISSION_ID*)data->buf;
|
|
|
|
INITSTRUCT(sP_FE2CL_REP_PC_SET_CURRENT_MISSION_ID, response);
|
|
|
|
response.iCurrentMissionID = missionData->iCurrentMissionID;
|
2020-09-21 19:43:53 +00:00
|
|
|
plr->CurrentMissionID = missionData->iCurrentMissionID;
|
2020-09-28 18:11:13 +00:00
|
|
|
|
|
|
|
sock->sendPacket((void*)&response, P_FE2CL_REP_PC_SET_CURRENT_MISSION_ID, sizeof(sP_FE2CL_REP_PC_SET_CURRENT_MISSION_ID));
|
2020-09-10 13:01:35 +00:00
|
|
|
}
|
|
|
|
|
2021-03-16 21:06:10 +00:00
|
|
|
static void quitMission(CNSocket* sock, CNPacketData* data) {
|
2020-09-10 13:01:35 +00:00
|
|
|
sP_CL2FE_REQ_PC_TASK_STOP* missionData = (sP_CL2FE_REQ_PC_TASK_STOP*)data->buf;
|
2020-10-14 18:36:38 +00:00
|
|
|
quitTask(sock, missionData->iTaskNum, true);
|
2020-09-25 05:35:27 +00:00
|
|
|
}
|
|
|
|
|
2021-03-16 22:29:13 +00:00
|
|
|
void Missions::quitTask(CNSocket* sock, int32_t taskNum, bool manual) {
|
2020-09-25 05:35:27 +00:00
|
|
|
Player* plr = PlayerManager::getPlayer(sock);
|
2020-09-10 21:40:17 +00:00
|
|
|
|
2020-12-01 19:18:01 +00:00
|
|
|
if (Tasks.find(taskNum) == Tasks.end())
|
2020-10-25 22:33:02 +00:00
|
|
|
return; // sanity check
|
2020-09-28 18:11:13 +00:00
|
|
|
|
2020-09-10 21:40:17 +00:00
|
|
|
// update player
|
|
|
|
int i;
|
|
|
|
for (i = 0; i < ACTIVE_MISSION_COUNT; i++) {
|
2020-10-19 17:26:14 +00:00
|
|
|
if (plr->tasks[i] == taskNum) {
|
2020-09-10 21:40:17 +00:00
|
|
|
plr->tasks[i] = 0;
|
2020-09-21 19:43:53 +00:00
|
|
|
for (int j = 0; j < 3; j++) {
|
|
|
|
plr->RemainingNPCCount[i][j] = 0;
|
|
|
|
}
|
|
|
|
}
|
2020-09-10 21:40:17 +00:00
|
|
|
}
|
|
|
|
if (i == ACTIVE_MISSION_COUNT - 1 && plr->tasks[i] != 0) {
|
|
|
|
std::cout << "[WARN] Player quit non-active mission!?" << std::endl;
|
|
|
|
}
|
2020-09-21 19:43:53 +00:00
|
|
|
// remove current mission
|
|
|
|
plr->CurrentMissionID = 0;
|
2020-09-10 21:40:17 +00:00
|
|
|
|
2020-09-25 05:35:27 +00:00
|
|
|
TaskData& task = *Tasks[taskNum];
|
2020-09-10 21:40:17 +00:00
|
|
|
|
|
|
|
// clean up quest items
|
2020-12-30 20:42:10 +00:00
|
|
|
if (manual) {
|
|
|
|
for (i = 0; i < 3; i++) {
|
|
|
|
if (task["m_iSUItem"][i] == 0 && task["m_iCSUItemID"][i] == 0)
|
|
|
|
continue;
|
2020-09-10 13:01:35 +00:00
|
|
|
|
2020-12-30 20:42:10 +00:00
|
|
|
/*
|
|
|
|
* It's ok to do this only server-side, because the server decides which
|
|
|
|
* slot later items will be placed in.
|
|
|
|
*/
|
|
|
|
for (int j = 0; j < AQINVEN_COUNT; j++)
|
|
|
|
if (plr->QInven[j].iID == task["m_iSUItem"][i] || plr->QInven[j].iID == task["m_iCSUItemID"][i] || plr->QInven[j].iID == task["m_iSTItemID"][i])
|
|
|
|
memset(&plr->QInven[j], 0, sizeof(sItemBase));
|
|
|
|
}
|
|
|
|
} else {
|
2021-05-14 19:24:08 +00:00
|
|
|
for (i = 0; i < 3; i++) {
|
|
|
|
if (task["m_iFItemID"][i] == 0)
|
|
|
|
continue;
|
|
|
|
dropQuestItem(sock, taskNum, task["m_iFItemNumNeeded"][i], task["m_iFItemID"][i], 0);
|
|
|
|
}
|
|
|
|
|
2020-10-14 18:36:38 +00:00
|
|
|
INITSTRUCT(sP_FE2CL_REP_PC_TASK_END_FAIL, failResp);
|
|
|
|
failResp.iErrorCode = 1;
|
|
|
|
failResp.iTaskNum = taskNum;
|
|
|
|
sock->sendPacket((void*)&failResp, P_FE2CL_REP_PC_TASK_END_FAIL, sizeof(sP_FE2CL_REP_PC_TASK_END_FAIL));
|
|
|
|
}
|
2020-10-19 17:26:14 +00:00
|
|
|
|
2020-10-14 18:36:38 +00:00
|
|
|
INITSTRUCT(sP_FE2CL_REP_PC_TASK_STOP_SUCC, response);
|
2020-09-25 05:35:27 +00:00
|
|
|
response.iTaskNum = taskNum;
|
2020-09-10 13:01:35 +00:00
|
|
|
sock->sendPacket((void*)&response, P_FE2CL_REP_PC_TASK_STOP_SUCC, sizeof(sP_FE2CL_REP_PC_TASK_STOP_SUCC));
|
|
|
|
}
|
|
|
|
|
2021-03-16 22:29:13 +00:00
|
|
|
void Missions::updateFusionMatter(CNSocket* sock, int fusion) {
|
2020-09-23 19:44:27 +00:00
|
|
|
Player *plr = PlayerManager::getPlayer(sock);
|
|
|
|
|
2020-09-23 21:04:58 +00:00
|
|
|
plr->fusionmatter += fusion;
|
|
|
|
|
2020-09-26 01:48:45 +00:00
|
|
|
// there's a much lower FM cap in the Future
|
2020-12-06 16:53:41 +00:00
|
|
|
int fmCap = AvatarGrowth[plr->level]["m_iFMLimit"];
|
|
|
|
if (plr->fusionmatter > fmCap)
|
|
|
|
plr->fusionmatter = fmCap;
|
2020-09-26 01:48:45 +00:00
|
|
|
else if (plr->fusionmatter < 0) // if somehow lowered too far
|
|
|
|
plr->fusionmatter = 0;
|
2020-09-25 21:05:36 +00:00
|
|
|
|
2020-12-06 16:53:41 +00:00
|
|
|
// don't run nano mission logic at level 36
|
|
|
|
if (plr->level >= 36)
|
|
|
|
return;
|
|
|
|
|
2020-11-25 22:36:30 +00:00
|
|
|
// don't give the Blossom nano mission until the player's in the Past
|
|
|
|
if (plr->level == 4 && plr->PCStyle2.iPayzoneFlag == 0)
|
|
|
|
return;
|
|
|
|
|
2020-09-26 01:48:45 +00:00
|
|
|
// check if it is enough for the nano mission
|
2020-12-06 16:53:41 +00:00
|
|
|
int fmNano = AvatarGrowth[plr->level]["m_iReqBlob_NanoCreate"];
|
|
|
|
if (plr->fusionmatter < fmNano)
|
2020-09-25 21:05:36 +00:00
|
|
|
return;
|
2020-09-23 19:44:27 +00:00
|
|
|
|
2020-12-21 00:56:31 +00:00
|
|
|
#ifndef ACADEMY
|
2020-09-25 21:05:36 +00:00
|
|
|
// check if the nano task is already started
|
|
|
|
for (int i = 0; i < ACTIVE_MISSION_COUNT; i++) {
|
|
|
|
TaskData& task = *Tasks[plr->tasks[i]];
|
|
|
|
if (task["m_iSTNanoID"] != 0)
|
|
|
|
return; // nano mission was already started!
|
|
|
|
}
|
2020-09-23 19:44:27 +00:00
|
|
|
|
2020-09-25 21:05:36 +00:00
|
|
|
// start the nano mission
|
2020-12-22 21:18:46 +00:00
|
|
|
startTask(plr, AvatarGrowth[plr->level]["m_iNanoQuestTaskID"]);
|
2020-09-23 19:44:27 +00:00
|
|
|
|
2020-09-25 21:05:36 +00:00
|
|
|
INITSTRUCT(sP_FE2CL_REP_PC_TASK_START_SUCC, response);
|
|
|
|
response.iTaskNum = AvatarGrowth[plr->level]["m_iNanoQuestTaskID"];
|
|
|
|
sock->sendPacket((void*)&response, P_FE2CL_REP_PC_TASK_START_SUCC, sizeof(sP_FE2CL_REP_PC_TASK_START_SUCC));
|
2020-12-21 00:56:31 +00:00
|
|
|
#else
|
2021-03-16 22:29:13 +00:00
|
|
|
plr->fusionmatter -= (int)Missions::AvatarGrowth[plr->level]["m_iReqBlob_NanoCreate"];
|
2020-12-21 00:56:31 +00:00
|
|
|
plr->level++;
|
|
|
|
|
2020-12-01 00:16:47 +00:00
|
|
|
INITSTRUCT(sP_FE2CL_REP_PC_CHANGE_LEVEL_SUCC, response);
|
2020-12-21 00:56:31 +00:00
|
|
|
|
2020-12-01 00:16:47 +00:00
|
|
|
response.iFusionMatter = plr->fusionmatter;
|
|
|
|
response.iLevel = plr->level;
|
2020-12-21 00:56:31 +00:00
|
|
|
|
2020-12-01 00:16:47 +00:00
|
|
|
sock->sendPacket((void*)&response, P_FE2CL_REP_PC_CHANGE_LEVEL_SUCC, sizeof(sP_FE2CL_REP_PC_CHANGE_LEVEL_SUCC));
|
2020-11-25 22:36:30 +00:00
|
|
|
#endif
|
2020-12-01 01:36:20 +00:00
|
|
|
|
|
|
|
// play the beam animation for other players
|
|
|
|
INITSTRUCT(sP_FE2CL_PC_EVENT, bcast);
|
|
|
|
bcast.iEventID = 1; // beam effect
|
|
|
|
bcast.iPC_ID = plr->iID;
|
|
|
|
PlayerManager::sendToViewable(sock, (void*)&bcast, P_FE2CL_PC_EVENT, sizeof(sP_FE2CL_PC_EVENT));
|
2020-09-23 19:44:27 +00:00
|
|
|
}
|
|
|
|
|
2022-02-12 22:53:04 +00:00
|
|
|
void Missions::mobKilled(CNSocket *sock, int mobid, std::map<int, int>& rolls) {
|
2020-09-10 16:40:38 +00:00
|
|
|
Player *plr = PlayerManager::getPlayer(sock);
|
2020-09-28 18:11:13 +00:00
|
|
|
|
2020-09-10 21:40:17 +00:00
|
|
|
bool missionmob = false;
|
2020-09-10 16:40:38 +00:00
|
|
|
|
|
|
|
for (int i = 0; i < ACTIVE_MISSION_COUNT; i++) {
|
|
|
|
if (plr->tasks[i] == 0)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
// tasks[] should always have valid IDs
|
|
|
|
TaskData& task = *Tasks[plr->tasks[i]];
|
|
|
|
|
|
|
|
for (int j = 0; j < 3; j++) {
|
|
|
|
if (task["m_iCSUEnemyID"][j] != mobid)
|
|
|
|
continue;
|
|
|
|
|
2020-09-10 21:40:17 +00:00
|
|
|
// acknowledge killing of mission mob...
|
2020-10-19 17:26:14 +00:00
|
|
|
if (task["m_iCSUNumToKill"][j] != 0) {
|
2020-09-10 21:40:17 +00:00
|
|
|
missionmob = true;
|
2020-11-25 18:33:12 +00:00
|
|
|
if (plr->RemainingNPCCount[i][j] > 0) {
|
2020-09-22 08:18:29 +00:00
|
|
|
plr->RemainingNPCCount[i][j]--;
|
|
|
|
}
|
2020-09-21 19:43:53 +00:00
|
|
|
}
|
2022-02-12 19:59:00 +00:00
|
|
|
|
2020-09-10 16:40:38 +00:00
|
|
|
// drop quest item
|
2020-09-21 19:43:53 +00:00
|
|
|
if (task["m_iCSUItemNumNeeded"][j] != 0 && !isQuestItemFull(sock, task["m_iCSUItemID"][j], task["m_iCSUItemNumNeeded"][j]) ) {
|
2022-02-12 22:53:04 +00:00
|
|
|
bool drop = rolls[plr->tasks[i]] % 100 < task["m_iSTItemDropRate"][j];
|
2020-09-10 16:40:38 +00:00
|
|
|
if (drop) {
|
|
|
|
dropQuestItem(sock, plr->tasks[i], 1, task["m_iCSUItemID"][j], mobid);
|
2022-02-12 19:59:00 +00:00
|
|
|
|
|
|
|
/*
|
|
|
|
* Workaround: The client has a bug where it only sends a TASK_END request
|
|
|
|
* for the first task of multiple that met their quest item requirements
|
|
|
|
* at the same time. We deal with this by sending TASK_END response packets
|
|
|
|
* proactively and then silently ignoring the extra TASK_END requests it
|
|
|
|
* sends afterwards.
|
|
|
|
*/
|
|
|
|
if (isQuestItemFull(sock, task["m_iCSUItemID"][j], task["m_iCSUItemNumNeeded"][j])) {
|
|
|
|
INITSTRUCT(sP_FE2CL_REP_PC_TASK_END_SUCC, end);
|
|
|
|
end.iTaskNum = plr->tasks[i];
|
|
|
|
|
|
|
|
if (!endTask(sock, plr->tasks[i]))
|
|
|
|
continue;
|
|
|
|
|
|
|
|
sock->sendPacket(end, P_FE2CL_REP_PC_TASK_END_SUCC);
|
|
|
|
}
|
2020-09-10 16:40:38 +00:00
|
|
|
} else {
|
|
|
|
// fail to drop (itemID == 0)
|
|
|
|
dropQuestItem(sock, plr->tasks[i], 1, 0, mobid);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-09-10 21:40:17 +00:00
|
|
|
|
|
|
|
// ...but only once
|
|
|
|
// XXX: is it actually necessary to do it this way?
|
|
|
|
if (missionmob) {
|
|
|
|
INITSTRUCT(sP_FE2CL_REP_PC_KILL_QUEST_NPCs_SUCC, kill);
|
|
|
|
|
|
|
|
kill.iNPCID = mobid;
|
|
|
|
|
|
|
|
sock->sendPacket((void*)&kill, P_FE2CL_REP_PC_KILL_QUEST_NPCs_SUCC, sizeof(sP_FE2CL_REP_PC_KILL_QUEST_NPCs_SUCC));
|
|
|
|
}
|
2020-09-10 16:40:38 +00:00
|
|
|
}
|
2020-09-13 18:45:51 +00:00
|
|
|
|
2021-03-16 22:29:13 +00:00
|
|
|
void Missions::failInstancedMissions(CNSocket* sock) {
|
2020-10-14 04:26:30 +00:00
|
|
|
// loop through all tasks; if the required instance is being left, "fail" the task
|
|
|
|
Player* plr = PlayerManager::getPlayer(sock);
|
2020-10-14 18:36:38 +00:00
|
|
|
for (int i = 0; i < 6; i++) {
|
|
|
|
int taskNum = plr->tasks[i];
|
2021-03-16 22:29:13 +00:00
|
|
|
if (Missions::Tasks.find(taskNum) == Missions::Tasks.end())
|
2020-10-14 04:26:30 +00:00
|
|
|
continue; // sanity check
|
|
|
|
|
2021-03-16 22:29:13 +00:00
|
|
|
TaskData* task = Missions::Tasks[taskNum];
|
2020-10-14 18:36:38 +00:00
|
|
|
if (task->task["m_iRequireInstanceID"] != 0) { // mission is instanced
|
2020-10-14 04:26:30 +00:00
|
|
|
int failTaskID = task->task["m_iFOutgoingTask"];
|
|
|
|
if (failTaskID != 0) {
|
2021-03-16 22:29:13 +00:00
|
|
|
Missions::quitTask(sock, taskNum, false);
|
2020-12-22 21:47:49 +00:00
|
|
|
//plr->tasks[i] = failTaskID; // this causes the client to freak out and send a dupe task
|
2020-10-14 04:26:30 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-03-16 21:06:10 +00:00
|
|
|
|
2021-03-16 22:29:13 +00:00
|
|
|
void Missions::init() {
|
2021-03-16 21:06:10 +00:00
|
|
|
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TASK_START, taskStart);
|
|
|
|
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TASK_END, taskEnd);
|
|
|
|
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_SET_CURRENT_MISSION_ID, setMission);
|
|
|
|
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_TASK_STOP, quitMission);
|
|
|
|
}
|