#include "Transport.hpp" #include "servers/CNShardServer.hpp" #include "PlayerManager.hpp" #include "Nanos.hpp" #include "TableData.hpp" #include "Entities.hpp" #include "NPCManager.hpp" #include "MobAI.hpp" #include #include using namespace Transport; std::map Transport::Routes; std::map Transport::Locations; std::vector Transport::NPCPaths; std::map> Transport::SkywayPaths; std::unordered_map> Transport::SkywayQueues; std::unordered_map> Transport::NPCQueues; static void transportRegisterLocationHandler(CNSocket* sock, CNPacketData* data) { auto transport = (sP_CL2FE_REQ_REGIST_TRANSPORTATION_LOCATION*)data->buf; Player* plr = PlayerManager::getPlayer(sock); bool newReg = false; // this is a new registration //std::cout << "request to register transport, eTT " << transport->eTT << ", locID " << transport->iLocationID << ", npc " << transport->iNPC_ID << std::endl; if (transport->eTT == 1) { // S.C.A.M.P.E.R. if (transport->iLocationID < 1 || transport->iLocationID > 31) { // sanity check std::cout << "[WARN] S.C.A.M.P.E.R. location ID " << transport->iLocationID << " is out of bounds" << std::endl; INITSTRUCT(sP_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_FAIL, failResp); failResp.eTT = transport->eTT; failResp.iErrorCode = 0; // TODO: review what error code to use here failResp.iLocationID = transport->iLocationID; sock->sendPacket(failResp, P_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_FAIL); return; } // update registration bitfield using bitmask uint32_t newScamperFlag = plr->iWarpLocationFlag | (1UL << (transport->iLocationID - 1)); if (newScamperFlag != plr->iWarpLocationFlag) { plr->iWarpLocationFlag = newScamperFlag; newReg = true; } } else if (transport->eTT == 2) { // Monkey Skyway System if (transport->iLocationID < 1 || transport->iLocationID > 127) { // sanity check std::cout << "[WARN] Skyway location ID " << transport->iLocationID << " is out of bounds" << std::endl; INITSTRUCT(sP_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_FAIL, failResp); failResp.eTT = transport->eTT; failResp.iErrorCode = 0; // TODO: review what error code to use here failResp.iLocationID = transport->iLocationID; sock->sendPacket(failResp, P_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_FAIL); return; } /* * assuming the two bitfields are just stuck together to make a longer one, do a similar operation */ int index = transport->iLocationID > 64 ? 1 : 0; uint64_t newMonkeyFlag = plr->aSkywayLocationFlag[index] | (1ULL << (index ? transport->iLocationID - 65 : transport->iLocationID - 1)); if (newMonkeyFlag != plr->aSkywayLocationFlag[index]) { plr->aSkywayLocationFlag[index] = newMonkeyFlag; newReg = true; } } else { std::cout << "[WARN] Unknown mode of transport; eTT = " << transport->eTT << std::endl; INITSTRUCT(sP_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_FAIL, failResp); failResp.eTT = transport->eTT; failResp.iErrorCode = 0; // TODO: review what error code to use here failResp.iLocationID = transport->iLocationID; sock->sendPacket(failResp, P_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_FAIL); return; } if (!newReg) return; // don't send new registration message INITSTRUCT(sP_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_SUCC, resp); // response parameters resp.eTT = transport->eTT; resp.iLocationID = transport->iLocationID; resp.iWarpLocationFlag = plr->iWarpLocationFlag; resp.aWyvernLocationFlag[0] = plr->aSkywayLocationFlag[0]; resp.aWyvernLocationFlag[1] = plr->aSkywayLocationFlag[1]; sock->sendPacket(resp, P_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_SUCC); } static void transportWarpHandler(CNSocket* sock, CNPacketData* data) { auto req = (sP_CL2FE_REQ_PC_WARP_USE_TRANSPORTATION*)data->buf; Player* plr = PlayerManager::getPlayer(sock); /* * req: * eIL -- inventory type * iNPC_ID -- the ID of the NPC who is warping you * iTransporationID -- iVehicleID * iSlotNum -- inventory slot number */ if (Routes.find(req->iTransporationID) == Routes.end() || Routes[req->iTransporationID].cost > plr->money) { // sanity check INITSTRUCT(sP_FE2CL_REP_PC_WARP_USE_TRANSPORTATION_FAIL, failResp); failResp.iErrorCode = 0; // TODO: error code failResp.iTransportationID = req->iTransporationID; sock->sendPacket(failResp, P_FE2CL_REP_PC_WARP_USE_TRANSPORTATION_FAIL); return; } TransportRoute route = Routes[req->iTransporationID]; plr->money -= route.cost; TransportLocation* target = nullptr; switch (route.type) { case 1: // S.C.A.M.P.E.R. target = &Locations[route.end]; break; case 2: // Monkey Skyway // set last safe coords plr->lastX = plr->x; plr->lastY = plr->y; plr->lastZ = plr->z; if (SkywayPaths.find(route.mssRouteNum) != SkywayPaths.end()) { // check if route exists Nanos::summonNano(sock, -1); // make sure that no nano is active during the ride SkywayQueues[sock] = SkywayPaths[route.mssRouteNum]; // set socket point queue to route plr->onMonkey = true; break; } else if (TableData::RunningSkywayRoutes.find(route.mssRouteNum) != TableData::RunningSkywayRoutes.end()) { std::vector* _route = &TableData::RunningSkywayRoutes[route.mssRouteNum]; Nanos::summonNano(sock, -1); testMssRoute(sock, _route); plr->onMonkey = true; break; } // refund and send alert packet plr->money += route.cost; INITSTRUCT(sP_FE2CL_ANNOUNCE_MSG, alert); alert.iAnnounceType = 0; // don't think this lets us make a confirm dialog alert.iDuringTime = 3; U8toU16("Skyway route " + std::to_string(route.mssRouteNum) + " isn't pathed yet. You will not be charged any taros.", (char16_t*)alert.szAnnounceMsg, sizeof(alert.szAnnounceMsg)); sock->sendPacket(alert, P_FE2CL_ANNOUNCE_MSG); std::cout << "[WARN] MSS route " << route.mssRouteNum << " not pathed" << std::endl; break; default: std::cout << "[WARN] Unknown tranportation type " << route.type << std::endl; break; } INITSTRUCT(sP_FE2CL_REP_PC_WARP_USE_TRANSPORTATION_SUCC, resp); // response parameters resp.eTT = route.type; resp.iCandy = plr->money; resp.iX = (target == nullptr) ? plr->x : target->x; resp.iY = (target == nullptr) ? plr->y : target->y; resp.iZ = (target == nullptr) ? plr->z : target->z; sock->sendPacket(resp, P_FE2CL_REP_PC_WARP_USE_TRANSPORTATION_SUCC); if (target == nullptr) return; // we warped; update position and chunks PlayerManager::updatePlayerPositionForWarp(sock, target->x, target->y, target->z, INSTANCE_OVERWORLD); } void Transport::testMssRoute(CNSocket *sock, std::vector* route) { int speed = 1500; // TODO: make this adjustable std::queue path; Vec3 last = route->front(); // start pos for (int i = 1; i < route->size(); i++) { Vec3 coords = route->at(i); Transport::lerp(&path, last, coords, speed); path.push(coords); // add keyframe to the queue last = coords; // update start pos } SkywayQueues[sock] = path; } /* * Go through every socket that has broomstick points queued up, and advance to the next point. * If the player has disconnected or finished the route, clean up and remove them from the queue. */ static void stepSkywaySystem() { // using an unordered map so we can remove finished players in one iteration std::unordered_map>::iterator it = SkywayQueues.begin(); while (it != SkywayQueues.end()) { std::queue* queue = &it->second; if (PlayerManager::players.find(it->first) == PlayerManager::players.end()) { // pluck out dead socket + update iterator it = SkywayQueues.erase(it); continue; } Player* plr = PlayerManager::getPlayer(it->first); if (queue->empty()) { // send dismount packet INITSTRUCT(sP_FE2CL_REP_PC_RIDING_SUCC, rideSucc); INITSTRUCT(sP_FE2CL_PC_RIDING, rideBroadcast); rideSucc.iPC_ID = plr->iID; rideSucc.eRT = 0; rideBroadcast.iPC_ID = plr->iID; rideBroadcast.eRT = 0; it->first->sendPacket(rideSucc, P_FE2CL_REP_PC_RIDING_SUCC); // send packet to players in view PlayerManager::sendToViewable(it->first, rideBroadcast, P_FE2CL_PC_RIDING); it = SkywayQueues.erase(it); // remove player from tracking map + update iterator plr->onMonkey = false; } else { Vec3 point = queue->front(); // get point queue->pop(); // remove point from front of queue INITSTRUCT(sP_FE2CL_PC_BROOMSTICK_MOVE, bmstk); bmstk.iPC_ID = plr->iID; bmstk.iToX = point.x; bmstk.iToY = point.y; bmstk.iToZ = point.z; it->first->sendPacket(bmstk, P_FE2CL_PC_BROOMSTICK_MOVE); // set player location to point to update viewables PlayerManager::updatePlayerPosition(it->first, point.x, point.y, point.z, plr->instanceID, plr->angle); // send packet to players in view PlayerManager::sendToViewable(it->first, bmstk, P_FE2CL_PC_BROOMSTICK_MOVE); it++; // go to next entry in map } } } static void stepNPCPathing() { // all NPC pathing queues std::unordered_map>::iterator it = NPCQueues.begin(); while (it != NPCQueues.end()) { std::queue* queue = &it->second; BaseNPC* npc = nullptr; if (NPCManager::NPCs.find(it->first) != NPCManager::NPCs.end()) npc = NPCManager::NPCs[it->first]; if (npc == nullptr || queue->empty()) { // pluck out dead path + update iterator it = NPCQueues.erase(it); continue; } // skip if not simulating mobs if (npc->kind == EntityKind::MOB && !MobAI::simulateMobs) { it++; continue; } // do not roam if not roaming if (npc->kind == EntityKind::MOB && ((Mob*)npc)->state != AIState::ROAMING) { it++; continue; } Vec3 point = queue->front(); // get point queue->pop(); // remove point from front of queue // calculate displacement int dXY = hypot(point.x - npc->x, point.y - npc->y); // XY plane distance int distanceBetween = hypot(dXY, point.z - npc->z); // total distance // update NPC location to update viewables NPCManager::updateNPCPosition(npc->id, point.x, point.y, point.z, npc->instanceID, npc->angle); // TODO: move walking logic into Entity stack switch (npc->kind) { case EntityKind::BUS: INITSTRUCT(sP_FE2CL_TRANSPORTATION_MOVE, busMove); busMove.eTT = 3; busMove.iT_ID = npc->id; busMove.iMoveStyle = 0; // ??? busMove.iToX = point.x; busMove.iToY = point.y; busMove.iToZ = point.z; busMove.iSpeed = distanceBetween; // set to distance to match how monkeys work NPCManager::sendToViewable(npc, &busMove, P_FE2CL_TRANSPORTATION_MOVE, sizeof(sP_FE2CL_TRANSPORTATION_MOVE)); break; case EntityKind::MOB: MobAI::incNextMovement((Mob*)npc); /* fallthrough */ default: INITSTRUCT(sP_FE2CL_NPC_MOVE, move); move.iNPC_ID = npc->id; move.iMoveStyle = 0; // ??? move.iToX = point.x; move.iToY = point.y; move.iToZ = point.z; move.iSpeed = distanceBetween; NPCManager::sendToViewable(npc, &move, P_FE2CL_NPC_MOVE, sizeof(sP_FE2CL_NPC_MOVE)); break; } /* * If this path should be repeated, move processed point to the back to maintain cycle. */ if (npc->loopingPath) queue->push(point); it++; // go to next entry in map } } static void tickTransportationSystem(CNServer* serv, time_t currTime) { stepNPCPathing(); stepSkywaySystem(); } /* * Linearly interpolate between two points and insert the results into a queue. */ void Transport::lerp(std::queue* queue, Vec3 start, Vec3 end, int gapSize, float curve) { int dXY = hypot(end.x - start.x, end.y - start.y); // XY plane distance int distanceBetween = hypot(dXY, end.z - start.z); // total distance int lerps = distanceBetween / gapSize; // number of intermediate points to add for (int i = 1; i <= lerps; i++) { Vec3 lerp; // lerp math //float frac = i / (lerps + 1); float frac = powf(i, curve) / powf(lerps + 1, curve); lerp.x = (start.x * (1.0f - frac)) + (end.x * frac); lerp.y = (start.y * (1.0f - frac)) + (end.y * frac); lerp.z = (start.z * (1.0f - frac)) + (end.z * frac); queue->push(lerp); // add lerp'd point } } void Transport::lerp(std::queue* queue, Vec3 start, Vec3 end, int gapSize) { lerp(queue, start, end, gapSize, 1); } /* * Find and return the first path that targets either the type or the ID. * If no matches are found, return nullptr */ NPCPath* Transport::findApplicablePath(int32_t id, int32_t type, int taskID) { NPCPath* match = nullptr; for (auto _path = Transport::NPCPaths.begin(); _path != Transport::NPCPaths.end(); _path++) { // task ID for the path must match so escorts don't start early if (_path->escortTaskID != taskID) continue; // search target IDs for (int32_t pID : _path->targetIDs) { if (id == pID) { match = &(*_path); break; } } if (match != nullptr) break; // early break for ID matches, since ID has higher priority than type // search target types for (int32_t pType : _path->targetTypes) { if (type == pType) { match = &(*_path); break; } } if (match != nullptr) break; } return match; } void Transport::constructPathNPC(int32_t id, NPCPath* path) { BaseNPC* npc = NPCManager::NPCs[id]; if (npc->kind == EntityKind::MOB) { auto mob = (Mob*)npc; mob->staticPath = true; Vec3 firstPoint = path->points.front(); // Ensure that the first point coincides with the mob's spawn point. if (mob->spawnX != firstPoint.x || mob->spawnY != firstPoint.y) { std::cout << "[FATAL] The first point of the route for mob " << mob->id << " (type " << mob->type << ") does not correspond with its spawn point." << std::endl; exit(1); } } npc->loopingPath = path->isLoop; // Interpolate std::vector pathPoints = path->points; std::queue points; auto _point = pathPoints.begin(); Vec3 from = *_point; // point A coords for (_point++; _point != pathPoints.end(); _point++) { // loop through all point Bs Vec3 to = *_point; // point B coords // add point A to the queue if (path->isRelative) { // relative; the NPCs current position is assumed to be its spawn point Vec3 fromReal = { from.x + npc->x, from.y + npc->y, from.z + npc->z }; Vec3 toReal = { to.x + npc->x, to.y + npc->y, to.z + npc->z }; points.push(fromReal); Transport::lerp(&points, fromReal, toReal, path->speed); // lerp from A to B } else { // absolute points.push(from); Transport::lerp(&points, from, to, path->speed); // lerp from A to B } from = to; // update point A } Transport::NPCQueues[id] = points; } void Transport::init() { REGISTER_SHARD_TIMER(tickTransportationSystem, 1000); REGISTER_SHARD_PACKET(P_CL2FE_REQ_REGIST_TRANSPORTATION_LOCATION, transportRegisterLocationHandler); REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_WARP_USE_TRANSPORTATION, transportWarpHandler); }