diff --git a/gframe/cli_args.h b/gframe/cli_args.h index 4b785ef39..d0d53af82 100644 --- a/gframe/cli_args.h +++ b/gframe/cli_args.h @@ -13,6 +13,13 @@ enum LAUNCH_PARAM { WANTS_TO_RUN_AS_ADMIN, REPOS_READ_ONLY, ONLY_CLONE_REPOS, + EXIT_AFTER, + REPLAY, + HOST, + JOIN, + DECKBUILDER, + SET_NICKNAME, + SET_DECK, COUNT, }; diff --git a/gframe/deck_con.cpp b/gframe/deck_con.cpp index 6259a0f1e..0f3890877 100644 --- a/gframe/deck_con.cpp +++ b/gframe/deck_con.cpp @@ -92,6 +92,10 @@ void DeckBuilder::Initialize(bool refresh) { mainGame->device->setEventReceiver(this); } void DeckBuilder::Terminate(bool showmenu) { + if (showmenu && mainGame->exitAfter) { + mainGame->device->closeDevice(); + return; + } mainGame->is_building = false; mainGame->is_siding = false; if(showmenu) { diff --git a/gframe/duelclient.cpp b/gframe/duelclient.cpp index 9b6928c02..c149e92c3 100644 --- a/gframe/duelclient.cpp +++ b/gframe/duelclient.cpp @@ -685,7 +685,9 @@ void DuelClient::HandleSTOCPacketLanAsync(const std::vector& data) { break; } case STOC_CREATE_GAME: { - mainGame->dInfo.secret.game_id = BufferIO::getStruct(pdata, len).gameid; + auto gameid = BufferIO::getStruct(pdata, len).gameid; + mainGame->dInfo.secret.game_id = gameid; + epro::print("CREATE_GAME - id: {}\n", gameid); break; } case STOC_JOIN_GAME: { @@ -1031,6 +1033,10 @@ void DuelClient::HandleSTOCPacketLanAsync(const std::vector& data) { mainGame->HideElement(mainGame->wANCard); mainGame->PopupElement(mainGame->wMessage); mainGame->actionSignal.Wait(lock); + if (mainGame->exitAfter) { + mainGame->device->closeDevice(); + break; + } mainGame->closeDuelWindow = true; mainGame->closeDoneSignal.Wait(lock); mainGame->dInfo.isInLobby = false; diff --git a/gframe/edopro_main.cpp b/gframe/edopro_main.cpp index d9e1e0cd5..5a3fc033f 100644 --- a/gframe/edopro_main.cpp +++ b/gframe/edopro_main.cpp @@ -33,11 +33,22 @@ auto GetOption(epro::path_stringview option) { case EPRO_TEXT('u'): return LAUNCH_PARAM::OVERRIDE_UPDATE_URL; case EPRO_TEXT('r'): return LAUNCH_PARAM::REPOS_READ_ONLY; case EPRO_TEXT('c'): return LAUNCH_PARAM::ONLY_CLONE_REPOS; + case EPRO_TEXT('q'): return LAUNCH_PARAM::EXIT_AFTER; + case EPRO_TEXT('n'): return LAUNCH_PARAM::SET_NICKNAME; + case EPRO_TEXT('d'): return LAUNCH_PARAM::SET_DECK; default: return LAUNCH_PARAM::COUNT; } } if(option == EPRO_TEXT("i-want-to-be-admin"sv)) return LAUNCH_PARAM::WANTS_TO_RUN_AS_ADMIN; + if(option == EPRO_TEXT("replay"sv)) + return LAUNCH_PARAM::REPLAY; + if(option == EPRO_TEXT("host"sv)) + return LAUNCH_PARAM::HOST; + if(option == EPRO_TEXT("join"sv)) + return LAUNCH_PARAM::JOIN; + if(option == EPRO_TEXT("deckbuilder")) + return LAUNCH_PARAM::DECKBUILDER; return LAUNCH_PARAM::COUNT; } diff --git a/gframe/game.cpp b/gframe/game.cpp index fad396209..40428dca6 100644 --- a/gframe/game.cpp +++ b/gframe/game.cpp @@ -3633,6 +3633,320 @@ void Game::ReloadElementsStrings() { ReloadCBCurrentSkin(); } + +bool Game::TrySetDeck(std::string selectedDeck) { + auto selectedDeckW = Utils::ToUnicodeIfNeeded(selectedDeck); + RefreshDeck(cbDeckSelect); + RefreshDeck(cbDBDecks); + for(irr::u32 i = 0; i < cbDeckSelect->getItemCount(); ++i) { + if (selectedDeckW == cbDeckSelect->getItem(i)) { + cbDeckSelect->setSelected(static_cast(i)); + cbDBDecks->setSelected(static_cast(i)); + gGameConfig->lastdeck = selectedDeckW; + return true; + } + } + return false; +} + +void Game::LaunchReplay(epro::path_string replay) { + open_file = true; + open_file_name = replay; + wMainMenu->setVisible(false); + GUIUtils::ClickButton(device, btnReplayMode); + menuHandler.LoadReplay(); + ReplayMode::Pause(true, false); + btnReplayStart->setVisible(true); + btnReplayPause->setVisible(false); + btnReplayStep->setVisible(true); + btnReplayUndo->setVisible(true); +} + +void Game::LaunchHost(std::string config_raw) { + // host: string = ""; + // port: number = 7911; + // password: string = ""; + // startHand: number = 5; + // startLP: number = 8000; + // drawCount: number = 1; + // timeLimit: number = 0; + // lfList: number = 0; // hash, 0 for the null list + // duelParam: string; // bit field of params, 64 + // noCheckDeckSize: boolean = false; + // noCheckDeckContent: boolean = false; + // noShuffleDeck: boolean = false; + // forbiddenTypes: number = 0; // bit field, 32 + // extraRules: number = 0; // bit field, 16 + // serverIndex: number = -1; // host online + + struct { + std::wstring host; + int port; + std::wstring password; + int team1; + int team2; + int bestOf; + int startHand; + int startLP; + int drawCount; + int timeLimit; + uint32_t lfList; + uint64_t duelParam; + bool noCheckDeckSize; + bool noCheckDeckContent; + bool noShuffleDeck; + int forbiddenTypes; + int extraRules; + int serverIndex; + std::wstring notes; + } host; + + try { + const auto j = nlohmann::json::parse(config_raw); + if (!j.is_object()) { + return; + } +#define GET(field, type, def) j.template value(field, def) + host.host = Utils::ToUnicodeIfNeeded(GET("host", std::string, "")); + host.port = GET("port", int, 7911); + host.password = Utils::ToUnicodeIfNeeded(GET("password", std::string, "")); + host.notes = Utils::ToUnicodeIfNeeded(GET("notes", std::string, "")); + host.team1 = GET("team1", int, 1); + host.team2 = GET("team2", int, 1); + host.bestOf = GET("bestOf", int, 1); + host.startHand = GET("startHand", int, 5); + host.startLP = GET("startLP", int, 8000); + host.drawCount = GET("drawCount", int, 1); + host.timeLimit = GET("timeLimit", int, 0); + host.lfList = GET("lfList", uint32_t, 0); + host.noCheckDeckSize = GET("noCheckDeckSize", bool, false); + host.noCheckDeckContent = GET("noCheckDeckContent", bool, false); + host.noShuffleDeck = GET("noShuffleDeck", bool, false); + host.duelParam = static_cast(GET("duelParam", uint64_t, DUEL_MODE_MR5)); + host.forbiddenTypes = static_cast(GET("forbiddenTypes", int, -1)); + host.extraRules = static_cast(GET("extraRules", int, 0)); + host.serverIndex = GET("serverIndex", int, -1); +#undef GET + epro::print(L"parsed host config:\n"); + epro::print(L"host = {}\n", host.host); + epro::print(L"port = {}\n", host.port); + epro::print(L"password = {}\n", host.password); + epro::print(L"team1 = {}\n", host.team1); + epro::print(L"team2 = {}\n", host.team2); + epro::print(L"bestOf = {}\n", host.bestOf); + epro::print(L"startHand = {}\n", host.startHand); + epro::print(L"startLP = {}\n", host.startLP); + epro::print(L"drawCount = {}\n", host.drawCount); + epro::print(L"timeLimit = {}\n", host.timeLimit); + epro::print(L"lfList = {}\n", host.lfList); + epro::print(L"noCheckDeckSize = {}\n", host.noCheckDeckSize); + epro::print(L"noCheckDeckContent = {}\n", host.noCheckDeckContent); + epro::print(L"noShuffleDeck = {}\n", host.noShuffleDeck); + epro::print(L"duelParam = {}\n", host.duelParam); + epro::print(L"forbiddenTypes = {}\n", host.forbiddenTypes); + epro::print(L"extraRules = {}\n", host.extraRules); + epro::print(L"serverIndex = {}\n", host.serverIndex); + } catch(const std::exception& e) { + ErrorLog("Failed to parse host option \"{}\": {}", config_raw, e.what()); + return; + } + + ebServerName->setText(host.host.c_str()); + ebHostPort->setText(fmt::to_wstring(host.port).c_str()); + ebServerPass->setText(host.password.c_str()); + ebStartLP->setText(fmt::to_wstring(host.startLP).c_str()); + ebTeam1->setText(fmt::to_wstring(host.team1).c_str()); + ebTeam2->setText(fmt::to_wstring(host.team2).c_str()); + ebBestOf->setText(fmt::to_wstring(host.bestOf).c_str()); + ebStartHand->setText(fmt::to_wstring(host.startHand).c_str()); + ebDrawCount->setText(fmt::to_wstring(host.drawCount).c_str()); + ebHostNotes->setText(fmt::to_wstring(host.notes).c_str()); + + RefreshLFLists(); + for(irr::u32 i = 0; i < gdeckManager->_lfList.size(); ++i) { + if(gdeckManager->_lfList[i].hash == host.lfList) { + cbHostLFList->setSelected(i); + } + } + + duel_param = host.duelParam; + auto duel_param_ignoretcg = duel_param & ~DUEL_TCG_SEGOC_NONPUBLIC; + extra_rules = static_cast(host.extraRules); + if (host.forbiddenTypes < 0) { + if (duel_param == DUEL_MODE_SPEED) forbiddentypes = DUEL_MODE_MR5_FORB; + else if (duel_param == DUEL_MODE_RUSH) forbiddentypes = DUEL_MODE_MR5_FORB; + else if (duel_param == DUEL_MODE_GOAT) forbiddentypes = DUEL_MODE_MR1_FORB; + else if (duel_param_ignoretcg == DUEL_MODE_MR1) forbiddentypes = DUEL_MODE_MR1_FORB; + else if (duel_param_ignoretcg == DUEL_MODE_MR2) forbiddentypes = DUEL_MODE_MR2_FORB; + else if (duel_param_ignoretcg == DUEL_MODE_MR3) forbiddentypes = DUEL_MODE_MR3_FORB; + else if (duel_param_ignoretcg == DUEL_MODE_MR4) forbiddentypes = DUEL_MODE_MR4_FORB; + else if (duel_param_ignoretcg == DUEL_MODE_MR5) forbiddentypes = DUEL_MODE_MR5_FORB; + else forbiddentypes = DUEL_MODE_MR5_FORB; + } + else { + forbiddentypes = static_cast(host.forbiddenTypes); + } + + chkTcgRulings->setChecked(duel_param & DUEL_TCG_SEGOC_NONPUBLIC); + chkNoShuffleDeck->setChecked(host.noShuffleDeck); + chkNoShuffleDeckSecondary->setChecked(host.noShuffleDeck); + chkNoCheckDeckContent->setChecked(host.noCheckDeckContent); + chkNoCheckDeckContentSecondary->setChecked(host.noCheckDeckContent); + chkNoCheckDeckSize->setChecked(host.noCheckDeckSize); + chkNoCheckDeckSizeSecondary->setChecked(host.noCheckDeckSize); + + for (auto i = 0u; i < sizeofarr(chkCustomRules); ++i) { + if (i == 19) + chkCustomRules[i]->setChecked(duel_param & (DUEL_USE_TRAPS_IN_NEW_CHAIN)); + else if (i == 20) + chkCustomRules[i]->setChecked(duel_param & (DUEL_6_STEP_BATLLE_STEP)); + else if (i == 21) + chkCustomRules[i]->setChecked(duel_param & (DUEL_TRIGGER_WHEN_PRIVATE_KNOWLEDGE)); + else if (i > 21) + chkCustomRules[i]->setChecked(duel_param & (0x100ULL << (i - 3))); + else + chkCustomRules[i]->setChecked(duel_param & (0x100ULL << i)); + } + chkTypeLimit[0]->setChecked(forbiddentypes & TYPE_FUSION); + chkTypeLimit[1]->setChecked(forbiddentypes & TYPE_SYNCHRO); + chkTypeLimit[2]->setChecked(forbiddentypes & TYPE_XYZ); + chkTypeLimit[3]->setChecked(forbiddentypes & TYPE_PENDULUM); + chkTypeLimit[4]->setChecked(forbiddentypes & TYPE_LINK); + + if (host.serverIndex >= 0) { + isHostingOnline = true; + serverChoice->setSelected(host.serverIndex); + } else { + isHostingOnline = false; + } + + UpdateDuelParam(); + UpdateExtraRules(true); + + wMainMenu->setVisible(false); + btnHostConfirm->setEnabled(true); + btnHostCancel->setEnabled(true); + stHostPort->setVisible(true); + ebHostPort->setVisible(true); + stHostNotes->setVisible(false); + ebHostNotes->setVisible(true); + wCreateHost->setVisible(true); + GUIUtils::ClickButton(device, btnHostConfirm); +} + +void Game::LaunchJoin(std::string config_raw) { + // host: string = ""; + // port: number = 7911; + // password: string = ""; + // serverIndex: number = -1; // join online + // gameId: number = 0; // only for + struct { + std::wstring host; + int port; + std::wstring password; + int serverIndex; + int gameId; + } join; + + try { + const auto j = nlohmann::json::parse(config_raw); + if (!j.is_object()) { + return; + } + +#define GET(field, type, def) j.template value(field, def) + join.host = Utils::ToUnicodeIfNeeded(GET("host", std::string, "")); + join.port = GET("port", int, 7911); + join.password = Utils::ToUnicodeIfNeeded(GET("password", std::string, "")); + join.serverIndex = GET("serverIndex", int, -1); + join.gameId = GET("gameId", int, 0); +#undef GET + + epro::print(L"parsed join config:\n"); + epro::print(L"host = {}\n", join.host); + epro::print(L"port = {}\n", join.port); + epro::print(L"password = {}\n", join.password); + epro::print(L"serverIndex = {}\n", join.serverIndex); + epro::print(L"gameId = {}\n", join.gameId); + } catch(const std::exception& e) { + ErrorLog("Failed to parse join option \"{}\": {}", config_raw, e.what()); + return; + } + + wMainMenu->setVisible(false); + if(join.serverIndex >= 0) { + isHostingOnline = true; + serverChoice->setSelected(join.serverIndex); + const auto& serverinfo = ServerLobby::serversVector[join.serverIndex].Resolved(); + DuelClient::StartClient(serverinfo.address, serverinfo.port, join.gameId, false); + } else { + isHostingOnline = false; + auto addr = Utils::ToUTF8IfNeeded(join.host); + DuelClient::StartClient(static_cast(addr.c_str()), join.port, join.gameId, false); + } +} + +void Game::LaunchDeckbuilder(std::string config_raw) { + // testHand?: { + // noOpponent?: boolean = false; + // dontShuffleDeck?: boolean = false; + // startingHand?: number = 5; + // duelParam?: number = DUEL_MODE_MR5; + // saveReplay?: boolean = false; + // }; + struct { + struct { + bool noOpponent; + bool dontShuffleDeck; + int startingHand; + uint64_t duelParam; + bool saveReplay; + bool configured; + } testHand; + bool configured; + } db; + + try { + const auto j = nlohmann::json::parse(config_raw); + if (!j.is_object()) { + return; + } + +#define GET(from, field, type, def) from.template value(field, def) + auto testHandIt = j.find("testHand"); + if(testHandIt != j.end()) { + auto& testHand = *testHandIt; + db.testHand.noOpponent = GET(testHand, "noOpponent", bool, false); + db.testHand.dontShuffleDeck = GET(testHand, "dontShuffleDeck", bool, false); + db.testHand.startingHand = GET(testHand, "startingHand", int, 5); + db.testHand.saveReplay = GET(testHand, "saveReplay", bool, false); + db.testHand.duelParam = static_cast(GET(testHand, "duelParam", uint64_t, DUEL_MODE_MR5)); + db.testHand.configured = true; + } + db.configured = true; +#undef GET + + epro::print(L"parsed deckbuilder config:\n"); + if(db.testHand.configured) { + epro::print(L"testHand.noOpponent = {}\n", db.testHand.noOpponent); + epro::print(L"testHand.dontShuffleDeck = {}\n", db.testHand.dontShuffleDeck); + epro::print(L"testHand.startingHand = {}\n", db.testHand.startingHand); + epro::print(L"testHand.saveReplay = {}\n", db.testHand.saveReplay); + epro::print(L"testHand.duelParam = {}\n", db.testHand.duelParam); + } else { + epro::print(L"testHand = null\n"); + } + } catch (const std::exception& e) { + ErrorLog("Failed to parse join option \"{}\": {}", config_raw, e.what()); + return; + } + + deckBuilder.SetCurrentDeckFromFile(Utils::ToPathString(cbDBDecks->getItem(cbDBDecks->getSelected())), true); + ebDeckname->setText(L""); + HideElement(mainGame->wMainMenu); + deckBuilder.Initialize(); +} + void Game::OnResize() { env->getRootGUIElement()->setRelativePosition(irr::core::recti(0, 0, window_size.Width, window_size.Height)); { diff --git a/gframe/game.h b/gframe/game.h index f3a9e6520..9cd91edf5 100644 --- a/gframe/game.h +++ b/gframe/game.h @@ -597,6 +597,12 @@ class Game final : public info_panel_elements, public main_menu_panel_elements, void ReloadCBVsync(); void ReloadElementsStrings(); + bool TrySetDeck(std::string selectedDeck); + void LaunchReplay(epro::path_string file); + void LaunchHost(std::string params); + void LaunchJoin(std::string params); + void LaunchDeckbuilder(std::string params); + void OnResize(); template T Scale(T val) const; @@ -762,6 +768,7 @@ class Game final : public info_panel_elements, public main_menu_panel_elements, epro::mutex progressStatusLock; ProgressBarStatus progressStatus; + bool exitAfter; #define sizeofarr(arr) (sizeof(arr)/sizeof(decltype(*arr))) diff --git a/gframe/gframe.cpp b/gframe/gframe.cpp index 8f66f8487..ad2326c08 100644 --- a/gframe/gframe.cpp +++ b/gframe/gframe.cpp @@ -42,6 +42,34 @@ void CheckArguments(const args_t& args) { ygo::GUIUtils::SetCheckbox(ygo::mainGame->device, ygo::mainGame->tabSettings.chkEnableSound, false); ygo::GUIUtils::SetCheckbox(ygo::mainGame->device, ygo::mainGame->tabSettings.chkEnableMusic, false); } + if(args[LAUNCH_PARAM::SET_NICKNAME].enabled && !args[LAUNCH_PARAM::SET_NICKNAME].argument.empty()) { + auto nickname = ygo::Utils::ToUnicodeIfNeeded(args[LAUNCH_PARAM::SET_NICKNAME].argument); + ygo::mainGame->ebNickName->setText(nickname.c_str()); + ygo::mainGame->ebNickNameOnline->setText(nickname.c_str()); + } + if(args[LAUNCH_PARAM::SET_DECK].enabled && !args[LAUNCH_PARAM::SET_DECK].argument.empty()) { + auto selectedDeck = ygo::Utils::ToUTF8IfNeeded(args[LAUNCH_PARAM::SET_DECK].argument); + ygo::mainGame->TrySetDeck(selectedDeck); + } + if(args[LAUNCH_PARAM::EXIT_AFTER].enabled) { + ygo::mainGame->exitAfter = true; + } + if(args[LAUNCH_PARAM::REPLAY].enabled && !args[LAUNCH_PARAM::REPLAY].argument.empty()) { + auto replay = ygo::Utils::ToPathString(args[LAUNCH_PARAM::REPLAY].argument); + ygo::mainGame->LaunchReplay(replay); + } + if(args[LAUNCH_PARAM::HOST].enabled && !args[LAUNCH_PARAM::HOST].argument.empty()) { + auto host_params = ygo::Utils::ToUTF8IfNeeded(args[LAUNCH_PARAM::HOST].argument); + ygo::mainGame->LaunchHost(host_params); + } + if(args[LAUNCH_PARAM::JOIN].enabled && !args[LAUNCH_PARAM::JOIN].argument.empty()) { + auto join_params = ygo::Utils::ToUTF8IfNeeded(args[LAUNCH_PARAM::JOIN].argument); + ygo::mainGame->LaunchJoin(join_params); + } + if(args[LAUNCH_PARAM::DECKBUILDER].enabled && !args[LAUNCH_PARAM::DECKBUILDER].argument.empty()) { + auto deckbuilder_params = ygo::Utils::ToUTF8IfNeeded(args[LAUNCH_PARAM::DECKBUILDER].argument); + ygo::mainGame->LaunchDeckbuilder(deckbuilder_params); + } } inline void ThreadsStartup() { @@ -94,6 +122,13 @@ using Game = ygo::Game; #define ADMIN_STR "root" #endif +static bool args_require_repo_read_only() { + return cli_args[LAUNCH_PARAM::REPLAY].enabled + || cli_args[LAUNCH_PARAM::HOST].enabled + || cli_args[LAUNCH_PARAM::JOIN].enabled + || cli_args[LAUNCH_PARAM::DECKBUILDER].enabled; +} + int edopro_main(const args_t& args) { std::puts(EDOPRO_VERSION_STRING_DEBUG); if(ygo::Utils::IsRunningAsAdmin() && !args[LAUNCH_PARAM::WANTS_TO_RUN_AS_ADMIN].enabled) { @@ -127,6 +162,9 @@ int edopro_main(const args_t& args) { ygo::GUIUtils::ShowErrorWindow("Initialization fail", text); return EXIT_FAILURE; } + if(args_require_repo_read_only()) { + cli_args[LAUNCH_PARAM::REPOS_READ_ONLY].enabled = true; + } show_changelog = args[LAUNCH_PARAM::CHANGELOG].enabled; ygo::ClientUpdater updater(args[LAUNCH_PARAM::OVERRIDE_UPDATE_URL].argument); ygo::gClientUpdater = &updater; diff --git a/gframe/menu_handler.cpp b/gframe/menu_handler.cpp index 3dc7fc713..f5735a9e9 100644 --- a/gframe/menu_handler.cpp +++ b/gframe/menu_handler.cpp @@ -50,7 +50,7 @@ static void UpdateDeck() { DuelClient::SendBufferToServer(CTOS_UPDATE_DECK, deckbuf, pdeck - deckbuf); gdeckManager->sent_deck = mainGame->deckBuilder.GetCurrentDeck(); } -static void LoadReplay() { +void MenuHandler::LoadReplay() { auto& replay = ReplayMode::cur_replay; if(std::exchange(open_file, false)) { bool res = replay.OpenReplay(open_file_name); @@ -331,6 +331,12 @@ bool MenuHandler::OnEvent(const irr::SEvent& event) { } case BUTTON_HP_CANCEL: { DuelClient::StopClient(); + + if(mainGame->exitAfter) { + mainGame->device->closeDevice(); + break; + } + mainGame->dInfo.isInLobby = false; mainGame->btnCreateHost->setEnabled(mainGame->coreloaded); mainGame->btnJoinHost->setEnabled(true); diff --git a/gframe/menu_handler.h b/gframe/menu_handler.h index 553686467..6a48eb8ee 100644 --- a/gframe/menu_handler.h +++ b/gframe/menu_handler.h @@ -294,6 +294,7 @@ enum GUI { class MenuHandler final : public irr::IEventReceiver { public: + void LoadReplay(); bool OnEvent(const irr::SEvent& event) override; void SynchronizeElement(irr::gui::IGUIElement* elem) const; std::unordered_multimap synchronized_elements; diff --git a/gframe/replay_mode.cpp b/gframe/replay_mode.cpp index a396a112d..904f8d204 100644 --- a/gframe/replay_mode.cpp +++ b/gframe/replay_mode.cpp @@ -117,6 +117,15 @@ int ReplayMode::ReplayThread() { current_step = 0; if(mainGame->dInfo.isCatchingUp) mainGame->gMutex.lock(); + + // check pause after load + if (is_pausing) { + is_paused = true; + std::unique_lock lock(mainGame->gMutex); + mainGame->actionSignal.Wait(lock); + is_paused = false; + } + for(auto it = current_stream.begin(); is_continuing && !exit_pending && it != current_stream.end();) { is_continuing = ReplayAnalyze((*it)); if(is_restarting) { @@ -173,6 +182,8 @@ void ReplayMode::EndDuel() { mainGame->stTip->setVisible(false); gSoundManager->StopSounds(); mainGame->device->setEventReceiver(&mainGame->menuHandler); + if(mainGame->exitAfter) + mainGame->device->closeDevice(); } } void ReplayMode::Restart(bool refresh) {