From 3c4a20a6d457efda72c71b34639f3daa773a4a31 Mon Sep 17 00:00:00 2001 From: CraftyBoss Date: Sat, 9 Jul 2022 18:24:00 -0700 Subject: [PATCH] only save ip/port changes if actually changed, reconnect fixes --- include/Keyboard.hpp | 8 +- include/SocketBase.hpp | 2 +- .../StageSceneStateServerConfig.hpp | 2 + include/nn/swkbd/swkbd.h | 53 ++++---- include/server/Client.hpp | 19 ++- source/Keyboard.cpp | 11 +- source/puppets/PuppetMain.cpp | 2 +- source/server/Client.cpp | 121 +++++++++++++----- source/server/SocketBase.cpp | 8 +- source/server/SocketClient.cpp | 14 +- source/states/StageSceneStateServerConfig.cpp | 68 +++++++--- 11 files changed, 205 insertions(+), 103 deletions(-) diff --git a/include/Keyboard.hpp b/include/Keyboard.hpp index 88d1098..f4c5349 100644 --- a/include/Keyboard.hpp +++ b/include/Keyboard.hpp @@ -25,6 +25,8 @@ class Keyboard { return nullptr; }; + bool isKeyboardCancelled() const { return mIsCancelled; } + bool isThreadDone() { return mThread->isDone(); } void setHeaderText(const char16_t* text) { mHeaderText = text; } @@ -35,13 +37,13 @@ class Keyboard { al::AsyncFunctorThread* mThread; nn::swkbd::String mResultString; - bool mIsDoneKeyboard; - sead::FixedSafeString<0x10> mInitialText; KeyboardSetup mSetupFunc; const char16_t *mHeaderText = u"Enter Server IP Here!"; - const char16_t *mSubText = u"Must be a Valid Address."; + const char16_t* mSubText = u"Must be a Valid Address."; + + bool mIsCancelled = false; char* mWorkBuf; int mWorkBufSize; diff --git a/include/SocketBase.hpp b/include/SocketBase.hpp index 2437039..02feca5 100644 --- a/include/SocketBase.hpp +++ b/include/SocketBase.hpp @@ -16,7 +16,7 @@ class SocketBase { const char *getStateChar(); u8 getLogState(); - s32 getSocket(); + s32 getFd(); void set_sock_flags(int flags); diff --git a/include/game/StageScene/StageSceneStateServerConfig.hpp b/include/game/StageScene/StageSceneStateServerConfig.hpp index f3c3469..95d4c28 100644 --- a/include/game/StageScene/StageSceneStateServerConfig.hpp +++ b/include/game/StageScene/StageSceneStateServerConfig.hpp @@ -45,6 +45,7 @@ class StageSceneStateServerConfig : public al::HostStateBase, public void exeGamemodeConfig(); void exeGamemodeSelect(); void exeSaveData(); + void exeConnectError(); void endSubMenu(); @@ -84,4 +85,5 @@ namespace { NERVE_HEADER(StageSceneStateServerConfig, GamemodeConfig) NERVE_HEADER(StageSceneStateServerConfig, GamemodeSelect) NERVE_HEADER(StageSceneStateServerConfig, SaveData) + NERVE_HEADER(StageSceneStateServerConfig, ConnectError) } \ No newline at end of file diff --git a/include/nn/swkbd/swkbd.h b/include/nn/swkbd/swkbd.h index fb822f5..b12ebe0 100644 --- a/include/nn/swkbd/swkbd.h +++ b/include/nn/swkbd/swkbd.h @@ -2,13 +2,23 @@ #include #include +#include "types.h" typedef unsigned long int ulong; typedef unsigned short int ushort; typedef unsigned int uint; typedef unsigned char uchar; -namespace nn -{ +namespace nn { + + namespace applet { + enum ExitReason { + Normal = 0, + Canceled = 1, + Abnormal = 2, + Unexpected = 10 + }; + } + namespace swkbd { enum Preset @@ -103,40 +113,18 @@ namespace nn Max_DictionaryLang }; + enum CloseResult + { + Enter, + Cancel + }; + struct DictionaryInfo { uint offset; // 0x0 ushort size; // 0x4 DictionaryLang lang; // 0x6 }; - - // KeyboardMode keyboardMode; // 0x0 - // const char okText[0x8]; // 0x8 - // char leftOptionalSymbolKey; // 0x10 - // char rightOptionalSymbolKey; // 0x12 - // bool isPredictionEnabled; // 0x14 - // InvalidChar invalidCharFlag; // 0x18 - // InitialCursorPos initialCursorPos; // 0x1C - // const char headerText[0x40]; // 0x20 - // const char subText[0x80]; // 0x28 - // const char guideText[0x100]; // 0x30 - // int textMaxLength; // 0x38 - // int textMinLength; // 0x3C - // PasswordMode passwordMode; // 0x40 - // InputFormMode inputFormMode; // 0x44 - // bool isUseNewLine; // 0x48 - // bool isUseUtf8; // 0x49 - // bool isUseBlurBackground; // 0x4A - // int _initialStringOffset; // 0x4C - // int _initialStringLength; // 0x50 - // int _userDictionaryOffset; // 0x54 - // int _userDictionaryNum; // 0x58 - // bool _isUseTextCheck; // 0x5C - // void *_textCheckCallback; // 0x60 - // int* separateTextPos; // 0x68 - // DictionaryInfo* _customizedDicInfoList; // 0x70 - // unsigned char _customizedDicCount; // 0x78 - // unsigned char* _reserved; // 0x80 struct KeyboardConfig { @@ -193,6 +181,7 @@ namespace nn ulong GetRequiredWorkBufferSize(bool); ulong GetRequiredStringBufferSize(void); + nn::applet::ExitReason getExitReason(); void MakePreset(nn::swkbd::KeyboardConfig *,nn::swkbd::Preset); //void SetHeaderText(nn::swkbd::KeyboardConfig *,char16_t const*); //void SetSubText(nn::swkbd::KeyboardConfig*, char16_t const*); @@ -211,7 +200,9 @@ namespace nn void SetInitialText(nn::swkbd::ShowKeyboardArg *,char16_t const*); void SetInitialTextUtf8(nn::swkbd::ShowKeyboardArg *,char const*); //void SetUserWordList(nn::swkbd::ShowKeyboardArg *,nn::swkbd::UserWord const*,int); - void ShowKeyboard(nn::swkbd::String *,nn::swkbd::ShowKeyboardArg const&); + int ShowKeyboard(nn::swkbd::String*, nn::swkbd::ShowKeyboardArg const&); + + __attribute__((used)) static nn::applet::ExitReason g_ExitReason; } // namespace swkbd } // namespace nn diff --git a/include/server/Client.hpp b/include/server/Client.hpp index 651dd72..7682263 100644 --- a/include/server/Client.hpp +++ b/include/server/Client.hpp @@ -32,6 +32,7 @@ #include "game/GameData/GameDataHolderWriter.h" #include "game/GameData/GameDataFunction.h" +#include "heap/seadFrameHeap.h" #include "heap/seadHeap.h" #include "layouts/HideAndSeekIcon.h" #include "rs/util.hpp" @@ -180,8 +181,8 @@ class Client { static void updateShines(); - static void openKeyboardIP(); - static void openKeyboardPort(); + static bool openKeyboardIP(); + static bool openKeyboardPort(); static GameModeInfoBase* getModeInfo() { return sInstance ? sInstance->mModeInfo : nullptr; @@ -196,7 +197,15 @@ class Client { static bool isModeActive() { return sInstance ? sInstance->mIsModeActive : false; } - static bool isSelectedMode(GameMode mode) { return sInstance ? sInstance->mCurMode->getMode() == mode: false; } + static bool isSelectedMode(GameMode mode) { + return sInstance ? sInstance->mCurMode->getMode() == mode : false; + } + + static void showConnect(); + + static void showConnectError(const char16_t* msg); + + static void hideConnect(); void resetCollectedShines(); @@ -227,8 +236,6 @@ class Client { al::AsyncFunctorThread *mReadThread = nullptr; // TODO: use this thread to send any queued packets // al::AsyncFunctorThread *mRecvThread; // TODO: use this thread to recieve packets and update PuppetInfo - sead::SafeArray puppetPlayerID; - int mConnectCount = 0; nn::account::Uid mUserID; @@ -286,7 +293,7 @@ class Client { u8 mScenario = 0; - sead::Heap *mHeap = nullptr; // Heap that Client::sInstance was created in + sead::FrameHeap *mHeap = nullptr; // Custom FrameHeap used for all Client related memory // --- Mode Info --- diff --git a/source/Keyboard.cpp b/source/Keyboard.cpp index 047e7ec..63dfa25 100644 --- a/source/Keyboard.cpp +++ b/source/Keyboard.cpp @@ -15,14 +15,10 @@ Keyboard::Keyboard(ulong strSize) : mResultString(strSize) { mCustomizeDicSize = 0x400; mCustomizeDicBuf = (char*)malloc(mCustomizeDicSize); - - mIsDoneKeyboard = false; } void Keyboard::keyboardThread() { - - mIsDoneKeyboard = false; nn::swkbd::ShowKeyboardArg keyboardArg = nn::swkbd::ShowKeyboardArg(); nn::swkbd::MakePreset(&keyboardArg.keyboardConfig, nn::swkbd::Preset::Default); @@ -44,10 +40,9 @@ void Keyboard::keyboardThread() { nn::swkbd::SetInitialTextUtf8(&keyboardArg, mInitialText.cstr()); } - nn::swkbd::ShowKeyboard(&mResultString, keyboardArg); - - mIsDoneKeyboard = true; - + mIsCancelled = + nn::swkbd::ShowKeyboard(&mResultString, keyboardArg) == 671; // no idea what 671 could be + } void Keyboard::openKeyboard(const char* initialText, KeyboardSetup setupFunc) { diff --git a/source/puppets/PuppetMain.cpp b/source/puppets/PuppetMain.cpp index c485ec2..e222547 100644 --- a/source/puppets/PuppetMain.cpp +++ b/source/puppets/PuppetMain.cpp @@ -66,7 +66,7 @@ void initPuppetActors(al::Scene *scene, al::ActorInitInfo const &rootInfo, char } // create a debug puppet for testing purposes - createPuppetActorFromFactory(rootInfo, playerPlacement, true); + // createPuppetActorFromFactory(rootInfo, playerPlacement, true); } al::initPlacementObjectMap(scene, rootInfo, listName); // run init for ObjectList after we init our puppet actors diff --git a/source/server/Client.cpp b/source/server/Client.cpp index 2380692..6059bc1 100644 --- a/source/server/Client.cpp +++ b/source/server/Client.cpp @@ -30,6 +30,7 @@ #include "packets/PlayerConnect.h" #include "packets/PlayerDC.h" #include "packets/TagInf.h" +#include "prim/seadSafeString.h" #include "puppets/PuppetInfo.h" #include "sead/basis/seadRawPrint.h" #include "sead/math/seadQuat.h" @@ -61,7 +62,7 @@ Client::Client() { mPuppetHolder = new PuppetHolder(maxPuppets); - for (size_t i = 0; i < maxPuppets; i++) + for (size_t i = 0; i < MAXPUPINDEX; i++) { mPuppetInfoArr[i] = new PuppetInfo(); @@ -70,8 +71,6 @@ Client::Client() { strcpy(mDebugPuppetInfo.puppetName, "PuppetDebug"); - puppetPlayerID.fill({0}); - mConnectCount = 0; curCollectedShines.fill(-1); @@ -115,6 +114,8 @@ void Client::init(al::LayoutInitInfo const &initInfo, GameDataHolderAccessor hol StartThreads(); + Logger::log("Remaining Heap Size: %d\n", mHeap->getFreeSize()); + } /** @@ -190,8 +191,6 @@ void Client::restartConnection() { Logger::log("Sucessfully Closed Socket.\n"); } - sInstance->puppetPlayerID.fill({0,0}); - sInstance->mConnectCount = 0; sInstance->mIsConnectionActive = sInstance->mSocket->init(sInstance->mServerIP.cstr(), sInstance->mServerPort).isSuccess(); @@ -203,7 +202,14 @@ void Client::restartConnection() { PlayerConnect initPacket; initPacket.mUserID = sInstance->mUserID; strcpy(initPacket.clientName, sInstance->mUsername.cstr()); - initPacket.conType = ConnectionTypes::RECONNECT; + + if (sInstance->isFirstConnect) { + initPacket.conType = ConnectionTypes::INIT; + sInstance->isFirstConnect = false; + } else { + initPacket.conType = ConnectionTypes::RECONNECT; + } + sInstance->mSocket->SEND(&initPacket); } else { @@ -280,50 +286,58 @@ bool Client::startConnection() { /** * @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. */ -void Client::openKeyboardIP() { +bool Client::openKeyboardIP() { if (!sInstance) { Logger::log("Static Instance is null!\n"); - return; + 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::ModeNumeric; - config.leftOptionalSymbolKey = '.'; - config.textMaxLength = 15; - config.textMinLength = 1; - config.isUseUtf8 = true; - config.inputFormMode = nn::swkbd::InputFormMode::OneLine; - }); + sInstance->mKeyboard->openKeyboard( + sInstance->mServerIP.cstr(), [](nn::swkbd::KeyboardConfig& config) { + config.keyboardMode = nn::swkbd::KeyboardMode::ModeNumeric; + config.leftOptionalSymbolKey = '.'; + config.textMaxLength = 15; + config.textMinLength = 1; + config.isUseUtf8 = true; + config.inputFormMode = nn::swkbd::InputFormMode::OneLine; + }); + + sead::FixedSafeString<0x10> prevIp = sInstance->mServerIP; while (true) { if (sInstance->mKeyboard->isThreadDone()) { - sInstance->mServerIP = sInstance->mKeyboard->getResult(); + if(!sInstance->mKeyboard->isKeyboardCancelled()) + sInstance->mServerIP = sInstance->mKeyboard->getResult(); break; } nn::os::YieldThread(); // allow other threads to run } + + sInstance->isFirstConnect = prevIp != sInstance->mServerIP; + + return sInstance->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. */ -void Client::openKeyboardPort() { +bool Client::openKeyboardPort() { if (!sInstance) { Logger::log("Static Instance is null!\n"); - return; + 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) { + + sInstance->mKeyboard->openKeyboard(buf, [](nn::swkbd::KeyboardConfig& config) { config.keyboardMode = nn::swkbd::KeyboardMode::ModeNumeric; config.textMaxLength = 5; config.textMinLength = 2; @@ -331,13 +345,20 @@ void Client::openKeyboardPort() { config.inputFormMode = nn::swkbd::InputFormMode::OneLine; }); + int prevPort = sInstance->mServerPort; + while (true) { if (sInstance->mKeyboard->isThreadDone()) { - sInstance->mServerPort = ::atoi(sInstance->mKeyboard->getResult()); + if(!sInstance->mKeyboard->isKeyboardCancelled()) + sInstance->mServerPort = ::atoi(sInstance->mKeyboard->getResult()); break; } nn::os::YieldThread(); // allow other threads to run } + + sInstance->isFirstConnect = prevPort != sInstance->mServerPort; + + return sInstance->isFirstConnect; } /** @@ -380,6 +401,7 @@ void Client::readFunc() { if (isFirstConnect) { initPacket.conType = ConnectionTypes::INIT; + isFirstConnect = false; } else { initPacket.conType = ConnectionTypes::RECONNECT; } @@ -390,8 +412,6 @@ void Client::readFunc() { mConnectionWait->tryEnd(); - isFirstConnect = false; - while(mIsConnectionActive) { if (mSocket->getLogState() != SOCKET_LOG_CONNECTED) { @@ -411,8 +431,16 @@ void Client::readFunc() { Logger::log("Connected!\n"); - initPacket.conType = ConnectionTypes::RECONNECT; - mSocket->SEND(&initPacket); // re-send init packet as reconnect packet + if (isFirstConnect) { + initPacket.conType = + ConnectionTypes::INIT; // if we've changed the IP/Port since last connect, + // send init instead of reconnect + isFirstConnect = false; + } else { + initPacket.conType = ConnectionTypes::RECONNECT; + } + + mSocket->SEND(&initPacket); mConnectionWait->tryEnd(); continue; } else { @@ -421,7 +449,6 @@ void Client::readFunc() { nn::os::YieldThread(); // if we're currently waiting on the socket to be initialized, wait until it is nn::os::SleepThread(nn::TimeSpan::FromSeconds(5)); - } if(mSocket->RECV()) { // will block until a packet has been recieved, or socket disconnected @@ -1511,3 +1538,39 @@ GameModeConfigMenu* Client::tryCreateModeMenu() { 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(); +} \ No newline at end of file diff --git a/source/server/SocketBase.cpp b/source/server/SocketBase.cpp index 4dbe7ac..649a4fe 100644 --- a/source/server/SocketBase.cpp +++ b/source/server/SocketBase.cpp @@ -59,7 +59,7 @@ s32 SocketBase::socket_read_char(char *out) { return valread; } -s32 SocketBase::getSocket() { +s32 SocketBase::getFd() { if(this->socket_log_state == SOCKET_LOG_CONNECTED) { return this->socket_log_socket; }else { @@ -69,11 +69,9 @@ s32 SocketBase::getSocket() { bool SocketBase::closeSocket() { - nn::Result result = nn::socket::Close(this->socket_log_socket); + this->socket_log_state = SOCKET_LOG_DISCONNECTED; // probably not safe to assume socket will be closed - if (result.isSuccess()) { - this->socket_log_state = SOCKET_LOG_DISCONNECTED; - } + nn::Result result = nn::socket::Close(this->socket_log_socket); return result.isSuccess(); } diff --git a/source/server/SocketClient.cpp b/source/server/SocketClient.cpp index 041984a..9dd5d97 100644 --- a/source/server/SocketClient.cpp +++ b/source/server/SocketClient.cpp @@ -66,6 +66,8 @@ nn::Result SocketClient::init(const char* ip, u16 port) { this->socket_log_state = SOCKET_LOG_CONNECTED; + Logger::log("Socket fd: %d\n", socket_log_socket); + return result; } @@ -126,7 +128,7 @@ bool SocketClient::RECV() { int fullSize = header->mPacketSize + sizeof(Packet); - if (header->mType != PacketType::UNKNOWN && fullSize <= MAXPACKSIZE && fullSize > 0) { + if (header->mType != PacketType::UNKNOWN && fullSize <= MAXPACKSIZE && fullSize > 0 && valread == sizeof(Packet)) { if (header->mType != PLAYERINF && header->mType != HACKCAPINF) Logger::log("Received packet (from %02X%02X): %s\n", @@ -163,6 +165,8 @@ bool SocketClient::RECV() { free(packetBuf); } } + } else { + Logger::log("Failed to aquire valid data! Packet Type: %d Full Packet Size %d valread size: %d", header->mType, fullSize, valread); } return true; @@ -194,5 +198,11 @@ bool SocketClient::closeSocket() { Logger::log("Closing Socket.\n"); - return SocketBase::closeSocket(); + bool result = false; + + if (!(result = SocketBase::closeSocket())) { + Logger::log("Failed to close socket!\n"); + } + + return result; } \ No newline at end of file diff --git a/source/states/StageSceneStateServerConfig.cpp b/source/states/StageSceneStateServerConfig.cpp index 642db84..c4eeb51 100644 --- a/source/states/StageSceneStateServerConfig.cpp +++ b/source/states/StageSceneStateServerConfig.cpp @@ -170,10 +170,15 @@ void StageSceneStateServerConfig::exeOpenKeyboardIP() { Client::getKeyboard()->setHeaderText(u"Set a Server IP Below."); Client::getKeyboard()->setSubText(u""); - Client::openKeyboardIP(); - // anything that happens after this will be ran after the keyboard closes + + bool isSave = Client::openKeyboardIP(); // anything that happens after this will be ran after the keyboard closes + al::startHitReaction(mCurrentMenu, "リセット", 0); - al::setNerve(this, &nrvStageSceneStateServerConfigSaveData); + + if(isSave) + al::setNerve(this, &nrvStageSceneStateServerConfigSaveData); + else + al::setNerve(this, &nrvStageSceneStateServerConfigMainMenu); } } @@ -184,21 +189,36 @@ void StageSceneStateServerConfig::exeOpenKeyboardPort() { Client::getKeyboard()->setHeaderText(u"Set a Server Port Below."); Client::getKeyboard()->setSubText(u""); - Client::openKeyboardPort(); - // anything that happens after this will be ran after the keyboard closes - al::setNerve(this, &nrvStageSceneStateServerConfigSaveData); + + bool isSave = Client::openKeyboardPort(); // anything that happens after this will be ran after the keyboard closes + + al::startHitReaction(mCurrentMenu, "リセット", 0); + + if(isSave) + al::setNerve(this, &nrvStageSceneStateServerConfigSaveData); + else + al::setNerve(this, &nrvStageSceneStateServerConfigMainMenu); } } void StageSceneStateServerConfig::exeRestartServer() { if (al::isFirstStep(this)) { mCurrentList->deactivate(); + + Client::showConnect(); + Client::restartConnection(); } if (Client::isSocketActive()) { + + Client::hideConnect(); + al::startHitReaction(mCurrentMenu, "リセット", 0); + al::setNerve(this, &nrvStageSceneStateServerConfigMainMenu); + } else { + al::setNerve(this, &nrvStageSceneStateServerConfigConnectError); } } @@ -237,6 +257,30 @@ void StageSceneStateServerConfig::exeGamemodeSelect() { } } +void StageSceneStateServerConfig::exeConnectError() { + if (al::isFirstStep(this)) { + Client::showConnectError(u"Failed to Reconnect!"); + } + + if (al::isGreaterEqualStep(this, 60)) { // close after 1 second + Client::hideConnect(); + al::startHitReaction(mCurrentMenu, "リセット", 0); + al::setNerve(this, &nrvStageSceneStateServerConfigMainMenu); + } +} + +void StageSceneStateServerConfig::exeSaveData() { + + if (al::isFirstStep(this)) { + SaveDataAccessFunction::startSaveDataWrite(mGameDataHolder); + } + + if (SaveDataAccessFunction::updateSaveDataAccess(mGameDataHolder, false)) { + al::startHitReaction(mCurrentMenu, "リセット", 0); + al::setNerve(this, &nrvStageSceneStateServerConfigMainMenu); + } +} + void StageSceneStateServerConfig::endSubMenu() { mCurrentList->deactivate(); mCurrentMenu->kill(); @@ -291,17 +335,6 @@ void StageSceneStateServerConfig::subMenuUpdate() { } } -void StageSceneStateServerConfig::exeSaveData() { - - if (al::isFirstStep(this)) { - SaveDataAccessFunction::startSaveDataWrite(mGameDataHolder); - } - - if (SaveDataAccessFunction::updateSaveDataAccess(mGameDataHolder, false)) { - al::startHitReaction(mCurrentMenu, "リセット", 0); - al::setNerve(this, &nrvStageSceneStateServerConfigMainMenu); - } -} namespace { NERVE_IMPL(StageSceneStateServerConfig, MainMenu) @@ -311,4 +344,5 @@ NERVE_IMPL(StageSceneStateServerConfig, RestartServer) NERVE_IMPL(StageSceneStateServerConfig, GamemodeConfig) NERVE_IMPL(StageSceneStateServerConfig, GamemodeSelect) NERVE_IMPL(StageSceneStateServerConfig, SaveData) +NERVE_IMPL(StageSceneStateServerConfig, ConnectError) } \ No newline at end of file