From 957a980caafc83370de1fcbd65ea30484857036f Mon Sep 17 00:00:00 2001 From: lmaosssdll Date: Wed, 1 Apr 2026 12:42:03 +0800 Subject: [PATCH 01/30] fix the crash when create lobby --- src/layers/Lobby.hpp | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/layers/Lobby.hpp b/src/layers/Lobby.hpp index 254c9f4..dda8a81 100755 --- a/src/layers/Lobby.hpp +++ b/src/layers/Lobby.hpp @@ -1,3 +1,5 @@ +#pragma once + #include #include @@ -7,7 +9,7 @@ using namespace geode::prelude; class CR_DLL PlayerCell : public CCLayer { protected: - Account m_account; + Account m_account{}; // ✅ value-initialize bool init(Account account, float width, bool canKick); void onKickUser(CCObject*); @@ -23,19 +25,19 @@ class CR_DLL LobbyLayer : public CCLayer { std::string lobbyNspace; - CCMenuItemSpriteExtra* closeBtn; - CCSprite* background; + CCMenuItemSpriteExtra* closeBtn = nullptr; // ✅ + CCSprite* background = nullptr; // ✅ - CCArray* playerListItems; - CustomListView* playerList; + CCArray* playerListItems = nullptr; // ✅ + CustomListView* playerList = nullptr; // ✅ ГЛАВНЫЙ ВИНОВНИК КРАША - CCLabelBMFont* titleLabel; + CCLabelBMFont* titleLabel = nullptr; // ✅ - CCMenuItemSpriteExtra* settingsBtn; - CCMenuItemSpriteExtra* startBtn; + CCMenuItemSpriteExtra* settingsBtn = nullptr; // ✅ + CCMenuItemSpriteExtra* startBtn = nullptr; // ✅ - LoadingCircle* loadingCircle; - CCNode* mainLayer; + LoadingCircle* loadingCircle = nullptr; // ✅ + CCNode* mainLayer = nullptr; // ✅ bool init(std::string code); void keyBackClicked(); @@ -54,4 +56,3 @@ class CR_DLL LobbyLayer : public CCLayer { public: static LobbyLayer* create(std::string code); }; - From 248a0e30380cb97fe905e182e2a1baca7f68519f Mon Sep 17 00:00:00 2001 From: lmaosssdll Date: Wed, 1 Apr 2026 12:42:38 +0800 Subject: [PATCH 02/30] fix the crash when create lobby --- src/layers/Lobby.cpp | 106 ++++++++++++++++++++++--------------------- 1 file changed, 55 insertions(+), 51 deletions(-) diff --git a/src/layers/Lobby.cpp b/src/layers/Lobby.cpp index 0f2f0d4..51df9d1 100755 --- a/src/layers/Lobby.cpp +++ b/src/layers/Lobby.cpp @@ -10,13 +10,11 @@ #include +// ==================== PlayerCell ==================== // + PlayerCell* PlayerCell::create(Account account, float width, bool canKick) { auto ret = new PlayerCell; - if (ret->init( - account, - width, - canKick - )) { + if (ret->init(account, width, canKick)) { ret->autorelease(); return ret; } @@ -32,6 +30,7 @@ bool PlayerCell::init(Account account, float width, bool canKick) { CELL_HEIGHT }); + // === Иконка игрока === // auto player = SimplePlayer::create(0); auto gm = GameManager::get(); @@ -46,36 +45,48 @@ bool PlayerCell::init(Account account, float width, bool canKick) { } player->setScale(0.65f); - player->setPosition({ 25.f, CELL_HEIGHT / 2.f}); + player->setPosition({ 25.f, CELL_HEIGHT / 2.f }); player->setAnchorPoint({ 0.5f, 0.5f }); this->addChild(player); + // === Имя игрока (кликабельное — открывает профиль) === // auto nameLabel = CCLabelBMFont::create(account.name.c_str(), "bigFont.fnt"); nameLabel->limitLabelWidth(225.f, 0.8f, 0.1f); - nameLabel->setPosition({ - 45.f, CELL_HEIGHT / 2.f - }); nameLabel->setAnchorPoint({ 0.f, 0.5f }); - nameLabel->ignoreAnchorPointForPosition(false); - this->addChild(nameLabel); + auto nameBtn = CCMenuItemExt::createSpriteExtra( + nameLabel, + [this](CCObject*) { + // Открываем профиль игрока в GD + // Примечание: ProfilePage::create() ожидает accountID. + // Если профиль открывается неправильно, проверь структуру Account + // в types/lobby.hpp — возможно нужно использовать account.accountID + ProfilePage::create(m_account.userID, false)->show(); + } + ); + nameBtn->setAnchorPoint({ 0.f, 0.5f }); + nameBtn->setPosition({ 45.f, CELL_HEIGHT / 2.f }); + + // === Общее меню ячейки === // + auto cellMenu = CCMenu::create(); + cellMenu->setPosition({ 0.f, 0.f }); + cellMenu->setContentSize({ width, CELL_HEIGHT }); + cellMenu->addChild(nameBtn); + // === Кнопка кика === // if (canKick && account.userID != GameManager::get()->m_playerUserID.value()) { auto kickSpr = CCSprite::createWithSpriteFrameName("accountBtn_removeFriend_001.png"); kickSpr->setScale(0.725f); auto kickBtn = CCMenuItemSpriteExtra::create( kickSpr, this, menu_selector(PlayerCell::onKickUser) ); - auto kickMenu = CCMenu::create(); - kickMenu->addChild(kickBtn); - kickMenu->setPosition( - width - 25.f, CELL_HEIGHT / 2.f - ); - kickMenu->setAnchorPoint({ 0.5f, 0.5f }); - this->addChild(kickMenu); + kickBtn->setPosition({ width - 25.f, CELL_HEIGHT / 2.f }); + cellMenu->addChild(kickBtn); } + this->addChild(cellMenu); + return true; } @@ -86,13 +97,15 @@ void PlayerCell::onKickUser(CCObject* sender) { "Close", "Kick", [this](auto, bool btn2) { if (!btn2) return; - + auto& nm = NetworkManager::get(); nm.send(KickUserPacket::create(m_account.userID)); } ); } +// ==================== LobbyLayer ==================== // + LobbyLayer* LobbyLayer::create(std::string code) { auto ret = new LobbyLayer(); if (ret->init(code)) { @@ -185,7 +198,7 @@ bool LobbyLayer::init(std::string code) { 1.f, [this](CCObject* sender) { SwapManager::get().getLobbyInfo([this](LobbyInfo info) { - refresh(info); + refresh(info); }); } ); @@ -234,7 +247,7 @@ bool LobbyLayer::init(std::string code) { this->addChild(bottomMenu); SwapManager::get().getLobbyInfo([this](LobbyInfo info) { - refresh(info, true); + refresh(info, true); }); registerListeners(); @@ -269,23 +282,17 @@ void LobbyLayer::unregisterListeners() { LobbyLayer::~LobbyLayer() { unregisterListeners(); - // if (mainLayer) mainLayer->release(); } void LobbyLayer::refresh(LobbyInfo info, bool isFirstRefresh) { isOwner = GameManager::get()->m_playerUserID == info.settings.owner.userID; - // loadingCircle = LoadingCircle::create(); - // loadingCircle->setParentLayer(this); - // loadingCircle->setFade(true); - // loadingCircle->show(); - auto size = CCDirector::sharedDirector()->getWinSize(); auto listWidth = size.width / 1.5f; if (!mainLayer) return; - // mainLayer->retain(); + // === Заголовок (создаётся только при первом вызове) === // if (isFirstRefresh) { titleLabel = CCLabelBMFont::create( info.settings.name.c_str(), @@ -313,6 +320,7 @@ void LobbyLayer::refresh(LobbyInfo info, bool isFirstRefresh) { mainLayer->addChild(menu); } + // ✅ Безопасная проверка перед использованием if (titleLabel) titleLabel->setString( fmt::format("{} ({})", info.settings.name, @@ -320,21 +328,27 @@ void LobbyLayer::refresh(LobbyInfo info, bool isFirstRefresh) { ).c_str() ); + // ✅ Теперь playerList = nullptr при первом вызове (благодаря инициализации в .hpp) + // Поэтому эта проверка работает корректно if (!playerList && !isFirstRefresh) return; if (playerList) playerList->removeFromParent(); - using namespace geode::utils; + // === Построение списка игроков === // playerListItems = CCArray::create(); - for (auto acc : info.accounts) { - playerListItems->addObject( - PlayerCell::create( - acc, - listWidth, - isOwner - ) - ); + for (auto& acc : info.accounts) { // ✅ & чтобы не копировать + auto cell = PlayerCell::create(acc, listWidth, isOwner); + if (cell) playerListItems->addObject(cell); // ✅ null-check } + + // ✅ Защита от пустого списка + if (playerListItems->count() == 0) { + playerList = nullptr; + return; + } + playerList = ListView::create(playerListItems, PlayerCell::CELL_HEIGHT, listWidth); + if (!playerList) return; // ✅ защита + playerList->setPosition({ size.width / 2, size.height / 2 - 10.f }); playerList->setAnchorPoint({ 0.5f, 0.5f }); playerList->ignoreAnchorPointForPosition(false); @@ -356,10 +370,9 @@ void LobbyLayer::refresh(LobbyInfo info, bool isFirstRefresh) { mainLayer->addChild(listBG); } - settingsBtn->setVisible(isOwner); - startBtn->setVisible(isOwner); - - // loadingCircle->fadeAndRemove(); + // ✅ Безопасные проверки + if (settingsBtn) settingsBtn->setVisible(isOwner); + if (startBtn) startBtn->setVisible(isOwner); } void LobbyLayer::onStart(CCObject* sender) { @@ -389,15 +402,12 @@ void LobbyLayer::onSettings(CCObject* sender) { } void LobbyLayer::createBorders() { - // SIDES // - #define CREATE_SIDE() CCSprite::createWithSpriteFrameName("GJ_table_side_001.png") - + const int SIDE_OFFSET = 7; const int TOP_BOTTOM_OFFSET = 8; // TOP // - auto topSide = CREATE_SIDE(); topSide->setScaleY( playerList->getContentWidth() / topSide->getContentHeight() @@ -411,7 +421,6 @@ void LobbyLayer::createBorders() { topSide->setZOrder(3); // BOTTOM // - auto bottomSide = CREATE_SIDE(); bottomSide->setScaleY( playerList->getContentWidth() / bottomSide->getContentHeight() @@ -425,7 +434,6 @@ void LobbyLayer::createBorders() { bottomSide->setZOrder(3); // LEFT // - auto leftSide = CREATE_SIDE(); leftSide->setScaleY( (playerList->getContentHeight() + TOP_BOTTOM_OFFSET) / leftSide->getContentHeight() @@ -437,7 +445,6 @@ void LobbyLayer::createBorders() { leftSide->setID("left-border"); // RIGHT // - auto rightSide = CREATE_SIDE(); rightSide->setScaleY( (playerList->getContentHeight() + TOP_BOTTOM_OFFSET) / rightSide->getContentHeight() @@ -455,11 +462,9 @@ void LobbyLayer::createBorders() { playerList->addChild(rightSide); // CORNERS // - #define CREATE_CORNER() CCSprite::createWithSpriteFrameName("GJ_table_corner_001.png") // TOP-LEFT // - auto topLeftCorner = CREATE_CORNER(); topLeftCorner->setPosition({ leftSide->getPositionX(), topSide->getPositionY() @@ -477,7 +482,6 @@ void LobbyLayer::createBorders() { topRightCorner->setID("top-right-corner"); // BOTTOM-LEFT // - auto bottomLeftCorner = CREATE_CORNER(); bottomLeftCorner->setFlipY(true); bottomLeftCorner->setPosition({ From 961ce6e6aece9e35b4c88a9683c6ff76b6098ebc Mon Sep 17 00:00:00 2001 From: lmaosssdll Date: Wed, 1 Apr 2026 13:17:10 +0800 Subject: [PATCH 03/30] uptade to new version --- mod.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mod.json b/mod.json index 67f6be7..a354349 100755 --- a/mod.json +++ b/mod.json @@ -1,5 +1,5 @@ { - "geode": "5.3.0", + "geode": "5.5.1", "gd": { "win": "2.2081", "android": "2.2081", From 998e2ead765dfd698324fed342dc566e290c0c6a Mon Sep 17 00:00:00 2001 From: lmaosssdll Date: Wed, 1 Apr 2026 13:47:15 +0800 Subject: [PATCH 04/30] fix the id for gd --- src/layers/Lobby.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/layers/Lobby.cpp b/src/layers/Lobby.cpp index 51df9d1..a480f08 100755 --- a/src/layers/Lobby.cpp +++ b/src/layers/Lobby.cpp @@ -62,7 +62,7 @@ bool PlayerCell::init(Account account, float width, bool canKick) { // Примечание: ProfilePage::create() ожидает accountID. // Если профиль открывается неправильно, проверь структуру Account // в types/lobby.hpp — возможно нужно использовать account.accountID - ProfilePage::create(m_account.userID, false)->show(); + ProfilePage::create(m_account.accountID, false)->show(); } ); nameBtn->setAnchorPoint({ 0.f, 0.5f }); From bf6040d10d28b941ee877f9ce2a4e25fb9aeead0 Mon Sep 17 00:00:00 2001 From: lmaosssdll Date: Wed, 1 Apr 2026 14:26:23 +0800 Subject: [PATCH 05/30] delete comments --- src/layers/Lobby.hpp | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/layers/Lobby.hpp b/src/layers/Lobby.hpp index dda8a81..b4d3730 100755 --- a/src/layers/Lobby.hpp +++ b/src/layers/Lobby.hpp @@ -9,7 +9,7 @@ using namespace geode::prelude; class CR_DLL PlayerCell : public CCLayer { protected: - Account m_account{}; // ✅ value-initialize + Account m_account{}; bool init(Account account, float width, bool canKick); void onKickUser(CCObject*); @@ -25,19 +25,19 @@ class CR_DLL LobbyLayer : public CCLayer { std::string lobbyNspace; - CCMenuItemSpriteExtra* closeBtn = nullptr; // ✅ - CCSprite* background = nullptr; // ✅ + CCMenuItemSpriteExtra* closeBtn = nullptr; + CCSprite* background = nullptr; - CCArray* playerListItems = nullptr; // ✅ - CustomListView* playerList = nullptr; // ✅ ГЛАВНЫЙ ВИНОВНИК КРАША + CCArray* playerListItems = nullptr; + CustomListView* playerList = nullptr; - CCLabelBMFont* titleLabel = nullptr; // ✅ + CCLabelBMFont* titleLabel = nullptr; - CCMenuItemSpriteExtra* settingsBtn = nullptr; // ✅ - CCMenuItemSpriteExtra* startBtn = nullptr; // ✅ + CCMenuItemSpriteExtra* settingsBtn = nullptr; + CCMenuItemSpriteExtra* startBtn = nullptr; - LoadingCircle* loadingCircle = nullptr; // ✅ - CCNode* mainLayer = nullptr; // ✅ + LoadingCircle* loadingCircle = nullptr; + CCNode* mainLayer = nullptr; bool init(std::string code); void keyBackClicked(); From efb2cf2d8a9e0498567776c3d5f2edcdc37b18f1 Mon Sep 17 00:00:00 2001 From: lmaosssdll Date: Wed, 1 Apr 2026 14:27:38 +0800 Subject: [PATCH 06/30] delete comments --- src/layers/Lobby.cpp | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/src/layers/Lobby.cpp b/src/layers/Lobby.cpp index a480f08..670e3a1 100755 --- a/src/layers/Lobby.cpp +++ b/src/layers/Lobby.cpp @@ -30,7 +30,6 @@ bool PlayerCell::init(Account account, float width, bool canKick) { CELL_HEIGHT }); - // === Иконка игрока === // auto player = SimplePlayer::create(0); auto gm = GameManager::get(); @@ -58,23 +57,17 @@ bool PlayerCell::init(Account account, float width, bool canKick) { auto nameBtn = CCMenuItemExt::createSpriteExtra( nameLabel, [this](CCObject*) { - // Открываем профиль игрока в GD - // Примечание: ProfilePage::create() ожидает accountID. - // Если профиль открывается неправильно, проверь структуру Account - // в types/lobby.hpp — возможно нужно использовать account.accountID ProfilePage::create(m_account.accountID, false)->show(); } ); nameBtn->setAnchorPoint({ 0.f, 0.5f }); nameBtn->setPosition({ 45.f, CELL_HEIGHT / 2.f }); - // === Общее меню ячейки === // auto cellMenu = CCMenu::create(); cellMenu->setPosition({ 0.f, 0.f }); cellMenu->setContentSize({ width, CELL_HEIGHT }); cellMenu->addChild(nameBtn); - // === Кнопка кика === // if (canKick && account.userID != GameManager::get()->m_playerUserID.value()) { auto kickSpr = CCSprite::createWithSpriteFrameName("accountBtn_removeFriend_001.png"); kickSpr->setScale(0.725f); @@ -320,7 +313,6 @@ void LobbyLayer::refresh(LobbyInfo info, bool isFirstRefresh) { mainLayer->addChild(menu); } - // ✅ Безопасная проверка перед использованием if (titleLabel) titleLabel->setString( fmt::format("{} ({})", info.settings.name, @@ -328,26 +320,23 @@ void LobbyLayer::refresh(LobbyInfo info, bool isFirstRefresh) { ).c_str() ); - // ✅ Теперь playerList = nullptr при первом вызове (благодаря инициализации в .hpp) - // Поэтому эта проверка работает корректно if (!playerList && !isFirstRefresh) return; if (playerList) playerList->removeFromParent(); // === Построение списка игроков === // playerListItems = CCArray::create(); - for (auto& acc : info.accounts) { // ✅ & чтобы не копировать + for (auto& acc : info.accounts) { auto cell = PlayerCell::create(acc, listWidth, isOwner); - if (cell) playerListItems->addObject(cell); // ✅ null-check + if (cell) playerListItems->addObject(cell); } - // ✅ Защита от пустого списка if (playerListItems->count() == 0) { playerList = nullptr; return; } playerList = ListView::create(playerListItems, PlayerCell::CELL_HEIGHT, listWidth); - if (!playerList) return; // ✅ защита + if (!playerList) return; playerList->setPosition({ size.width / 2, size.height / 2 - 10.f }); playerList->setAnchorPoint({ 0.5f, 0.5f }); @@ -370,7 +359,6 @@ void LobbyLayer::refresh(LobbyInfo info, bool isFirstRefresh) { mainLayer->addChild(listBG); } - // ✅ Безопасные проверки if (settingsBtn) settingsBtn->setVisible(isOwner); if (startBtn) startBtn->setVisible(isOwner); } From a2398e05f3e30dc8be93c7d50ead9592a23701be Mon Sep 17 00:00:00 2001 From: lmaosssdll Date: Wed, 1 Apr 2026 14:28:46 +0800 Subject: [PATCH 07/30] Remove comment Removed comment about building the player list. --- src/layers/Lobby.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/layers/Lobby.cpp b/src/layers/Lobby.cpp index 670e3a1..8abbf90 100755 --- a/src/layers/Lobby.cpp +++ b/src/layers/Lobby.cpp @@ -323,7 +323,6 @@ void LobbyLayer::refresh(LobbyInfo info, bool isFirstRefresh) { if (!playerList && !isFirstRefresh) return; if (playerList) playerList->removeFromParent(); - // === Построение списка игроков === // playerListItems = CCArray::create(); for (auto& acc : info.accounts) { auto cell = PlayerCell::create(acc, listWidth, isOwner); From c9ab7bde1bc4bc1ab21c459af5de82c27e9f9389 Mon Sep 17 00:00:00 2001 From: lmaosssdll Date: Wed, 1 Apr 2026 14:48:15 +0800 Subject: [PATCH 08/30] fix other crash error (desync server and client) --- src/layers/Lobby.cpp | 171 +++++++++++++------------------------------ 1 file changed, 52 insertions(+), 119 deletions(-) diff --git a/src/layers/Lobby.cpp b/src/layers/Lobby.cpp index 8abbf90..ddeca43 100755 --- a/src/layers/Lobby.cpp +++ b/src/layers/Lobby.cpp @@ -1,17 +1,12 @@ #include "Lobby.hpp" #include - #include #include #include - #include "LobbySettings.hpp" #include "ChatPanel.hpp" - #include -// ==================== PlayerCell ==================== // - PlayerCell* PlayerCell::create(Account account, float width, bool canKick) { auto ret = new PlayerCell; if (ret->init(account, width, canKick)) { @@ -25,10 +20,7 @@ PlayerCell* PlayerCell::create(Account account, float width, bool canKick) { bool PlayerCell::init(Account account, float width, bool canKick) { m_account = account; - this->setContentSize({ - width, - CELL_HEIGHT - }); + this->setContentSize({ width, CELL_HEIGHT }); auto player = SimplePlayer::create(0); auto gm = GameManager::get(); @@ -49,7 +41,6 @@ bool PlayerCell::init(Account account, float width, bool canKick) { this->addChild(player); - // === Имя игрока (кликабельное — открывает профиль) === // auto nameLabel = CCLabelBMFont::create(account.name.c_str(), "bigFont.fnt"); nameLabel->limitLabelWidth(225.f, 0.8f, 0.1f); nameLabel->setAnchorPoint({ 0.f, 0.5f }); @@ -90,15 +81,12 @@ void PlayerCell::onKickUser(CCObject* sender) { "Close", "Kick", [this](auto, bool btn2) { if (!btn2) return; - auto& nm = NetworkManager::get(); nm.send(KickUserPacket::create(m_account.userID)); } ); } -// ==================== LobbyLayer ==================== // - LobbyLayer* LobbyLayer::create(std::string code) { auto ret = new LobbyLayer(); if (ret->init(code)) { @@ -111,6 +99,7 @@ LobbyLayer* LobbyLayer::create(std::string code) { bool LobbyLayer::init(std::string code) { lobbyCode = code; + m_alive = std::make_shared(true); ChatPanel::initialize(); @@ -121,12 +110,8 @@ bool LobbyLayer::init(std::string code) { mainLayer->setContentSize(size); background = CCSprite::create("GJ_gradientBG.png"); - background->setScaleX( - size.width / background->getContentWidth() - ); - background->setScaleY( - size.height / background->getContentHeight() - ); + background->setScaleX(size.width / background->getContentWidth()); + background->setScaleY(size.height / background->getContentHeight()); background->setAnchorPoint({ 0, 0 }); background->setColor({ 0, 102, 255 }); background->setZOrder(-10); @@ -150,9 +135,7 @@ bool LobbyLayer::init(std::string code) { auto closeMenu = CCMenu::create(); closeMenu->addChild(closeBtn); closeMenu->addChild(disconnectBtn); - closeMenu->setLayout( - RowLayout::create() - ); + closeMenu->setLayout(RowLayout::create()); closeMenu->setPosition({ 45, size.height - 25 }); mainLayer->addChild(closeMenu); @@ -167,9 +150,7 @@ bool LobbyLayer::init(std::string code) { auto startMenu = CCMenu::create(); startMenu->setZOrder(5); startMenu->addChild(startBtn); - startMenu->setPosition({ - size.width / 2, 30.f - }); + startMenu->setPosition({ size.width / 2, 30.f }); mainLayer->addChild(startMenu); @@ -189,8 +170,9 @@ bool LobbyLayer::init(std::string code) { auto refreshBtn = CCMenuItemExt::createSpriteExtraWithFrameName( "GJ_getSongInfoBtn_001.png", 1.f, - [this](CCObject* sender) { - SwapManager::get().getLobbyInfo([this](LobbyInfo info) { + [this, alive = m_alive](CCObject* sender) { + SwapManager::get().getLobbyInfo([this, alive](LobbyInfo info) { + if (!*alive) return; refresh(info); }); } @@ -202,29 +184,21 @@ bool LobbyLayer::init(std::string code) { bottomMenu->addChild(msgButton); bottomMenu->addChild(settingsBtn); bottomMenu->addChild(refreshBtn); - bottomMenu->setPosition( - size.width - 25.f, bottomMenu->getChildrenCount() * 25.f - ); - - bottomMenu->setLayout( - ColumnLayout::create() - ->setAxisReverse(true) - ); + bottomMenu->setPosition(size.width - 25.f, bottomMenu->getChildrenCount() * 25.f); + bottomMenu->setLayout(ColumnLayout::create()->setAxisReverse(true)); auto discordSpr = CCSprite::createWithSpriteFrameName("gj_discordIcon_001.png"); discordSpr->setScale(1.25f); auto discordBtn = CCMenuItemExt::createSpriteExtra( discordSpr, - [this](CCObject* target) { + [](CCObject* target) { geode::createQuickPopup( "Discord Server", "We have a Discord Server! If you have questions, want to suggest a feature, or find someone to swap with, join here! \nPlease not that you need to be at least 13 years of age to join our server.", "Cancel", "Join!", - [this](auto, bool btn2) { + [](auto, bool btn2) { if (!btn2) return; - CCApplication::get()->openURL( - Mod::get()->getMetadata().getLinks().getCommunityURL()->c_str() - ); + CCApplication::get()->openURL(Mod::get()->getMetadata().getLinks().getCommunityURL()->c_str()); } ); } @@ -239,7 +213,8 @@ bool LobbyLayer::init(std::string code) { this->addChild(mainLayer); this->addChild(bottomMenu); - SwapManager::get().getLobbyInfo([this](LobbyInfo info) { + SwapManager::get().getLobbyInfo([this, alive = m_alive](LobbyInfo info) { + if (!*alive) return; refresh(info, true); }); registerListeners(); @@ -249,18 +224,19 @@ bool LobbyLayer::init(std::string code) { void LobbyLayer::registerListeners() { auto& nm = NetworkManager::get(); - nm.on([this](LobbyUpdatedPacket packet) { + nm.on([this, alive = m_alive](LobbyUpdatedPacket packet) { + if (!*alive) return; this->refresh(std::move(packet.info)); }); - nm.on([this](SwapStartedPacket packet) { + nm.on([](SwapStartedPacket packet) { auto& sm = SwapManager::get(); sm.startSwap(std::move(packet)); NetworkManager::get().unbind(); }); nm.showDisconnectPopup = true; - nm.setDisconnectCallback([this](std::string reason) { + nm.setDisconnectCallback([this, alive = m_alive](std::string reason) { + if (!*alive) return; unregisterListeners(); - auto& nm = NetworkManager::get(); nm.isConnected = false; nm.showDisconnectPopup = false; @@ -271,9 +247,14 @@ void LobbyLayer::registerListeners() { void LobbyLayer::unregisterListeners() { auto& nm = NetworkManager::get(); nm.unbind(); + nm.unbind(); + nm.setDisconnectCallback(nullptr); } LobbyLayer::~LobbyLayer() { + if (m_alive) { + *m_alive = false; + } unregisterListeners(); } @@ -285,26 +266,18 @@ void LobbyLayer::refresh(LobbyInfo info, bool isFirstRefresh) { if (!mainLayer) return; - // === Заголовок (создаётся только при первом вызове) === // if (isFirstRefresh) { - titleLabel = CCLabelBMFont::create( - info.settings.name.c_str(), - "bigFont.fnt" - ); + titleLabel = CCLabelBMFont::create(info.settings.name.c_str(), "bigFont.fnt"); titleLabel->limitLabelWidth(275.f, 1.f, 0.1f); auto menu = CCMenu::create(); - menu->setPosition({ - size.width / 2, size.height - 25 - }); + menu->setPosition({ size.width / 2, size.height - 25 }); menu->addChild( CCMenuItemExt::createSpriteExtra( titleLabel, [info](CCObject* sender) { - geode::utils::clipboard::write( - info.code - ); + geode::utils::clipboard::write(info.code); Notification::create("Copied code to clipboard")->show(); } ) @@ -345,18 +318,17 @@ void LobbyLayer::refresh(LobbyInfo info, bool isFirstRefresh) { mainLayer->addChild(playerList); - if (!mainLayer->getChildByIDRecursive("list-bg")) { - auto listBG = CCLayerColor::create({ 0, 0, 0, 85 }); + auto listBG = static_cast(mainLayer->getChildByIDRecursive("list-bg")); + if (!listBG) { + listBG = CCLayerColor::create({ 0, 0, 0, 85 }); listBG->ignoreAnchorPointForPosition(false); listBG->setAnchorPoint({ 0.5f, 0.5f }); - listBG->setPosition( - playerList->getPosition() - ); listBG->setZOrder(-1); listBG->setID("list-bg"); - listBG->setContentSize(playerList->getContentSize()); mainLayer->addChild(listBG); } + listBG->setPosition(playerList->getPosition()); + listBG->setContentSize(playerList->getContentSize()); if (settingsBtn) settingsBtn->setVisible(isOwner); if (startBtn) startBtn->setVisible(isOwner); @@ -379,8 +351,10 @@ void LobbyLayer::onStart(CCObject* sender) { void LobbyLayer::onSettings(CCObject* sender) { auto& lm = SwapManager::get(); - lm.getLobbyInfo([this](LobbyInfo info) { - auto settingsPopup = LobbySettingsPopup::create(info.settings, [this](LobbySettings settings) { + lm.getLobbyInfo([this, alive = m_alive](LobbyInfo info) { + if (!*alive) return; + auto settingsPopup = LobbySettingsPopup::create(info.settings, [this, alive](LobbySettings settings) { + if (!*alive) return; auto& lm = SwapManager::get(); lm.updateLobby(settings); }); @@ -394,53 +368,29 @@ void LobbyLayer::createBorders() { const int SIDE_OFFSET = 7; const int TOP_BOTTOM_OFFSET = 8; - // TOP // auto topSide = CREATE_SIDE(); - topSide->setScaleY( - playerList->getContentWidth() / topSide->getContentHeight() - ); + topSide->setScaleY(playerList->getContentWidth() / topSide->getContentHeight()); topSide->setRotation(90.f); - topSide->setPosition({ - playerList->m_width / 2, - playerList->m_height + TOP_BOTTOM_OFFSET - }); + topSide->setPosition({ playerList->m_width / 2, playerList->m_height + TOP_BOTTOM_OFFSET }); topSide->setID("top-border"); topSide->setZOrder(3); - // BOTTOM // auto bottomSide = CREATE_SIDE(); - bottomSide->setScaleY( - playerList->getContentWidth() / bottomSide->getContentHeight() - ); + bottomSide->setScaleY(playerList->getContentWidth() / bottomSide->getContentHeight()); bottomSide->setRotation(-90.f); - bottomSide->setPosition({ - playerList->m_width / 2, - 0 - TOP_BOTTOM_OFFSET - }); + bottomSide->setPosition({ playerList->m_width / 2, 0.f - TOP_BOTTOM_OFFSET }); bottomSide->setID("bottom-border"); bottomSide->setZOrder(3); - // LEFT // auto leftSide = CREATE_SIDE(); - leftSide->setScaleY( - (playerList->getContentHeight() + TOP_BOTTOM_OFFSET) / leftSide->getContentHeight() - ); - leftSide->setPosition({ - -SIDE_OFFSET, - playerList->m_height / 2 - }); + leftSide->setScaleY((playerList->getContentHeight() + TOP_BOTTOM_OFFSET) / leftSide->getContentHeight()); + leftSide->setPosition({ -SIDE_OFFSET, playerList->m_height / 2 }); leftSide->setID("left-border"); - // RIGHT // auto rightSide = CREATE_SIDE(); - rightSide->setScaleY( - (playerList->getContentHeight() + TOP_BOTTOM_OFFSET) / rightSide->getContentHeight() - ); + rightSide->setScaleY((playerList->getContentHeight() + TOP_BOTTOM_OFFSET) / rightSide->getContentHeight()); rightSide->setRotation(180.f); - rightSide->setPosition({ - playerList->m_width + SIDE_OFFSET, - playerList->m_height / 2 - }); + rightSide->setPosition({ playerList->m_width + SIDE_OFFSET, playerList->m_height / 2 }); rightSide->setID("right-border"); playerList->addChild(topSide); @@ -448,42 +398,29 @@ void LobbyLayer::createBorders() { playerList->addChild(leftSide); playerList->addChild(rightSide); - // CORNERS // #define CREATE_CORNER() CCSprite::createWithSpriteFrameName("GJ_table_corner_001.png") - // TOP-LEFT // auto topLeftCorner = CREATE_CORNER(); - topLeftCorner->setPosition({ - leftSide->getPositionX(), topSide->getPositionY() - }); + topLeftCorner->setPosition({ leftSide->getPositionX(), topSide->getPositionY() }); topLeftCorner->setZOrder(2); topLeftCorner->setID("top-left-corner"); - // TOP-RIGHT // auto topRightCorner = CREATE_CORNER(); topRightCorner->setFlipX(true); - topRightCorner->setPosition({ - rightSide->getPositionX(), topSide->getPositionY() - }); + topRightCorner->setPosition({ rightSide->getPositionX(), topSide->getPositionY() }); topRightCorner->setZOrder(2); topRightCorner->setID("top-right-corner"); - // BOTTOM-LEFT // auto bottomLeftCorner = CREATE_CORNER(); bottomLeftCorner->setFlipY(true); - bottomLeftCorner->setPosition({ - leftSide->getPositionX(), bottomSide->getPositionY() - }); + bottomLeftCorner->setPosition({ leftSide->getPositionX(), bottomSide->getPositionY() }); bottomLeftCorner->setZOrder(2); bottomLeftCorner->setID("bottom-left-corner"); - // BOTTOM-RIGHT // auto bottomRightCorner = CREATE_CORNER(); bottomRightCorner->setFlipX(true); bottomRightCorner->setFlipY(true); - bottomRightCorner->setPosition({ - rightSide->getPositionX(), bottomSide->getPositionY() - }); + bottomRightCorner->setPosition({ rightSide->getPositionX(), bottomSide->getPositionY() }); bottomRightCorner->setZOrder(2); bottomRightCorner->setID("bottom-right-corner"); @@ -498,15 +435,12 @@ void LobbyLayer::onDisconnect(CCObject* sender) { "Disconnect", "Are you sure you want to disconnect from the lobby and server?", "Cancel", "Yes", - [this](auto, bool btn2) { + [](auto, bool btn2) { if (!btn2) return; - auto& nm = NetworkManager::get(); nm.showDisconnectPopup = false; - auto& lm = SwapManager::get(); lm.disconnectLobby(); - cr::utils::popScene(); } ); @@ -517,9 +451,8 @@ void LobbyLayer::keyBackClicked() { "Leave Layer?", "Are you sure you want to leave the layer? This will not disconnect you and will allow you to do other things ingame. You will be booted to the level page when the swap begins.", "No", "Yes", - [this](auto, bool btn2) { + [](auto, bool btn2) { if (!btn2) return; - cr::utils::popScene(); } ); From a2f01cb538fe4087389e47ab03a4e401d30be7d1 Mon Sep 17 00:00:00 2001 From: lmaosssdll Date: Wed, 1 Apr 2026 14:48:59 +0800 Subject: [PATCH 09/30] fix other crash error (desync server and client) --- src/layers/Lobby.hpp | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/layers/Lobby.hpp b/src/layers/Lobby.hpp index b4d3730..c7a59d7 100755 --- a/src/layers/Lobby.hpp +++ b/src/layers/Lobby.hpp @@ -2,8 +2,8 @@ #include #include - #include +#include using namespace geode::prelude; @@ -22,32 +22,25 @@ class CR_DLL LobbyLayer : public CCLayer { protected: std::string lobbyCode; bool isOwner = false; - std::string lobbyNspace; + std::shared_ptr m_alive; CCMenuItemSpriteExtra* closeBtn = nullptr; CCSprite* background = nullptr; - CCArray* playerListItems = nullptr; CustomListView* playerList = nullptr; - CCLabelBMFont* titleLabel = nullptr; - CCMenuItemSpriteExtra* settingsBtn = nullptr; CCMenuItemSpriteExtra* startBtn = nullptr; - LoadingCircle* loadingCircle = nullptr; CCNode* mainLayer = nullptr; bool init(std::string code); void keyBackClicked(); void createBorders(); - void refresh(LobbyInfo info, bool isFirstRefresh = false); - void registerListeners(); void unregisterListeners(); - void onDisconnect(CCObject*); void onStart(CCObject*); void onSettings(CCObject*); From 854aae86b5710d527f35f7c51041d2c28a5b86ec Mon Sep 17 00:00:00 2001 From: lmaosssdll Date: Wed, 1 Apr 2026 15:47:14 +0800 Subject: [PATCH 10/30] uptade chatpanel.cpp --- src/layers/ChatPanel.cpp | 42 ++++++++++++++-------------------------- 1 file changed, 15 insertions(+), 27 deletions(-) diff --git a/src/layers/ChatPanel.cpp b/src/layers/ChatPanel.cpp index 9174ded..0d4451d 100755 --- a/src/layers/ChatPanel.cpp +++ b/src/layers/ChatPanel.cpp @@ -13,12 +13,17 @@ ChatPanel* ChatPanel::create() { } void ChatPanel::initialize() { - // initialize the listeners n' stuff if they havent already if (!hasInitialized) { auto& nm = NetworkManager::get(); nm.on([](MessageSentPacket packet) { messages.push_back(packet.message); - messagesQueue.push_back(std::move(packet.message)); + messagesQueue.push_back(packet.message); + + // Уведомление о новом сообщении + Notification::create( + fmt::format("{} sent a message", packet.message.author.name), + CCSprite::createWithSpriteFrameName("GJ_chatBtn_001.png") + )->show(); }); hasInitialized = true; @@ -31,7 +36,6 @@ bool ChatPanel::init() { } this->setTitle("Chat"); - ChatPanel::initialize(); auto scrollContainer = CCScale9Sprite::create("square02b_001.png"); @@ -44,7 +48,6 @@ bool ChatPanel::init() { scrollLayer = ScrollLayer::create(scrollContainer->getContentSize() - 10.f); scrollLayer->ignoreAnchorPointForPosition(false); - // stolen from https://github.com/HJfod/LevelTrashcan/blob/main/src/TrashcanPopup.cpp#L41 scrollLayer->m_contentLayer->setLayout( ColumnLayout::create() ->setAxisReverse(true) @@ -53,19 +56,16 @@ bool ChatPanel::init() { ->setGap(0) ); scrollContainer->addChildAtPosition(scrollLayer, Anchor::Center); - m_mainLayer->addChildAtPosition(scrollContainer, Anchor::Center, ccp(0, 5.f)); auto inputContainer = CCNode::create(); - inputContainer->setContentSize({ - scrollContainer->getContentWidth(), - 75.f - }); - inputContainer->setAnchorPoint({ - 0.5f, 0.f - }); + inputContainer->setContentSize({ scrollContainer->getContentWidth(), 75.f }); + inputContainer->setAnchorPoint({ 0.5f, 0.f }); messageInput = TextInput::create(inputContainer->getContentWidth(), "Send a message to the lobby!", "chatFont.fnt"); + + // --- ИСПРАВЛЕНИЕ: Разрешаем любые символы (кириллицу, спецсимволы) --- + messageInput->setCommonFilter(CommonFilter::Any); auto sendMsgBtn = CCMenuItemExt::createSpriteExtraWithFrameName( "GJ_chatBtn_001.png", @@ -82,10 +82,7 @@ bool ChatPanel::init() { inputContainer->addChild(messageInput); inputContainer->addChild(msgBtnMenu); - - inputContainer->setLayout( - RowLayout::create() - ); + inputContainer->setLayout(RowLayout::create()); m_mainLayer->addChildAtPosition(inputContainer, Anchor::Bottom, ccp(0, 10.f)); for (auto message : ChatPanel::messages) { @@ -101,12 +98,7 @@ void ChatPanel::renderMessage(Message const& message) { auto msgNode = CCNode::create(); auto msgText = TextArea::create( fmt::format("{}: {}", message.author.name, message.message), - "chatFont.fnt", - 0.5f, - scrollLayer->getContentWidth(), - {0.f, 0.f}, - 17.f, - false + "chatFont.fnt", 0.5f, scrollLayer->getContentWidth(), {0.f, 0.f}, 17.f, false ); msgText->setAnchorPoint({ 0.f, 0.f }); msgNode->setContentHeight(msgText->m_label->m_lines->count() * 17.f); @@ -114,9 +106,7 @@ void ChatPanel::renderMessage(Message const& message) { msgText->setPosition({ 0.f, 0.f }); msgNode->addChild(msgText); - scrollLayer->m_contentLayer->addChild( - msgNode - ); + scrollLayer->m_contentLayer->addChild(msgNode); scrollLayer->m_contentLayer->updateLayout(); } @@ -129,7 +119,6 @@ void ChatPanel::updateMessages(float dt) { void ChatPanel::clearMessages() { messages.clear(); - auto& nm = NetworkManager::get(); nm.unbind(); hasInitialized = false; @@ -148,7 +137,6 @@ void ChatPanel::sendMessage() { void ChatPanel::keyDown(cocos2d::enumKeyCodes keycode, double timestamp) { if (keycode == cocos2d::KEY_Enter && CCIMEDispatcher::sharedDispatcher()->hasDelegate()) { - log::debug("sending via keybind"); sendMessage(); } else { Popup::keyDown(keycode, timestamp); From 0fca948d429c168b67754751289d83fdeca359d0 Mon Sep 17 00:00:00 2001 From: lmaosssdll Date: Wed, 1 Apr 2026 15:48:53 +0800 Subject: [PATCH 11/30] Add m_currentPlayers vector to Lobby class --- src/layers/Lobby.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/layers/Lobby.hpp b/src/layers/Lobby.hpp index c7a59d7..5e678c0 100755 --- a/src/layers/Lobby.hpp +++ b/src/layers/Lobby.hpp @@ -24,6 +24,7 @@ class CR_DLL LobbyLayer : public CCLayer { bool isOwner = false; std::string lobbyNspace; std::shared_ptr m_alive; + std::vector m_currentPlayers; CCMenuItemSpriteExtra* closeBtn = nullptr; CCSprite* background = nullptr; From 210adddd12bfd0072202576655bc5d485efb357c Mon Sep 17 00:00:00 2001 From: lmaosssdll Date: Wed, 1 Apr 2026 15:58:41 +0800 Subject: [PATCH 12/30] add tags for lobby, notifications when someone leaves or joins, and when someone sends a message. --- src/layers/Lobby.cpp | 72 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 62 insertions(+), 10 deletions(-) diff --git a/src/layers/Lobby.cpp b/src/layers/Lobby.cpp index ddeca43..0692ded 100755 --- a/src/layers/Lobby.cpp +++ b/src/layers/Lobby.cpp @@ -7,6 +7,8 @@ #include "ChatPanel.hpp" #include +// ==================== PlayerCell ==================== // + PlayerCell* PlayerCell::create(Account account, float width, bool canKick) { auto ret = new PlayerCell; if (ret->init(account, width, canKick)) { @@ -87,6 +89,8 @@ void PlayerCell::onKickUser(CCObject* sender) { ); } +// ==================== LobbyLayer ==================== // + LobbyLayer* LobbyLayer::create(std::string code) { auto ret = new LobbyLayer(); if (ret->init(code)) { @@ -266,23 +270,71 @@ void LobbyLayer::refresh(LobbyInfo info, bool isFirstRefresh) { if (!mainLayer) return; + // --- Логика Уведомлений Входа/Выхода --- + if (isFirstRefresh) { + m_currentPlayers.clear(); + for (auto& acc : info.accounts) { + m_currentPlayers.push_back(acc.userID); + } + } else { + std::vector newPlayers; + for (auto& acc : info.accounts) newPlayers.push_back(acc.userID); + + for (auto& acc : info.accounts) { + if (std::find(m_currentPlayers.begin(), m_currentPlayers.end(), acc.userID) == m_currentPlayers.end()) { + Notification::create( + fmt::format("{} joined", acc.name), + CCSprite::createWithSpriteFrameName("GJ_sStarsIcon_001.png") + )->show(); + } + } + for (int id : m_currentPlayers) { + if (std::find(newPlayers.begin(), newPlayers.end(), id) == newPlayers.end()) { + Notification::create( + "Someone left", + CCSprite::createWithSpriteFrameName("GJ_deleteIcon_001.png") + )->show(); + } + } + m_currentPlayers = newPlayers; + } + + // --- Заголовок и Кнопка Информации --- if (isFirstRefresh) { titleLabel = CCLabelBMFont::create(info.settings.name.c_str(), "bigFont.fnt"); titleLabel->limitLabelWidth(275.f, 1.f, 0.1f); - auto menu = CCMenu::create(); - menu->setPosition({ size.width / 2, size.height - 25 }); + auto titleBtn = CCMenuItemExt::createSpriteExtra( + titleLabel, + [info](CCObject* sender) { + geode::utils::clipboard::write(info.code); + Notification::create("Copied code to clipboard")->show(); + } + ); - menu->addChild( - CCMenuItemExt::createSpriteExtra( - titleLabel, - [info](CCObject* sender) { - geode::utils::clipboard::write(info.code); - Notification::create("Copied code to clipboard")->show(); - } - ) + auto infoBtnSpr = CCSprite::createWithSpriteFrameName("GJ_infoIcon_001.png"); + infoBtnSpr->setScale(0.8f); + auto infoBtn = CCMenuItemExt::createSpriteExtra( + infoBtnSpr, + [info](CCObject*) { + // Если в будущем добавишь теги в настройки, замени "Default" на info.settings.tags + std::string settingsText = fmt::format( + "Turns: {}\nMinutes per turn: {}\nTags: {}", + info.settings.turns, + info.settings.minutesPerTurn, + "Default" + ); + FLAlertLayer::create("Lobby Info", settingsText, "OK")->show(); + } ); + auto menu = CCMenu::create(); + menu->setPosition({ size.width / 2, size.height - 25 }); + menu->addChild(titleBtn); + menu->addChild(infoBtn); + menu->setLayout(RowLayout::create()->setGap(10.f)); + menu->updateLayout(); + mainLayer->addChild(menu); } From 2d86a9ba0611f0d23d4d1de3ccb0d09008532f61 Mon Sep 17 00:00:00 2001 From: lmaosssdll Date: Wed, 1 Apr 2026 18:24:09 +0800 Subject: [PATCH 13/30] add better tags --- src/types/lobby.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/types/lobby.hpp b/src/types/lobby.hpp index 9e69d01..82936fb 100755 --- a/src/types/lobby.hpp +++ b/src/types/lobby.hpp @@ -31,6 +31,7 @@ struct LobbySettings { // std::string password; int turns; int minutesPerTurn; + int tag = 0; Account owner; bool isPublic; From f465a868eecf58a40636eed0a0d0df56fcefc1ad Mon Sep 17 00:00:00 2001 From: lmaosssdll Date: Wed, 1 Apr 2026 18:25:10 +0800 Subject: [PATCH 14/30] add better tags --- src/layers/LobbySettings.cpp | 168 ++++++++++++----------------------- 1 file changed, 59 insertions(+), 109 deletions(-) diff --git a/src/layers/LobbySettings.cpp b/src/layers/LobbySettings.cpp index 50427ba..c157741 100755 --- a/src/layers/LobbySettings.cpp +++ b/src/layers/LobbySettings.cpp @@ -5,7 +5,13 @@ enum LobbySettingType { Turns, MinsPerTurn, Password, - IsPublic + IsPublic, + Tag // Добавили новый тип настройки +}; + +// Список всех доступных тегов +static const std::vector TAG_NAMES = { + "None", "Short", "Medium", "Long", "Layout", "Deco", "Impossible", "Triggers" }; class LobbySettingsCell : public CCNode { @@ -13,37 +19,19 @@ class LobbySettingsCell : public CCNode { bool init(float width, std::string name, LobbySettingType type, std::string desc, std::string defaultStr = "", std::string filter = "") { this->type = type; - this->setContentSize({ - width, CELL_HEIGHT - }); - - auto nameLabel = CCLabelBMFont::create( - name.c_str(), - "bigFont.fnt" - ); - - nameLabel->setPosition({ - 5.f, CELL_HEIGHT / 2.f - }); - nameLabel->setAnchorPoint({ - 0.f, 0.5f - }); + this->setContentSize({ width, CELL_HEIGHT }); + + auto nameLabel = CCLabelBMFont::create(name.c_str(), "bigFont.fnt"); + nameLabel->setPosition({ 5.f, CELL_HEIGHT / 2.f }); + nameLabel->setAnchorPoint({ 0.f, 0.5f }); nameLabel->limitLabelWidth(80.f, 0.5f, 0.1f); this->addChild(nameLabel); - auto infoBtn = InfoAlertButton::create( - name, - desc, - 0.5f - ); + auto infoBtn = InfoAlertButton::create(name, desc, 0.5f); auto menu = CCMenu::create(); menu->addChild(infoBtn); - menu->setPosition({ - nameLabel->getScaledContentWidth() + 15.f, CELL_HEIGHT / 2.f - }); - menu->setAnchorPoint({ - 0.f, 0.5f - }); + menu->setPosition({ nameLabel->getScaledContentWidth() + 15.f, CELL_HEIGHT / 2.f }); + menu->setAnchorPoint({ 0.f, 0.5f }); this->addChild(menu); switch (this->type) { @@ -54,12 +42,8 @@ class LobbySettingsCell : public CCNode { input = TextInput::create(95.f, name); input->setString(defaultStr); if (filter != "") input->getInputNode()->setAllowedChars(filter); - input->setPosition({ - width - input->getContentWidth() - 5.f, CELL_HEIGHT / 2.f - }); - input->setAnchorPoint({ - 0.f, 0.5f - }); + input->setPosition({ width - input->getContentWidth() - 5.f, CELL_HEIGHT / 2.f }); + input->setAnchorPoint({ 0.f, 0.5f }); this->addChild(input); break; } @@ -70,18 +54,29 @@ class LobbySettingsCell : public CCNode { togglerVal = !toggler->isOn(); } ); - toggler->toggle( - geode::utils::numFromString(defaultStr).unwrapOr(0) - ); - auto menu = CCMenu::create(); - menu->addChild(toggler); - menu->setPosition({ - width - toggler->getContentWidth() - 5.f, CELL_HEIGHT / 2.f - }); - menu->setAnchorPoint({ - 0.f, 0.5f + toggler->toggle(geode::utils::numFromString(defaultStr).unwrapOr(0)); + auto menu2 = CCMenu::create(); + menu2->addChild(toggler); + menu2->setPosition({ width - toggler->getContentWidth() - 5.f, CELL_HEIGHT / 2.f }); + menu2->setAnchorPoint({ 0.f, 0.5f }); + this->addChild(menu2); + break; + } + case Tag: { // Логика для кнопки выбора тегов + tagIdx = geode::utils::numFromString(defaultStr).unwrapOr(0); + if (tagIdx < 0 || tagIdx >= TAG_NAMES.size()) tagIdx = 0; + + auto btnSpr = ButtonSprite::create(TAG_NAMES[tagIdx].c_str(), 80, true, "bigFont.fnt", "GJ_button_04.png", 30.f, 0.6f); + tagBtn = CCMenuItemExt::createSpriteExtra(btnSpr, [this](CCObject*) { + tagIdx = (tagIdx + 1) % TAG_NAMES.size(); + auto newSpr = ButtonSprite::create(TAG_NAMES[tagIdx].c_str(), 80, true, "bigFont.fnt", "GJ_button_04.png", 30.f, 0.6f); + tagBtn->setNormalImage(newSpr); }); - this->addChild(menu); + + auto menu3 = CCMenu::create(); + menu3->addChild(tagBtn); + menu3->setPosition({ width - 50.f, CELL_HEIGHT / 2.f }); + this->addChild(menu3); break; } } @@ -91,38 +86,22 @@ class LobbySettingsCell : public CCNode { public: static constexpr int CELL_HEIGHT = 35.f; - TextInput* input; - CCMenuItemToggler* toggler; + TextInput* input = nullptr; + CCMenuItemToggler* toggler = nullptr; + CCMenuItemSpriteExtra* tagBtn = nullptr; LobbySettingType type; bool togglerVal = false; + int tagIdx = 0; // Сохраняем индекс тега void save(LobbySettings& settings) { switch (this->type) { - case Name: { - settings.name = this->input->getString(); - break; - } - case Turns: { - settings.turns = geode::utils::numFromString( - this->input->getString() - ).unwrapOr(0); - break; - } - case MinsPerTurn: { - settings.minutesPerTurn = geode::utils::numFromString( - this->input->getString() - ).unwrapOr(1); - break; - } - case Password: { - // settings.password = this->input->getString(); - break; - } - case IsPublic: { - settings.isPublic = togglerVal; - break; - } + case Name: settings.name = this->input->getString(); break; + case Turns: settings.turns = geode::utils::numFromString(this->input->getString()).unwrapOr(0); break; + case MinsPerTurn: settings.minutesPerTurn = geode::utils::numFromString(this->input->getString()).unwrapOr(1); break; + case Password: break; + case IsPublic: settings.isPublic = togglerVal; break; + case Tag: settings.tag = tagIdx; break; // Сохраняем выбранный тег } } @@ -148,64 +127,36 @@ LobbySettingsPopup* LobbySettingsPopup::create(LobbySettings const& settings, Ca } bool LobbySettingsPopup::init(LobbySettings const& settings, Callback callback) { - if (!Popup::init(250.f, 230.f)) { + if (!Popup::init(250.f, 250.f)) { // Немного увеличил высоту для новой кнопки return false; } m_noElasticity = true; this->setTitle("Lobby Settings"); - CCNode* nameContainer = CCNode::create(); - nameContainer->setLayout( - RowLayout::create() - ->setGap(-5.f) - ); - auto settingsContents = CCArray::create(); #define ADD_SETTING(name, element, desc, type) settingsContents->addObject( \ - LobbySettingsCell::create( \ - 220.f, \ - name,\ - LobbySettingType::type, \ - desc, \ - settings.element \ - )); + LobbySettingsCell::create(220.f, name, LobbySettingType::type, desc, settings.element)); #define ADD_SETTING_INT(name, element, desc, type) settingsContents->addObject( \ - LobbySettingsCell::create( \ - 220.f, \ - name,\ - LobbySettingType::type, \ - desc, \ - std::to_string(settings.element), \ - "0123456789" \ - )); + LobbySettingsCell::create(220.f, name, LobbySettingType::type, desc, std::to_string(settings.element), "0123456789")); #define ADD_SETTING_BOOL(name, element, desc, type) settingsContents->addObject( \ - LobbySettingsCell::create( \ - 220.f, \ - name,\ - LobbySettingType::type, \ - desc, \ - std::to_string(settings.element), \ - "" \ - )); + LobbySettingsCell::create(220.f, name, LobbySettingType::type, desc, std::to_string(settings.element), "")); ADD_SETTING("Name", name, "Name of the lobby", Name) ADD_SETTING_INT("Turns", turns, "Number of turns per level", Turns) - ADD_SETTING_INT("Minutes per turn", minutesPerTurn, "Amount of minutes per turn", MinsPerTurn) + ADD_SETTING_INT("Minutes/Turn", minutesPerTurn, "Amount of minutes per turn", MinsPerTurn) ADD_SETTING_BOOL("Public", isPublic, "Whether the swap is public. If it is, it will be shown in the room list for others to join.", IsPublic) - // ADD_SETTING("Password", password, Password) + + // Добавляем тег в меню + ADD_SETTING_INT("Tag", tag, "Select a tag to describe your lobby", Tag) - auto settingsList = ListView::create(settingsContents, LobbySettingsCell::CELL_HEIGHT, 220.f, 180.f); + auto settingsList = ListView::create(settingsContents, LobbySettingsCell::CELL_HEIGHT, 220.f, 200.f); settingsList->ignoreAnchorPointForPosition(false); - auto border = Border::create( - settingsList, - {0, 0, 0, 75}, - {220.f, 180.f} - ); + auto border = Border::create(settingsList, {0, 0, 0, 75}, {220.f, 200.f}); if(auto borderSprite = typeinfo_cast(border->getChildByID("geode.loader/border_sprite"))) { float scaleFactor = 1.7f; borderSprite->setContentSize(CCSize{borderSprite->getContentSize().width, borderSprite->getContentSize().height + 3} / scaleFactor); @@ -231,7 +182,6 @@ bool LobbySettingsPopup::init(LobbySettings const& settings, Callback callback) ); auto menu = CCMenu::create(); menu->addChild(submitBtn); - m_mainLayer->addChildAtPosition(menu, Anchor::Bottom); return true; From c011621ec8e0c67f00daa71b876fc26f6240416d Mon Sep 17 00:00:00 2001 From: lmaosssdll Date: Wed, 1 Apr 2026 18:25:47 +0800 Subject: [PATCH 15/30] add better tags --- src/layers/LobbySettings.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/layers/LobbySettings.cpp b/src/layers/LobbySettings.cpp index c157741..e4de627 100755 --- a/src/layers/LobbySettings.cpp +++ b/src/layers/LobbySettings.cpp @@ -6,10 +6,9 @@ enum LobbySettingType { MinsPerTurn, Password, IsPublic, - Tag // Добавили новый тип настройки + Tag }; -// Список всех доступных тегов static const std::vector TAG_NAMES = { "None", "Short", "Medium", "Long", "Layout", "Deco", "Impossible", "Triggers" }; From 1adc6ecfdcce8a2bb82b1b8e158e107f5480e57a Mon Sep 17 00:00:00 2001 From: lmaosssdll Date: Wed, 1 Apr 2026 18:26:11 +0800 Subject: [PATCH 16/30] add better tags --- src/layers/Lobby.cpp | 110 ++++++++++++++++++++++++------------------- 1 file changed, 62 insertions(+), 48 deletions(-) diff --git a/src/layers/Lobby.cpp b/src/layers/Lobby.cpp index 0692ded..e48ec40 100755 --- a/src/layers/Lobby.cpp +++ b/src/layers/Lobby.cpp @@ -7,8 +7,6 @@ #include "ChatPanel.hpp" #include -// ==================== PlayerCell ==================== // - PlayerCell* PlayerCell::create(Account account, float width, bool canKick) { auto ret = new PlayerCell; if (ret->init(account, width, canKick)) { @@ -89,8 +87,6 @@ void PlayerCell::onKickUser(CCObject* sender) { ); } -// ==================== LobbyLayer ==================== // - LobbyLayer* LobbyLayer::create(std::string code) { auto ret = new LobbyLayer(); if (ret->init(code)) { @@ -160,10 +156,7 @@ bool LobbyLayer::init(std::string code) { auto msgButtonSpr = CCSprite::create("messagesBtn.png"_spr); auto msgButton = CCMenuItemExt::createSpriteExtra( - msgButtonSpr, - [](CCObject*) { - ChatPanel::create()->show(); - } + msgButtonSpr, [](CCObject*) { ChatPanel::create()->show(); } ); auto settingsBtnSpr = CCSprite::createWithSpriteFrameName("GJ_optionsBtn02_001.png"); @@ -172,8 +165,7 @@ bool LobbyLayer::init(std::string code) { ); auto refreshBtn = CCMenuItemExt::createSpriteExtraWithFrameName( - "GJ_getSongInfoBtn_001.png", - 1.f, + "GJ_getSongInfoBtn_001.png", 1.f, [this, alive = m_alive](CCObject* sender) { SwapManager::get().getLobbyInfo([this, alive](LobbyInfo info) { if (!*alive) return; @@ -198,7 +190,7 @@ bool LobbyLayer::init(std::string code) { [](CCObject* target) { geode::createQuickPopup( "Discord Server", - "We have a Discord Server! If you have questions, want to suggest a feature, or find someone to swap with, join here! \nPlease not that you need to be at least 13 years of age to join our server.", + "We have a Discord Server! If you have questions, want to suggest a feature, or find someone to swap with, join here!", "Cancel", "Join!", [](auto, bool btn2) { if (!btn2) return; @@ -264,7 +256,6 @@ LobbyLayer::~LobbyLayer() { void LobbyLayer::refresh(LobbyInfo info, bool isFirstRefresh) { isOwner = GameManager::get()->m_playerUserID == info.settings.owner.userID; - auto size = CCDirector::sharedDirector()->getWinSize(); auto listWidth = size.width / 1.5f; @@ -273,33 +264,79 @@ void LobbyLayer::refresh(LobbyInfo info, bool isFirstRefresh) { // --- Логика Уведомлений Входа/Выхода --- if (isFirstRefresh) { m_currentPlayers.clear(); - for (auto& acc : info.accounts) { - m_currentPlayers.push_back(acc.userID); - } + for (auto& acc : info.accounts) m_currentPlayers.push_back(acc.userID); } else { std::vector newPlayers; for (auto& acc : info.accounts) newPlayers.push_back(acc.userID); for (auto& acc : info.accounts) { if (std::find(m_currentPlayers.begin(), m_currentPlayers.end(), acc.userID) == m_currentPlayers.end()) { - Notification::create( - fmt::format("{} joined", acc.name), - CCSprite::createWithSpriteFrameName("GJ_sStarsIcon_001.png") - )->show(); + Notification::create(fmt::format("{} joined", acc.name), CCSprite::createWithSpriteFrameName("GJ_sStarsIcon_001.png"))->show(); } } for (int id : m_currentPlayers) { if (std::find(newPlayers.begin(), newPlayers.end(), id) == newPlayers.end()) { - Notification::create( - "Someone left", - CCSprite::createWithSpriteFrameName("GJ_deleteIcon_001.png") - )->show(); + Notification::create("Someone left", CCSprite::createWithSpriteFrameName("GJ_deleteIcon_001.png"))->show(); } } m_currentPlayers = newPlayers; } - // --- Заголовок и Кнопка Информации --- + // --- Обновление визуальных настроек лобби (СЛЕВА СВЕРХУ) --- + // Удаляем старый UI настроек, если он есть + if (auto oldSettingsUI = mainLayer->getChildByID("lobby-settings-display")) { + oldSettingsUI->removeFromParent(); + } + + // Создаем новый контейнер для настроек + auto settingsDisplay = CCNode::create(); + settingsDisplay->setID("lobby-settings-display"); + + // Текст: Ходы и время + auto statsLabel = CCLabelBMFont::create( + fmt::format("Turns: {} | Mins/Turn: {}", info.settings.turns, info.settings.minutesPerTurn).c_str(), + "chatFont.fnt" + ); + statsLabel->setAnchorPoint({ 0.f, 1.f }); + + // Текст: Выбранный тег с цветом + std::string tagNames[] = {"None", "Short", "Medium", "Long", "Layout", "Deco", "Impossible", "Triggers"}; + ccColor3B tagColors[] = { + {255, 255, 255}, // 0 None (Белый) + {100, 255, 100}, // 1 Short (Светло-зеленый) + {255, 255, 100}, // 2 Medium (Желтый) + {255, 150, 50}, // 3 Long (Оранжевый) + {100, 255, 255}, // 4 Layout (Бирюзовый) + {255, 100, 255}, // 5 Deco (Розовый/Пурпурный) + {255, 50, 50}, // 6 Impossible (Красный) + {200, 100, 255} // 7 Triggers (Фиолетовый) + }; + + int tagId = info.settings.tag; + if (tagId < 0 || tagId >= 8) tagId = 0; + + auto tagPrefix = CCLabelBMFont::create("Tag: ", "chatFont.fnt"); + auto tagValue = CCLabelBMFont::create(tagNames[tagId].c_str(), "chatFont.fnt"); + tagValue->setColor(tagColors[tagId]); + + auto tagContainer = CCMenu::create(); // Используем меню просто как Layout контейнер + tagContainer->addChild(tagPrefix); + tagContainer->addChild(tagValue); + tagContainer->setLayout(RowLayout::create()->setGap(2.f)->setAxisAlignment(AxisAlignment::Start)); + tagContainer->setAnchorPoint({ 0.f, 1.f }); + tagContainer->setContentSize({ 200.f, tagPrefix->getContentHeight() }); + + settingsDisplay->addChild(statsLabel); + settingsDisplay->addChild(tagContainer); + settingsDisplay->setLayout(ColumnLayout::create()->setGap(5.f)->setAxisReverse(true)->setAxisAlignment(AxisAlignment::Start)); + settingsDisplay->setContentSize({ 200.f, 50.f }); + settingsDisplay->setPosition({ 15.f, size.height - 15.f }); + settingsDisplay->setAnchorPoint({ 0.f, 1.f }); + + mainLayer->addChild(settingsDisplay); + // ----------------------------------------------------------- + + // Заголовок (только при первом заходе) if (isFirstRefresh) { titleLabel = CCLabelBMFont::create(info.settings.name.c_str(), "bigFont.fnt"); titleLabel->limitLabelWidth(275.f, 1.f, 0.1f); @@ -312,37 +349,14 @@ void LobbyLayer::refresh(LobbyInfo info, bool isFirstRefresh) { } ); - auto infoBtnSpr = CCSprite::createWithSpriteFrameName("GJ_infoIcon_001.png"); - infoBtnSpr->setScale(0.8f); - auto infoBtn = CCMenuItemExt::createSpriteExtra( - infoBtnSpr, - [info](CCObject*) { - // Если в будущем добавишь теги в настройки, замени "Default" на info.settings.tags - std::string settingsText = fmt::format( - "Turns: {}\nMinutes per turn: {}\nTags: {}", - info.settings.turns, - info.settings.minutesPerTurn, - "Default" - ); - FLAlertLayer::create("Lobby Info", settingsText, "OK")->show(); - } - ); - auto menu = CCMenu::create(); menu->setPosition({ size.width / 2, size.height - 25 }); menu->addChild(titleBtn); - menu->addChild(infoBtn); - menu->setLayout(RowLayout::create()->setGap(10.f)); - menu->updateLayout(); - mainLayer->addChild(menu); } if (titleLabel) titleLabel->setString( - fmt::format("{} ({})", - info.settings.name, - Mod::get()->getSettingValue("hide-code") ? "......" : info.code - ).c_str() + fmt::format("{} ({})", info.settings.name, Mod::get()->getSettingValue("hide-code") ? "......" : info.code).c_str() ); if (!playerList && !isFirstRefresh) return; From 9d7b6c710dda0dcaab83380b3eecc4d886af522b Mon Sep 17 00:00:00 2001 From: lmaosssdll Date: Fri, 3 Apr 2026 09:30:42 +0800 Subject: [PATCH 17/30] Fix memory leak --- src/managers/SwapManager.cpp | 75 ++++++++++++++++++++++++++---------- 1 file changed, 55 insertions(+), 20 deletions(-) diff --git a/src/managers/SwapManager.cpp b/src/managers/SwapManager.cpp index b22b478..293eae9 100755 --- a/src/managers/SwapManager.cpp +++ b/src/managers/SwapManager.cpp @@ -128,31 +128,57 @@ void SwapManager::registerListeners() { nm.setDisconnectCallback([](std::string reason) {}); + // --- SENDING THE LEVEL TO THE SERVER --- nm.on([this](TimeToSwapPacket p) { auto& nm = NetworkManager::get(); Notification::create("Swapping levels!", NotificationIcon::Info, 2.5f)->show(); - auto filePath = std::filesystem::temp_directory_path() / fmt::format("temp{}.gmd", rand()); - + // 1. Force save the level if (auto editorLayer = LevelEditorLayer::get()) { + // Update internal level string before saving to prevent race condition + editorLayer->updateLevelString(); auto fakePauseLayer = EditorPauseLayer::create(editorLayer); fakePauseLayer->saveLevel(); } + auto lvl = EditorIDs::getLevelByID(levelId); - // this crashes macos when recieving the level - // do not ask me why - // this game is taped-together jerry-rigged piece of software - // lvl->m_levelDesc = fmt::format("from: {}", this->createAccountType().name); + if (!lvl) { + log::error("[SwapManager] CRITICAL ERROR: Could not find level by ID {} before swapping!", levelId); + return; + } + // 2. Generate level string auto b = new DS_Dictionary(); lvl->encodeWithCoder(b); + std::string finalLevelString = b->saveRootSubDictToString(); + + // 3. FIX MEMORY LEAK: We must delete 'b' after using it! + delete b; + + // 4. PREVENT EMPTY LEVEL BUG + if (finalLevelString.empty() || finalLevelString.size() < 10) { + log::error("[SwapManager] CRITICAL ERROR: b->saveRootSubDictToString() generated an EMPTY string!"); + log::error("[SwapManager] The game engine did not save the level fast enough (Race Condition)."); + + // Fallback: Try to use the cached m_levelString directly + if (!lvl->m_levelString.empty()) { + finalLevelString = lvl->m_levelString; + log::warn("[SwapManager] Used fallback m_levelString (Size: {} bytes).", finalLevelString.size()); + } else { + log::error("[SwapManager] Fallback failed! m_levelString is also empty. Sending void level..."); + } + } else { +#ifdef CR_DEBUG + log::debug("[SwapManager] Successfully generated level string. Size: {} bytes.", finalLevelString.size()); +#endif + } LevelData lvlData = { .levelName = lvl->m_levelName, .songID = lvl->m_songID, .songIDs = lvl->m_songIDs, - .levelString = b->saveRootSubDictToString() + .levelString = finalLevelString }; nm.send( @@ -164,33 +190,41 @@ void SwapManager::registerListeners() { GameLevelManager::sharedState()->deleteLevel(lvl); } }); + + // --- RECEIVING THE LEVEL FROM THE SERVER --- nm.on([this](ReceiveSwappedLevelPacket packet) { - // if (packet->levels.size() < swapIdx) { - // FLAlertLayer::create( - // "Creation Rotation", - // "There was an error while fetching the swapped level. If you're reading this, something has gone terribly wrong, please report it at once.", - // "OK" - // )->show(); - // return; - // } LevelData lvlData; + bool foundMyLevel = false; for (auto& swappedLevel : packet.levels) { if (swappedLevel.accountID != cr::utils::createAccountType().accountID) continue; lvlData = std::move(swappedLevel.level); + foundMyLevel = true; break; } + // 1. PREVENT EMPTY LEVEL BUG ON RECEIVE + if (!foundMyLevel) { + log::error("[SwapManager] CRITICAL ERROR: Server did not send a level for my Account ID!"); + } else if (lvlData.levelString.empty()) { + log::error("[SwapManager] CRITICAL ERROR: The server sent a level, but the levelString is EMPTY!"); + log::error("[SwapManager] This means the previous player failed to upload it, or the server wiped it."); + } else { +#ifdef CR_DEBUG + log::debug("[SwapManager] Successfully received swapped level! String size: {} bytes.", lvlData.levelString.size()); +#endif + } + auto lvl = GJGameLevel::create(); - // lvl->m_levelName = lvlData.levelName; - // lvl->m_levelString = lvlData.levelString; - // lvl->m_songID = lvlData.songID; - // lvl->m_songIDs = lvlData.songIDs; + // 2. Load the level data auto b = new DS_Dictionary(); b->loadRootSubDictFromString(lvlData.levelString); lvl->dataLoaded(b); + + // 3. FIX MEMORY LEAK: We must delete 'b' after loading data! + delete b; lvl->m_levelType = GJLevelType::Editor; @@ -208,8 +242,9 @@ void SwapManager::registerListeners() { roundStartedTime = time(nullptr); }); + nm.on([this](SwapEndedPacket p) { - log::debug("swap ended; disconnecting from server"); + log::debug("[SwapManager] swap ended; disconnecting from server"); auto& nm = NetworkManager::get(); nm.showDisconnectPopup = false; this->disconnectLobby(); From 1298ad2efb29bb702664108f62976e390d075783 Mon Sep 17 00:00:00 2001 From: lmaosssdll Date: Fri, 3 Apr 2026 09:32:00 +0800 Subject: [PATCH 18/30] Fix empty level and small memory leak --- src/network/manager.cpp | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/network/manager.cpp b/src/network/manager.cpp index 3c922a8..22998d2 100755 --- a/src/network/manager.cpp +++ b/src/network/manager.cpp @@ -28,10 +28,10 @@ void NetworkManager::update(float dt) { } void NetworkManager::connect(bool shouldReconnect, std::function callback) { - log::debug("connecting"); + log::debug("[Network] connecting..."); if (this->isConnected && shouldReconnect) { // disconnect then reconnect - log::debug("already connected; disconnecting then reconnecting..."); + log::debug("[Network] already connected; disconnecting then reconnecting..."); this->showDisconnectPopup = false; this->disconnect(); } @@ -53,7 +53,7 @@ void NetworkManager::connect(bool shouldReconnect, std::function callbac socket.setOnMessageCallback([this, callback = std::move(callback)](const ix::WebSocketMessagePtr& msg) mutable { if (msg->type == ix::WebSocketMessageType::Error) { auto& errReason = msg->errorInfo.reason; - log::error("ixwebsocket error: {}", errReason); + log::error("[Network] ixwebsocket error: {}", errReason); Loader::get()->queueInMainThread([this, errReason = std::move(errReason)]() { FLAlertLayer::create( "CR Error", @@ -63,7 +63,7 @@ void NetworkManager::connect(bool shouldReconnect, std::function callbac }); return; } else if (msg->type == ix::WebSocketMessageType::Open) { - log::debug("connection success!"); + log::debug("[Network] connection success!"); // call middleware (this should run the provided cb) middleware([this, callback]() { @@ -99,7 +99,7 @@ void NetworkManager::connect(bool shouldReconnect, std::function callbac return; } else if (msg->type == ix::WebSocketMessageType::Close) { - log::debug("connection closed"); + log::debug("[Network] connection closed"); this->isConnected = false; auto& reason = msg->closeInfo.reason; Loader::get()->queueInMainThread([this, reason = std::move(reason)]() mutable { @@ -122,7 +122,10 @@ void NetworkManager::connect(bool shouldReconnect, std::function callbac }); return; } else if (msg->type == ix::WebSocketMessageType::Message) { - if (msg.get()->str == "") return; + if (msg.get()->str == "") { + log::error("[Network] Received completely empty message string from server!"); + return; + } this->onMessage(msg); } }); @@ -141,7 +144,7 @@ void NetworkManager::onMessage(const ix::WebSocketMessagePtr& msg) { auto packetIdIdx = strMsg.find("|"); if (packetIdIdx == std::string::npos) { - log::error("invalid packet received"); + log::error("[Network] invalid packet received (missing packet ID separator): {}", strMsg); return; } @@ -150,7 +153,7 @@ void NetworkManager::onMessage(const ix::WebSocketMessagePtr& msg) { auto it = listeners.find(packetId); if (it == listeners.end()) { - log::error("unhandled packed ID {}", packetId); + log::error("[Network] unhandled packed ID {}", packetId); return; } From 537717b77b4b54fe37c1aa2c0162f8cb94bbdcb1 Mon Sep 17 00:00:00 2001 From: lmaosssdll Date: Fri, 3 Apr 2026 09:33:10 +0800 Subject: [PATCH 19/30] Fix something --- src/network/manager.hpp | 56 ++++++++++++++++++++++++++++++++--------- 1 file changed, 44 insertions(+), 12 deletions(-) diff --git a/src/network/manager.hpp b/src/network/manager.hpp index 90e9571..4adc7e6 100755 --- a/src/network/manager.hpp +++ b/src/network/manager.hpp @@ -52,14 +52,25 @@ class CR_DLL NetworkManager : public CCObject { requires std::is_base_of_v inline void on(std::function callback, bool shouldUnbind = false) { listeners[T::PACKET_ID] = [callback = std::move(callback)](std::string msg) mutable { - std::stringstream ss(std::move(msg)); + +#ifdef CR_DEBUG + // Log the size of the incoming data to detect truncated or empty server responses + log::debug("[Network] Received packet {}, Raw string size: {} bytes", T::PACKET_NAME, msg.size()); +#endif + std::stringstream ss(std::move(msg)); T packet = T::create(); - { + try { cereal::JSONInputArchive iarchive(ss); - iarchive(cereal::make_nvp("packet", packet)); + } catch (const std::exception& e) { + // If the Java server sends missing fields, empty strings, or corrupted JSON, + // cereal will throw an exception here instead of silently passing a "void/empty" level. + log::error("[Network] CRITICAL ERROR: Failed to parse packet {} ({}).", T::PACKET_NAME, T::PACKET_ID); + log::error("[Network] Reason: The server sent corrupted, incomplete, or empty JSON data."); + log::error("[Network] Cereal exception detail: {}", e.what()); + return; // Prevent sending an empty/broken level to the main game thread } Loader::get()->queueInMainThread([callback, packet = std::move(packet)]() mutable { @@ -74,13 +85,13 @@ class CR_DLL NetworkManager : public CCObject { if (!this->isConnected) return; if (!listeners.contains(T::PACKET_ID)) { - log::error("unable to remove listener for {} ({}), listener does not exist", T::PACKET_NAME, T::PACKET_ID); + log::error("[Network] unable to remove listener for {} ({}), listener does not exist", T::PACKET_NAME, T::PACKET_ID); return; } listeners.erase(T::PACKET_ID); - log::debug("removed listener for {} ({})", T::PACKET_NAME, T::PACKET_ID); + log::debug("[Network] removed listener for {} ({})", T::PACKET_NAME, T::PACKET_ID); } template @@ -89,28 +100,45 @@ class CR_DLL NetworkManager : public CCObject { return; } -#ifdef CR_DEBUG - log::debug("sending packet {} ({})", packet.getPacketName(), packet.getPacketID()); -#endif std::stringstream ss; - // cereal uses RAII, meaning - // the contents of `ss` is guaranteed + // cereal uses RAII, meaning the contents of `ss` is guaranteed // to be filled at the end of the braces { cereal::JSONOutputArchive oarchive(ss); oarchive(cereal::make_nvp("packet", packet)); } - auto json = matjson::parse(ss.str()).mapErr([](std::string err) { return err; }).unwrap(); + + auto jsonRes = matjson::parse(ss.str()).mapErr([](std::string err) { return err; }); + if (jsonRes.isErr()) { + log::error("[Network] Failed to parse local JSON for packet {}: {}", packet.getPacketID(), jsonRes.unwrapErr()); + return; + } + auto json = jsonRes.unwrap(); json["packet_id"] = packet.getPacketID(); auto uncompressedStr = json.dump(0); - unsigned char* compressedData; +#ifdef CR_DEBUG + // Log outgoing uncompressed size to see if the client generated an empty level string before sending + log::debug("[Network] Sending packet {} ({}), Uncompressed size: {} bytes", packet.getPacketName(), packet.getPacketID(), uncompressedStr.size()); +#endif + + unsigned char* compressedData = nullptr; size_t compressedSize = ZipUtils::ccDeflateMemory( reinterpret_cast(uncompressedStr.data()), uncompressedStr.size(), &compressedData ); + + // SAFETY CHECK: Prevent sending "garbage" or empty data if compression fails due to memory/sync issues + if (compressedSize == 0 || compressedData == nullptr) { + log::error("[Network] CRITICAL ERROR: ccDeflateMemory failed to compress packet {} ({})!", packet.getPacketName(), packet.getPacketID()); + log::error("[Network] Reason: The level string generated by the game might be completely empty, or the device ran out of memory."); + log::error("[Network] Packet was dropped to prevent sending a void level to the server."); + if (compressedData) free(compressedData); // Prevent memory leak just in case + return; + } + ix::IXWebSocketSendData data( reinterpret_cast(compressedData), compressedSize @@ -125,6 +153,10 @@ class CR_DLL NetworkManager : public CCObject { return true; } ); + + // VERY IMPORTANT: ZipUtils::ccDeflateMemory allocates memory using malloc internally. + // We MUST free it here, otherwise the game will slowly leak memory and start lagging. + free(compressedData); } protected: NetworkManager(); From e8ec9fac30ea083dec2f51bebc8d8bc00051f52d Mon Sep 17 00:00:00 2001 From: lmaosssdll Date: Fri, 3 Apr 2026 09:40:16 +0800 Subject: [PATCH 20/30] Fix empty level --- server/src/types/swap.ts | 79 +++++++++++++++++++++++++++------------- 1 file changed, 53 insertions(+), 26 deletions(-) diff --git a/server/src/types/swap.ts b/server/src/types/swap.ts index f557cad..6e83066 100755 --- a/server/src/types/swap.ts +++ b/server/src/types/swap.ts @@ -75,54 +75,82 @@ export class Swap { this.currentlySwapping = true this.isSwapEnding = ending this.closeReason = reason - log.debug(this.swapOrder) + log.debug(`[Swap] Turn ${this.currentTurn}/${this.totalTurns} started for lobby ${this.lobbyCode}. Swap order: ${this.swapOrder}`) emitToLobby(this.serverState, this.lobbyCode, Packet.TimeToSwapPacket, {}) } addLevel(level: LevelData, accId: number) { + if (!this.currentlySwapping) { + log.warn(`[Swap] Received late or duplicate level from ${accId} when not swapping. Ignoring.`) + return + } + const idx = this.swapOrder.indexOf(accId) - this.levels.push( - { - accountID: parseInt(offsetArray(this.swapOrder, 1)[idx]), - level - } - ) + if (idx === -1) { + log.error(`[Swap] Player ${accId} is not in the swapOrder list!`) + return + } + + const targetAccId = parseInt(offsetArray(this.swapOrder, 1)[idx] as string) + + // FIX: Prevent duplicate submissions messing up the array length. + // If they already submitted, overwrite it. Otherwise, add it. + const existingIdx = this.levels.findIndex(l => l.accountID === targetAccId) + + if (existingIdx !== -1) { + log.warn(`[Swap] Player ${accId} sent level data AGAIN. Overwriting previous submission.`) + this.levels[existingIdx] = { accountID: targetAccId, level } + } else { + this.levels.push({ accountID: targetAccId, level }) + } + + log.info(`[Swap] Received level from ${accId} (Size: ${level.levelString.length} bytes). Forwarding to player ${targetAccId}. (${this.levels.length}/${this.swapOrder.length})`) + this.checkSwap() } checkSwap() { if (!this.currentlySwapping) return - this.lobby.accounts.forEach((acc, index) => { - if (this.serverState.lobbies[this.lobbyCode].accounts.findIndex( - lobbyAcc => lobbyAcc.accountID === acc.accountID - ) !== -1) return - - this.levels.splice(index, 1) - }) - if (getLength(this.levels) < this.lobby.accounts.length) return + + const currentLobbyAccounts = this.serverState.lobbies[this.lobbyCode].accounts + + // FIX: Instead of checking array length (which breaks on duplicates or disconnects), + // we strictly check: Does EVERY player currently in the lobby have a level assigned to them? + let allReady = true + for (const acc of currentLobbyAccounts) { + const hasLevelForThisPlayer = this.levels.some(l => l.accountID === acc.accountID) + if (!hasLevelForThisPlayer) { + allReady = false + break + } + } + + if (!allReady) { + // We are still waiting for someone to finish uploading their level + return + } + + // Everyone has submitted their level! We can proceed. this.currentlySwapping = false + log.info(`[Swap] All levels received for lobby ${this.lobbyCode}. Executing swap!`) + + // Clean up: only send levels to players who are actually still in the lobby + const validLevels = this.levels.filter(l => + currentLobbyAccounts.some(acc => acc.accountID === l.accountID) + ) if (!this.isSwapEnding) { - emitToLobby(this.serverState, this.lobbyCode, Packet.ReceiveSwappedLevelPacket, { levels: this.levels }) + emitToLobby(this.serverState, this.lobbyCode, Packet.ReceiveSwappedLevelPacket, { levels: validLevels }) this.levels = [] if (this.currentTurn >= this.totalTurns) { this.swapEnded = true setTimeout(() => emitToLobby(this.serverState, this.lobbyCode, Packet.SwapEndedPacket, {}), 750) // 0.75 seconds - return } this.scheduleNextSwap() } - // else { - // this.swapEnded = true - // this.levels = offsetArray(this.levels, this.totalTurns - this.currentTurn) - // emitToLobby(this.serverState, this.lobbyCode, Packet.ReceiveSwappedLevelPacket, { levels: this.levels }) - // Object.values(this.serverState.sockets[this.lobbyCode]).forEach(socket => { - // socket.close(1000, this.closeReason) - // }) - // } } scheduleNextSwap() { @@ -136,4 +164,3 @@ export class Swap { clearTimeout(this.timeout) } } - From 60f0ab23f512eb172f701c50e2d67159c4a56098 Mon Sep 17 00:00:00 2001 From: lmaosssdll Date: Fri, 3 Apr 2026 11:25:08 +0800 Subject: [PATCH 21/30] Delete tag logic:( --- src/layers/LobbySettings.cpp | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/layers/LobbySettings.cpp b/src/layers/LobbySettings.cpp index e4de627..46269dd 100755 --- a/src/layers/LobbySettings.cpp +++ b/src/layers/LobbySettings.cpp @@ -5,12 +5,7 @@ enum LobbySettingType { Turns, MinsPerTurn, Password, - IsPublic, - Tag -}; - -static const std::vector TAG_NAMES = { - "None", "Short", "Medium", "Long", "Layout", "Deco", "Impossible", "Triggers" + IsPublic }; class LobbySettingsCell : public CCNode { From dc9cf44973d8bde907239465ea2cc3d31890ae58 Mon Sep 17 00:00:00 2001 From: lmaosssdll Date: Fri, 3 Apr 2026 11:27:28 +0800 Subject: [PATCH 22/30] Add chat bubble and icon --- src/layers/ChatPanel.cpp | 112 ++++++++++++++++++++++++++++++++++----- 1 file changed, 100 insertions(+), 12 deletions(-) diff --git a/src/layers/ChatPanel.cpp b/src/layers/ChatPanel.cpp index 0d4451d..6355ea5 100755 --- a/src/layers/ChatPanel.cpp +++ b/src/layers/ChatPanel.cpp @@ -1,7 +1,11 @@ #include "ChatPanel.hpp" #include #include +#include +using namespace geode::prelude; + +// Factory method to create and initialize the ChatPanel ChatPanel* ChatPanel::create() { auto ret = new ChatPanel; if (ret->init()) { @@ -12,14 +16,17 @@ ChatPanel* ChatPanel::create() { return nullptr; } +// Sets up the network listener for incoming chat messages void ChatPanel::initialize() { if (!hasInitialized) { auto& nm = NetworkManager::get(); + + // Listen for messages from the server nm.on([](MessageSentPacket packet) { messages.push_back(packet.message); messagesQueue.push_back(packet.message); - // Уведомление о новом сообщении + // Show an in-game notification when a new message arrives Notification::create( fmt::format("{} sent a message", packet.message.author.name), CCSprite::createWithSpriteFrameName("GJ_chatBtn_001.png") @@ -30,7 +37,9 @@ void ChatPanel::initialize() { } } +// Initializes the popup UI (layout, inputs, buttons) bool ChatPanel::init() { + // Initialize base popup with size 350x280 if (!Popup::init(350.f, 280.f)) { return false; } @@ -38,6 +47,7 @@ bool ChatPanel::init() { this->setTitle("Chat"); ChatPanel::initialize(); + // Create a dark background container for the scroll layer auto scrollContainer = CCScale9Sprite::create("square02b_001.png"); scrollContainer->setContentSize({ m_mainLayer->getContentWidth() - 50.f, @@ -46,27 +56,34 @@ bool ChatPanel::init() { scrollContainer->setColor(ccc3(0,0,0)); scrollContainer->setOpacity(75); + // Set up the scrollable list for chat messages scrollLayer = ScrollLayer::create(scrollContainer->getContentSize() - 10.f); scrollLayer->ignoreAnchorPointForPosition(false); + + // Layout for the scroll container (messages stack from bottom to top) scrollLayer->m_contentLayer->setLayout( ColumnLayout::create() ->setAxisReverse(true) ->setAutoGrowAxis(scrollLayer->getContentHeight()) ->setAxisAlignment(AxisAlignment::End) - ->setGap(0) + ->setGap(6.f) // Gap between message bubbles ); scrollContainer->addChildAtPosition(scrollLayer, Anchor::Center); m_mainLayer->addChildAtPosition(scrollContainer, Anchor::Center, ccp(0, 5.f)); + // Create a container for the text input field and the send button auto inputContainer = CCNode::create(); inputContainer->setContentSize({ scrollContainer->getContentWidth(), 75.f }); inputContainer->setAnchorPoint({ 0.5f, 0.f }); messageInput = TextInput::create(inputContainer->getContentWidth(), "Send a message to the lobby!", "chatFont.fnt"); - // --- ИСПРАВЛЕНИЕ: Разрешаем любые символы (кириллицу, спецсимволы) --- - messageInput->setCommonFilter(CommonFilter::Any); + // Explicitly allow specific characters including: " ' ? ! * / . , + // Add Russian alphabet here if Cyrillic support is needed + std::string allowedChars = " abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\"'?!*/.,"; + messageInput->getInputNode()->setAllowedChars(allowedChars); + // Create the send message button auto sendMsgBtn = CCMenuItemExt::createSpriteExtraWithFrameName( "GJ_chatBtn_001.png", 0.75f, @@ -85,38 +102,103 @@ bool ChatPanel::init() { inputContainer->setLayout(RowLayout::create()); m_mainLayer->addChildAtPosition(inputContainer, Anchor::Bottom, ccp(0, 10.f)); + // Render any previously loaded messages for (auto message : ChatPanel::messages) { renderMessage(message); } + // Check for new messages every frame this->schedule(schedule_selector(ChatPanel::updateMessages)); return true; } +// Renders a single message with Player Icon (Cube) and Chat Bubble void ChatPanel::renderMessage(Message const& message) { - auto msgNode = CCNode::create(); + // 1. Create row container for Icon + Bubble + auto rowNode = CCNode::create(); + rowNode->setAnchorPoint({0.f, 0.f}); + + // 2. Create Player Icon (Cube) using logic from PlayerCell + auto playerIcon = SimplePlayer::create(0); + auto gm = GameManager::get(); + + playerIcon->updatePlayerFrame(message.author.iconID, IconType::Cube); + playerIcon->setColor(gm->colorForIdx(message.author.color1)); + playerIcon->setSecondColor(gm->colorForIdx(message.author.color2)); + + if (message.author.color3 == -1) { + playerIcon->disableGlowOutline(); + } else { + playerIcon->setGlowOutline(gm->colorForIdx(message.author.color3)); + } + + playerIcon->setScale(0.65f); + + // Wrap icon in a CCNode to keep Layout predictable + auto iconContainer = CCNode::create(); + iconContainer->setContentSize({30.f, 30.f}); + playerIcon->setPosition(iconContainer->getContentSize() / 2); + iconContainer->addChild(playerIcon); + + // 3. Create Text + float maxTextWidth = scrollLayer->getContentWidth() - 50.f; // Account for icon size + auto msgText = TextArea::create( fmt::format("{}: {}", message.author.name, message.message), - "chatFont.fnt", 0.5f, scrollLayer->getContentWidth(), {0.f, 0.f}, 17.f, false + "chatFont.fnt", 0.5f, maxTextWidth, {0.f, 1.f}, 17.f, false ); - msgText->setAnchorPoint({ 0.f, 0.f }); - msgNode->setContentHeight(msgText->m_label->m_lines->count() * 17.f); - msgNode->setContentWidth(scrollLayer->getContentWidth()); - msgText->setPosition({ 0.f, 0.f }); - msgNode->addChild(msgText); + msgText->setAnchorPoint({ 0.f, 1.f }); // Anchor top-left - scrollLayer->m_contentLayer->addChild(msgNode); + float textHeight = msgText->m_label->m_lines->count() * 17.f; + msgText->setContentSize({maxTextWidth, textHeight}); + + // 4. Create Chat Bubble (Background) + auto bubble = CCScale9Sprite::create("square02b_001.png"); + bubble->setColor(ccc3(0, 0, 0)); // Black bubble + bubble->setOpacity(120); // Semi-transparent + + float paddingX = 8.f; + float paddingY = 8.f; + bubble->setContentSize({maxTextWidth + (paddingX * 2), textHeight + (paddingY * 2)}); + + // Position text inside bubble + msgText->setPosition({paddingX, bubble->getContentHeight() - paddingY}); + bubble->addChild(msgText); + + // 5. Build the Row Layout + rowNode->addChild(iconContainer); + rowNode->addChild(bubble); + + // Make row container match bubble's height (or icon height, whichever is larger) + float rowHeight = std::max(bubble->getContentHeight(), iconContainer->getContentHeight()); + rowNode->setContentSize({scrollLayer->getContentWidth(), rowHeight}); + + // Align items horizontally + rowNode->setLayout( + RowLayout::create() + ->setGap(5.f) + ->setAxisAlignment(AxisAlignment::Start) + ->setCrossAxisOverflow(false) + ); + rowNode->updateLayout(); + + // 6. Add to Scroll Layer + scrollLayer->m_contentLayer->addChild(rowNode); scrollLayer->m_contentLayer->updateLayout(); } +// Processes the message queue and renders new incoming messages void ChatPanel::updateMessages(float dt) { + if (messagesQueue.empty()) return; + for (auto const& message : messagesQueue) { renderMessage(message); } messagesQueue.clear(); } +// Clears the message history and unbinds the network listener void ChatPanel::clearMessages() { messages.clear(); auto& nm = NetworkManager::get(); @@ -124,21 +206,27 @@ void ChatPanel::clearMessages() { hasInitialized = false; } +// Validates and sends the message to the server void ChatPanel::sendMessage() { auto msgInput = messageInput->getString(); + + // Prevent sending empty messages or messages containing only spaces if (msgInput.empty()) return; if (geode::utils::string::replace(" ", msgInput, "") == "") return; auto& nm = NetworkManager::get(); nm.send(SendMessagePacket::create(msgInput)); + // Clear the input field after sending messageInput->setString(""); } +// Allows sending a message by pressing the "Enter" key void ChatPanel::keyDown(cocos2d::enumKeyCodes keycode, double timestamp) { if (keycode == cocos2d::KEY_Enter && CCIMEDispatcher::sharedDispatcher()->hasDelegate()) { sendMessage(); } else { + // Forward other keys to the base Popup class Popup::keyDown(keycode, timestamp); } } From 2d5a56c48324676886176a9fd7b9a644af2a80c5 Mon Sep 17 00:00:00 2001 From: lmaosssdll Date: Fri, 3 Apr 2026 11:28:37 +0800 Subject: [PATCH 23/30] Remove tag logic and debug --- src/layers/Lobby.cpp | 58 +++----------------------------------------- 1 file changed, 3 insertions(+), 55 deletions(-) diff --git a/src/layers/Lobby.cpp b/src/layers/Lobby.cpp index e48ec40..27ed4b0 100755 --- a/src/layers/Lobby.cpp +++ b/src/layers/Lobby.cpp @@ -261,7 +261,7 @@ void LobbyLayer::refresh(LobbyInfo info, bool isFirstRefresh) { if (!mainLayer) return; - // --- Логика Уведомлений Входа/Выхода --- + // --- Join/Leave Notifications Logic --- if (isFirstRefresh) { m_currentPlayers.clear(); for (auto& acc : info.accounts) m_currentPlayers.push_back(acc.userID); @@ -282,61 +282,9 @@ void LobbyLayer::refresh(LobbyInfo info, bool isFirstRefresh) { m_currentPlayers = newPlayers; } - // --- Обновление визуальных настроек лобби (СЛЕВА СВЕРХУ) --- - // Удаляем старый UI настроек, если он есть - if (auto oldSettingsUI = mainLayer->getChildByID("lobby-settings-display")) { - oldSettingsUI->removeFromParent(); - } - - // Создаем новый контейнер для настроек - auto settingsDisplay = CCNode::create(); - settingsDisplay->setID("lobby-settings-display"); + // [REMOVED: The visual tags and turns/mins display code that was here previously] - // Текст: Ходы и время - auto statsLabel = CCLabelBMFont::create( - fmt::format("Turns: {} | Mins/Turn: {}", info.settings.turns, info.settings.minutesPerTurn).c_str(), - "chatFont.fnt" - ); - statsLabel->setAnchorPoint({ 0.f, 1.f }); - - // Текст: Выбранный тег с цветом - std::string tagNames[] = {"None", "Short", "Medium", "Long", "Layout", "Deco", "Impossible", "Triggers"}; - ccColor3B tagColors[] = { - {255, 255, 255}, // 0 None (Белый) - {100, 255, 100}, // 1 Short (Светло-зеленый) - {255, 255, 100}, // 2 Medium (Желтый) - {255, 150, 50}, // 3 Long (Оранжевый) - {100, 255, 255}, // 4 Layout (Бирюзовый) - {255, 100, 255}, // 5 Deco (Розовый/Пурпурный) - {255, 50, 50}, // 6 Impossible (Красный) - {200, 100, 255} // 7 Triggers (Фиолетовый) - }; - - int tagId = info.settings.tag; - if (tagId < 0 || tagId >= 8) tagId = 0; - - auto tagPrefix = CCLabelBMFont::create("Tag: ", "chatFont.fnt"); - auto tagValue = CCLabelBMFont::create(tagNames[tagId].c_str(), "chatFont.fnt"); - tagValue->setColor(tagColors[tagId]); - - auto tagContainer = CCMenu::create(); // Используем меню просто как Layout контейнер - tagContainer->addChild(tagPrefix); - tagContainer->addChild(tagValue); - tagContainer->setLayout(RowLayout::create()->setGap(2.f)->setAxisAlignment(AxisAlignment::Start)); - tagContainer->setAnchorPoint({ 0.f, 1.f }); - tagContainer->setContentSize({ 200.f, tagPrefix->getContentHeight() }); - - settingsDisplay->addChild(statsLabel); - settingsDisplay->addChild(tagContainer); - settingsDisplay->setLayout(ColumnLayout::create()->setGap(5.f)->setAxisReverse(true)->setAxisAlignment(AxisAlignment::Start)); - settingsDisplay->setContentSize({ 200.f, 50.f }); - settingsDisplay->setPosition({ 15.f, size.height - 15.f }); - settingsDisplay->setAnchorPoint({ 0.f, 1.f }); - - mainLayer->addChild(settingsDisplay); - // ----------------------------------------------------------- - - // Заголовок (только при первом заходе) + // Title label (only on first refresh) if (isFirstRefresh) { titleLabel = CCLabelBMFont::create(info.settings.name.c_str(), "bigFont.fnt"); titleLabel->limitLabelWidth(275.f, 1.f, 0.1f); From a7303f1bde7e711264e7097b9b94ba81eefc98a0 Mon Sep 17 00:00:00 2001 From: lmaosssdll Date: Fri, 3 Apr 2026 11:45:22 +0800 Subject: [PATCH 24/30] Opss! --- src/network/manager.cpp | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/network/manager.cpp b/src/network/manager.cpp index 22998d2..3c922a8 100755 --- a/src/network/manager.cpp +++ b/src/network/manager.cpp @@ -28,10 +28,10 @@ void NetworkManager::update(float dt) { } void NetworkManager::connect(bool shouldReconnect, std::function callback) { - log::debug("[Network] connecting..."); + log::debug("connecting"); if (this->isConnected && shouldReconnect) { // disconnect then reconnect - log::debug("[Network] already connected; disconnecting then reconnecting..."); + log::debug("already connected; disconnecting then reconnecting..."); this->showDisconnectPopup = false; this->disconnect(); } @@ -53,7 +53,7 @@ void NetworkManager::connect(bool shouldReconnect, std::function callbac socket.setOnMessageCallback([this, callback = std::move(callback)](const ix::WebSocketMessagePtr& msg) mutable { if (msg->type == ix::WebSocketMessageType::Error) { auto& errReason = msg->errorInfo.reason; - log::error("[Network] ixwebsocket error: {}", errReason); + log::error("ixwebsocket error: {}", errReason); Loader::get()->queueInMainThread([this, errReason = std::move(errReason)]() { FLAlertLayer::create( "CR Error", @@ -63,7 +63,7 @@ void NetworkManager::connect(bool shouldReconnect, std::function callbac }); return; } else if (msg->type == ix::WebSocketMessageType::Open) { - log::debug("[Network] connection success!"); + log::debug("connection success!"); // call middleware (this should run the provided cb) middleware([this, callback]() { @@ -99,7 +99,7 @@ void NetworkManager::connect(bool shouldReconnect, std::function callbac return; } else if (msg->type == ix::WebSocketMessageType::Close) { - log::debug("[Network] connection closed"); + log::debug("connection closed"); this->isConnected = false; auto& reason = msg->closeInfo.reason; Loader::get()->queueInMainThread([this, reason = std::move(reason)]() mutable { @@ -122,10 +122,7 @@ void NetworkManager::connect(bool shouldReconnect, std::function callbac }); return; } else if (msg->type == ix::WebSocketMessageType::Message) { - if (msg.get()->str == "") { - log::error("[Network] Received completely empty message string from server!"); - return; - } + if (msg.get()->str == "") return; this->onMessage(msg); } }); @@ -144,7 +141,7 @@ void NetworkManager::onMessage(const ix::WebSocketMessagePtr& msg) { auto packetIdIdx = strMsg.find("|"); if (packetIdIdx == std::string::npos) { - log::error("[Network] invalid packet received (missing packet ID separator): {}", strMsg); + log::error("invalid packet received"); return; } @@ -153,7 +150,7 @@ void NetworkManager::onMessage(const ix::WebSocketMessagePtr& msg) { auto it = listeners.find(packetId); if (it == listeners.end()) { - log::error("[Network] unhandled packed ID {}", packetId); + log::error("unhandled packed ID {}", packetId); return; } From 5c19096efa51219935a5a23f3e99d9657b664fe7 Mon Sep 17 00:00:00 2001 From: lmaosssdll Date: Fri, 3 Apr 2026 11:46:05 +0800 Subject: [PATCH 25/30] Opss! --- src/network/manager.hpp | 56 +++++++++-------------------------------- 1 file changed, 12 insertions(+), 44 deletions(-) diff --git a/src/network/manager.hpp b/src/network/manager.hpp index 4adc7e6..90e9571 100755 --- a/src/network/manager.hpp +++ b/src/network/manager.hpp @@ -52,25 +52,14 @@ class CR_DLL NetworkManager : public CCObject { requires std::is_base_of_v inline void on(std::function callback, bool shouldUnbind = false) { listeners[T::PACKET_ID] = [callback = std::move(callback)](std::string msg) mutable { - -#ifdef CR_DEBUG - // Log the size of the incoming data to detect truncated or empty server responses - log::debug("[Network] Received packet {}, Raw string size: {} bytes", T::PACKET_NAME, msg.size()); -#endif - std::stringstream ss(std::move(msg)); + T packet = T::create(); - try { + { cereal::JSONInputArchive iarchive(ss); + iarchive(cereal::make_nvp("packet", packet)); - } catch (const std::exception& e) { - // If the Java server sends missing fields, empty strings, or corrupted JSON, - // cereal will throw an exception here instead of silently passing a "void/empty" level. - log::error("[Network] CRITICAL ERROR: Failed to parse packet {} ({}).", T::PACKET_NAME, T::PACKET_ID); - log::error("[Network] Reason: The server sent corrupted, incomplete, or empty JSON data."); - log::error("[Network] Cereal exception detail: {}", e.what()); - return; // Prevent sending an empty/broken level to the main game thread } Loader::get()->queueInMainThread([callback, packet = std::move(packet)]() mutable { @@ -85,13 +74,13 @@ class CR_DLL NetworkManager : public CCObject { if (!this->isConnected) return; if (!listeners.contains(T::PACKET_ID)) { - log::error("[Network] unable to remove listener for {} ({}), listener does not exist", T::PACKET_NAME, T::PACKET_ID); + log::error("unable to remove listener for {} ({}), listener does not exist", T::PACKET_NAME, T::PACKET_ID); return; } listeners.erase(T::PACKET_ID); - log::debug("[Network] removed listener for {} ({})", T::PACKET_NAME, T::PACKET_ID); + log::debug("removed listener for {} ({})", T::PACKET_NAME, T::PACKET_ID); } template @@ -100,45 +89,28 @@ class CR_DLL NetworkManager : public CCObject { return; } +#ifdef CR_DEBUG + log::debug("sending packet {} ({})", packet.getPacketName(), packet.getPacketID()); +#endif std::stringstream ss; - // cereal uses RAII, meaning the contents of `ss` is guaranteed + // cereal uses RAII, meaning + // the contents of `ss` is guaranteed // to be filled at the end of the braces { cereal::JSONOutputArchive oarchive(ss); oarchive(cereal::make_nvp("packet", packet)); } - - auto jsonRes = matjson::parse(ss.str()).mapErr([](std::string err) { return err; }); - if (jsonRes.isErr()) { - log::error("[Network] Failed to parse local JSON for packet {}: {}", packet.getPacketID(), jsonRes.unwrapErr()); - return; - } - auto json = jsonRes.unwrap(); + auto json = matjson::parse(ss.str()).mapErr([](std::string err) { return err; }).unwrap(); json["packet_id"] = packet.getPacketID(); auto uncompressedStr = json.dump(0); + unsigned char* compressedData; -#ifdef CR_DEBUG - // Log outgoing uncompressed size to see if the client generated an empty level string before sending - log::debug("[Network] Sending packet {} ({}), Uncompressed size: {} bytes", packet.getPacketName(), packet.getPacketID(), uncompressedStr.size()); -#endif - - unsigned char* compressedData = nullptr; size_t compressedSize = ZipUtils::ccDeflateMemory( reinterpret_cast(uncompressedStr.data()), uncompressedStr.size(), &compressedData ); - - // SAFETY CHECK: Prevent sending "garbage" or empty data if compression fails due to memory/sync issues - if (compressedSize == 0 || compressedData == nullptr) { - log::error("[Network] CRITICAL ERROR: ccDeflateMemory failed to compress packet {} ({})!", packet.getPacketName(), packet.getPacketID()); - log::error("[Network] Reason: The level string generated by the game might be completely empty, or the device ran out of memory."); - log::error("[Network] Packet was dropped to prevent sending a void level to the server."); - if (compressedData) free(compressedData); // Prevent memory leak just in case - return; - } - ix::IXWebSocketSendData data( reinterpret_cast(compressedData), compressedSize @@ -153,10 +125,6 @@ class CR_DLL NetworkManager : public CCObject { return true; } ); - - // VERY IMPORTANT: ZipUtils::ccDeflateMemory allocates memory using malloc internally. - // We MUST free it here, otherwise the game will slowly leak memory and start lagging. - free(compressedData); } protected: NetworkManager(); From 7e08e53c5c4507b0413e9a1dd1b713071731114f Mon Sep 17 00:00:00 2001 From: lmaosssdll Date: Fri, 3 Apr 2026 12:03:11 +0800 Subject: [PATCH 26/30] Opss! --- src/managers/SwapManager.cpp | 75 ++++++++++-------------------------- 1 file changed, 20 insertions(+), 55 deletions(-) diff --git a/src/managers/SwapManager.cpp b/src/managers/SwapManager.cpp index 293eae9..b22b478 100755 --- a/src/managers/SwapManager.cpp +++ b/src/managers/SwapManager.cpp @@ -128,57 +128,31 @@ void SwapManager::registerListeners() { nm.setDisconnectCallback([](std::string reason) {}); - // --- SENDING THE LEVEL TO THE SERVER --- nm.on([this](TimeToSwapPacket p) { auto& nm = NetworkManager::get(); Notification::create("Swapping levels!", NotificationIcon::Info, 2.5f)->show(); - // 1. Force save the level + auto filePath = std::filesystem::temp_directory_path() / fmt::format("temp{}.gmd", rand()); + if (auto editorLayer = LevelEditorLayer::get()) { - // Update internal level string before saving to prevent race condition - editorLayer->updateLevelString(); auto fakePauseLayer = EditorPauseLayer::create(editorLayer); fakePauseLayer->saveLevel(); } - auto lvl = EditorIDs::getLevelByID(levelId); - if (!lvl) { - log::error("[SwapManager] CRITICAL ERROR: Could not find level by ID {} before swapping!", levelId); - return; - } + // this crashes macos when recieving the level + // do not ask me why + // this game is taped-together jerry-rigged piece of software + // lvl->m_levelDesc = fmt::format("from: {}", this->createAccountType().name); - // 2. Generate level string auto b = new DS_Dictionary(); lvl->encodeWithCoder(b); - std::string finalLevelString = b->saveRootSubDictToString(); - - // 3. FIX MEMORY LEAK: We must delete 'b' after using it! - delete b; - - // 4. PREVENT EMPTY LEVEL BUG - if (finalLevelString.empty() || finalLevelString.size() < 10) { - log::error("[SwapManager] CRITICAL ERROR: b->saveRootSubDictToString() generated an EMPTY string!"); - log::error("[SwapManager] The game engine did not save the level fast enough (Race Condition)."); - - // Fallback: Try to use the cached m_levelString directly - if (!lvl->m_levelString.empty()) { - finalLevelString = lvl->m_levelString; - log::warn("[SwapManager] Used fallback m_levelString (Size: {} bytes).", finalLevelString.size()); - } else { - log::error("[SwapManager] Fallback failed! m_levelString is also empty. Sending void level..."); - } - } else { -#ifdef CR_DEBUG - log::debug("[SwapManager] Successfully generated level string. Size: {} bytes.", finalLevelString.size()); -#endif - } LevelData lvlData = { .levelName = lvl->m_levelName, .songID = lvl->m_songID, .songIDs = lvl->m_songIDs, - .levelString = finalLevelString + .levelString = b->saveRootSubDictToString() }; nm.send( @@ -190,41 +164,33 @@ void SwapManager::registerListeners() { GameLevelManager::sharedState()->deleteLevel(lvl); } }); - - // --- RECEIVING THE LEVEL FROM THE SERVER --- nm.on([this](ReceiveSwappedLevelPacket packet) { + // if (packet->levels.size() < swapIdx) { + // FLAlertLayer::create( + // "Creation Rotation", + // "There was an error while fetching the swapped level. If you're reading this, something has gone terribly wrong, please report it at once.", + // "OK" + // )->show(); + // return; + // } LevelData lvlData; - bool foundMyLevel = false; for (auto& swappedLevel : packet.levels) { if (swappedLevel.accountID != cr::utils::createAccountType().accountID) continue; lvlData = std::move(swappedLevel.level); - foundMyLevel = true; break; } - // 1. PREVENT EMPTY LEVEL BUG ON RECEIVE - if (!foundMyLevel) { - log::error("[SwapManager] CRITICAL ERROR: Server did not send a level for my Account ID!"); - } else if (lvlData.levelString.empty()) { - log::error("[SwapManager] CRITICAL ERROR: The server sent a level, but the levelString is EMPTY!"); - log::error("[SwapManager] This means the previous player failed to upload it, or the server wiped it."); - } else { -#ifdef CR_DEBUG - log::debug("[SwapManager] Successfully received swapped level! String size: {} bytes.", lvlData.levelString.size()); -#endif - } - auto lvl = GJGameLevel::create(); - // 2. Load the level data + // lvl->m_levelName = lvlData.levelName; + // lvl->m_levelString = lvlData.levelString; + // lvl->m_songID = lvlData.songID; + // lvl->m_songIDs = lvlData.songIDs; auto b = new DS_Dictionary(); b->loadRootSubDictFromString(lvlData.levelString); lvl->dataLoaded(b); - - // 3. FIX MEMORY LEAK: We must delete 'b' after loading data! - delete b; lvl->m_levelType = GJLevelType::Editor; @@ -242,9 +208,8 @@ void SwapManager::registerListeners() { roundStartedTime = time(nullptr); }); - nm.on([this](SwapEndedPacket p) { - log::debug("[SwapManager] swap ended; disconnecting from server"); + log::debug("swap ended; disconnecting from server"); auto& nm = NetworkManager::get(); nm.showDisconnectPopup = false; this->disconnectLobby(); From 3ff7c8d38a6639ed1aa5d4bbc40fc25a6c766150 Mon Sep 17 00:00:00 2001 From: lmaosssdll Date: Fri, 3 Apr 2026 12:16:25 +0800 Subject: [PATCH 27/30] Update Lobby.cpp --- src/layers/Lobby.cpp | 53 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 45 insertions(+), 8 deletions(-) diff --git a/src/layers/Lobby.cpp b/src/layers/Lobby.cpp index 27ed4b0..351fcb0 100755 --- a/src/layers/Lobby.cpp +++ b/src/layers/Lobby.cpp @@ -7,6 +7,7 @@ #include "ChatPanel.hpp" #include +// Factory method to create a player cell PlayerCell* PlayerCell::create(Account account, float width, bool canKick) { auto ret = new PlayerCell; if (ret->init(account, width, canKick)) { @@ -17,11 +18,13 @@ PlayerCell* PlayerCell::create(Account account, float width, bool canKick) { return nullptr; } +// Initializes the player cell UI (icon, name, kick button) bool PlayerCell::init(Account account, float width, bool canKick) { m_account = account; this->setContentSize({ width, CELL_HEIGHT }); + // Create player icon based on account data auto player = SimplePlayer::create(0); auto gm = GameManager::get(); @@ -29,6 +32,7 @@ bool PlayerCell::init(Account account, float width, bool canKick) { player->setColor(gm->colorForIdx(account.color1)); player->setSecondColor(gm->colorForIdx(account.color2)); + // Disable glow if color3 is not set if (account.color3 == -1) { player->disableGlowOutline(); } else { @@ -41,10 +45,12 @@ bool PlayerCell::init(Account account, float width, bool canKick) { this->addChild(player); + // Create player name label auto nameLabel = CCLabelBMFont::create(account.name.c_str(), "bigFont.fnt"); nameLabel->limitLabelWidth(225.f, 0.8f, 0.1f); nameLabel->setAnchorPoint({ 0.f, 0.5f }); + // Clicking the name opens their profile auto nameBtn = CCMenuItemExt::createSpriteExtra( nameLabel, [this](CCObject*) { @@ -59,6 +65,7 @@ bool PlayerCell::init(Account account, float width, bool canKick) { cellMenu->setContentSize({ width, CELL_HEIGHT }); cellMenu->addChild(nameBtn); + // Add kick button if current user is host and not kicking themselves if (canKick && account.userID != GameManager::get()->m_playerUserID.value()) { auto kickSpr = CCSprite::createWithSpriteFrameName("accountBtn_removeFriend_001.png"); kickSpr->setScale(0.725f); @@ -74,6 +81,7 @@ bool PlayerCell::init(Account account, float width, bool canKick) { return true; } +// Sends a kick request to the server void PlayerCell::onKickUser(CCObject* sender) { geode::createQuickPopup( "Kick User", @@ -87,6 +95,7 @@ void PlayerCell::onKickUser(CCObject* sender) { ); } +// Factory method for LobbyLayer LobbyLayer* LobbyLayer::create(std::string code) { auto ret = new LobbyLayer(); if (ret->init(code)) { @@ -97,6 +106,7 @@ LobbyLayer* LobbyLayer::create(std::string code) { return nullptr; } +// Initializes the lobby layer (background, buttons, UI) bool LobbyLayer::init(std::string code) { lobbyCode = code; m_alive = std::make_shared(true); @@ -109,6 +119,7 @@ bool LobbyLayer::init(std::string code) { mainLayer = CCNode::create(); mainLayer->setContentSize(size); + // Setup background background = CCSprite::create("GJ_gradientBG.png"); background->setScaleX(size.width / background->getContentWidth()); background->setScaleY(size.height / background->getContentHeight()); @@ -117,6 +128,7 @@ bool LobbyLayer::init(std::string code) { background->setZOrder(-10); mainLayer->addChild(background); + // Setup close and disconnect buttons auto closeBtnSprite = CCSprite::createWithSpriteFrameName("GJ_arrow_01_001.png"); closeBtn = CCMenuItemExt::createSpriteExtra( closeBtnSprite, [this](CCObject* target) { @@ -132,6 +144,7 @@ bool LobbyLayer::init(std::string code) { onDisconnect(target); } ); + auto closeMenu = CCMenu::create(); closeMenu->addChild(closeBtn); closeMenu->addChild(disconnectBtn); @@ -142,18 +155,20 @@ bool LobbyLayer::init(std::string code) { geode::addSideArt(mainLayer, SideArt::Bottom); geode::addSideArt(mainLayer, SideArt::TopRight); + // Setup start button auto startBtnSpr = CCSprite::create("swap-btn.png"_spr); startBtnSpr->setScale(0.3f); startBtn = CCMenuItemSpriteExtra::create( startBtnSpr, this, menu_selector(LobbyLayer::onStart) ); + auto startMenu = CCMenu::create(); startMenu->setZOrder(5); startMenu->addChild(startBtn); startMenu->setPosition({ size.width / 2, 30.f }); - mainLayer->addChild(startMenu); + // Setup chat, settings and refresh buttons auto msgButtonSpr = CCSprite::create("messagesBtn.png"_spr); auto msgButton = CCMenuItemExt::createSpriteExtra( msgButtonSpr, [](CCObject*) { ChatPanel::create()->show(); } @@ -183,6 +198,7 @@ bool LobbyLayer::init(std::string code) { bottomMenu->setPosition(size.width - 25.f, bottomMenu->getChildrenCount() * 25.f); bottomMenu->setLayout(ColumnLayout::create()->setAxisReverse(true)); + // Setup Discord button auto discordSpr = CCSprite::createWithSpriteFrameName("gj_discordIcon_001.png"); discordSpr->setScale(1.25f); auto discordBtn = CCMenuItemExt::createSpriteExtra( @@ -199,6 +215,7 @@ bool LobbyLayer::init(std::string code) { ); } ); + auto discordMenu = CCMenu::create(); discordMenu->setPosition({ 25.f, 25.f }); discordMenu->addChild(discordBtn); @@ -209,26 +226,32 @@ bool LobbyLayer::init(std::string code) { this->addChild(mainLayer); this->addChild(bottomMenu); + // Load initial lobby info SwapManager::get().getLobbyInfo([this, alive = m_alive](LobbyInfo info) { if (!*alive) return; refresh(info, true); }); + registerListeners(); return true; } +// Binds network packet listeners void LobbyLayer::registerListeners() { auto& nm = NetworkManager::get(); + nm.on([this, alive = m_alive](LobbyUpdatedPacket packet) { if (!*alive) return; this->refresh(std::move(packet.info)); }); + nm.on([](SwapStartedPacket packet) { auto& sm = SwapManager::get(); sm.startSwap(std::move(packet)); NetworkManager::get().unbind(); }); + nm.showDisconnectPopup = true; nm.setDisconnectCallback([this, alive = m_alive](std::string reason) { if (!*alive) return; @@ -240,6 +263,7 @@ void LobbyLayer::registerListeners() { }); } +// Unbinds network packet listeners void LobbyLayer::unregisterListeners() { auto& nm = NetworkManager::get(); nm.unbind(); @@ -247,6 +271,7 @@ void LobbyLayer::unregisterListeners() { nm.setDisconnectCallback(nullptr); } +// Destructor LobbyLayer::~LobbyLayer() { if (m_alive) { *m_alive = false; @@ -254,6 +279,7 @@ LobbyLayer::~LobbyLayer() { unregisterListeners(); } +// Updates UI with new lobby data from the server void LobbyLayer::refresh(LobbyInfo info, bool isFirstRefresh) { isOwner = GameManager::get()->m_playerUserID == info.settings.owner.userID; auto size = CCDirector::sharedDirector()->getWinSize(); @@ -264,7 +290,9 @@ void LobbyLayer::refresh(LobbyInfo info, bool isFirstRefresh) { // --- Join/Leave Notifications Logic --- if (isFirstRefresh) { m_currentPlayers.clear(); - for (auto& acc : info.accounts) m_currentPlayers.push_back(acc.userID); + for (auto& acc : info.accounts) { + m_currentPlayers.push_back(acc.userID); + } } else { std::vector newPlayers; for (auto& acc : info.accounts) newPlayers.push_back(acc.userID); @@ -282,9 +310,7 @@ void LobbyLayer::refresh(LobbyInfo info, bool isFirstRefresh) { m_currentPlayers = newPlayers; } - // [REMOVED: The visual tags and turns/mins display code that was here previously] - - // Title label (only on first refresh) + // Title label (Only create on first refresh) if (isFirstRefresh) { titleLabel = CCLabelBMFont::create(info.settings.name.c_str(), "bigFont.fnt"); titleLabel->limitLabelWidth(275.f, 1.f, 0.1f); @@ -303,10 +329,14 @@ void LobbyLayer::refresh(LobbyInfo info, bool isFirstRefresh) { mainLayer->addChild(menu); } - if (titleLabel) titleLabel->setString( - fmt::format("{} ({})", info.settings.name, Mod::get()->getSettingValue("hide-code") ? "......" : info.code).c_str() - ); + // Update title text (Hide code if setting is enabled) + if (titleLabel) { + titleLabel->setString( + fmt::format("{} ({})", info.settings.name, Mod::get()->getSettingValue("hide-code") ? "......" : info.code).c_str() + ); + } + // Rebuild the player list if (!playerList && !isFirstRefresh) return; if (playerList) playerList->removeFromParent(); @@ -332,6 +362,7 @@ void LobbyLayer::refresh(LobbyInfo info, bool isFirstRefresh) { mainLayer->addChild(playerList); + // Recreate or update list background auto listBG = static_cast(mainLayer->getChildByIDRecursive("list-bg")); if (!listBG) { listBG = CCLayerColor::create({ 0, 0, 0, 85 }); @@ -344,10 +375,12 @@ void LobbyLayer::refresh(LobbyInfo info, bool isFirstRefresh) { listBG->setPosition(playerList->getPosition()); listBG->setContentSize(playerList->getContentSize()); + // Toggle button visibility based on host status if (settingsBtn) settingsBtn->setVisible(isOwner); if (startBtn) startBtn->setVisible(isOwner); } +// Triggered when host clicks start void LobbyLayer::onStart(CCObject* sender) { if (!isOwner) return; @@ -363,6 +396,7 @@ void LobbyLayer::onStart(CCObject* sender) { ); } +// Triggered when host clicks settings void LobbyLayer::onSettings(CCObject* sender) { auto& lm = SwapManager::get(); lm.getLobbyInfo([this, alive = m_alive](LobbyInfo info) { @@ -376,6 +410,7 @@ void LobbyLayer::onSettings(CCObject* sender) { }); } +// Draws borders and corners around the player list void LobbyLayer::createBorders() { #define CREATE_SIDE() CCSprite::createWithSpriteFrameName("GJ_table_side_001.png") @@ -444,6 +479,7 @@ void LobbyLayer::createBorders() { playerList->addChild(bottomRightCorner); } +// Disconnects from the server and lobby completely void LobbyLayer::onDisconnect(CCObject* sender) { geode::createQuickPopup( "Disconnect", @@ -460,6 +496,7 @@ void LobbyLayer::onDisconnect(CCObject* sender) { ); } +// Called when player clicks the back arrow (Leaves menu, keeps connection alive) void LobbyLayer::keyBackClicked() { geode::createQuickPopup( "Leave Layer?", From 5e3cc2947bbb988a6ee20cae723fa51a0e8dd7fe Mon Sep 17 00:00:00 2001 From: lmaosssdll Date: Fri, 3 Apr 2026 12:21:05 +0800 Subject: [PATCH 28/30] My code is too bad --- src/layers/LobbySettings.cpp | 162 +++++++++++++++++++++++------------ 1 file changed, 109 insertions(+), 53 deletions(-) diff --git a/src/layers/LobbySettings.cpp b/src/layers/LobbySettings.cpp index 46269dd..50427ba 100755 --- a/src/layers/LobbySettings.cpp +++ b/src/layers/LobbySettings.cpp @@ -5,7 +5,7 @@ enum LobbySettingType { Turns, MinsPerTurn, Password, - IsPublic + IsPublic }; class LobbySettingsCell : public CCNode { @@ -13,19 +13,37 @@ class LobbySettingsCell : public CCNode { bool init(float width, std::string name, LobbySettingType type, std::string desc, std::string defaultStr = "", std::string filter = "") { this->type = type; - this->setContentSize({ width, CELL_HEIGHT }); - - auto nameLabel = CCLabelBMFont::create(name.c_str(), "bigFont.fnt"); - nameLabel->setPosition({ 5.f, CELL_HEIGHT / 2.f }); - nameLabel->setAnchorPoint({ 0.f, 0.5f }); + this->setContentSize({ + width, CELL_HEIGHT + }); + + auto nameLabel = CCLabelBMFont::create( + name.c_str(), + "bigFont.fnt" + ); + + nameLabel->setPosition({ + 5.f, CELL_HEIGHT / 2.f + }); + nameLabel->setAnchorPoint({ + 0.f, 0.5f + }); nameLabel->limitLabelWidth(80.f, 0.5f, 0.1f); this->addChild(nameLabel); - auto infoBtn = InfoAlertButton::create(name, desc, 0.5f); + auto infoBtn = InfoAlertButton::create( + name, + desc, + 0.5f + ); auto menu = CCMenu::create(); menu->addChild(infoBtn); - menu->setPosition({ nameLabel->getScaledContentWidth() + 15.f, CELL_HEIGHT / 2.f }); - menu->setAnchorPoint({ 0.f, 0.5f }); + menu->setPosition({ + nameLabel->getScaledContentWidth() + 15.f, CELL_HEIGHT / 2.f + }); + menu->setAnchorPoint({ + 0.f, 0.5f + }); this->addChild(menu); switch (this->type) { @@ -36,8 +54,12 @@ class LobbySettingsCell : public CCNode { input = TextInput::create(95.f, name); input->setString(defaultStr); if (filter != "") input->getInputNode()->setAllowedChars(filter); - input->setPosition({ width - input->getContentWidth() - 5.f, CELL_HEIGHT / 2.f }); - input->setAnchorPoint({ 0.f, 0.5f }); + input->setPosition({ + width - input->getContentWidth() - 5.f, CELL_HEIGHT / 2.f + }); + input->setAnchorPoint({ + 0.f, 0.5f + }); this->addChild(input); break; } @@ -48,29 +70,18 @@ class LobbySettingsCell : public CCNode { togglerVal = !toggler->isOn(); } ); - toggler->toggle(geode::utils::numFromString(defaultStr).unwrapOr(0)); - auto menu2 = CCMenu::create(); - menu2->addChild(toggler); - menu2->setPosition({ width - toggler->getContentWidth() - 5.f, CELL_HEIGHT / 2.f }); - menu2->setAnchorPoint({ 0.f, 0.5f }); - this->addChild(menu2); - break; - } - case Tag: { // Логика для кнопки выбора тегов - tagIdx = geode::utils::numFromString(defaultStr).unwrapOr(0); - if (tagIdx < 0 || tagIdx >= TAG_NAMES.size()) tagIdx = 0; - - auto btnSpr = ButtonSprite::create(TAG_NAMES[tagIdx].c_str(), 80, true, "bigFont.fnt", "GJ_button_04.png", 30.f, 0.6f); - tagBtn = CCMenuItemExt::createSpriteExtra(btnSpr, [this](CCObject*) { - tagIdx = (tagIdx + 1) % TAG_NAMES.size(); - auto newSpr = ButtonSprite::create(TAG_NAMES[tagIdx].c_str(), 80, true, "bigFont.fnt", "GJ_button_04.png", 30.f, 0.6f); - tagBtn->setNormalImage(newSpr); + toggler->toggle( + geode::utils::numFromString(defaultStr).unwrapOr(0) + ); + auto menu = CCMenu::create(); + menu->addChild(toggler); + menu->setPosition({ + width - toggler->getContentWidth() - 5.f, CELL_HEIGHT / 2.f }); - - auto menu3 = CCMenu::create(); - menu3->addChild(tagBtn); - menu3->setPosition({ width - 50.f, CELL_HEIGHT / 2.f }); - this->addChild(menu3); + menu->setAnchorPoint({ + 0.f, 0.5f + }); + this->addChild(menu); break; } } @@ -80,22 +91,38 @@ class LobbySettingsCell : public CCNode { public: static constexpr int CELL_HEIGHT = 35.f; - TextInput* input = nullptr; - CCMenuItemToggler* toggler = nullptr; - CCMenuItemSpriteExtra* tagBtn = nullptr; + TextInput* input; + CCMenuItemToggler* toggler; LobbySettingType type; bool togglerVal = false; - int tagIdx = 0; // Сохраняем индекс тега void save(LobbySettings& settings) { switch (this->type) { - case Name: settings.name = this->input->getString(); break; - case Turns: settings.turns = geode::utils::numFromString(this->input->getString()).unwrapOr(0); break; - case MinsPerTurn: settings.minutesPerTurn = geode::utils::numFromString(this->input->getString()).unwrapOr(1); break; - case Password: break; - case IsPublic: settings.isPublic = togglerVal; break; - case Tag: settings.tag = tagIdx; break; // Сохраняем выбранный тег + case Name: { + settings.name = this->input->getString(); + break; + } + case Turns: { + settings.turns = geode::utils::numFromString( + this->input->getString() + ).unwrapOr(0); + break; + } + case MinsPerTurn: { + settings.minutesPerTurn = geode::utils::numFromString( + this->input->getString() + ).unwrapOr(1); + break; + } + case Password: { + // settings.password = this->input->getString(); + break; + } + case IsPublic: { + settings.isPublic = togglerVal; + break; + } } } @@ -121,36 +148,64 @@ LobbySettingsPopup* LobbySettingsPopup::create(LobbySettings const& settings, Ca } bool LobbySettingsPopup::init(LobbySettings const& settings, Callback callback) { - if (!Popup::init(250.f, 250.f)) { // Немного увеличил высоту для новой кнопки + if (!Popup::init(250.f, 230.f)) { return false; } m_noElasticity = true; this->setTitle("Lobby Settings"); + CCNode* nameContainer = CCNode::create(); + nameContainer->setLayout( + RowLayout::create() + ->setGap(-5.f) + ); + auto settingsContents = CCArray::create(); #define ADD_SETTING(name, element, desc, type) settingsContents->addObject( \ - LobbySettingsCell::create(220.f, name, LobbySettingType::type, desc, settings.element)); + LobbySettingsCell::create( \ + 220.f, \ + name,\ + LobbySettingType::type, \ + desc, \ + settings.element \ + )); #define ADD_SETTING_INT(name, element, desc, type) settingsContents->addObject( \ - LobbySettingsCell::create(220.f, name, LobbySettingType::type, desc, std::to_string(settings.element), "0123456789")); + LobbySettingsCell::create( \ + 220.f, \ + name,\ + LobbySettingType::type, \ + desc, \ + std::to_string(settings.element), \ + "0123456789" \ + )); #define ADD_SETTING_BOOL(name, element, desc, type) settingsContents->addObject( \ - LobbySettingsCell::create(220.f, name, LobbySettingType::type, desc, std::to_string(settings.element), "")); + LobbySettingsCell::create( \ + 220.f, \ + name,\ + LobbySettingType::type, \ + desc, \ + std::to_string(settings.element), \ + "" \ + )); ADD_SETTING("Name", name, "Name of the lobby", Name) ADD_SETTING_INT("Turns", turns, "Number of turns per level", Turns) - ADD_SETTING_INT("Minutes/Turn", minutesPerTurn, "Amount of minutes per turn", MinsPerTurn) + ADD_SETTING_INT("Minutes per turn", minutesPerTurn, "Amount of minutes per turn", MinsPerTurn) ADD_SETTING_BOOL("Public", isPublic, "Whether the swap is public. If it is, it will be shown in the room list for others to join.", IsPublic) - - // Добавляем тег в меню - ADD_SETTING_INT("Tag", tag, "Select a tag to describe your lobby", Tag) + // ADD_SETTING("Password", password, Password) - auto settingsList = ListView::create(settingsContents, LobbySettingsCell::CELL_HEIGHT, 220.f, 200.f); + auto settingsList = ListView::create(settingsContents, LobbySettingsCell::CELL_HEIGHT, 220.f, 180.f); settingsList->ignoreAnchorPointForPosition(false); - auto border = Border::create(settingsList, {0, 0, 0, 75}, {220.f, 200.f}); + auto border = Border::create( + settingsList, + {0, 0, 0, 75}, + {220.f, 180.f} + ); if(auto borderSprite = typeinfo_cast(border->getChildByID("geode.loader/border_sprite"))) { float scaleFactor = 1.7f; borderSprite->setContentSize(CCSize{borderSprite->getContentSize().width, borderSprite->getContentSize().height + 3} / scaleFactor); @@ -176,6 +231,7 @@ bool LobbySettingsPopup::init(LobbySettings const& settings, Callback callback) ); auto menu = CCMenu::create(); menu->addChild(submitBtn); + m_mainLayer->addChildAtPosition(menu, Anchor::Bottom); return true; From 4ae1526c0015100fb3a1f75e9ec1c13ade1dc67a Mon Sep 17 00:00:00 2001 From: lmaosssdll Date: Fri, 3 Apr 2026 13:02:16 +0800 Subject: [PATCH 29/30] Better chat --- src/layers/ChatPanel.cpp | 171 ++++++++++++++++++++------------------- 1 file changed, 88 insertions(+), 83 deletions(-) diff --git a/src/layers/ChatPanel.cpp b/src/layers/ChatPanel.cpp index 6355ea5..f0fe57c 100755 --- a/src/layers/ChatPanel.cpp +++ b/src/layers/ChatPanel.cpp @@ -5,7 +5,6 @@ using namespace geode::prelude; -// Factory method to create and initialize the ChatPanel ChatPanel* ChatPanel::create() { auto ret = new ChatPanel; if (ret->init()) { @@ -16,17 +15,14 @@ ChatPanel* ChatPanel::create() { return nullptr; } -// Sets up the network listener for incoming chat messages void ChatPanel::initialize() { if (!hasInitialized) { auto& nm = NetworkManager::get(); - - // Listen for messages from the server + nm.on([](MessageSentPacket packet) { messages.push_back(packet.message); messagesQueue.push_back(packet.message); - - // Show an in-game notification when a new message arrives + Notification::create( fmt::format("{} sent a message", packet.message.author.name), CCSprite::createWithSpriteFrameName("GJ_chatBtn_001.png") @@ -37,9 +33,7 @@ void ChatPanel::initialize() { } } -// Initializes the popup UI (layout, inputs, buttons) bool ChatPanel::init() { - // Initialize base popup with size 350x280 if (!Popup::init(350.f, 280.f)) { return false; } @@ -47,49 +41,44 @@ bool ChatPanel::init() { this->setTitle("Chat"); ChatPanel::initialize(); - // Create a dark background container for the scroll layer + // Dark container behind the scroll area auto scrollContainer = CCScale9Sprite::create("square02b_001.png"); scrollContainer->setContentSize({ m_mainLayer->getContentWidth() - 50.f, m_mainLayer->getContentHeight() - 85.f }); - scrollContainer->setColor(ccc3(0,0,0)); + scrollContainer->setColor(ccc3(0, 0, 0)); scrollContainer->setOpacity(75); - // Set up the scrollable list for chat messages scrollLayer = ScrollLayer::create(scrollContainer->getContentSize() - 10.f); scrollLayer->ignoreAnchorPointForPosition(false); - - // Layout for the scroll container (messages stack from bottom to top) + scrollLayer->m_contentLayer->setLayout( ColumnLayout::create() ->setAxisReverse(true) ->setAutoGrowAxis(scrollLayer->getContentHeight()) ->setAxisAlignment(AxisAlignment::End) - ->setGap(6.f) // Gap between message bubbles + ->setGap(6.f) ); scrollContainer->addChildAtPosition(scrollLayer, Anchor::Center); m_mainLayer->addChildAtPosition(scrollContainer, Anchor::Center, ccp(0, 5.f)); - // Create a container for the text input field and the send button + // Input + send button auto inputContainer = CCNode::create(); inputContainer->setContentSize({ scrollContainer->getContentWidth(), 75.f }); inputContainer->setAnchorPoint({ 0.5f, 0.f }); - messageInput = TextInput::create(inputContainer->getContentWidth(), "Send a message to the lobby!", "chatFont.fnt"); - - // Explicitly allow specific characters including: " ' ? ! * / . , - // Add Russian alphabet here if Cyrillic support is needed - std::string allowedChars = " abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\"'?!*/.,"; + messageInput = TextInput::create( + inputContainer->getContentWidth(), "Send a message to the lobby!", "chatFont.fnt" + ); + + std::string allowedChars = + " abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\"'?!*/.,"; messageInput->getInputNode()->setAllowedChars(allowedChars); - // Create the send message button auto sendMsgBtn = CCMenuItemExt::createSpriteExtraWithFrameName( - "GJ_chatBtn_001.png", - 0.75f, - [this](CCObject*) { - this->sendMessage(); - } + "GJ_chatBtn_001.png", 0.75f, + [this](CCObject*) { this->sendMessage(); } ); sendMsgBtn->ignoreAnchorPointForPosition(true); @@ -102,79 +91,101 @@ bool ChatPanel::init() { inputContainer->setLayout(RowLayout::create()); m_mainLayer->addChildAtPosition(inputContainer, Anchor::Bottom, ccp(0, 10.f)); - // Render any previously loaded messages for (auto message : ChatPanel::messages) { renderMessage(message); } - // Check for new messages every frame this->schedule(schedule_selector(ChatPanel::updateMessages)); return true; } -// Renders a single message with Player Icon (Cube) and Chat Bubble +// ───────────────────────────────────────────────────────────── +// Render a single message: [Icon + Name] [Comment Bubble] +// ───────────────────────────────────────────────────────────── void ChatPanel::renderMessage(Message const& message) { - // 1. Create row container for Icon + Bubble - auto rowNode = CCNode::create(); - rowNode->setAnchorPoint({0.f, 0.f}); - - // 2. Create Player Icon (Cube) using logic from PlayerCell - auto playerIcon = SimplePlayer::create(0); auto gm = GameManager::get(); + // ── Left column: cube icon + username underneath ────────── + constexpr float iconColumnWidth = 55.f; + + auto playerIcon = SimplePlayer::create(0); playerIcon->updatePlayerFrame(message.author.iconID, IconType::Cube); playerIcon->setColor(gm->colorForIdx(message.author.color1)); playerIcon->setSecondColor(gm->colorForIdx(message.author.color2)); - if (message.author.color3 == -1) { playerIcon->disableGlowOutline(); } else { playerIcon->setGlowOutline(gm->colorForIdx(message.author.color3)); } - playerIcon->setScale(0.65f); - - // Wrap icon in a CCNode to keep Layout predictable - auto iconContainer = CCNode::create(); - iconContainer->setContentSize({30.f, 30.f}); - playerIcon->setPosition(iconContainer->getContentSize() / 2); - iconContainer->addChild(playerIcon); - - // 3. Create Text - float maxTextWidth = scrollLayer->getContentWidth() - 50.f; // Account for icon size - - auto msgText = TextArea::create( - fmt::format("{}: {}", message.author.name, message.message), - "chatFont.fnt", 0.5f, maxTextWidth, {0.f, 1.f}, 17.f, false + + // Gold username label (like GD comments) + auto nameLabel = CCLabelBMFont::create( + message.author.name.c_str(), "goldFont.fnt" ); - msgText->setAnchorPoint({ 0.f, 1.f }); // Anchor top-left + nameLabel->setAnchorPoint({0.5f, 0.5f}); + nameLabel->setScale(0.45f); + // Clamp so the name fits under the icon + if (nameLabel->getContentWidth() * nameLabel->getScale() > iconColumnWidth) { + nameLabel->setScale(iconColumnWidth / nameLabel->getContentWidth()); + } + + // ── Right side: comment-style bubble ───────────────────── + constexpr float padX = 10.f; + constexpr float padY = 8.f; + float maxBubbleW = scrollLayer->getContentWidth() - iconColumnWidth - 10.f; + float maxTextW = maxBubbleW - padX * 2.f; + + // Message label — main GD font + auto msgLabel = CCLabelBMFont::create( + message.message.c_str(), "bigFont.fnt" + ); + constexpr float textScale = 0.35f; + msgLabel->setScale(textScale); + msgLabel->setAnchorPoint({0.f, 0.5f}); + + // Enable word-wrap when the line is too long + if (msgLabel->getContentWidth() * textScale > maxTextW) { + msgLabel->setWidth(maxTextW / textScale); + } - float textHeight = msgText->m_label->m_lines->count() * 17.f; - msgText->setContentSize({maxTextWidth, textHeight}); + float textH = msgLabel->getContentHeight() * textScale; + float bubbleH = std::max(textH + padY * 2.f, 30.f); - // 4. Create Chat Bubble (Background) + // Brown bubble background (classic GD comment look) auto bubble = CCScale9Sprite::create("square02b_001.png"); - bubble->setColor(ccc3(0, 0, 0)); // Black bubble - bubble->setOpacity(120); // Semi-transparent - - float paddingX = 8.f; - float paddingY = 8.f; - bubble->setContentSize({maxTextWidth + (paddingX * 2), textHeight + (paddingY * 2)}); - - // Position text inside bubble - msgText->setPosition({paddingX, bubble->getContentHeight() - paddingY}); - bubble->addChild(msgText); - - // 5. Build the Row Layout - rowNode->addChild(iconContainer); + bubble->setContentSize({maxBubbleW, bubbleH}); + bubble->setColor(ccc3(130, 64, 33)); // GD-comment brown + bubble->setOpacity(210); + + // Place text centred vertically inside the bubble + msgLabel->setPosition({padX, bubbleH / 2.f}); + bubble->addChild(msgLabel); + + // ── Assemble the row ───────────────────────────────────── + float rowH = std::max(bubbleH, 48.f); + + // Icon column (icon on top, name below, both centred) + auto iconColumn = CCNode::create(); + iconColumn->setContentSize({iconColumnWidth, rowH}); + iconColumn->addChild(playerIcon); + iconColumn->addChild(nameLabel); + iconColumn->setLayout( + ColumnLayout::create() + ->setAxisReverse(true) // top → bottom + ->setGap(2.f) + ->setAxisAlignment(AxisAlignment::Center) + ->setCrossAxisAlignment(AxisAlignment::Center) + ); + iconColumn->updateLayout(); + + // Row node holds icon column + bubble side-by-side + auto rowNode = CCNode::create(); + rowNode->setAnchorPoint({0.f, 0.f}); + rowNode->setContentSize({scrollLayer->getContentWidth(), rowH}); + rowNode->addChild(iconColumn); rowNode->addChild(bubble); - - // Make row container match bubble's height (or icon height, whichever is larger) - float rowHeight = std::max(bubble->getContentHeight(), iconContainer->getContentHeight()); - rowNode->setContentSize({scrollLayer->getContentWidth(), rowHeight}); - - // Align items horizontally rowNode->setLayout( RowLayout::create() ->setGap(5.f) @@ -183,12 +194,11 @@ void ChatPanel::renderMessage(Message const& message) { ); rowNode->updateLayout(); - // 6. Add to Scroll Layer + // Add to the scrollable message list scrollLayer->m_contentLayer->addChild(rowNode); scrollLayer->m_contentLayer->updateLayout(); } -// Processes the message queue and renders new incoming messages void ChatPanel::updateMessages(float dt) { if (messagesQueue.empty()) return; @@ -198,7 +208,6 @@ void ChatPanel::updateMessages(float dt) { messagesQueue.clear(); } -// Clears the message history and unbinds the network listener void ChatPanel::clearMessages() { messages.clear(); auto& nm = NetworkManager::get(); @@ -206,27 +215,23 @@ void ChatPanel::clearMessages() { hasInitialized = false; } -// Validates and sends the message to the server void ChatPanel::sendMessage() { auto msgInput = messageInput->getString(); - - // Prevent sending empty messages or messages containing only spaces + if (msgInput.empty()) return; if (geode::utils::string::replace(" ", msgInput, "") == "") return; auto& nm = NetworkManager::get(); nm.send(SendMessagePacket::create(msgInput)); - // Clear the input field after sending messageInput->setString(""); } -// Allows sending a message by pressing the "Enter" key void ChatPanel::keyDown(cocos2d::enumKeyCodes keycode, double timestamp) { - if (keycode == cocos2d::KEY_Enter && CCIMEDispatcher::sharedDispatcher()->hasDelegate()) { + if (keycode == cocos2d::KEY_Enter && + CCIMEDispatcher::sharedDispatcher()->hasDelegate()) { sendMessage(); } else { - // Forward other keys to the base Popup class Popup::keyDown(keycode, timestamp); } } From 66bbe6ed35056f0c1d3b0691105b5303854e131d Mon Sep 17 00:00:00 2001 From: lmaosssdll Date: Fri, 3 Apr 2026 13:22:40 +0800 Subject: [PATCH 30/30] backup because i cant do a noraml bubbles --- src/layers/ChatPanel.cpp | 141 +++++++-------------------------------- 1 file changed, 24 insertions(+), 117 deletions(-) diff --git a/src/layers/ChatPanel.cpp b/src/layers/ChatPanel.cpp index f0fe57c..0d4451d 100755 --- a/src/layers/ChatPanel.cpp +++ b/src/layers/ChatPanel.cpp @@ -1,9 +1,6 @@ #include "ChatPanel.hpp" #include #include -#include - -using namespace geode::prelude; ChatPanel* ChatPanel::create() { auto ret = new ChatPanel; @@ -18,11 +15,11 @@ ChatPanel* ChatPanel::create() { void ChatPanel::initialize() { if (!hasInitialized) { auto& nm = NetworkManager::get(); - nm.on([](MessageSentPacket packet) { messages.push_back(packet.message); messagesQueue.push_back(packet.message); - + + // Уведомление о новом сообщении Notification::create( fmt::format("{} sent a message", packet.message.author.name), CCSprite::createWithSpriteFrameName("GJ_chatBtn_001.png") @@ -41,44 +38,41 @@ bool ChatPanel::init() { this->setTitle("Chat"); ChatPanel::initialize(); - // Dark container behind the scroll area auto scrollContainer = CCScale9Sprite::create("square02b_001.png"); scrollContainer->setContentSize({ m_mainLayer->getContentWidth() - 50.f, m_mainLayer->getContentHeight() - 85.f }); - scrollContainer->setColor(ccc3(0, 0, 0)); + scrollContainer->setColor(ccc3(0,0,0)); scrollContainer->setOpacity(75); scrollLayer = ScrollLayer::create(scrollContainer->getContentSize() - 10.f); scrollLayer->ignoreAnchorPointForPosition(false); - scrollLayer->m_contentLayer->setLayout( ColumnLayout::create() ->setAxisReverse(true) ->setAutoGrowAxis(scrollLayer->getContentHeight()) ->setAxisAlignment(AxisAlignment::End) - ->setGap(6.f) + ->setGap(0) ); scrollContainer->addChildAtPosition(scrollLayer, Anchor::Center); m_mainLayer->addChildAtPosition(scrollContainer, Anchor::Center, ccp(0, 5.f)); - // Input + send button auto inputContainer = CCNode::create(); inputContainer->setContentSize({ scrollContainer->getContentWidth(), 75.f }); inputContainer->setAnchorPoint({ 0.5f, 0.f }); - messageInput = TextInput::create( - inputContainer->getContentWidth(), "Send a message to the lobby!", "chatFont.fnt" - ); - - std::string allowedChars = - " abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\"'?!*/.,"; - messageInput->getInputNode()->setAllowedChars(allowedChars); + messageInput = TextInput::create(inputContainer->getContentWidth(), "Send a message to the lobby!", "chatFont.fnt"); + + // --- ИСПРАВЛЕНИЕ: Разрешаем любые символы (кириллицу, спецсимволы) --- + messageInput->setCommonFilter(CommonFilter::Any); auto sendMsgBtn = CCMenuItemExt::createSpriteExtraWithFrameName( - "GJ_chatBtn_001.png", 0.75f, - [this](CCObject*) { this->sendMessage(); } + "GJ_chatBtn_001.png", + 0.75f, + [this](CCObject*) { + this->sendMessage(); + } ); sendMsgBtn->ignoreAnchorPointForPosition(true); @@ -100,108 +94,23 @@ bool ChatPanel::init() { return true; } -// ───────────────────────────────────────────────────────────── -// Render a single message: [Icon + Name] [Comment Bubble] -// ───────────────────────────────────────────────────────────── void ChatPanel::renderMessage(Message const& message) { - auto gm = GameManager::get(); - - // ── Left column: cube icon + username underneath ────────── - constexpr float iconColumnWidth = 55.f; - - auto playerIcon = SimplePlayer::create(0); - playerIcon->updatePlayerFrame(message.author.iconID, IconType::Cube); - playerIcon->setColor(gm->colorForIdx(message.author.color1)); - playerIcon->setSecondColor(gm->colorForIdx(message.author.color2)); - if (message.author.color3 == -1) { - playerIcon->disableGlowOutline(); - } else { - playerIcon->setGlowOutline(gm->colorForIdx(message.author.color3)); - } - playerIcon->setScale(0.65f); - - // Gold username label (like GD comments) - auto nameLabel = CCLabelBMFont::create( - message.author.name.c_str(), "goldFont.fnt" + auto msgNode = CCNode::create(); + auto msgText = TextArea::create( + fmt::format("{}: {}", message.author.name, message.message), + "chatFont.fnt", 0.5f, scrollLayer->getContentWidth(), {0.f, 0.f}, 17.f, false ); - nameLabel->setAnchorPoint({0.5f, 0.5f}); - nameLabel->setScale(0.45f); - // Clamp so the name fits under the icon - if (nameLabel->getContentWidth() * nameLabel->getScale() > iconColumnWidth) { - nameLabel->setScale(iconColumnWidth / nameLabel->getContentWidth()); - } - - // ── Right side: comment-style bubble ───────────────────── - constexpr float padX = 10.f; - constexpr float padY = 8.f; - float maxBubbleW = scrollLayer->getContentWidth() - iconColumnWidth - 10.f; - float maxTextW = maxBubbleW - padX * 2.f; + msgText->setAnchorPoint({ 0.f, 0.f }); + msgNode->setContentHeight(msgText->m_label->m_lines->count() * 17.f); + msgNode->setContentWidth(scrollLayer->getContentWidth()); + msgText->setPosition({ 0.f, 0.f }); + msgNode->addChild(msgText); - // Message label — main GD font - auto msgLabel = CCLabelBMFont::create( - message.message.c_str(), "bigFont.fnt" - ); - constexpr float textScale = 0.35f; - msgLabel->setScale(textScale); - msgLabel->setAnchorPoint({0.f, 0.5f}); - - // Enable word-wrap when the line is too long - if (msgLabel->getContentWidth() * textScale > maxTextW) { - msgLabel->setWidth(maxTextW / textScale); - } - - float textH = msgLabel->getContentHeight() * textScale; - float bubbleH = std::max(textH + padY * 2.f, 30.f); - - // Brown bubble background (classic GD comment look) - auto bubble = CCScale9Sprite::create("square02b_001.png"); - bubble->setContentSize({maxBubbleW, bubbleH}); - bubble->setColor(ccc3(130, 64, 33)); // GD-comment brown - bubble->setOpacity(210); - - // Place text centred vertically inside the bubble - msgLabel->setPosition({padX, bubbleH / 2.f}); - bubble->addChild(msgLabel); - - // ── Assemble the row ───────────────────────────────────── - float rowH = std::max(bubbleH, 48.f); - - // Icon column (icon on top, name below, both centred) - auto iconColumn = CCNode::create(); - iconColumn->setContentSize({iconColumnWidth, rowH}); - iconColumn->addChild(playerIcon); - iconColumn->addChild(nameLabel); - iconColumn->setLayout( - ColumnLayout::create() - ->setAxisReverse(true) // top → bottom - ->setGap(2.f) - ->setAxisAlignment(AxisAlignment::Center) - ->setCrossAxisAlignment(AxisAlignment::Center) - ); - iconColumn->updateLayout(); - - // Row node holds icon column + bubble side-by-side - auto rowNode = CCNode::create(); - rowNode->setAnchorPoint({0.f, 0.f}); - rowNode->setContentSize({scrollLayer->getContentWidth(), rowH}); - rowNode->addChild(iconColumn); - rowNode->addChild(bubble); - rowNode->setLayout( - RowLayout::create() - ->setGap(5.f) - ->setAxisAlignment(AxisAlignment::Start) - ->setCrossAxisOverflow(false) - ); - rowNode->updateLayout(); - - // Add to the scrollable message list - scrollLayer->m_contentLayer->addChild(rowNode); + scrollLayer->m_contentLayer->addChild(msgNode); scrollLayer->m_contentLayer->updateLayout(); } void ChatPanel::updateMessages(float dt) { - if (messagesQueue.empty()) return; - for (auto const& message : messagesQueue) { renderMessage(message); } @@ -217,7 +126,6 @@ void ChatPanel::clearMessages() { void ChatPanel::sendMessage() { auto msgInput = messageInput->getString(); - if (msgInput.empty()) return; if (geode::utils::string::replace(" ", msgInput, "") == "") return; @@ -228,8 +136,7 @@ void ChatPanel::sendMessage() { } void ChatPanel::keyDown(cocos2d::enumKeyCodes keycode, double timestamp) { - if (keycode == cocos2d::KEY_Enter && - CCIMEDispatcher::sharedDispatcher()->hasDelegate()) { + if (keycode == cocos2d::KEY_Enter && CCIMEDispatcher::sharedDispatcher()->hasDelegate()) { sendMessage(); } else { Popup::keyDown(keycode, timestamp);