#include "server/Client.hpp" #include "al/layout/SimpleLayoutAppearWaitEnd.h" #include "al/util/LiveActorUtil.h" #include "game/SaveData/SaveDataAccessFunction.h" #include "heap/seadHeapMgr.h" #include "logger.hpp" #include "packets/Packet.h" #include "server/hns/HideAndSeekMode.hpp" SEAD_SINGLETON_DISPOSER_IMPL(Client) typedef void (Client::*ClientThreadFunc)(void); /** * @brief Construct a new Client:: Client object * * @param bufferSize defines the maximum amount of puppets the client can handle */ Client::Client() { mHeap = sead::ExpHeap::create(0x50000, "ClientHeap", sead::HeapMgr::instance()->getCurrentHeap(), 8, sead::Heap::cHeapDirection_Forward, false); sead::ScopedCurrentHeapSetter heapSetter( mHeap); // every new call after this will use ClientHeap instead of SequenceHeap mReadThread = new al::AsyncFunctorThread("ClientReadThread", al::FunctorV0M(this, &Client::readFunc), 0, 0x1000, {0}); mKeyboard = new Keyboard(nn::swkbd::GetRequiredStringBufferSize()); mSocket = new SocketClient("SocketClient", mHeap); mPuppetHolder = new PuppetHolder(maxPuppets); for (size_t i = 0; i < MAXPUPINDEX; i++) { mPuppetInfoArr[i] = new PuppetInfo(); sprintf(mPuppetInfoArr[i]->puppetName, "Puppet%zu", i); } strcpy(mDebugPuppetInfo.puppetName, "PuppetDebug"); mConnectCount = 0; curCollectedShines.fill(-1); collectedShineCount = 0; mShineArray.allocBuffer(100, nullptr); // max of 100 shine actors in buffer nn::account::GetLastOpenedUser(&mUserID); nn::account::Nickname playerName; nn::account::GetNickname(&playerName, mUserID); Logger::setLogName(playerName.name); // set Debug logger name to player name mUsername = playerName.name; mUserID.print(); Logger::log("Player Name: %s\n", playerName.name); Logger::log("%s Build Number: %s\n", playerName.name, TOSTRING(BUILDVERSTR)); } /** * @brief initializes client class using initInfo obtained from StageScene::init * * @param initInfo init info used to create layouts used by client */ void Client::init(al::LayoutInitInfo const &initInfo, GameDataHolderAccessor holder) { mConnectionWait = new (mHeap) al::WindowConfirmWait("ServerWaitConnect", "WindowConfirmWait", initInfo); mConnectStatus = new (mHeap) al::SimpleLayoutAppearWaitEnd("", "SaveMessage", initInfo, 0, false); mConnectionWait->setTxtMessage(u"Connecting to Server."); mConnectionWait->setTxtMessageConfirm(u"Failed to Connect!"); al::setPaneString(mConnectStatus, "TxtSave", u"Connecting to Server.", 0); al::setPaneString(mConnectStatus, "TxtSaveSh", u"Connecting to Server.", 0); mHolder = holder; startThread(); Logger::log("Heap Free Size: %f/%f\n", mHeap->getFreeSize() * 0.001f, mHeap->getSize() * 0.001f); } /** * @brief starts client read thread * * @return true if read thread was sucessfully started * @return false if read thread was unable to start, or thread was already started. */ bool Client::startThread() { if(mReadThread->isDone() ) { mReadThread->start(); Logger::log("Read Thread Sucessfully Started.\n"); return true; }else { Logger::log("Read Thread has already started! Or other unknown reason.\n"); return false; } } /** * @brief restarts currently active connection to server * */ void Client::restartConnection() { if (!sInstance) { Logger::log("Static Instance is null!\n"); return; } sead::ScopedCurrentHeapSetter setter(sInstance->mHeap); Logger::log("Sending Disconnect.\n"); PlayerDC *playerDC = new PlayerDC(); playerDC->mUserID = sInstance->mUserID; sInstance->mSocket->setQueueOpen(false); sInstance->mSocket->clearMessageQueues(); sInstance->mSocket->send(playerDC); sInstance->mHeap->free(playerDC); if (sInstance->mSocket->closeSocket()) { Logger::log("Sucessfully Closed Socket.\n"); } sInstance->mConnectCount = 0; sInstance->mIsConnectionActive = sInstance->mSocket->init(sInstance->mServerIP.cstr(), sInstance->mServerPort).isSuccess(); if(sInstance->mSocket->getLogState() == SOCKET_LOG_CONNECTED) { Logger::log("Reconnect Sucessful!\n"); } else { Logger::log("Reconnect Unsuccessful.\n"); } } /** * @brief starts a connection using client's TCP socket class, pulling up the software keyboard for user inputted IP if save file does not have one saved. * * @return true if successful connection to server * @return false if connection was unable to establish */ bool Client::startConnection() { bool isNeedSave = false; bool isOverride = al::isPadHoldZL(-1); if (mServerIP.isEmpty() || isOverride) { mKeyboard->setHeaderText(u"Save File does not contain an IP!"); mKeyboard->setSubText(u"Please set a Server IP Below."); mServerIP = "127.0.0.1"; Client::openKeyboardIP(); isNeedSave = true; } if (!mServerPort || isOverride) { mKeyboard->setHeaderText(u"Save File does not contain a port!"); mKeyboard->setSubText(u"Please set a Server Port Below."); mServerPort = 1027; Client::openKeyboardPort(); isNeedSave = true; } if (isNeedSave) { SaveDataAccessFunction::startSaveDataWrite(mHolder.mData); } mIsConnectionActive = mSocket->init(mServerIP.cstr(), mServerPort).isSuccess(); if (mIsConnectionActive) { Logger::log("Sucessful Connection. Waiting to recieve init packet.\n"); bool waitingForInitPacket = true; // wait for client init packet while (waitingForInitPacket) { Packet *curPacket = mSocket->tryGetPacket(); if (curPacket) { if (curPacket->mType == PacketType::CLIENTINIT) { InitPacket* initPacket = (InitPacket*)curPacket; Logger::log("Server Max Player Size: %d\n", initPacket->maxPlayers); maxPuppets = initPacket->maxPlayers - 1; waitingForInitPacket = false; } mHeap->free(curPacket); } else { Logger::log("Recieve failed! Stopping Connection.\n"); mIsConnectionActive = false; waitingForInitPacket = false; } } } return mIsConnectionActive; } /** * @brief Opens up OS's software keyboard in order to change the currently used server IP. * @returns whether or not a new IP has been defined and needs to be saved. */ bool Client::openKeyboardIP() { if (!sInstance) { Logger::log("Static Instance is null!\n"); return false; } // opens swkbd with the initial text set to the last saved IP sInstance->mKeyboard->openKeyboard( sInstance->mServerIP.cstr(), [](nn::swkbd::KeyboardConfig& config) { config.keyboardMode = nn::swkbd::KeyboardMode::ModeASCII; config.textMaxLength = MAX_HOSTNAME_LENGTH; config.textMinLength = 1; config.isUseUtf8 = true; config.inputFormMode = nn::swkbd::InputFormMode::OneLine; }); hostname prevIp = sInstance->mServerIP; while (true) { if (sInstance->mKeyboard->isThreadDone()) { if(!sInstance->mKeyboard->isKeyboardCancelled()) sInstance->mServerIP = sInstance->mKeyboard->getResult(); break; } nn::os::YieldThread(); // allow other threads to run } bool isFirstConnect = prevIp != sInstance->mServerIP; sInstance->mSocket->setIsFirstConn(isFirstConnect); return isFirstConnect; } /** * @brief Opens up OS's software keyboard in order to change the currently used server port. * @returns whether or not a new port has been defined and needs to be saved. */ bool Client::openKeyboardPort() { if (!sInstance) { Logger::log("Static Instance is null!\n"); return false; } // opens swkbd with the initial text set to the last saved port char buf[6]; nn::util::SNPrintf(buf, 6, "%u", sInstance->mServerPort); sInstance->mKeyboard->openKeyboard(buf, [](nn::swkbd::KeyboardConfig& config) { config.keyboardMode = nn::swkbd::KeyboardMode::ModeNumeric; config.textMaxLength = 5; config.textMinLength = 2; config.isUseUtf8 = true; config.inputFormMode = nn::swkbd::InputFormMode::OneLine; }); int prevPort = sInstance->mServerPort; while (true) { if (sInstance->mKeyboard->isThreadDone()) { if(!sInstance->mKeyboard->isKeyboardCancelled()) sInstance->mServerPort = ::atoi(sInstance->mKeyboard->getResult()); break; } nn::os::YieldThread(); // allow other threads to run } bool isFirstConnect = prevPort != sInstance->mServerPort; sInstance->mSocket->setIsFirstConn(isFirstConnect); return isFirstConnect; } /** * @brief main thread function for read thread, responsible for processing packets from server * */ void Client::readFunc() { if (waitForGameInit) { nn::os::YieldThread(); // sleep the thread for the first thing we do so that game init can finish nn::os::SleepThread(nn::TimeSpan::FromSeconds(2)); waitForGameInit = false; } mConnectStatus->appear(); al::startAction(mConnectStatus, "Loop", "Loop"); if (!startConnection()) { Logger::log("Failed to Connect to Server.\n"); nn::os::SleepThread(nn::TimeSpan::FromNanoSeconds(250000000)); // sleep active thread for 0.25 seconds mConnectStatus->end(); return; } nn::os::SleepThread(nn::TimeSpan::FromNanoSeconds(500000000)); // sleep for 0.5 seconds to let connection layout fully show (probably should find a better way to do this) mConnectStatus->end(); while(mIsConnectionActive) { Packet *curPacket = mSocket->tryGetPacket(); // will block until a packet has been recieved, or socket disconnected if (curPacket) { switch (curPacket->mType) { case PacketType::PLAYERINF: updatePlayerInfo((PlayerInf*)curPacket); break; case PacketType::GAMEINF: updateGameInfo((GameInf*)curPacket); break; case PacketType::HACKCAPINF: updateHackCapInfo((HackCapInf *)curPacket); break; case PacketType::CAPTUREINF: updateCaptureInfo((CaptureInf*)curPacket); break; case PacketType::PLAYERCON: updatePlayerConnect((PlayerConnect*)curPacket); // Send relevant info packets when another client is connected // Assume game packets are empty from first connection if (lastGameInfPacket.mUserID != mUserID) lastGameInfPacket.mUserID = mUserID; mSocket->send(&lastGameInfPacket); // No need to send player/costume packets if they're empty if (lastPlayerInfPacket.mUserID == mUserID) mSocket->send(&lastPlayerInfPacket); if (lastCostumeInfPacket.mUserID == mUserID) mSocket->send(&lastCostumeInfPacket); break; case PacketType::COSTUMEINF: updateCostumeInfo((CostumeInf*)curPacket); break; case PacketType::SHINECOLL: updateShineInfo((ShineCollect*)curPacket); break; case PacketType::PLAYERDC: Logger::log("Received Player Disconnect!\n"); curPacket->mUserID.print(); disconnectPlayer((PlayerDC*)curPacket); break; case PacketType::TAGINF: updateTagInfo((TagInf*)curPacket); break; case PacketType::CHANGESTAGE: sendToStage((ChangeStagePacket*)curPacket); break; case PacketType::CLIENTINIT: { InitPacket* initPacket = (InitPacket*)curPacket; Logger::log("Server Max Player Size: %d\n", initPacket->maxPlayers); maxPuppets = initPacket->maxPlayers - 1; break; } case PacketType::UDPINIT: { UdpInit* initPacket = (UdpInit*)curPacket; Logger::log("Received udp init packet from server\n"); sInstance->mSocket->setPeerUdpPort(initPacket->port); sendUdpHolePunch(); sendUdpInit(); break; } case PacketType::HOLEPUNCH: sendUdpHolePunch(); break; default: Logger::log("Discarding Unknown Packet Type.\n"); break; } mHeap->free(curPacket); }else { // if false, socket has errored or disconnected, so close the socket and end this thread. Logger::log("Client Socket Encountered an Error! Errno: 0x%x\n", mSocket->socket_errno); } } Logger::log("Client Read Thread ending.\n"); } /** * @brief sends player info packet to current server * * @param player pointer to current player class, used to get translation, animation, and capture data */ void Client::sendPlayerInfPacket(const PlayerActorBase *playerBase, bool isYukimaru) { if (!sInstance) { Logger::log("Static Instance is Null!\n"); return; } if(!playerBase) { Logger::log("Error: Null Player Reference\n"); return; } sead::ScopedCurrentHeapSetter setter(sInstance->mHeap); PlayerInf *packet = new PlayerInf(); packet->mUserID = sInstance->mUserID; packet->playerPos = al::getTrans(playerBase); al::calcQuat(&packet->playerRot, playerBase); // calculate rotation based off pose instead of using quat rotation if (!isYukimaru) { PlayerActorHakoniwa* player = (PlayerActorHakoniwa*)playerBase; for (size_t i = 0; i < 6; i++) { packet->animBlendWeights[i] = player->mPlayerAnimator->getBlendWeight(i); } const char *hackName = player->mHackKeeper->getCurrentHackName(); if (hackName != nullptr) { sInstance->isClientCaptured = true; const char* actName = al::getActionName(player->mHackKeeper->currentHackActor); if (actName) { packet->actName = PlayerAnims::FindType(actName); packet->subActName = PlayerAnims::Type::Unknown; //strcpy(packet.actName, actName); } else { packet->actName = PlayerAnims::Type::Unknown; packet->subActName = PlayerAnims::Type::Unknown; } } else { packet->actName = PlayerAnims::FindType(player->mPlayerAnimator->mAnimFrameCtrl->getActionName()); packet->subActName = PlayerAnims::FindType(player->mPlayerAnimator->curSubAnim.cstr()); sInstance->isClientCaptured = false; } } else { // TODO: implement YukimaruRacePlayer syncing for (size_t i = 0; i < 6; i++) { packet->animBlendWeights[i] = 0; } sInstance->isClientCaptured = false; packet->actName = PlayerAnims::Type::Unknown; packet->subActName = PlayerAnims::Type::Unknown; } if(sInstance->lastPlayerInfPacket != *packet) { sInstance->lastPlayerInfPacket = *packet; // deref packet and store in client memory sInstance->mSocket->queuePacket(packet); } else { sInstance->mHeap->free(packet); // free packet if we're not using it } } /** * @brief sends info related to player's cap actor to server * * @param hackCap pointer to cap actor, used to get translation, animation, and state info */ void Client::sendHackCapInfPacket(const HackCap* hackCap) { if (!sInstance) { Logger::log("Static Instance is Null!\n"); return; } sead::ScopedCurrentHeapSetter setter(sInstance->mHeap); bool isFlying = hackCap->isFlying(); // if cap is in flying state, send packet as often as this function is called if (isFlying) { HackCapInf *packet = new HackCapInf(); packet->mUserID = sInstance->mUserID; packet->capPos = al::getTrans(hackCap); packet->isCapVisible = isFlying; packet->capQuat.x = hackCap->mJointKeeper->mJointRot.x; packet->capQuat.y = hackCap->mJointKeeper->mJointRot.y; packet->capQuat.z = hackCap->mJointKeeper->mJointRot.z; packet->capQuat.w = hackCap->mJointKeeper->mSkew; strcpy(packet->capAnim, al::getActionName(hackCap)); sInstance->mSocket->queuePacket(packet); sInstance->isSentHackInf = true; } else if (sInstance->isSentHackInf) { // if cap is not flying, check to see if previous function call sent a packet, and if so, send one final packet resetting cap data. HackCapInf *packet = new HackCapInf(); packet->mUserID = sInstance->mUserID; packet->isCapVisible = false; packet->capPos = sead::Vector3f::zero; packet->capQuat = sead::Quatf::unit; sInstance->mSocket->queuePacket(packet); sInstance->isSentHackInf = false; } } /** * @brief * Sends both stage info and player 2D info to the server. * @param player * @param holder */ void Client::sendGameInfPacket(const PlayerActorHakoniwa* player, GameDataHolderAccessor holder) { if (!sInstance) { Logger::log("Static Instance is Null!\n"); return; } sead::ScopedCurrentHeapSetter setter(sInstance->mHeap); GameInf *packet = new GameInf(); packet->mUserID = sInstance->mUserID; if (player) { packet->is2D = player->mDimKeeper->is2DModel; } else { packet->is2D = false; } packet->scenarioNo = holder.mData->mGameDataFile->getScenarioNo(); strcpy(packet->stageName, GameDataFunction::getCurrentStageName(holder)); if(*packet != sInstance->lastGameInfPacket) { sInstance->lastGameInfPacket = *packet; sInstance->mSocket->queuePacket(packet); } else { sInstance->mHeap->free(packet); // free packet if we're not using it } } /** * @brief * Sends only stage info to the server. * @param holder */ void Client::sendGameInfPacket(GameDataHolderAccessor holder) { if (!sInstance) { Logger::log("Static Instance is Null!\n"); return; } sead::ScopedCurrentHeapSetter setter(sInstance->mHeap); GameInf *packet = new GameInf(); packet->mUserID = sInstance->mUserID; packet->is2D = false; packet->scenarioNo = holder.mData->mGameDataFile->getScenarioNo(); strcpy(packet->stageName, GameDataFunction::getCurrentStageName(holder)); sInstance->lastGameInfPacket = *packet; sInstance->mSocket->queuePacket(packet); } /** * @brief * */ void Client::sendTagInfPacket() { if (!sInstance) { Logger::log("Static Instance is Null!\n"); return; } sead::ScopedCurrentHeapSetter setter(sInstance->mHeap); HideAndSeekMode* hsMode = GameModeManager::instance()->getMode(); if (!GameModeManager::instance()->isMode(GameMode::HIDEANDSEEK)) { Logger::log("State is not Hide and Seek!\n"); return; } HideAndSeekInfo* curInfo = GameModeManager::instance()->getInfo(); TagInf *packet = new TagInf(); packet->mUserID = sInstance->mUserID; packet->isIt = hsMode->isPlayerIt(); packet->minutes = curInfo->mHidingTime.mMinutes; packet->seconds = curInfo->mHidingTime.mSeconds; packet->updateType = static_cast(TagUpdateType::STATE | TagUpdateType::TIME); sInstance->mSocket->queuePacket(packet); } /** * @brief * * @param body * @param cap */ void Client::sendCostumeInfPacket(const char* body, const char* cap) { if (!sInstance) { Logger::log("Static Instance is Null!\n"); return; } sead::ScopedCurrentHeapSetter setter(sInstance->mHeap); CostumeInf *packet = new CostumeInf(body, cap); packet->mUserID = sInstance->mUserID; sInstance->lastCostumeInfPacket = *packet; sInstance->mSocket->queuePacket(packet); } /** * @brief * * @param player */ void Client::sendCaptureInfPacket(const PlayerActorHakoniwa* player) { if (!sInstance) { Logger::log("Static Instance is Null!\n"); return; } sead::ScopedCurrentHeapSetter setter(sInstance->mHeap); if (sInstance->isClientCaptured && !sInstance->isSentCaptureInf) { CaptureInf *packet = new CaptureInf(); packet->mUserID = sInstance->mUserID; strcpy(packet->hackName, tryConvertName(player->mHackKeeper->getCurrentHackName())); sInstance->mSocket->queuePacket(packet); sInstance->isSentCaptureInf = true; } else if (!sInstance->isClientCaptured && sInstance->isSentCaptureInf) { CaptureInf *packet = new CaptureInf(); packet->mUserID = sInstance->mUserID; strcpy(packet->hackName, ""); sInstance->mSocket->queuePacket(packet); sInstance->isSentCaptureInf = false; } } /** * @brief * * @param shineID */ void Client::sendShineCollectPacket(int shineID) { if (!sInstance) { Logger::log("Static Instance is Null!\n"); return; } sead::ScopedCurrentHeapSetter setter(sInstance->mHeap); if(sInstance->lastCollectedShine != shineID) { ShineCollect *packet = new ShineCollect(); packet->mUserID = sInstance->mUserID; packet->shineId = shineID; sInstance->lastCollectedShine = shineID; sInstance->mSocket->queuePacket(packet); } } /** * @brief * * @param packet */ void Client::updatePlayerInfo(PlayerInf *packet) { PuppetInfo* curInfo = findPuppetInfo(packet->mUserID, false); if (!curInfo) { return; } if(!curInfo->isConnected) { curInfo->isConnected = true; } curInfo->playerPos = packet->playerPos; // check if rotation is larger than zero and less than or equal to 1 if(abs(packet->playerRot.x) > 0.f || abs(packet->playerRot.y) > 0.f || abs(packet->playerRot.z) > 0.f || abs(packet->playerRot.w) > 0.f) { if(abs(packet->playerRot.x) <= 1.f || abs(packet->playerRot.y) <= 1.f || abs(packet->playerRot.z) <= 1.f || abs(packet->playerRot.w) <= 1.f) { curInfo->playerRot = packet->playerRot; } } if (packet->actName != PlayerAnims::Type::Unknown) { strcpy(curInfo->curAnimStr, PlayerAnims::FindStr(packet->actName)); if (curInfo->curAnimStr[0] == '\0') Logger::log("[ERROR] %s: actName was out of bounds: %d\n", __func__, packet->actName); } else { strcpy(curInfo->curAnimStr, "Wait"); } if(packet->subActName != PlayerAnims::Type::Unknown) { strcpy(curInfo->curSubAnimStr, PlayerAnims::FindStr(packet->subActName)); if (curInfo->curSubAnimStr[0] == '\0') Logger::log("[ERROR] %s: subActName was out of bounds: %d\n", __func__, packet->subActName); } else { strcpy(curInfo->curSubAnimStr, ""); } curInfo->curAnim = packet->actName; curInfo->curSubAnim = packet->subActName; for (size_t i = 0; i < 6; i++) { // weights can only be between 0 and 1 if(packet->animBlendWeights[i] >= 0.f && packet->animBlendWeights[i] <= 1.f) { curInfo->blendWeights[i] = packet->animBlendWeights[i]; } } //TEMP if(!curInfo->isCapThrow) { curInfo->capPos = packet->playerPos; } } /** * @brief * * @param packet */ void Client::updateHackCapInfo(HackCapInf *packet) { PuppetInfo* curInfo = findPuppetInfo(packet->mUserID, false); if (curInfo) { curInfo->capPos = packet->capPos; curInfo->capRot = packet->capQuat; curInfo->isCapThrow = packet->isCapVisible; strcpy(curInfo->capAnim, packet->capAnim); } } /** * @brief * * @param packet */ void Client::updateCaptureInfo(CaptureInf* packet) { PuppetInfo* curInfo = findPuppetInfo(packet->mUserID, false); if (!curInfo) { return; } curInfo->isCaptured = strlen(packet->hackName) > 0; if (curInfo->isCaptured) { strcpy(curInfo->curHack, packet->hackName); } } /** * @brief * * @param packet */ void Client::updateCostumeInfo(CostumeInf *packet) { PuppetInfo* curInfo = findPuppetInfo(packet->mUserID, false); if (!curInfo) { return; } strcpy(curInfo->costumeBody, packet->bodyModel); strcpy(curInfo->costumeHead, packet->capModel); } /** * @brief * * @param packet */ void Client::updateShineInfo(ShineCollect* packet) { if(collectedShineCount < curCollectedShines.size() - 1) { curCollectedShines[collectedShineCount] = packet->shineId; collectedShineCount++; } } /** * @brief * * @param packet */ void Client::updatePlayerConnect(PlayerConnect* packet) { PuppetInfo* curInfo = findPuppetInfo(packet->mUserID, true); if (!curInfo) { return; } if (curInfo->isConnected) { Logger::log("Info is already being used by another connected player!\n"); packet->mUserID.print("Connection ID"); curInfo->playerID.print("Target Info"); } else { packet->mUserID.print("Player Connected! ID"); curInfo->playerID = packet->mUserID; curInfo->isConnected = true; strcpy(curInfo->puppetName, packet->clientName); mConnectCount++; } } /** * @brief * * @param packet */ void Client::updateGameInfo(GameInf *packet) { PuppetInfo* curInfo = findPuppetInfo(packet->mUserID, false); if (!curInfo) { return; } if(curInfo->isConnected) { curInfo->scenarioNo = packet->scenarioNo; if(strcmp(packet->stageName, "") != 0 && strlen(packet->stageName) > 3) { strcpy(curInfo->stageName, packet->stageName); } curInfo->is2D = packet->is2D; } } /** * @brief * * @param packet */ void Client::updateTagInfo(TagInf *packet) { // if the packet is for our player, edit info for our player if (packet->mUserID == mUserID && GameModeManager::instance()->isMode(GameMode::HIDEANDSEEK)) { HideAndSeekMode* mMode = GameModeManager::instance()->getMode(); HideAndSeekInfo* curInfo = GameModeManager::instance()->getInfo(); if (packet->updateType & TagUpdateType::STATE) { mMode->setPlayerTagState(packet->isIt); } if (packet->updateType & TagUpdateType::TIME) { curInfo->mHidingTime.mSeconds = packet->seconds; curInfo->mHidingTime.mMinutes = packet->minutes; } return; } PuppetInfo* curInfo = findPuppetInfo(packet->mUserID, false); if (!curInfo) { return; } curInfo->isIt = packet->isIt; curInfo->seconds = packet->seconds; curInfo->minutes = packet->minutes; } /** * @brief * * @param packet */ void Client::sendToStage(ChangeStagePacket* packet) { if (mSceneInfo && mSceneInfo->mSceneObjHolder) { GameDataHolderAccessor accessor(mSceneInfo->mSceneObjHolder); Logger::log("Sending Player to %s at Entrance %s in Scenario %d\n", packet->changeStage, packet->changeID, packet->scenarioNo); ChangeStageInfo info(accessor.mData, packet->changeID, packet->changeStage, false, packet->scenarioNo, static_cast(packet->subScenarioType)); GameDataFunction::tryChangeNextStage(accessor, &info); } } /** * @brief * Send a udp holepunch packet to the server */ void Client::sendUdpHolePunch() { if (!sInstance) { Logger::log("Static Instance is Null!\n"); return; } sead::ScopedCurrentHeapSetter setter(sInstance->mHeap); HolePunch *packet = new HolePunch(); packet->mUserID = sInstance->mUserID; sInstance->mSocket->queuePacket(packet); } /** * @brief * Send a udp init packet to server */ void Client::sendUdpInit() { if (!sInstance) { Logger::log("Static Instance is Null!\n"); return; } sead::ScopedCurrentHeapSetter setter(sInstance->mHeap); UdpInit *packet = new UdpInit(); packet->mUserID = sInstance->mUserID; packet->port = sInstance->mSocket->getLocalUdpPort(); sInstance->mSocket->queuePacket(packet); } /** * @brief * * @param packet */ void Client::disconnectPlayer(PlayerDC *packet) { PuppetInfo* curInfo = findPuppetInfo(packet->mUserID, false); if (!curInfo || !curInfo->isConnected) { return; } curInfo->isConnected = false; curInfo->scenarioNo = -1; strcpy(curInfo->stageName, ""); curInfo->isInSameStage = false; mConnectCount--; } /** * @brief * * @param shineId * @return true * @return false */ bool Client::isShineCollected(int shineId) { for (size_t i = 0; i < curCollectedShines.size(); i++) { if(curCollectedShines[i] >= 0) { if(curCollectedShines[i] == shineId) { return true; } } } return false; } /** * @brief * * @param id * @return int */ PuppetInfo* Client::findPuppetInfo(const nn::account::Uid& id, bool isFindAvailable) { PuppetInfo *firstAvailable = nullptr; for (size_t i = 0; i < getMaxPlayerCount() - 1; i++) { PuppetInfo* curInfo = mPuppetInfoArr[i]; if (curInfo->playerID == id) { return curInfo; } else if (isFindAvailable && !firstAvailable && !curInfo->isConnected) { firstAvailable = curInfo; } } if (!firstAvailable) { Logger::log("Unable to find Assigned Puppet for Player!\n"); id.print("User ID"); } return firstAvailable; } /** * @brief * * @param holder */ void Client::setStageInfo(GameDataHolderAccessor holder) { if (sInstance) { sInstance->mStageName = GameDataFunction::getCurrentStageName(holder); sInstance->mScenario = holder.mData->mGameDataFile->getScenarioNo(); //holder.mData->mGameDataFile->getMainScenarioNoCurrent(); sInstance->mPuppetHolder->setStageInfo(sInstance->mStageName.cstr(), sInstance->mScenario); } } /** * @brief * * @param puppet * @return true * @return false */ bool Client::tryAddPuppet(PuppetActor *puppet) { if(sInstance) { return sInstance->mPuppetHolder->tryRegisterPuppet(puppet); }else { return false; } } /** * @brief * * @param puppet * @return true * @return false */ bool Client::tryAddDebugPuppet(PuppetActor *puppet) { if(sInstance) { return sInstance->mPuppetHolder->tryRegisterDebugPuppet(puppet); }else { return false; } } /** * @brief * * @param idx * @return PuppetActor* */ PuppetActor *Client::getPuppet(int idx) { if(sInstance) { return sInstance->mPuppetHolder->getPuppetActor(idx); }else { return nullptr; } } /** * @brief * * @return PuppetInfo* */ PuppetInfo *Client::getLatestInfo() { if(sInstance) { return Client::getPuppetInfo(sInstance->mPuppetHolder->getSize() - 1); }else { return nullptr; } } /** * @brief * * @param idx * @return PuppetInfo* */ PuppetInfo *Client::getPuppetInfo(int idx) { if(sInstance) { // unsafe get PuppetInfo *curInfo = sInstance->mPuppetInfoArr[idx]; if (!curInfo) { Logger::log("Attempting to Access Puppet Out of Bounds! Value: %d\n", idx); return nullptr; } return curInfo; }else { return nullptr; } } /** * @brief * */ void Client::resetCollectedShines() { collectedShineCount = 0; curCollectedShines.fill(-1); } /** * @brief * * @param shineId */ void Client::removeShine(int shineId) { for (size_t i = 0; i < curCollectedShines.size(); i++) { if(curCollectedShines[i] == shineId) { curCollectedShines[i] = -1; collectedShineCount--; } } } /** * @brief * * @return true * @return false */ bool Client::isNeedUpdateShines() { return sInstance ? sInstance->collectedShineCount > 0 : false; } /** * @brief * */ void Client::updateShines() { if (!sInstance) { Logger::log("Client Null!\n"); return; } // skip shine sync if player is in cap kingdom scenario zero (very start of the game) if (sInstance->mStageName == "CapWorldHomeStage" && (sInstance->mScenario == 0 || sInstance->mScenario == 1)) { return; } GameDataHolderAccessor accessor(sInstance->mCurStageScene); for (size_t i = 0; i < sInstance->getCollectedShinesCount(); i++) { int shineID = sInstance->getShineID(i); if(shineID < 0) continue; Logger::log("Shine UID: %d\n", shineID); GameDataFile::HintInfo* shineInfo = CustomGameDataFunction::getHintInfoByUniqueID(accessor, shineID); if (shineInfo) { if (!GameDataFunction::isGotShine(accessor, shineInfo->mStageName.cstr(), shineInfo->mObjId.cstr())) { Shine* stageShine = findStageShine(shineID); if (stageShine) { if (al::isDead(stageShine)) { stageShine->makeActorAlive(); } stageShine->getDirect(); stageShine->onSwitchGet(); } accessor.mData->mGameDataFile->setGotShine(shineInfo); } } } sInstance->resetCollectedShines(); sInstance->mCurStageScene->mSceneLayout->startShineCountAnim(false); sInstance->mCurStageScene->mSceneLayout->updateCounterParts(); // updates shine chip layout to (maybe) prevent softlocks } /** * @brief * */ void Client::update() { if (sInstance) { sInstance->mPuppetHolder->update(); if (isNeedUpdateShines()) { updateShines(); } GameModeManager::instance()->update(); } } /** * @brief * */ void Client::clearArrays() { if(sInstance) { sInstance->mPuppetHolder->clearPuppets(); sInstance->mShineArray.clear(); } } /** * @brief * * @return PuppetInfo* */ PuppetInfo *Client::getDebugPuppetInfo() { if(sInstance) { return &sInstance->mDebugPuppetInfo; }else { return nullptr; } } /** * @brief * * @return PuppetActor* */ PuppetActor *Client::getDebugPuppet() { if(sInstance) { return sInstance->mPuppetHolder->getDebugPuppet(); }else { return nullptr; } } /** * @brief * * @return Keyboard* */ Keyboard* Client::getKeyboard() { if (sInstance) { return sInstance->mKeyboard; } return nullptr; } /** * @brief * * @return const char* */ const char* Client::getCurrentIP() { if (sInstance) { return sInstance->mServerIP.cstr(); } return nullptr; } /** * @brief * * @return const int */ const int Client::getCurrentPort() { if (sInstance) { return sInstance->mServerPort; } return -1; } /** * @brief sets server IP to supplied string, used specifically for loading IP from the save file. * * @param ip */ void Client::setLastUsedIP(const char* ip) { if (sInstance) { sInstance->mServerIP = ip; } } /** * @brief sets server port to supplied string, used specifically for loading port from the save file. * * @param port */ void Client::setLastUsedPort(const int port) { if (sInstance) { sInstance->mServerPort = port; } } /** * @brief creates new scene info and copies supplied info to the new info, as well as stores a const ptr to the current stage scene. * * @param initInfo * @param stageScene */ void Client::setSceneInfo(const al::ActorInitInfo& initInfo, const StageScene *stageScene) { if (!sInstance) { Logger::log("Client Null!\n"); return; } sInstance->mSceneInfo = new al::ActorSceneInfo(); memcpy(sInstance->mSceneInfo, &initInfo.mActorSceneInfo, sizeof(al::ActorSceneInfo)); sInstance->mCurStageScene = stageScene; } /** * @brief stores shine pointer supplied into a ptr array if space is available, and shine is not collected. * * @param shine * @return true if shine was able to be successfully stored * @return false if shine is already collected, or ptr array is full */ bool Client::tryRegisterShine(Shine* shine) { if (sInstance) { if (!sInstance->mShineArray.isFull()) { if (!shine->isGot()) { sInstance->mShineArray.pushBack(shine); return true; } } } return false; } /** * @brief finds the actor pointer stored in the shine ptr array based off shine ID * * @param shineID Unique ID used for shine actor * @return Shine* if shine ptr array contains actor with supplied shine ID. */ Shine* Client::findStageShine(int shineID) { if (sInstance) { for (int i = 0; i < sInstance->mShineArray.size(); i++) { Shine* curShine = sInstance->mShineArray[i]; if (curShine) { auto hintInfo = CustomGameDataFunction::getHintInfoByIndex(curShine, curShine->mShineIdx); if (hintInfo->mUniqueID == shineID) { return curShine; } } } } return nullptr; } void Client::showConnectError(const char16_t* msg) { if (!sInstance) return; sInstance->mConnectionWait->setTxtMessageConfirm(msg); al::hidePane(sInstance->mConnectionWait, "Page01"); // hide A button prompt if (!sInstance->mConnectionWait->mIsAlive) { sInstance->mConnectionWait->appear(); sInstance->mConnectionWait->playLoop(); } al::startAction(sInstance->mConnectionWait, "Confirm", "State"); } void Client::showConnect() { if (!sInstance) return; sInstance->mConnectionWait->appear(); sInstance->mConnectionWait->playLoop(); } void Client::hideConnect() { if (!sInstance) return; sInstance->mConnectionWait->tryEnd(); }