diff --git a/.devin/wiki.json b/.devin/wiki.json index f1ed1b0e4b6..251e9a0e9fa 100644 --- a/.devin/wiki.json +++ b/.devin/wiki.json @@ -5,7 +5,7 @@ "author": "GeneralsX Team" }, { - "content": "Three reference repositories guide the port: (1) TheSuperHackers (upstream Windows baseline, stable VC6 builds), (2) fighter19's DXVK port (references/fighter19-dxvk-port/ — primary graphics/platform reference, fully functional on Linux), (3) jmarshall's modern port (references/jmarshall-win64-modern/ — primary audio/OpenAL reference, Generals-only). Platform-specific code must be isolated to Core/GameEngineDevice/ and Core/Libraries/Source/Platform/. Never put platform-specific code inside gameplay logic (GameLogic/). SDL3 is the unified platform layer — no native POSIX, Win32, or Cocoa calls in game code.", + "content": "Three reference repositories guide the port: (1) TheSuperHackers (upstream Windows baseline, stable VC6 builds), (2) fighter19's DXVK port (references/old-refs/fighter19-dxvk-port/ — primary graphics/platform reference, fully functional on Linux), (3) jmarshall's modern port (references/old-refs/jmarshall-win64-modern/ — primary audio/OpenAL reference, Generals-only). Platform-specific code must be isolated to Core/GameEngineDevice/ and Core/Libraries/Source/Platform/. Never put platform-specific code inside gameplay logic (GameLogic/). SDL3 is the unified platform layer — no native POSIX, Win32, or Cocoa calls in game code.", "author": "GeneralsX Team" }, { @@ -49,12 +49,12 @@ }, { "title": "Rendering Device Layer", - "purpose": "Document Core/GameEngineDevice/: current DirectX8 implementation, entry points, device initialization, and how the W3D renderer is structured. This is the primary integration point for DXVK. Reference fighter19's changes in references/fighter19-dxvk-port/GeneralsMD/Code/GameEngineDevice/ as the porting target.", + "purpose": "Document Core/GameEngineDevice/: current DirectX8 implementation, entry points, device initialization, and how the W3D renderer is structured. This is the primary integration point for DXVK. Reference fighter19's changes in references/old-refs/fighter19-dxvk-port/GeneralsMD/Code/GameEngineDevice/ as the porting target.", "parent": "Core Engine Architecture" }, { "title": "Audio System", - "purpose": "Document the current Miles Sound System integration: where it initializes, how it interfaces with the game engine (Core/GameEngine/Audio/). Document jmarshall's OpenAL implementation in references/jmarshall-win64-modern/Code/Audio/ as the cross-platform replacement target. Highlight Zero Hour-specific audio hooks that differ from base Generals.", + "purpose": "Document the current Miles Sound System integration: where it initializes, how it interfaces with the game engine (Core/GameEngine/Audio/). Document jmarshall's OpenAL implementation in references/old-refs/jmarshall-win64-modern/Code/Audio/ as the cross-platform replacement target. Highlight Zero Hour-specific audio hooks that differ from base Generals.", "parent": "Core Engine Architecture" }, { diff --git a/.github/agents/Bender.agent.md b/.github/agents/Bender.agent.md index 37b1057cbd3..fa4b62d7e60 100644 --- a/.github/agents/Bender.agent.md +++ b/.github/agents/Bender.agent.md @@ -48,12 +48,12 @@ These are the authoritative sources for understanding original game behavior and References under the `references/` folder: - **`references/jmarshall-win64-modern/`** - Windows 64-bit modernization with comprehensive fixes - Game base (Generals) Only + **`references/old-refs/jmarshall-win64-modern/`** - Windows 64-bit modernization with comprehensive fixes - Game base (Generals) Only - **Primary use**: Cross-platform compatibility solutions, INI parser fixes, memory management - **Key success**: Provided the breakthrough End token parsing solution (Phase 22.7-22.8) - **Coverage**: Full Windows 64-bit port with modern toolchain compatibility -- **`references/fighter19-dxvk-port/`** - Linux port with DXVK graphics integration +- **`references/old-refs/fighter19-dxvk-port/`** - Linux port with DXVK graphics integration - **Primary use**: Graphics layer solutions (DirectX→Vulkan via DXVK), Linux compatibility - **Focus areas**: OpenGL/Vulkan rendering, graphics pipeline modernization - **Coverage**: Complete Linux port with advanced graphics compatibility diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7f1c66c8452..6186bf94e09 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -138,4 +138,3 @@ jobs: needs.replay-test-linux.result == 'failure' || needs.replay-test-macos.result == 'failure' run: exit 1 - diff --git a/.gitmodules b/.gitmodules index a393cf63908..60165fa589b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,11 +1,11 @@ [submodule "references/fighter19-dxvk-port"] - path = references/fighter19-dxvk-port + path = references/old-refs/fighter19-dxvk-port url = https://github.com/Fighter19/CnC_Generals_Zero_Hour.git [submodule "references/jmarshall-win64-modern"] - path = references/jmarshall-win64-modern + path = references/old-refs/jmarshall-win64-modern url = https://github.com/jmarshall2323/CnC_Generals_Zero_Hour.git [submodule "references/thesuperhackers-main"] - path = references/thesuperhackers-main + path = references/old-refs/thesuperhackers-main url = https://github.com/TheSuperHackers/GeneralsGameCode.git [submodule "references/generals-online-client"] path = references/generals-online-client diff --git a/AGENTS.md b/AGENTS.md index 5b793ada66c..95bb97aeed6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -230,7 +230,7 @@ printf "%s" "$body" | rg '\\n' && echo "HAS_LITERAL_BACKSLASH_N=YES" || echo "HA - `GeneralsMD/`: Zero Hour. - `Generals/`: base game. - `Core/`: shared libraries. -- `references/`: thesuperhackers-main, fbraz3-dxvk (active); archive/ (historical). +- `references/`: old-refs/thesuperhackers-main, fbraz3-dxvk (active); old-refs/ (historical). - `docs/WORKDIR/`: current work docs. - `docs/HOWTO/`: user-facing step-by-step tutorials (SagePatch config, etc.) - `logs/`: build/run/debug logs. diff --git a/Core/GameEngine/Include/Common/GameDefines.h b/Core/GameEngine/Include/Common/GameDefines.h index 1bf9f096fcf..f0aef9c836a 100644 --- a/Core/GameEngine/Include/Common/GameDefines.h +++ b/Core/GameEngine/Include/Common/GameDefines.h @@ -107,7 +107,7 @@ // Disable non retail fixes in the networking, such as putting more data per UDP packet #ifndef RETAIL_COMPATIBLE_NETWORKING -#define RETAIL_COMPATIBLE_NETWORKING (1) +#define RETAIL_COMPATIBLE_NETWORKING (0) #endif // This is essentially synonymous for RETAIL_COMPATIBLE_CRC. There is a lot wrong with AIGroup, such as use-after-free, double-free, leaks, diff --git a/Core/GameEngine/Source/Common/FrameRateLimit.cpp b/Core/GameEngine/Source/Common/FrameRateLimit.cpp index 50445b6ea76..893793d79f6 100644 --- a/Core/GameEngine/Source/Common/FrameRateLimit.cpp +++ b/Core/GameEngine/Source/Common/FrameRateLimit.cpp @@ -49,7 +49,7 @@ Real FrameRateLimit::wait(UnsignedInt maxFps) // GeneralsX @bugfix BenderAI 11/05/2026 Validate FPS limit to prevent division by zero and underflow // Skip limiting if maxFps is 0 or extremely high (uncapped mode) - if (maxFps == 0 || maxFps > 1000000) + if (maxFps == 0 || maxFps >= 1000000) { // Uncapped or invalid: just return elapsed time without limiting #ifdef _WIN32 diff --git a/Core/GameEngine/Source/GameNetwork/ConnectionManager.cpp b/Core/GameEngine/Source/GameNetwork/ConnectionManager.cpp index 6aed5ac39dc..cd1b4d46403 100644 --- a/Core/GameEngine/Source/GameNetwork/ConnectionManager.cpp +++ b/Core/GameEngine/Source/GameNetwork/ConnectionManager.cpp @@ -958,6 +958,9 @@ void ConnectionManager::processLoadComplete( NetCommandMsg *msg ) void ConnectionManager::processTimeOutGameStart( NetCommandMsg *msg ) { + // GeneralsX @build GitHubCopilot 12/04/2026 Trace forced-start timeout command reception for cross-platform start debugging. + /* fprintf(stderr, "[LAN86] processTimeOutGameStart localSlot=%d sender=%d id=%d execFrame=%u logicFrame=%u\n", + m_localSlot, msg->getPlayerID(), msg->getID(), msg->getExecutionFrame(), TheGameLogic->getFrame()); */ TheGameLogic->timeOutGameStart(); } @@ -2447,6 +2450,9 @@ void ConnectionManager::sendTimeOutGameStart() if (DoesCommandRequireACommandID(msg->getNetCommandType()) == TRUE) { msg->setID(GenerateNextCommandID()); } + // GeneralsX @build GitHubCopilot 12/04/2026 Trace who initiates forced game start before the command fans out. + /* fprintf(stderr, "[LAN86] sendTimeOutGameStart localSlot=%d id=%d logicFrame=%u\n", + m_localSlot, msg->getID(), TheGameLogic->getFrame()); */ processTimeOutGameStart(msg); sendLocalCommand(msg, 0xff ^ (1 << m_localSlot)); diff --git a/Core/GameEngine/Source/GameNetwork/IPEnumeration.cpp b/Core/GameEngine/Source/GameNetwork/IPEnumeration.cpp index a7a4844bebc..c32921827b0 100644 --- a/Core/GameEngine/Source/GameNetwork/IPEnumeration.cpp +++ b/Core/GameEngine/Source/GameNetwork/IPEnumeration.cpp @@ -116,6 +116,13 @@ EnumeratedIP * IPEnumeration::getAddresses() // GeneralsX @bugfix BenderAI 31/03/2026 Use ntohl to convert from network byte order before extracting octets; // reading s_addr byte-by-byte on little-endian platforms reverses the IPv4 octets. const UnsignedInt hostAddr = ntohl(addr->sin_addr.s_addr); + // GeneralsX @build GitHubCopilot 11/04/2026 Log POSIX interface candidates used for LAN IP selection. + DEBUG_LOG(("IPEnumeration::getAddresses - interface=%s flags=0x%X ip=%d.%d.%d.%d", + (ifa->ifa_name != nullptr) ? ifa->ifa_name : "", ifa->ifa_flags, + PRINTF_IP_AS_4_INTS(hostAddr))); + /* fprintf(stderr, "[LAN86] iface=%s flags=0x%X ip=%d.%d.%d.%d\n", + (ifa->ifa_name != nullptr) ? ifa->ifa_name : "", ifa->ifa_flags, + PRINTF_IP_AS_4_INTS(hostAddr)); */ addNewIP( (UnsignedByte)((hostAddr >> 24) & 0xFF), (UnsignedByte)((hostAddr >> 16) & 0xFF), @@ -181,6 +188,7 @@ void IPEnumeration::addNewIP( UnsignedByte a, UnsignedByte b, UnsignedByte c, Un { if (current->getIP() == ip) { + /* fprintf(stderr, "[LAN86] addNewIP duplicate-skip %d.%d.%d.%d\n", (int)a, (int)b, (int)c, (int)d); */ return; } } @@ -194,6 +202,7 @@ void IPEnumeration::addNewIP( UnsignedByte a, UnsignedByte b, UnsignedByte c, Un newIP->setIP(ip); DEBUG_LOG(("IP: 0x%8.8X (%s)", ip, str.str())); + /* fprintf(stderr, "[LAN86] addNewIP accepted %s numeric=0x%8.8X\n", str.str(), ip); */ // Add the IP to the list in ascending order if (!m_IPlist) diff --git a/Core/GameEngine/Source/GameNetwork/LANAPI.cpp b/Core/GameEngine/Source/GameNetwork/LANAPI.cpp index 1ad1550003c..19fe64789d0 100644 --- a/Core/GameEngine/Source/GameNetwork/LANAPI.cpp +++ b/Core/GameEngine/Source/GameNetwork/LANAPI.cpp @@ -38,11 +38,87 @@ #include "Common/UserPreferences.h" #include "GameLogic/GameLogic.h" +#ifndef _WIN32 +#include +#include +#endif static const UnsignedShort lobbyPort = 8086; ///< This is the UDP port used by all LANAPI communication AsciiString GetMessageTypeString(UnsignedInt type); +#ifndef _WIN32 +// GeneralsX @feature GitHubCopilot 12/04/2026 Discover per-interface IPv4 subnet broadcast addresses for LAN discovery on POSIX. +static Int GatherSubnetBroadcastAddrs(UnsignedInt localIP, UnsignedInt *outAddrs, Int maxAddrs) +{ + if (outAddrs == nullptr || maxAddrs <= 0) + { + return 0; + } + + Int count = 0; + struct ifaddrs *ifaddr = nullptr; + if (getifaddrs(&ifaddr) != 0) + { + return 0; + } + + for (struct ifaddrs *ifa = ifaddr; ifa != nullptr; ifa = ifa->ifa_next) + { + if (ifa->ifa_addr == nullptr || ifa->ifa_addr->sa_family != AF_INET) + { + continue; + } + if ((ifa->ifa_flags & IFF_UP) == 0 || (ifa->ifa_flags & IFF_LOOPBACK) != 0) + { + continue; + } + + const sockaddr_in *addr = reinterpret_cast(ifa->ifa_addr); + const UnsignedInt hostAddr = ntohl(addr->sin_addr.s_addr); + if (localIP != 0 && hostAddr != localIP) + { + continue; + } + + UnsignedInt bcast = 0; + if (ifa->ifa_broadaddr != nullptr && ifa->ifa_broadaddr->sa_family == AF_INET) + { + const sockaddr_in *baddr = reinterpret_cast(ifa->ifa_broadaddr); + bcast = ntohl(baddr->sin_addr.s_addr); + } + else if (ifa->ifa_netmask != nullptr && ifa->ifa_netmask->sa_family == AF_INET) + { + const sockaddr_in *nmask = reinterpret_cast(ifa->ifa_netmask); + const UnsignedInt mask = ntohl(nmask->sin_addr.s_addr); + bcast = (hostAddr & mask) | (~mask); + } + else + { + continue; + } + + Bool duplicate = FALSE; + for (Int i = 0; i < count; ++i) + { + if (outAddrs[i] == bcast) + { + duplicate = TRUE; + break; + } + } + + if (!duplicate && count < maxAddrs) + { + outAddrs[count++] = bcast; + } + } + + freeifaddrs(ifaddr); + return count; +} +#endif + const UnsignedInt LANAPI::s_resendDelta = 10 * 1000; ///< This is how often we announce ourselves to the world /* LANGame::LANGame() @@ -183,24 +259,84 @@ void LANAPI::sendMessage(LANMessage *msg, UnsignedInt ip /* = 0 */) { if (ip != 0) { - m_transport->queueSend(ip, lobbyPort, (unsigned char *)msg, sizeof(LANMessage) /*, 0, 0 */); + // GeneralsX @build GitHubCopilot 11/04/2026 Instrument direct LAN sends for cross-platform diagnostics. + Bool queued = m_transport->queueSend(ip, lobbyPort, (unsigned char *)msg, sizeof(LANMessage) /*, 0, 0 */); + DEBUG_LOG(("LANAPI::sendMessage - direct type=%s dst=%d.%d.%d.%d:%d queued=%d", + GetMessageTypeString(msg->messageType).str(), PRINTF_IP_AS_4_INTS(ip), lobbyPort, queued)); + /* fprintf(stderr, "[LAN86] send direct type=%u dst=%d.%d.%d.%d:%d queued=%d\n", + msg->messageType, PRINTF_IP_AS_4_INTS(ip), lobbyPort, queued); */ } - else if ((m_currentGame != nullptr) && (m_currentGame->getIsDirectConnect())) + // GeneralsX @bugfix GitHubCopilot 12/04/2026 Prefer directed fan-out for in-game state/control packets to avoid cross-platform broadcast loss. + const Bool shouldUseDirectedFanout = (m_currentGame != nullptr) + && ((m_currentGame->getIsDirectConnect()) + || (!m_inLobby && msg != nullptr && ( + msg->messageType == LANMessage::MSG_GAME_OPTIONS + || msg->messageType == LANMessage::MSG_GAME_START + || msg->messageType == LANMessage::MSG_GAME_START_TIMER + || msg->messageType == LANMessage::MSG_REQUEST_GAME_LEAVE + || msg->messageType == LANMessage::MSG_SET_ACCEPT + || msg->messageType == LANMessage::MSG_MAP_AVAILABILITY + || msg->messageType == LANMessage::MSG_CHAT + || msg->messageType == LANMessage::MSG_INACTIVE))); + + if (shouldUseDirectedFanout) { Int localSlot = m_currentGame->getLocalSlotNum(); + Bool sentAny = FALSE; for (Int i = 0; i < MAX_SLOTS; ++i) { if (i != localSlot) { GameSlot *slot = m_currentGame->getSlot(i); if ((slot != nullptr) && (slot->isHuman())) { - m_transport->queueSend(slot->getIP(), lobbyPort, (unsigned char *)msg, sizeof(LANMessage) /*, 0, 0 */); + // GeneralsX @build GitHubCopilot 11/04/2026 Instrument direct-connect fan-out sends. + Bool queued = m_transport->queueSend(slot->getIP(), lobbyPort, (unsigned char *)msg, sizeof(LANMessage) /*, 0, 0 */); + sentAny = TRUE; + DEBUG_LOG(("LANAPI::sendMessage - direct-connect type=%s dst=%d.%d.%d.%d:%d queued=%d", + GetMessageTypeString(msg->messageType).str(), PRINTF_IP_AS_4_INTS(slot->getIP()), lobbyPort, queued)); + /* fprintf(stderr, "[LAN86] send directed-fanout type=%u dst=%d.%d.%d.%d:%d queued=%d inLobby=%d directGame=%d\n", + msg->messageType, PRINTF_IP_AS_4_INTS(slot->getIP()), lobbyPort, queued); */ } } } + + if (!sentAny) + { + Bool queued = m_transport->queueSend(m_broadcastAddr, lobbyPort, (unsigned char *)msg, sizeof(LANMessage) /*, 0, 0 */); + DEBUG_LOG(("LANAPI::sendMessage - directed-fanout fallback broadcast type=%s dst=%d.%d.%d.%d:%d local=%d.%d.%d.%d queued=%d", + GetMessageTypeString(msg->messageType).str(), PRINTF_IP_AS_4_INTS(m_broadcastAddr), lobbyPort, + PRINTF_IP_AS_4_INTS(m_localIP), queued)); + /* fprintf(stderr, "[LAN86] send directed-fanout-fallback-broadcast type=%u dst=%d.%d.%d.%d:%d local=%d.%d.%d.%d queued=%d\n", + msg->messageType, PRINTF_IP_AS_4_INTS(m_broadcastAddr), lobbyPort, PRINTF_IP_AS_4_INTS(m_localIP), queued); */ + } } else { - m_transport->queueSend(m_broadcastAddr, lobbyPort, (unsigned char *)msg, sizeof(LANMessage) /*, 0, 0 */); + // GeneralsX @feature GitHubCopilot 12/04/2026 Send discovery/control broadcast packets to interface subnet broadcast addresses before global broadcast. + Bool sentAny = FALSE; +#ifndef _WIN32 + UnsignedInt subnetBroadcasts[8]; + Int subnetCount = GatherSubnetBroadcastAddrs(m_localIP, subnetBroadcasts, ARRAY_SIZE(subnetBroadcasts)); + for (Int i = 0; i < subnetCount; ++i) + { + UnsignedInt dst = subnetBroadcasts[i]; + Bool queued = m_transport->queueSend(dst, lobbyPort, (unsigned char *)msg, sizeof(LANMessage) /*, 0, 0 */); + sentAny = TRUE; + DEBUG_LOG(("LANAPI::sendMessage - subnet-broadcast type=%s dst=%d.%d.%d.%d:%d local=%d.%d.%d.%d queued=%d", + GetMessageTypeString(msg->messageType).str(), PRINTF_IP_AS_4_INTS(dst), lobbyPort, + PRINTF_IP_AS_4_INTS(m_localIP), queued)); + /* fprintf(stderr, "[LAN86] send subnet-broadcast type=%u dst=%d.%d.%d.%d:%d local=%d.%d.%d.%d queued=%d\n", + msg->messageType, PRINTF_IP_AS_4_INTS(dst), lobbyPort, PRINTF_IP_AS_4_INTS(m_localIP), queued); */ + } +#endif + if (!sentAny) + { + Bool queued = m_transport->queueSend(m_broadcastAddr, lobbyPort, (unsigned char *)msg, sizeof(LANMessage) /*, 0, 0 */); + DEBUG_LOG(("LANAPI::sendMessage - broadcast type=%s dst=%d.%d.%d.%d:%d local=%d.%d.%d.%d queued=%d", + GetMessageTypeString(msg->messageType).str(), PRINTF_IP_AS_4_INTS(m_broadcastAddr), lobbyPort, + PRINTF_IP_AS_4_INTS(m_localIP), queued)); + /* fprintf(stderr, "[LAN86] send broadcast type=%u dst=%d.%d.%d.%d:%d local=%d.%d.%d.%d queued=%d\n", + msg->messageType, PRINTF_IP_AS_4_INTS(m_broadcastAddr), lobbyPort, PRINTF_IP_AS_4_INTS(m_localIP), queued); */ + } } } @@ -337,6 +473,8 @@ void LANAPI::update() if ((m_transport->update() == FALSE) && (LANSocketErrorDetected == FALSE)) { if (m_isInLANMenu == TRUE) { LANSocketErrorDetected = TRUE; + /* fprintf(stderr, "[LAN86] LANAPI::update transport update failed while in LAN menu local=%d.%d.%d.%d\n", + PRINTF_IP_AS_4_INTS(m_localIP)); */ } } @@ -350,11 +488,16 @@ void LANAPI::update() UnsignedInt senderIP = m_transport->m_inBuffer[i].addr; if (senderIP == m_localIP) { + /* fprintf(stderr, "[LAN86] recv self-echo type=%u from %d.%d.%d.%d ignored\n", + ((LANMessage *)(m_transport->m_inBuffer[i].data))->messageType, PRINTF_IP_AS_4_INTS(senderIP)); */ m_transport->m_inBuffer[i].length = 0; continue; } LANMessage *msg = (LANMessage *)(m_transport->m_inBuffer[i].data); + /* fprintf(stderr, "[LAN86] recv type=%u len=%d from %d.%d.%d.%d local=%d.%d.%d.%d\n", + msg->messageType, m_transport->m_inBuffer[i].length, + PRINTF_IP_AS_4_INTS(senderIP), PRINTF_IP_AS_4_INTS(m_localIP)); */ //DEBUG_LOG(("LAN message type %s from %ls (%s@%s)", GetMessageTypeString(msg->messageType).str(), // msg->name, msg->userName, msg->hostName)); switch (msg->messageType) @@ -427,6 +570,8 @@ void LANAPI::update() default: DEBUG_LOG(("Unknown LAN message type %d", msg->messageType)); + /* fprintf(stderr, "[LAN86] recv unknown type=%u from %d.%d.%d.%d\n", + msg->messageType, PRINTF_IP_AS_4_INTS(senderIP)); */ } // Mark it as read @@ -439,20 +584,25 @@ void LANAPI::update() if (now > s_resendDelta + m_lastResendTime) { m_lastResendTime = now; + /* fprintf(stderr, "[LAN86] periodic resend tick local=%d.%d.%d.%d inLobby=%d currentGame=%d amHost=%d\n", + PRINTF_IP_AS_4_INTS(m_localIP), m_inLobby, (m_currentGame != nullptr), AmIHost()); */ if (m_inLobby) { + /* fprintf(stderr, "[LAN86] periodic action=RequestSetName lobby\n"); */ RequestSetName(m_name); } else if (m_currentGame && !m_currentGame->isGameInProgress()) { if (AmIHost()) { + /* fprintf(stderr, "[LAN86] periodic action=host-announce/options\n"); */ RequestGameOptions( GenerateGameOptionsString(), true ); RequestGameAnnounce(); } else { + /* fprintf(stderr, "[LAN86] periodic action=joiner-hello\n"); */ #if TELL_COMPUTER_IDENTITY_IN_LAN_LOBBY AsciiString text; text.format("User=%s", m_userName.str()); @@ -499,6 +649,9 @@ void LANAPI::update() if (game != m_currentGame && game->getLastHeard() + s_resendDelta*2 < now) { // He's gone! + // GeneralsX @build GitHubCopilot 12/04/2026 Trace lobby-game pruning to verify whether hosts disappear due to hearbeat expiry. + /* fprintf(stderr, "[LAN86] prune game host=%d.%d.%d.%d name=%ls lastHeard=%u now=%u delta=%u\n", + PRINTF_IP_AS_4_INTS(game->getHostIP()), game->getName().str(), game->getLastHeard(), now, s_resendDelta * 2); */ removeGame(game); LANGameInfo *nextGame = game->getNext(); delete game; @@ -563,6 +716,10 @@ void LANAPI::update() switch (m_pendingAction) { case ACT_JOIN: + // GeneralsX @build GitHubCopilot 12/04/2026 Surface join timeout details to stderr for LAN/direct-connect diagnostics. + /* fprintf(stderr, "[LAN86] action timeout action=ACT_JOIN local=%d.%d.%d.%d remote=%d.%d.%d.%d currentGame=%ls\n", + PRINTF_IP_AS_4_INTS(m_localIP), PRINTF_IP_AS_4_INTS(m_directConnectRemoteIP), + (m_currentGame != nullptr) ? m_currentGame->getName().str() : L""); */ OnGameJoin(RET_TIMEOUT, nullptr); m_pendingAction = ACT_NONE; m_currentGame = nullptr; @@ -575,6 +732,8 @@ void LANAPI::update() m_inLobby = true; break; case ACT_JOINDIRECTCONNECT: + /* fprintf(stderr, "[LAN86] action timeout action=ACT_JOINDIRECTCONNECT local=%d.%d.%d.%d remote=%d.%d.%d.%d\n", + PRINTF_IP_AS_4_INTS(m_localIP), PRINTF_IP_AS_4_INTS(m_directConnectRemoteIP)); */ OnGameJoin(RET_TIMEOUT, nullptr); m_pendingAction = ACT_NONE; m_currentGame = nullptr; @@ -616,6 +775,11 @@ void LANAPI::RequestLocations() LANMessage msg; msg.messageType = LANMessage::MSG_REQUEST_LOCATIONS; fillInLANMessage( &msg ); + // GeneralsX @build GitHubCopilot 11/04/2026 Trace LAN discovery probes emitted by this client. + DEBUG_LOG(("LANAPI::RequestLocations - local=%d.%d.%d.%d broadcast=%d.%d.%d.%d port=%d", + PRINTF_IP_AS_4_INTS(m_localIP), PRINTF_IP_AS_4_INTS(m_broadcastAddr), lobbyPort)); + /* fprintf(stderr, "[LAN86] RequestLocations local=%d.%d.%d.%d broadcast=%d.%d.%d.%d port=%d\n", + PRINTF_IP_AS_4_INTS(m_localIP), PRINTF_IP_AS_4_INTS(m_broadcastAddr), lobbyPort); */ sendMessage(&msg); } @@ -643,6 +807,10 @@ void LANAPI::RequestGameJoin( LANGameInfo *game, UnsignedInt ip /* = 0 */ ) AsciiString s; GetStringFromRegistry("\\ergc", "", s); strlcpy(msg.GameToJoin.serial, s.str(), ARRAY_SIZE(msg.GameToJoin.serial)); + // GeneralsX @build GitHubCopilot 12/04/2026 Trace REQUEST_JOIN targets and pending-action transitions for LAN/direct-connect joins. + /* fprintf(stderr, "[LAN86] RequestGameJoin local=%d.%d.%d.%d hostIP=%d.%d.%d.%d sendIP=%d.%d.%d.%d prevPending=%d game=%ls direct=%d\n", + PRINTF_IP_AS_4_INTS(m_localIP), PRINTF_IP_AS_4_INTS(game->getSlot(0)->getIP()), PRINTF_IP_AS_4_INTS(ip), + m_pendingAction, game->getName().str(), game->getIsDirectConnect()); */ sendMessage(&msg, ip); @@ -665,6 +833,9 @@ void LANAPI::RequestGameJoinDirectConnect(UnsignedInt ipaddress) } m_directConnectRemoteIP = ipaddress; + // GeneralsX @build GitHubCopilot 12/04/2026 Trace direct-connect discovery requests and pending-action transitions. + /* fprintf(stderr, "[LAN86] RequestGameJoinDirectConnect local=%d.%d.%d.%d remote=%d.%d.%d.%d prevPending=%d\n", + PRINTF_IP_AS_4_INTS(m_localIP), PRINTF_IP_AS_4_INTS(ipaddress), m_pendingAction); */ LANMessage msg; msg.messageType = LANMessage::MSG_REQUEST_GAME_INFO; @@ -1272,11 +1443,21 @@ void LANAPI::addPlayer( LANPlayer *player ) Bool LANAPI::SetLocalIP( UnsignedInt localIP ) { Bool retval = TRUE; + UnsignedInt oldIP = m_localIP; m_localIP = localIP; + // GeneralsX @build GitHubCopilot 11/04/2026 Trace LAN socket rebind lifecycle for issue #86 diagnostics. + DEBUG_LOG(("LANAPI::SetLocalIP - rebinding LAN transport from %d.%d.%d.%d to %d.%d.%d.%d:%d", + PRINTF_IP_AS_4_INTS(oldIP), PRINTF_IP_AS_4_INTS(m_localIP), lobbyPort)); + /* fprintf(stderr, "[LAN86] SetLocalIP rebind from %d.%d.%d.%d to %d.%d.%d.%d:%d\n", + PRINTF_IP_AS_4_INTS(oldIP), PRINTF_IP_AS_4_INTS(m_localIP), lobbyPort); */ m_transport->reset(); retval = m_transport->init(m_localIP, lobbyPort); - m_transport->allowBroadcasts(true); + Bool broadcastsEnabled = m_transport->allowBroadcasts(true); + DEBUG_LOG(("LANAPI::SetLocalIP - init=%d allowBroadcasts=%d local=%d.%d.%d.%d:%d", + retval, broadcastsEnabled, PRINTF_IP_AS_4_INTS(m_localIP), lobbyPort)); + /* fprintf(stderr, "[LAN86] SetLocalIP result init=%d allowBroadcasts=%d local=%d.%d.%d.%d:%d\n", + retval, broadcastsEnabled, PRINTF_IP_AS_4_INTS(m_localIP), lobbyPort); */ return retval; } diff --git a/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp b/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp index f8f6743f850..031d0daa2c7 100644 --- a/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp +++ b/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp @@ -79,6 +79,8 @@ wchar_t *GetWindowsWideCharAsWchar( WideCharWindows *src ) void LANAPI::handleRequestLocations( LANMessage *msg, UnsignedInt senderIP ) { + /* fprintf(stderr, "[LAN86] handleRequestLocations sender=%d.%d.%d.%d inLobby=%d currentGame=%d\n", + PRINTF_IP_AS_4_INTS(senderIP), m_inLobby, (m_currentGame != nullptr)); */ if (m_inLobby) { LANMessage reply; @@ -131,12 +133,17 @@ void LANAPI::handleRequestLocations( LANMessage *msg, UnsignedInt senderIP ) player->setLastHeard(timeGetTime()); addPlayer(player); + /* fprintf(stderr, "[LAN86] handleRequestLocations player=%d.%d.%d.%d name=%ls host=%s login=%s\n", + PRINTF_IP_AS_4_INTS(player->getIP()), player->getName().str(), player->getHost().str(), player->getLogin().str()); */ OnNameChange(player->getIP(), player->getName()); } void LANAPI::handleGameAnnounce( LANMessage *msg, UnsignedInt senderIP ) { + /* fprintf(stderr, "[LAN86] handleGameAnnounce sender=%d.%d.%d.%d game=%ls inProgress=%d direct=%d\n", + PRINTF_IP_AS_4_INTS(senderIP), GetWindowsWideCharAsWchar(msg->GameInfo.gameName), + msg->GameInfo.inProgress, msg->GameInfo.isDirectConnect); */ if (senderIP == m_localIP) { return; // Don't try to update own info @@ -194,6 +201,13 @@ void LANAPI::handleGameAnnounce( LANMessage *msg, UnsignedInt senderIP ) removeGame(game); delete game; game = nullptr; + /* fprintf(stderr, "[LAN86] handleGameAnnounce parse failed sender=%d.%d.%d.%d\n", + PRINTF_IP_AS_4_INTS(senderIP)); */ + } + else if (game != nullptr) + { + /* fprintf(stderr, "[LAN86] handleGameAnnounce accepted game host=%d.%d.%d.%d name=%ls\n", + PRINTF_IP_AS_4_INTS(senderIP), game->getName().str()); */ } OnGameList( m_games ); @@ -204,6 +218,8 @@ void LANAPI::handleGameAnnounce( LANMessage *msg, UnsignedInt senderIP ) void LANAPI::handleLobbyAnnounce( LANMessage *msg, UnsignedInt senderIP ) { + /* fprintf(stderr, "[LAN86] handleLobbyAnnounce sender=%d.%d.%d.%d name=%ls host=%s login=%s\n", + PRINTF_IP_AS_4_INTS(senderIP), GetWindowsWideCharAsWchar(msg->name), msg->hostName, msg->userName); */ LANPlayer *player = LookupPlayer(senderIP); if (!player) { @@ -221,12 +237,16 @@ void LANAPI::handleLobbyAnnounce( LANMessage *msg, UnsignedInt senderIP ) player->setLastHeard(timeGetTime()); addPlayer(player); + /* fprintf(stderr, "[LAN86] handleLobbyAnnounce updated player=%d.%d.%d.%d name=%ls\n", + PRINTF_IP_AS_4_INTS(player->getIP()), player->getName().str()); */ OnNameChange(player->getIP(), player->getName()); } void LANAPI::handleRequestGameInfo( LANMessage *msg, UnsignedInt senderIP ) { + /* fprintf(stderr, "[LAN86] handleRequestGameInfo sender=%d.%d.%d.%d requesterIP=%d.%d.%d.%d requesterName=%ls\n", + PRINTF_IP_AS_4_INTS(senderIP), PRINTF_IP_AS_4_INTS(msg->PlayerInfo.ip), GetWindowsWideCharAsWchar(msg->PlayerInfo.playerName)); */ // In game - are we a game host? if (m_currentGame) { @@ -297,9 +317,15 @@ void LANAPI::handleRequestJoin( LANMessage *msg, UnsignedInt senderIP ) { UnsignedInt responseIP = senderIP; // need this cause the player may or may not be // in the player list at the sendMessage. + // GeneralsX @bugfix GitHubCopilot 12/04/2026 Keep join responses unicast so the joining client does not depend on LAN broadcast delivery. + /* fprintf(stderr, "[LAN86] handleRequestJoin sender=%d.%d.%d.%d targetGameIP=%d.%d.%d.%d localIP=%d.%d.%d.%d pending=%d inLobby=%d\n", + PRINTF_IP_AS_4_INTS(senderIP), PRINTF_IP_AS_4_INTS(msg->GameToJoin.gameIP), PRINTF_IP_AS_4_INTS(m_localIP), + m_pendingAction, m_inLobby); */ if (msg->GameToJoin.gameIP != m_localIP) { + /* fprintf(stderr, "[LAN86] handleRequestJoin ignored sender=%d.%d.%d.%d reason=wrong-game-ip\n", + PRINTF_IP_AS_4_INTS(senderIP)); */ return; // Not us. Ignore it. } LANMessage reply; @@ -312,6 +338,8 @@ void LANAPI::handleRequestJoin( LANMessage *msg, UnsignedInt senderIP ) reply.GameNotJoined.reason = LANAPIInterface::RET_GAME_STARTED; reply.GameNotJoined.gameIP = m_localIP; reply.GameNotJoined.playerIP = senderIP; + /* fprintf(stderr, "[LAN86] handleRequestJoin deny sender=%d.%d.%d.%d reason=game-started responseIP=%d.%d.%d.%d\n", + PRINTF_IP_AS_4_INTS(senderIP), PRINTF_IP_AS_4_INTS(responseIP)); */ DEBUG_LOG(("LANAPI::handleRequestJoin - join denied because game already started.")); } else @@ -395,6 +423,8 @@ void LANAPI::handleRequestJoin( LANMessage *msg, UnsignedInt senderIP ) reply.GameNotJoined.gameIP = m_localIP; reply.GameNotJoined.playerIP = senderIP; canJoin = false; + /* fprintf(stderr, "[LAN86] handleRequestJoin deny sender=%d.%d.%d.%d reason=invalid-name responseIP=%d.%d.%d.%d\n", + PRINTF_IP_AS_4_INTS(senderIP), PRINTF_IP_AS_4_INTS(responseIP)); */ DEBUG_LOG(("LANAPI::handleRequestJoin - join denied because of illegal characters in the player name.")); } @@ -413,6 +443,8 @@ void LANAPI::handleRequestJoin( LANMessage *msg, UnsignedInt senderIP ) reply.GameNotJoined.gameIP = m_localIP; reply.GameNotJoined.playerIP = senderIP; canJoin = false; + /* fprintf(stderr, "[LAN86] handleRequestJoin deny sender=%d.%d.%d.%d reason=duplicate-name responseIP=%d.%d.%d.%d\n", + PRINTF_IP_AS_4_INTS(senderIP), PRINTF_IP_AS_4_INTS(responseIP)); */ DEBUG_LOG(("LANAPI::handleRequestJoin - join denied because of duplicate names.")); break; @@ -441,10 +473,11 @@ void LANAPI::handleRequestJoin( LANMessage *msg, UnsignedInt senderIP ) newSlot.setSerial(msg->GameToJoin.serial); m_currentGame->setSlot(player,newSlot); DEBUG_LOG(("LANAPI::handleRequestJoin - added player %ls at ip 0x%08x to the game", msg->name, senderIP)); + /* fprintf(stderr, "[LAN86] handleRequestJoin accept sender=%d.%d.%d.%d slot=%d responseIP=%d.%d.%d.%d game=%ls\n", + PRINTF_IP_AS_4_INTS(senderIP), player, PRINTF_IP_AS_4_INTS(responseIP), m_currentGame->getName().str()); */ // GeneralsX @bugfix BenderAI 13/02/2026 Wrap WideCharWindows with GetWindowsWideCharAsWchar (fighter19 pattern) OnPlayerJoin(player, UnicodeString(GetWindowsWideCharAsWchar(msg->name))); - responseIP = 0; break; } @@ -458,6 +491,8 @@ void LANAPI::handleRequestJoin( LANMessage *msg, UnsignedInt senderIP ) reply.GameNotJoined.reason = LANAPIInterface::RET_GAME_FULL; reply.GameNotJoined.gameIP = m_localIP; reply.GameNotJoined.playerIP = senderIP; + /* fprintf(stderr, "[LAN86] handleRequestJoin deny sender=%d.%d.%d.%d reason=game-full responseIP=%d.%d.%d.%d\n", + PRINTF_IP_AS_4_INTS(senderIP), PRINTF_IP_AS_4_INTS(responseIP)); */ DEBUG_LOG(("LANAPI::handleRequestJoin - join denied because game is full.")); } } @@ -468,6 +503,8 @@ void LANAPI::handleRequestJoin( LANMessage *msg, UnsignedInt senderIP ) reply.GameNotJoined.reason = LANAPIInterface::RET_GAME_GONE; reply.GameNotJoined.gameIP = m_localIP; reply.GameNotJoined.playerIP = senderIP; + /* fprintf(stderr, "[LAN86] handleRequestJoin deny sender=%d.%d.%d.%d reason=game-gone responseIP=%d.%d.%d.%d\n", + PRINTF_IP_AS_4_INTS(senderIP), PRINTF_IP_AS_4_INTS(responseIP)); */ } sendMessage(&reply, responseIP); RequestGameOptions(GenerateGameOptionsString(), true); @@ -475,6 +512,10 @@ void LANAPI::handleRequestJoin( LANMessage *msg, UnsignedInt senderIP ) void LANAPI::handleJoinAccept( LANMessage *msg, UnsignedInt senderIP ) { + // GeneralsX @build GitHubCopilot 12/04/2026 Trace directed join-accept processing and pending action transitions for LAN/direct-connect debugging. + /* fprintf(stderr, "[LAN86] handleJoinAccept sender=%d.%d.%d.%d playerIP=%d.%d.%d.%d localIP=%d.%d.%d.%d pending=%d slot=%d game=%ls\n", + PRINTF_IP_AS_4_INTS(senderIP), PRINTF_IP_AS_4_INTS(msg->GameJoined.playerIP), PRINTF_IP_AS_4_INTS(m_localIP), + m_pendingAction, msg->GameJoined.slotPosition, GetWindowsWideCharAsWchar(msg->GameJoined.gameName)); */ if (msg->GameJoined.playerIP == m_localIP) // Is it for us? { if (m_pendingAction == ACT_JOIN) // Are we trying to join? @@ -521,11 +562,19 @@ void LANAPI::handleJoinAccept( LANMessage *msg, UnsignedInt senderIP ) m_pendingAction = ACT_NONE; m_expiration = 0; } + else + { + /* fprintf(stderr, "[LAN86] handleJoinAccept ignored sender=%d.%d.%d.%d reason=unexpected-pending-action pending=%d\n", + PRINTF_IP_AS_4_INTS(senderIP), m_pendingAction); */ + } } } void LANAPI::handleJoinDeny( LANMessage *msg, UnsignedInt senderIP ) { + /* fprintf(stderr, "[LAN86] handleJoinDeny sender=%d.%d.%d.%d playerIP=%d.%d.%d.%d localIP=%d.%d.%d.%d pending=%d reason=%d game=%ls\n", + PRINTF_IP_AS_4_INTS(senderIP), PRINTF_IP_AS_4_INTS(msg->GameJoined.playerIP), PRINTF_IP_AS_4_INTS(m_localIP), + m_pendingAction, msg->GameNotJoined.reason, GetWindowsWideCharAsWchar(msg->GameNotJoined.gameName)); */ if (msg->GameJoined.playerIP == m_localIP) // Is it for us? { if (m_pendingAction == ACT_JOIN) // Are we trying to join? diff --git a/Core/GameEngine/Source/GameNetwork/LANGameInfo.cpp b/Core/GameEngine/Source/GameNetwork/LANGameInfo.cpp index 4bb80647e94..217c666d45b 100644 --- a/Core/GameEngine/Source/GameNetwork/LANGameInfo.cpp +++ b/Core/GameEngine/Source/GameNetwork/LANGameInfo.cpp @@ -201,6 +201,7 @@ void LANDisplayGameList( GameWindow *gameListbox, LANGameInfo *gameList ) LANGameInfo *selectedPtr = nullptr; Int selectedIndex = -1; Int indexToSelect = -1; + Int gameCount = 0; if (gameListbox) { GadgetListBoxGetSelected(gameListbox, &selectedIndex); @@ -226,6 +227,11 @@ void LANDisplayGameList( GameWindow *gameListbox, LANGameInfo *gameList ) } Int addedIndex = GadgetListBoxAddEntryText(gameListbox, txtGName, (gameList->isGameInProgress())?gameInProgressColor:gameColor, -1, -1); GadgetListBoxSetItemData(gameListbox, (void *)gameList, addedIndex, 0 ); + ++gameCount; + // GeneralsX @build GitHubCopilot 12/04/2026 Trace rendered LAN lobby rows to catch announce-vs-UI mismatches. + /* fprintf(stderr, "[LAN86] lobby render row=%d host=%d.%d.%d.%d hostName=%ls gameName=%ls inProgress=%d direct=%d\n", + addedIndex, PRINTF_IP_AS_4_INTS(gameList->getHostIP()), gameList->getPlayerName(0).str(), gameList->getName().str(), + gameList->isGameInProgress(), gameList->getIsDirectConnect()); */ if (selectedPtr == gameList) indexToSelect = addedIndex; @@ -237,6 +243,10 @@ void LANDisplayGameList( GameWindow *gameListbox, LANGameInfo *gameList ) GadgetListBoxSetSelected(gameListbox, indexToSelect); else HideGameInfoWindow(TRUE); + + // GeneralsX @build GitHubCopilot 12/04/2026 Surface final LAN lobby row count after each refresh. + /* fprintf(stderr, "[LAN86] lobby render complete count=%d selectedIndex=%d restoredIndex=%d\n", + gameCount, selectedIndex, indexToSelect); */ } } diff --git a/Core/GameEngine/Source/GameNetwork/Network.cpp b/Core/GameEngine/Source/GameNetwork/Network.cpp index 837ff693f6c..b476ec31f74 100644 --- a/Core/GameEngine/Source/GameNetwork/Network.cpp +++ b/Core/GameEngine/Source/GameNetwork/Network.cpp @@ -371,6 +371,9 @@ void Network::init() void Network::setSawCRCMismatch() { m_sawCRCMismatch = TRUE; + // GeneralsX @build GitHubCopilot 12/04/2026 Surface mismatch UI activation in manual Linux/macOS captures. + /* fprintf(stderr, "[LAN86] setSawCRCMismatch logicFrame=%u latestMismatchFrame=%d runAhead=%d\n", + TheGameLogic->getFrame(), TheGameLogic->getFrame() - m_runAhead - 1, m_runAhead); */ TheScriptActions->closeWindows( TRUE ); m_messageWindow = TheWindowManager->winCreateFromScript("Menus/CRCMismatch.wnd"); diff --git a/Core/GameEngine/Source/GameNetwork/Transport.cpp b/Core/GameEngine/Source/GameNetwork/Transport.cpp index 58098745c5a..8d07e39ae35 100644 --- a/Core/GameEngine/Source/GameNetwork/Transport.cpp +++ b/Core/GameEngine/Source/GameNetwork/Transport.cpp @@ -85,6 +85,10 @@ Bool Transport::init( AsciiString ip, UnsignedShort port ) Bool Transport::init( UnsignedInt ip, UnsignedShort port ) { + // GeneralsX @build GitHubCopilot 11/04/2026 Trace UDP transport bind inputs for LAN troubleshooting. + DEBUG_LOG(("Transport::init - requested bind %d.%d.%d.%d:%d", PRINTF_IP_AS_4_INTS(ip), port)); + /* fprintf(stderr, "[LAN86] Transport::init bind request %d.%d.%d.%d:%d\n", PRINTF_IP_AS_4_INTS(ip), port); */ + // ----- Initialize Winsock ----- if (!m_winsockInit) { @@ -124,6 +128,10 @@ Bool Transport::init( UnsignedInt ip, UnsignedShort port ) return false; } + // GeneralsX @build GitHubCopilot 11/04/2026 Confirm successful UDP bind endpoint for LAN diagnostics. + DEBUG_LOG(("Transport::init - bind success %d.%d.%d.%d:%d", PRINTF_IP_AS_4_INTS(ip), port)); + /* fprintf(stderr, "[LAN86] Transport::init bind success %d.%d.%d.%d:%d\n", PRINTF_IP_AS_4_INTS(ip), port); */ + // ------- Clear buffers -------- int i=0; for (; i> 24) & 0xFF, (ipHostOrder >> 16) & 0xFF, (ipHostOrder >> 8) & 0xFF, ipHostOrder & 0xFF, + portHostOrder, m_lastError)); + /* fprintf(stderr, "[LAN86] UDP::Bind socket failed %d.%d.%d.%d:%d err=%d\n", + (ipHostOrder >> 24) & 0xFF, (ipHostOrder >> 16) & 0xFF, (ipHostOrder >> 8) & 0xFF, ipHostOrder & 0xFF, + portHostOrder, m_lastError); */ return(UNKNOWN); + } retval=bind(fd,(struct sockaddr *)&addr,sizeof(addr)); - #ifdef _WIN32 if (retval==SOCKET_ERROR) { - retval=-1; + retval=-1; m_lastError = WSAGetLastError(); } - #endif if (retval==-1) { + // GeneralsX @build GitHubCopilot 11/04/2026 Capture bind failure endpoint and error code. + DEBUG_LOG(("UDP::Bind - bind() failed for %d.%d.%d.%d:%d err=%d", + (ipHostOrder >> 24) & 0xFF, (ipHostOrder >> 16) & 0xFF, (ipHostOrder >> 8) & 0xFF, ipHostOrder & 0xFF, + portHostOrder, m_lastError)); + /* fprintf(stderr, "[LAN86] UDP::Bind bind failed %d.%d.%d.%d:%d err=%d\n", + (ipHostOrder >> 24) & 0xFF, (ipHostOrder >> 16) & 0xFF, (ipHostOrder >> 8) & 0xFF, ipHostOrder & 0xFF, + portHostOrder, m_lastError); */ status=GetStatus(); //CERR("Bind failure (" << status << ") IP " << IP << " PORT " << Port ) return(status); @@ -240,17 +257,23 @@ Int UDP::Write(const unsigned char *msg,UnsignedInt len,UnsignedInt IP,UnsignedS ClearStatus(); retval=sendto(fd,(const char *)msg,len,0,(struct sockaddr *)&to,sizeof(to)); - #ifdef _WIN32 + if (retval==SOCKET_ERROR) { retval=-1; m_lastError = WSAGetLastError(); + // GeneralsX @build GitHubCopilot 11/04/2026 Capture UDP send failure endpoint and error code. + DEBUG_LOG(("UDP::Write - sendto failed dst=%d.%d.%d.%d:%d len=%d err=%d", + (IP >> 24) & 0xFF, (IP >> 16) & 0xFF, (IP >> 8) & 0xFF, IP & 0xFF, + port, len, m_lastError)); + /* fprintf(stderr, "[LAN86] UDP::Write sendto failed dst=%d.%d.%d.%d:%d len=%d err=%d\n", + (IP >> 24) & 0xFF, (IP >> 16) & 0xFF, (IP >> 8) & 0xFF, IP & 0xFF, + port, len, m_lastError); */ #ifdef DEBUG_LOGGING static Int errCount = 0; #endif DEBUG_ASSERTLOG(errCount++ > 100, ("UDP::Write() - WSA error is %s", GetWSAErrorString(WSAGetLastError()).str())); } - #endif return(retval); } @@ -264,13 +287,16 @@ Int UDP::Read(unsigned char *msg,UnsignedInt len,sockaddr_in *from) if (from!=nullptr) { retval=recvfrom(fd,(char *)msg,len,0,(struct sockaddr *)from,&alen); - #ifdef _WIN32 + if (retval == SOCKET_ERROR) { if (WSAGetLastError() != WSAEWOULDBLOCK) { // failing because of a blocking error isn't really such a bad thing. m_lastError = WSAGetLastError(); + // GeneralsX @build GitHubCopilot 11/04/2026 Capture UDP receive failure details for LAN diagnostics. + DEBUG_LOG(("UDP::Read - recvfrom failed len=%d err=%d", len, m_lastError)); + /* fprintf(stderr, "[LAN86] UDP::Read recvfrom failed len=%d err=%d\n", len, m_lastError); */ #ifdef DEBUG_LOGGING static Int errCount = 0; #endif @@ -280,18 +306,20 @@ Int UDP::Read(unsigned char *msg,UnsignedInt len,sockaddr_in *from) retval = 0; } } - #endif } else { retval=recvfrom(fd,(char *)msg,len,0,nullptr,nullptr); - #ifdef _WIN32 + if (retval==SOCKET_ERROR) { if (WSAGetLastError() != WSAEWOULDBLOCK) { // failing because of a blocking error isn't really such a bad thing. m_lastError = WSAGetLastError(); + // GeneralsX @build GitHubCopilot 11/04/2026 Capture UDP receive failure details for LAN diagnostics. + DEBUG_LOG(("UDP::Read - recvfrom failed len=%d err=%d", len, m_lastError)); + /* fprintf(stderr, "[LAN86] UDP::Read recvfrom failed len=%d err=%d\n", len, m_lastError); */ #ifdef DEBUG_LOGGING static Int errCount = 0; #endif @@ -301,7 +329,6 @@ Int UDP::Read(unsigned char *msg,UnsignedInt len,sockaddr_in *from) retval = 0; } } - #endif } return(retval); } diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp index e659ea8f6e7..535a2e56c4c 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp @@ -161,6 +161,7 @@ W3DView::W3DView() m_3DCamera = nullptr; m_2DCamera = nullptr; + m_timeMultiplier = 1; #if PRESERVE_RETAIL_SCRIPTED_CAMERA m_initialGroundLevel = 10.0f; diff --git a/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/LanLobbyMenu.cpp b/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/LanLobbyMenu.cpp index 0dae7701864..78ddd67dc7f 100644 --- a/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/LanLobbyMenu.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/LanLobbyMenu.cpp @@ -416,12 +416,36 @@ void LanLobbyMenuInit( WindowLayout *layout, void *userData ) } // Choose an IP address, then initialize the LAN singleton - UnsignedInt IP = TheGlobalData->m_defaultIP; + OptionPreferences optionPrefs; + UnsignedInt preferredLANIP = optionPrefs.getLANIPAddress(); + UnsignedInt IP = preferredLANIP ? preferredLANIP : TheGlobalData->m_defaultIP; IPEnumeration IPs; + EnumeratedIP *IPlist = IPs.getAddresses(); const WideChar* IPSource; - if (!IP) + Bool foundPreferredIP = FALSE; + + for (EnumeratedIP *candidate = IPlist; candidate != nullptr; candidate = candidate->getNext()) + { + /* fprintf(stderr, "[LAN86] LanLobbyMenuInit enumerated candidate %d.%d.%d.%d\n", + PRINTF_IP_AS_4_INTS(candidate->getIP())); */ + if (candidate->getIP() == IP) + { + foundPreferredIP = TRUE; + break; + } + } + /* fprintf(stderr, "[LAN86] LanLobbyMenuInit preferredLANIP=%d.%d.%d.%d globalDefaultIP=%d.%d.%d.%d foundPreferred=%d\n", + PRINTF_IP_AS_4_INTS(preferredLANIP), PRINTF_IP_AS_4_INTS(TheGlobalData->m_defaultIP), foundPreferredIP); */ + + if (IP != 0 && foundPreferredIP) + { + IPSource = (preferredLANIP == IP) ? L"Options LAN IP" : L"Global default LAN IP"; + // GeneralsX @build GitHubCopilot 12/04/2026 Make LAN lobby explicitly honor configured LAN IP preference when it is still valid. + DEBUG_LOG(("LanLobbyMenuInit - using preferred LAN IP %d.%d.%d.%d from %ls", + PRINTF_IP_AS_4_INTS(IP), IPSource)); + } + else { - EnumeratedIP *IPlist = IPs.getAddresses(); /* while (IPlist && IPlist->getNext()) { @@ -434,12 +458,19 @@ void LanLobbyMenuInit( WindowLayout *layout, void *userData ) /// @todo: display error and exit lan lobby if no IPs are found } - IPSource = L"Local IP chosen"; + IPSource = L"Enumerated LAN IP fallback"; IP = IPlist->getIP(); + // GeneralsX @build GitHubCopilot 11/04/2026 Log auto-selected LAN IP to diagnose cross-platform discovery failures. + DEBUG_LOG(("LanLobbyMenuInit - auto-selected LAN IP %d.%d.%d.%d from enumeration", + PRINTF_IP_AS_4_INTS(IP))); + /* fprintf(stderr, "[LAN86] LanLobbyMenuInit fallback IP %d.%d.%d.%d preferred=%d.%d.%d.%d found=%d\n", + PRINTF_IP_AS_4_INTS(IP), PRINTF_IP_AS_4_INTS(preferredLANIP), foundPreferredIP); */ } - else + + if (foundPreferredIP) { - IPSource = L"Default local IP"; + /* fprintf(stderr, "[LAN86] LanLobbyMenuInit preferred IP source=%ls ip=%d.%d.%d.%d\n", + IPSource, PRINTF_IP_AS_4_INTS(IP)); */ } #if defined(RTS_DEBUG) UnicodeString str; @@ -449,8 +480,20 @@ void LanLobbyMenuInit( WindowLayout *layout, void *userData ) // TheLAN->init() sets us to be in a LAN menu screen automatically. TheLAN->init(); + // GeneralsX @build GitHubCopilot 11/04/2026 Log LAN bind attempt from lobby startup path. + DEBUG_LOG(("LanLobbyMenuInit - calling SetLocalIP(%d.%d.%d.%d)", PRINTF_IP_AS_4_INTS(IP))); + /* fprintf(stderr, "[LAN86] LanLobbyMenuInit SetLocalIP begin %d.%d.%d.%d\n", PRINTF_IP_AS_4_INTS(IP)); */ if (TheLAN->SetLocalIP(IP) == FALSE) { LANSocketErrorDetected = TRUE; + // GeneralsX @build GitHubCopilot 11/04/2026 Explicit failure breadcrumb for LAN socket initialization. + DEBUG_LOG(("LanLobbyMenuInit - SetLocalIP failed for %d.%d.%d.%d", PRINTF_IP_AS_4_INTS(IP))); + /* fprintf(stderr, "[LAN86] LanLobbyMenuInit SetLocalIP failed %d.%d.%d.%d\n", PRINTF_IP_AS_4_INTS(IP)); */ + } + else + { + // GeneralsX @build GitHubCopilot 11/04/2026 Explicit success breadcrumb for LAN socket initialization. + DEBUG_LOG(("LanLobbyMenuInit - SetLocalIP succeeded for %d.%d.%d.%d", PRINTF_IP_AS_4_INTS(IP))); + /* fprintf(stderr, "[LAN86] LanLobbyMenuInit SetLocalIP ok %d.%d.%d.%d\n", PRINTF_IP_AS_4_INTS(IP)); */ } //Initialize the gadgets on the window @@ -469,6 +512,9 @@ void LanLobbyMenuInit( WindowLayout *layout, void *userData ) defaultName.truncateTo(g_lanPlayerNameLength); TheLAN->RequestSetName(defaultName); + // GeneralsX @build GitHubCopilot 11/04/2026 Trace initial LAN discovery request from menu bootstrap. + DEBUG_LOG(("LanLobbyMenuInit - issuing initial RequestLocations() from LAN lobby")); + /* fprintf(stderr, "[LAN86] LanLobbyMenuInit RequestLocations initial\n"); */ TheLAN->RequestLocations(); /* diff --git a/Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp b/Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp index 0dcad375a34..f68c22412db 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp @@ -2365,6 +2365,9 @@ void GameLogic::processCommandList( CommandList *list ) if (m_cachedCRCs.size() < numPlayers) { DEBUG_CRASH(("Not enough CRCs!")); + // GeneralsX @build GitHubCopilot 12/04/2026 Surface CRC quorum failures for cross-platform mismatch diagnosis. + /* fprintf(stderr, "[LAN86] CRC quorum failure frame=%u cached=%zu players=%d\n", + m_frame, m_cachedCRCs.size(), numPlayers); */ sawCRCMismatch = TRUE; } else @@ -2391,6 +2394,9 @@ void GameLogic::processCommandList( CommandList *list ) if (referenceCRC != crc) { DEBUG_CRASH(("CRC mismatch!")); + // GeneralsX @build GitHubCopilot 12/04/2026 Surface validator/validated CRC divergence before mismatch UI triggers. + /* fprintf(stderr, "[LAN86] CRC mismatch frame=%u validatorPlayer=%d validator=%08X validatedPlayer=%d validated=%08X\n", + m_frame, m_cachedCRCs.begin()->first, referenceCRC, it->first, crc); */ sawCRCMismatch = TRUE; } } @@ -2399,6 +2405,15 @@ void GameLogic::processCommandList( CommandList *list ) if (sawCRCMismatch) { + // GeneralsX @build GitHubCopilot 12/04/2026 Dump frame CRC set to stderr so Linux/macOS logs can be compared directly. + /* fprintf(stderr, "[LAN86] CRC mismatch summary frame=%u cached=%zu players=%d\n", + m_frame, m_cachedCRCs.size(), numPlayers); */ + for (std::map::const_iterator crcIt = m_cachedCRCs.begin(); crcIt != m_cachedCRCs.end(); ++crcIt) + { + Player *player = ThePlayerList->getNthPlayer(crcIt->first); + /* fprintf(stderr, "[LAN86] CRC mismatch entry player=%d name=%ls crc=%08X\n", + crcIt->first, player ? player->getPlayerDisplayName().str() : L"", crcIt->second); */ + } #ifdef DEBUG_LOGGING DEBUG_LOG(("CRC Mismatch - saw %d CRCs from %d players", m_cachedCRCs.size(), numPlayers)); for (CachedCRCMap::const_iterator crcIt = m_cachedCRCs.begin(); crcIt != m_cachedCRCs.end(); ++crcIt) @@ -4107,6 +4122,8 @@ void GameLogic::testTimeOut() void GameLogic::timeOutGameStart() { DEBUG_LOG(("We got the Force TimeOut Start Message")); + // GeneralsX @build GitHubCopilot 12/04/2026 Surface game-start timeout path alongside CRC mismatch diagnostics. + /* fprintf(stderr, "[LAN86] timeOutGameStart frame=%u forceStartBefore=%d\n", m_frame, m_forceGameStartByTimeOut); */ m_forceGameStartByTimeOut = TRUE; } diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/LanLobbyMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/LanLobbyMenu.cpp index 8ca705b9e12..a8c588c2208 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/LanLobbyMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/LanLobbyMenu.cpp @@ -417,12 +417,36 @@ void LanLobbyMenuInit( WindowLayout *layout, void *userData ) } // Choose an IP address, then initialize the LAN singleton - UnsignedInt IP = TheGlobalData->m_defaultIP; + OptionPreferences optionPrefs; + UnsignedInt preferredLANIP = optionPrefs.getLANIPAddress(); + UnsignedInt IP = preferredLANIP ? preferredLANIP : TheGlobalData->m_defaultIP; IPEnumeration IPs; + EnumeratedIP *IPlist = IPs.getAddresses(); const WideChar* IPSource; - if (!IP) + Bool foundPreferredIP = FALSE; + + for (EnumeratedIP *candidate = IPlist; candidate != nullptr; candidate = candidate->getNext()) + { + /* fprintf(stderr, "[LAN86] LanLobbyMenuInit enumerated candidate %d.%d.%d.%d\n", + PRINTF_IP_AS_4_INTS(candidate->getIP())); */ + if (candidate->getIP() == IP) + { + foundPreferredIP = TRUE; + break; + } + } + /* fprintf(stderr, "[LAN86] LanLobbyMenuInit preferredLANIP=%d.%d.%d.%d globalDefaultIP=%d.%d.%d.%d foundPreferred=%d\n", + PRINTF_IP_AS_4_INTS(preferredLANIP), PRINTF_IP_AS_4_INTS(TheGlobalData->m_defaultIP), foundPreferredIP); */ + + if (IP != 0 && foundPreferredIP) + { + IPSource = (preferredLANIP == IP) ? L"Options LAN IP" : L"Global default LAN IP"; + // GeneralsX @build GitHubCopilot 12/04/2026 Make LAN lobby explicitly honor configured LAN IP preference when it is still valid. + DEBUG_LOG(("LanLobbyMenuInit - using preferred LAN IP %d.%d.%d.%d from %ls", + PRINTF_IP_AS_4_INTS(IP), IPSource)); + } + else { - EnumeratedIP *IPlist = IPs.getAddresses(); /* while (IPlist && IPlist->getNext()) { @@ -435,12 +459,19 @@ void LanLobbyMenuInit( WindowLayout *layout, void *userData ) /// @todo: display error and exit lan lobby if no IPs are found } - IPSource = L"Local IP chosen"; + IPSource = L"Enumerated LAN IP fallback"; IP = IPlist->getIP(); + // GeneralsX @build GitHubCopilot 11/04/2026 Log auto-selected LAN IP to diagnose cross-platform discovery failures. + DEBUG_LOG(("LanLobbyMenuInit - auto-selected LAN IP %d.%d.%d.%d from enumeration", + PRINTF_IP_AS_4_INTS(IP))); + /* fprintf(stderr, "[LAN86] LanLobbyMenuInit fallback IP %d.%d.%d.%d preferred=%d.%d.%d.%d found=%d\n", + PRINTF_IP_AS_4_INTS(IP), PRINTF_IP_AS_4_INTS(preferredLANIP), foundPreferredIP); */ } - else + + if (foundPreferredIP) { - IPSource = L"Default local IP"; + /* fprintf(stderr, "[LAN86] LanLobbyMenuInit preferred IP source=%ls ip=%d.%d.%d.%d\n", + IPSource, PRINTF_IP_AS_4_INTS(IP)); */ } #if defined(RTS_DEBUG) UnicodeString str; @@ -450,8 +481,20 @@ void LanLobbyMenuInit( WindowLayout *layout, void *userData ) // TheLAN->init() sets us to be in a LAN menu screen automatically. TheLAN->init(); + // GeneralsX @build GitHubCopilot 11/04/2026 Log LAN bind attempt from lobby startup path. + DEBUG_LOG(("LanLobbyMenuInit - calling SetLocalIP(%d.%d.%d.%d)", PRINTF_IP_AS_4_INTS(IP))); + /* fprintf(stderr, "[LAN86] LanLobbyMenuInit SetLocalIP begin %d.%d.%d.%d\n", PRINTF_IP_AS_4_INTS(IP)); */ if (TheLAN->SetLocalIP(IP) == FALSE) { LANSocketErrorDetected = TRUE; + // GeneralsX @build GitHubCopilot 11/04/2026 Explicit failure breadcrumb for LAN socket initialization. + DEBUG_LOG(("LanLobbyMenuInit - SetLocalIP failed for %d.%d.%d.%d", PRINTF_IP_AS_4_INTS(IP))); + /* fprintf(stderr, "[LAN86] LanLobbyMenuInit SetLocalIP failed %d.%d.%d.%d\n", PRINTF_IP_AS_4_INTS(IP)); */ + } + else + { + // GeneralsX @build GitHubCopilot 11/04/2026 Explicit success breadcrumb for LAN socket initialization. + DEBUG_LOG(("LanLobbyMenuInit - SetLocalIP succeeded for %d.%d.%d.%d", PRINTF_IP_AS_4_INTS(IP))); + /* fprintf(stderr, "[LAN86] LanLobbyMenuInit SetLocalIP ok %d.%d.%d.%d\n", PRINTF_IP_AS_4_INTS(IP)); */ } //Initialize the gadgets on the window @@ -470,6 +513,9 @@ void LanLobbyMenuInit( WindowLayout *layout, void *userData ) defaultName.truncateTo(g_lanPlayerNameLength); TheLAN->RequestSetName(defaultName); + // GeneralsX @build GitHubCopilot 11/04/2026 Trace initial LAN discovery request from menu bootstrap. + DEBUG_LOG(("LanLobbyMenuInit - issuing initial RequestLocations() from LAN lobby")); + /* fprintf(stderr, "[LAN86] LanLobbyMenuInit RequestLocations initial\n"); */ TheLAN->RequestLocations(); /* diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/OptionsMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/OptionsMenu.cpp index f801a31807f..fdbee3c5fe4 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/OptionsMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/OptionsMenu.cpp @@ -1535,7 +1535,6 @@ WindowMsgHandledType OptionsMenuSystem( GameWindow *window, UnsignedInt msg, static NameKeyType buttonAccept = NAMEKEY_INVALID; static NameKeyType buttonReplayMenu = NAMEKEY_INVALID; static NameKeyType buttonKeyboardOptionsMenu = NAMEKEY_INVALID; - static NameKeyType buttonExtrasMenu = NAMEKEY_INVALID; switch( msg ) { @@ -1550,31 +1549,6 @@ WindowMsgHandledType OptionsMenuSystem( GameWindow *window, UnsignedInt msg, buttonAccept = TheNameKeyGenerator->nameToKey( "OptionsMenu.wnd:ButtonAccept" ); buttonKeyboardOptionsMenu = TheNameKeyGenerator->nameToKey( "OptionsMenu.wnd:ButtonKeyboardOptions" ); - // GeneralsX @feature fbraz3 08/06/2026 Create Extras button dynamically - // (OptionsMenu.wnd in WindowZH.big has no ButtonExtras, so we add it at runtime) - { - GameWindow *backBtn = TheWindowManager->winGetWindowFromId(window, buttonBack); - if (backBtn) { - WinInstanceData instData; - instData.init(); - BitSet(instData.m_style, GWS_PUSH_BUTTON | GWS_MOUSE_TRACK); - instData.m_textLabelString = "Extras"; - - GameWindow *extrasBtn = TheWindowManager->gogoGadgetPushButton( - backBtn->winGetParent(), - WIN_STATUS_ENABLED | WIN_STATUS_IMAGE, - 320, 528, - 145, 32, - &instData, nullptr, TRUE); - - if (extrasBtn) { - buttonExtrasMenu = TheNameKeyGenerator->nameToKey("OptionsMenu.wnd:ButtonExtras"); - extrasBtn->winSetWindowId(buttonExtrasMenu); - extrasBtn->winSetSystemFunc(OptionsMenuSystem); - } - } - } - break; } @@ -1693,10 +1667,6 @@ WindowMsgHandledType OptionsMenuSystem( GameWindow *window, UnsignedInt msg, { TheShell->push( "Menus/KeyboardOptionsMenu.wnd" ); } - else if ( controlID == buttonExtrasMenu ) - { - TheShell->push( "Menus/ExtrasMenu.wnd" ); - } else if(controlID == checkDrawAnchorID ) { if( GadgetCheckBoxIsChecked( control ) ) diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp index 0a2f82be920..dac0557097b 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp @@ -2716,6 +2716,9 @@ void GameLogic::processCommandList( CommandList *list ) if (m_cachedCRCs.size() < numPlayers) { DEBUG_CRASH(("Not enough CRCs!")); + // GeneralsX @build GitHubCopilot 12/04/2026 Surface CRC quorum failures for cross-platform mismatch diagnosis. + /* fprintf(stderr, "[LAN86] CRC quorum failure frame=%u cached=%zu players=%d\n", + m_frame, m_cachedCRCs.size(), numPlayers); */ sawCRCMismatch = TRUE; } else @@ -2742,6 +2745,9 @@ void GameLogic::processCommandList( CommandList *list ) if (referenceCRC != crc) { DEBUG_CRASH(("CRC mismatch!")); + // GeneralsX @build GitHubCopilot 12/04/2026 Surface validator/validated CRC divergence before mismatch UI triggers. + /* fprintf(stderr, "[LAN86] CRC mismatch frame=%u validatorPlayer=%d validator=%08X validatedPlayer=%d validated=%08X\n", + m_frame, m_cachedCRCs.begin()->first, referenceCRC, it->first, crc); */ sawCRCMismatch = TRUE; } } @@ -2750,6 +2756,15 @@ void GameLogic::processCommandList( CommandList *list ) if (sawCRCMismatch) { + // GeneralsX @build GitHubCopilot 12/04/2026 Dump frame CRC set to stderr so Linux/macOS logs can be compared directly. + /* fprintf(stderr, "[LAN86] CRC mismatch summary frame=%u cached=%zu players=%d\n", + m_frame, m_cachedCRCs.size(), numPlayers); */ + for (std::map::const_iterator crcIt = m_cachedCRCs.begin(); crcIt != m_cachedCRCs.end(); ++crcIt) + { + Player *player = ThePlayerList->getNthPlayer(crcIt->first); + /* fprintf(stderr, "[LAN86] CRC mismatch entry player=%d name=%ls crc=%08X\n", + crcIt->first, player ? player->getPlayerDisplayName().str() : L"", crcIt->second); */ + } #ifdef DEBUG_LOGGING DEBUG_LOG(("CRC Mismatch - saw %d CRCs from %d players", m_cachedCRCs.size(), numPlayers)); for (CachedCRCMap::const_iterator crcIt = m_cachedCRCs.begin(); crcIt != m_cachedCRCs.end(); ++crcIt) @@ -4700,6 +4715,8 @@ void GameLogic::testTimeOut() void GameLogic::timeOutGameStart() { DEBUG_LOG(("We got the Force TimeOut Start Message")); + // GeneralsX @build GitHubCopilot 12/04/2026 Surface game-start timeout path alongside CRC mismatch diagnostics. + /* fprintf(stderr, "[LAN86] timeOutGameStart frame=%u forceStartBefore=%d\n", m_frame, m_forceGameStartByTimeOut); */ m_forceGameStartByTimeOut = TRUE; } diff --git a/docs/BUILD/LINUX.md b/docs/BUILD/LINUX.md index d57d75199d5..c2688ba6298 100644 --- a/docs/BUILD/LINUX.md +++ b/docs/BUILD/LINUX.md @@ -343,7 +343,7 @@ The Linux port **Phase 1 (Graphics) is COMPLETE** ✅ - **Development Diary**: See [../DEV_BLOG/README.md](../DEV_BLOG/README.md) - **Phase Documentation**: See [../WORKDIR/phases/](../WORKDIR/phases/) - **Docker Scripts**: See [../../scripts/README_DOCKER_SCRIPTS.md](../../scripts/README_DOCKER_SCRIPTS.md) -- **Reference Repos**: See [../../references/fighter19-dxvk-port/](../../references/fighter19-dxvk-port/) +- **Reference Repos**: See [../../references/old-refs/fighter19-dxvk-port/](../../references/old-refs/fighter19-dxvk-port/) - **DXVK Architecture**: See [../WORKDIR/support/](../WORKDIR/support/) ## Support diff --git a/docs/DEV_BLOG/2026-02-DIARY.md b/docs/DEV_BLOG/2026-02-DIARY.md index fa037334b30..fb921ea0f9c 100644 --- a/docs/DEV_BLOG/2026-02-DIARY.md +++ b/docs/DEV_BLOG/2026-02-DIARY.md @@ -1205,7 +1205,7 @@ b4c4a963e Session 54: Linux build working + system cursor visible - Found: **Complete FFmpeg video player already implemented** (not a research spike, actual working code!) **4. VideoDevice Framework Copied** -- Copied from `references/fighter19-dxvk-port/GeneralsMD/Code/GameEngineDevice/` +- Copied from `references/old-refs/fighter19-dxvk-port/GeneralsMD/Code/GameEngineDevice/` - **Headers** → `GeneralsMD/Code/GameEngineDevice/Include/VideoDevice/`: - `Bink/BinkVideoPlayer.h` — Windows video player stub - `FFmpeg/FFmpegFile.h` — FFmpeg file I/O abstraction @@ -1385,7 +1385,7 @@ This is identical to the SDL2 port issue mentioned by the user and exactly what ### Fix -Ported `scaleMouseCoordinates()` from fighter19's reference implementation (`references/fighter19-dxvk-port/GeneralsMD/Code/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Mouse.cpp`): +Ported `scaleMouseCoordinates()` from fighter19's reference implementation (`references/old-refs/fighter19-dxvk-port/GeneralsMD/Code/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Mouse.cpp`): ```cpp void SDL3Mouse::scaleMouseCoordinates(int rawX, int rawY, Uint32 windowID, int& scaledX, int& scaledY) @@ -1577,7 +1577,7 @@ Test output confirmed fix works: ``` ### Reference -Same approach as `references/fighter19-dxvk-port/` — fighter19's Linux port uses identical hardware hardcoding in `GameLOD.cpp`. +Same approach as `references/old-refs/fighter19-dxvk-port/` — fighter19's Linux port uses identical hardware hardcoding in `GameLOD.cpp`. --- diff --git a/docs/DEV_BLOG/2026-06-DIARY.md b/docs/DEV_BLOG/2026-06-DIARY.md index f2748836044..49060dfc7ec 100644 --- a/docs/DEV_BLOG/2026-06-DIARY.md +++ b/docs/DEV_BLOG/2026-06-DIARY.md @@ -1,5 +1,14 @@ # Development Diary - June 2026 +## 2026-06-29: LAN Cross-Platform Success (macOS and Linux) + +- **LAN Network Portability Confirmed**: Successfully played a multiplayer LAN match between macOS and Linux builds for several minutes without a CRC mismatch or desync. This confirms that our platform abstraction (SDL3 + DXVK + MiniAudio) preserves the game's strict deterministic logic requirement, and that our networking fixes for IP binding and diagnostic logging (Issue #86) function correctly across different POSIX environments. + +## 2026-06-28: Fix Uncapped FPS Frame Limiter + +- **Uninitialized Variable Fix**: Fixed an issue where the game would run at uncapped framerates (e.g. 130 FPS instead of 60 FPS) in both Generals base game and Zero Hour. Discovered that the `W3DView` implementation of `getTimeMultiplier()` returned `m_timeMultiplier`, which was never initialized in the constructor. Because `m_timeMultiplier` contained garbage memory, the condition `getTimeMultiplier() <= 1` evaluated to false, leading `TheFramePacer` to bypass the FPS limit by setting it to `UncappedFpsValue` (1,000,000). Initializing `m_timeMultiplier = 1;` in `W3DView` constructor ([W3DView.cpp](file:///Users/felipebraz/PhpstormProjects/pessoal/GeneralsX/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp)) fixes the issue. +- **Uncapped FPS Fast-Path Fix**: Corrected the fast-path check in `FrameRateLimit::wait()` ([FrameRateLimit.cpp](file:///Users/felipebraz/PhpstormProjects/pessoal/GeneralsX/Core/GameEngine/Source/Common/FrameRateLimit.cpp)) from `maxFps > 1000000` to `maxFps >= 1000000`. This ensures `UncappedFpsValue` (which is exactly `1000000`) is correctly matched by the fast-path check, bypassing the unnecessary 1-microsecond spin-wait loop. + ## 2026-06-27: Fix Voicelines on Linux and Disable Flatpak DXVK HUD - **Audio Backend Sample Counts**: Fixed an issue where `OpenALAudioManager` and `MiniAudioManager` were incorrectly returning `0` for available 2D/3D samples, which caused the engine to aggressively cull UI sounds (including voicelines). Updated `getNumAvailable2DSamples()` and `getNumAvailable3DSamples()` to return the correct calculation based on `m_playingSounds` and the configured maximum sample limits. diff --git a/docs/ETC/archive/PHASE01_LINUX_GRAPHICS.md b/docs/ETC/archive/PHASE01_LINUX_GRAPHICS.md index 0979cab5c3f..4ac84174712 100644 --- a/docs/ETC/archive/PHASE01_LINUX_GRAPHICS.md +++ b/docs/ETC/archive/PHASE01_LINUX_GRAPHICS.md @@ -131,7 +131,7 @@ Phase 1 is **COMPLETE** when: ## Reference Materials -- fighter19 DXVK integration: `references/fighter19-dxvk-port/` +- fighter19 DXVK integration: `references/old-refs/fighter19-dxvk-port/` - Phase 0 analysis: `docs/WORKDIR/support/phase0-fighter19-analysis.md` - Platform abstraction design: `docs/WORKDIR/support/phase0-platform-abstraction.md` diff --git a/docs/WORKDIR/archive/phase0-analysis/phase0-fighter19-analysis.md b/docs/WORKDIR/archive/phase0-analysis/phase0-fighter19-analysis.md index 5c0a911671a..4e8b77bd473 100644 --- a/docs/WORKDIR/archive/phase0-analysis/phase0-fighter19-analysis.md +++ b/docs/WORKDIR/archive/phase0-analysis/phase0-fighter19-analysis.md @@ -2,7 +2,7 @@ **Created**: 2026-02-07 **Status**: In Progress -**Reference**: `references/fighter19-dxvk-port/` +**Reference**: `references/old-refs/fighter19-dxvk-port/` ## Objective diff --git a/docs/WORKDIR/archive/phase0-analysis/phase0-fighter19-complete-analysis.md b/docs/WORKDIR/archive/phase0-analysis/phase0-fighter19-complete-analysis.md index 9ede0984075..61bd51d03d0 100644 --- a/docs/WORKDIR/archive/phase0-analysis/phase0-fighter19-complete-analysis.md +++ b/docs/WORKDIR/archive/phase0-analysis/phase0-fighter19-complete-analysis.md @@ -2,7 +2,7 @@ **Created**: 2026-02-08 **Status**: Complete Investigation -**Reference**: `references/fighter19-dxvk-port/` +**Reference**: `references/old-refs/fighter19-dxvk-port/` --- @@ -384,7 +384,7 @@ resources/ ← Build resources ## References -- fighter19 repo: `references/fighter19-dxvk-port/` +- fighter19 repo: `references/old-refs/fighter19-dxvk-port/` - Current analysis: This document - Phase planning: `docs/WORKDIR/phases/` - Build references: `cmake/`, `CMakePresets.json` diff --git a/docs/WORKDIR/archive/phase0-analysis/phase0-jmarshall-analysis.md b/docs/WORKDIR/archive/phase0-analysis/phase0-jmarshall-analysis.md index 93785711f7a..6df32fd9c9f 100644 --- a/docs/WORKDIR/archive/phase0-analysis/phase0-jmarshall-analysis.md +++ b/docs/WORKDIR/archive/phase0-analysis/phase0-jmarshall-analysis.md @@ -2,7 +2,7 @@ **Created**: 2026-02-07 **Status**: In Progress -**Reference**: `references/jmarshall-win64-modern/` +**Reference**: `references/old-refs/jmarshall-win64-modern/` ## Objective diff --git a/docs/WORKDIR/archive/phase0-analysis/phase0-jmarshall-openal-details.md b/docs/WORKDIR/archive/phase0-analysis/phase0-jmarshall-openal-details.md index dfcba351703..cefa5420171 100644 --- a/docs/WORKDIR/archive/phase0-analysis/phase0-jmarshall-openal-details.md +++ b/docs/WORKDIR/archive/phase0-analysis/phase0-jmarshall-openal-details.md @@ -197,7 +197,7 @@ ALuint buffer = OpenALAudioLoader::load("sound.wav"); ### Step 1: Copy OpenAL Implementation ```bash # From jmarshall reference -cp -r references/jmarshall-win64-modern/Code/GameEngineDevice/OpenALAudioDevice/ \ +cp -r references/old-refs/jmarshall-win64-modern/Code/GameEngineDevice/OpenALAudioDevice/ \ Core/GameEngineDevice/Source/ ``` diff --git a/docs/WORKDIR/archive/planning/PHASE_2_AUDIO_PLAN.md b/docs/WORKDIR/archive/planning/PHASE_2_AUDIO_PLAN.md index 9c278af7abc..34a0bd0eff7 100644 --- a/docs/WORKDIR/archive/planning/PHASE_2_AUDIO_PLAN.md +++ b/docs/WORKDIR/archive/planning/PHASE_2_AUDIO_PLAN.md @@ -3,7 +3,7 @@ **Status**: 📋 PLANNING **Priority**: 🔴 HIGH (after Phase 1 graphics) **Est. Duration**: 2-3 sessions -**Reference**: `references/jmarshall-win64-modern/Code/Audio/` (adapt for Zero Hour) +**Reference**: `references/old-refs/jmarshall-win64-modern/Code/Audio/` (adapt for Zero Hour) --- @@ -50,17 +50,17 @@ Platform (Windows: Miles SDK, Linux: OpenAL) ### Key Discovery Points (Generals-only, adapt for Zero Hour!) -**1. File**: `references/jmarshall-win64-modern/Code/Audio/AudioManager.cpp` +**1. File**: `references/old-refs/jmarshall-win64-modern/Code/Audio/AudioManager.cpp` - Audio system initialization flow - How Miles functions map to OpenAL calls - Format conversion (Miles → PCM) -**2. File**: `references/jmarshall-win64-modern/Code/Audio/MusicManager.cpp` +**2. File**: `references/old-refs/jmarshall-win64-modern/Code/Audio/MusicManager.cpp` - Background music management - Audio format detection - Cross-platform stream handling -**3. File**: `references/jmarshall-win64-modern/Code/Audio/OpenALAudioDevice.cpp` +**3. File**: `references/old-refs/jmarshall-win64-modern/Code/Audio/OpenALAudioDevice.cpp` - Complete OpenAL device setup - Buffer/source management - Distance attenuation (3D audio) @@ -304,6 +304,6 @@ public: See also: - [SESSION_38_BUILD_SUCCESS.md](../WORKDIR/support/SESSION_38_BUILD_SUCCESS.md) - Phase 1 technical details - [SIDEQUEST_CD_REMOVAL.md](../WORKDIR/reports/SIDEQUEST_CD_REMOVAL.md) - Portable gameplay context -- `references/jmarshall-win64-modern/Code/Audio/` - Detailed OpenAL implementation +- `references/old-refs/jmarshall-win64-modern/Code/Audio/` - Detailed OpenAL implementation - `docs/DEV_BLOG/2026-02-DIARY.md` - Session history diff --git a/docs/WORKDIR/archive/planning/SETUP_SUMMARY.md b/docs/WORKDIR/archive/planning/SETUP_SUMMARY.md index 026d2cb6c36..f56e7b25297 100644 --- a/docs/WORKDIR/archive/planning/SETUP_SUMMARY.md +++ b/docs/WORKDIR/archive/planning/SETUP_SUMMARY.md @@ -106,7 +106,7 @@ Your mission (should you choose to accept it): 2. **Study fighter19's DXVK Port**: - Query with DeepWiki: `Fighter19/CnC_Generals_Zero_Hour` - - Compare files: `diff -r GeneralsMD/ references/fighter19-dxvk-port/GeneralsMD/` + - Compare files: `diff -r GeneralsMD/ references/old-refs/fighter19-dxvk-port/GeneralsMD/` - Document what applies to Zero Hour - **Important**: fighter19 compiles NATIVE Linux ELF binaries (not Windows PE) - Uses GCC/Clang on Linux (not MinGW) @@ -132,7 +132,7 @@ Your mission (should you choose to accept it): ```bash # Set up git remotes for easy diffing -cd references/fighter19-dxvk-port +cd references/old-refs/fighter19-dxvk-port git remote add upstream https://github.com/Fighter19/CnC_Generals_Zero_Hour.git cd ../jmarshall-win64-modern @@ -194,7 +194,7 @@ docker run --rm -v "$PWD:/work" -w /work ubuntu:22.04 bash -c " grep -r "IDirect3D8" --include="*.cpp" --include="*.h" # Compare with fighter19's changes -diff -r Core/GameEngineDevice/ references/fighter19-dxvk-port/Core/GameEngineDevice/ +diff -r Core/GameEngineDevice/ references/old-refs/fighter19-dxvk-port/Core/GameEngineDevice/ # Open dev blog code docs/DEV_BLOG/2026-02-DIARY.md diff --git a/docs/WORKDIR/archive/planning/upstream-syncs/PLAN-2026-04-28_THESUPERHACKERS_SYNC.md b/docs/WORKDIR/archive/planning/upstream-syncs/PLAN-2026-04-28_THESUPERHACKERS_SYNC.md index bd281457bff..166c4020b9e 100644 --- a/docs/WORKDIR/archive/planning/upstream-syncs/PLAN-2026-04-28_THESUPERHACKERS_SYNC.md +++ b/docs/WORKDIR/archive/planning/upstream-syncs/PLAN-2026-04-28_THESUPERHACKERS_SYNC.md @@ -22,7 +22,7 @@ Perform an upstream sync from `thesuperhackers/main` into branch `thesuperhacker - Local instructions and project strategy documents under `.github/instructions/` - Existing project lessons in `docs/WORKDIR/lessons/2026-04-LESSONS.md` -- Local reference snapshot under `references/thesuperhackers-main/` +- Local reference snapshot under `references/old-refs/thesuperhackers-main/` - Direct upstream file state from `thesuperhackers/main` for each conflicted file ## Conflict Resolution Plan diff --git a/docs/WORKDIR/archive/reports/PHASE1_5_STATUS.md b/docs/WORKDIR/archive/reports/PHASE1_5_STATUS.md index 028dd7c35ba..57da481877e 100644 --- a/docs/WORKDIR/archive/reports/PHASE1_5_STATUS.md +++ b/docs/WORKDIR/archive/reports/PHASE1_5_STATUS.md @@ -265,8 +265,8 @@ m=To_Matrix4x4(render_state.world); // D3DMATRIX → Matrix4x4 **Investigation Required**: ```bash # Check fighter19's approach -grep -r "To_D3DMATRIX\|To_Matrix4x4" references/fighter19-dxvk-port/ -grep -r "Matrix4x4.*D3DMATRIX\|D3DMATRIX.*Matrix4x4" references/fighter19-dxvk-port/ +grep -r "To_D3DMATRIX\|To_Matrix4x4" references/old-refs/fighter19-dxvk-port/ +grep -r "Matrix4x4.*D3DMATRIX\|D3DMATRIX.*Matrix4x4" references/old-refs/fighter19-dxvk-port/ ``` #### Blocker 2: Unterminated Preprocessor Directive @@ -807,7 +807,7 @@ rm -rf build/linux64-deploy/GeneralsMD/Code/Main/ ## References -**Pattern Source**: fighter19 DXVK port (`references/fighter19-dxvk-port/`) +**Pattern Source**: fighter19 DXVK port (`references/old-refs/fighter19-dxvk-port/`) - SDL3 integration patterns - Ring buffer architecture - Factory conditional compilation @@ -817,7 +817,7 @@ rm -rf build/linux64-deploy/GeneralsMD/Code/Main/ - Event translation patterns - Click detection algorithms -**CMake Patterns**: TheSuperHackers upstream (`references/thesuperhackers-main/`) +**CMake Patterns**: TheSuperHackers upstream (`references/old-refs/thesuperhackers-main/`) - Conditional source compilation - Platform detection - Library target configuration diff --git a/docs/WORKDIR/archive/reports/PHASE1_FIGHTER19_DIFF_ANALYSIS.md b/docs/WORKDIR/archive/reports/PHASE1_FIGHTER19_DIFF_ANALYSIS.md index fdcc0d3fb12..30e2fe7ce39 100644 --- a/docs/WORKDIR/archive/reports/PHASE1_FIGHTER19_DIFF_ANALYSIS.md +++ b/docs/WORKDIR/archive/reports/PHASE1_FIGHTER19_DIFF_ANALYSIS.md @@ -2,7 +2,7 @@ **Status**: Complete **Goal**: Diagnose magenta screen (0xFF00FF) on main menu — menu text renders but no 3D terrain/objects appear -**Scope**: 8 investigation areas comparing `references/fighter19-dxvk-port/` (working Linux build) with our codebase +**Scope**: 8 investigation areas comparing `references/old-refs/fighter19-dxvk-port/` (working Linux build) with our codebase --- diff --git a/docs/WORKDIR/archive/reports/SESSION_24_REPORT.md b/docs/WORKDIR/archive/reports/SESSION_24_REPORT.md index c17e9f1e51d..6ce5e635b91 100644 --- a/docs/WORKDIR/archive/reports/SESSION_24_REPORT.md +++ b/docs/WORKDIR/archive/reports/SESSION_24_REPORT.md @@ -423,7 +423,7 @@ grep "^\[" logs/session25_fix_.log | tail -5 - **Missing:** `target_compile_definitions()` in CMake (NOW FIXED) ### Key Reference Files -- **GLM Pattern:** `references/fighter19-dxvk-port/GeneralsMD/Code/GameEngine/Source/Common/Bezier/BezierSegment.cpp` +- **GLM Pattern:** `references/old-refs/fighter19-dxvk-port/GeneralsMD/Code/GameEngine/Source/Common/Bezier/BezierSegment.cpp` - **POSIX Compat:** `GeneralsMD/Code/CompatLib/Include/file_compat.h` (already in project) - **Time Compat:** `GeneralsMD/Code/CompatLib/Include/time_compat.h` (already in project) diff --git a/docs/WORKDIR/archive/reports/SESSION_26_FINAL_STATUS.md b/docs/WORKDIR/archive/reports/SESSION_26_FINAL_STATUS.md index 950d51e69ef..e2c2ea254c6 100644 --- a/docs/WORKDIR/archive/reports/SESSION_26_FINAL_STATUS.md +++ b/docs/WORKDIR/archive/reports/SESSION_26_FINAL_STATUS.md @@ -301,11 +301,11 @@ GeneralsMD/Code/ ## Reference Materials ### Code References -- **fighter19 DXVK port**: `references/fighter19-dxvk-port/` +- **fighter19 DXVK port**: `references/old-refs/fighter19-dxvk-port/` - Graphics layer (DXVK), SDL3 integration - MinGW build patterns -- **jmarshall modern port**: `references/jmarshall-win64-modern/` +- **jmarshall modern port**: `references/old-refs/jmarshall-win64-modern/` - OpenAL audio implementation - Modern C++ patterns diff --git a/docs/WORKDIR/archive/sessions/SESSION20_HANDOFF.md b/docs/WORKDIR/archive/sessions/SESSION20_HANDOFF.md index e30dfcf80a1..e9e7c6c44be 100644 --- a/docs/WORKDIR/archive/sessions/SESSION20_HANDOFF.md +++ b/docs/WORKDIR/archive/sessions/SESSION20_HANDOFF.md @@ -180,7 +180,7 @@ grep -n "#include.*windows_compat" Core/Libraries/Source/WWVegas/WW3D2/agg_def.c **Se GDI errors persist**: ```bash # Check if BITMAP struct size matches Windows -# Fighter19 reference: references/fighter19-dxvk-port/GeneralsMD/Code/CompatLib/Include/gdi_compat.h +# Fighter19 reference: references/old-refs/fighter19-dxvk-port/GeneralsMD/Code/CompatLib/Include/gdi_compat.h ``` **Se file API errors persist**: @@ -402,23 +402,23 @@ void GameObject::Update() { ## 🔗 Key References ### Fighter19 DXVK Port (PRIMARY) -- **Path**: `references/fighter19-dxvk-port/` +- **Path**: `references/old-refs/fighter19-dxvk-port/` - **Use**: Graphics (DXVK), SDL3, POSIX compat, MinGW builds - **Coverage**: Generals + Zero Hour (full) ### jmarshall Modern Port (SECONDARY) -- **Path**: `references/jmarshall-win64-modern/` +- **Path**: `references/old-refs/jmarshall-win64-modern/` - **Use**: OpenAL audio, INI parser fixes, 64-bit compat - **Coverage**: Generals ONLY (no Zero Hour) ### DeepWiki Repos ```bash # Fighter19 patterns: -references/fighter19-dxvk-port/ (local copy) +references/old-refs/fighter19-dxvk-port/ (local copy) Fighter19/CnC_Generals_Zero_Hour (deepwiki) # jmarshall patterns: -references/jmarshall-win64-modern/ (local copy) +references/old-refs/jmarshall-win64-modern/ (local copy) jmarshall2323/CnC_Generals_Zero_Hour (deepwiki) ``` diff --git a/docs/WORKDIR/archive/sessions/SESSION21_HANDOFF.md b/docs/WORKDIR/archive/sessions/SESSION21_HANDOFF.md index b8749878478..cca55afc23f 100644 --- a/docs/WORKDIR/archive/sessions/SESSION21_HANDOFF.md +++ b/docs/WORKDIR/archive/sessions/SESSION21_HANDOFF.md @@ -323,9 +323,9 @@ Don't try to fully integrate SDL3 yet. Windowing comes after Core library compil ### Files to Consult ``` -references/fighter19-dxvk-port/GeneralsMD/Code/CompatLib/CMakeLists.txt -references/fighter19-dxvk-port/Core/Libraries/Source/WWVegas/WW3D2/agg_def.cpp -references/jmarshall-win64-modern/Code/Audio/ # For future OpenAL work +references/old-refs/fighter19-dxvk-port/GeneralsMD/Code/CompatLib/CMakeLists.txt +references/old-refs/fighter19-dxvk-port/Core/Libraries/Source/WWVegas/WW3D2/agg_def.cpp +references/old-refs/jmarshall-win64-modern/Code/Audio/ # For future OpenAL work ``` --- diff --git a/docs/WORKDIR/archive/sessions/SESSION22_HANDOFF.md b/docs/WORKDIR/archive/sessions/SESSION22_HANDOFF.md index 44693a6184f..ff370ade558 100644 --- a/docs/WORKDIR/archive/sessions/SESSION22_HANDOFF.md +++ b/docs/WORKDIR/archive/sessions/SESSION22_HANDOFF.md @@ -118,8 +118,8 @@ Errors: FTP.cpp pre-existing portability issues (out of scope) **Option 1: Check fighter19 approach** ```bash # Does fighter19 compile FTP.cpp? -find references/fighter19-dxvk-port -name "FTP.cpp" -grep -r "WWDownload" references/fighter19-dxvk-port/*/CMakeLists.txt +find references/old-refs/fighter19-dxvk-port -name "FTP.cpp" +grep -r "WWDownload" references/old-refs/fighter19-dxvk-port/*/CMakeLists.txt # If NOT found: fighter19 excludes this file (multiplayer download not needed) # Action: Remove from CMakeLists.txt or add #ifdef guards @@ -194,20 +194,20 @@ grep -rn "FTP_TRYING\|FTPSTAT_" Core/Libraries/Source/WWVegas/WWDownload/ ## 📚 REFERENCE MATERIALS ### fighter19 DXVK Port (PRIMARY GRAPHICS REFERENCE) -- **Location**: `references/fighter19-dxvk-port/` +- **Location**: `references/old-refs/fighter19-dxvk-port/` - **Deepwiki**: `Fighter19/CnC_Generals_Zero_Hour` - **FreeType implementation**: `GeneralsMD/Code/Libraries/Source/WWVegas/WW3D2/Render2DSentence.cpp` (lines ~1500-1900) - **Key patterns**: Buffer management, pixel format, font metrics ### jmarshall Modern Port (AUDIO REFERENCE) -- **Location**: `references/jmarshall-win64-modern/` +- **Location**: `references/old-refs/jmarshall-win64-modern/` - **Deepwiki**: `jmarshall2323/CnC_Generals_Zero_Hour` - **Coverage**: Generals base game ONLY (NOT Zero Hour) - **OpenAL implementation**: `Code/Audio/` directory - **Note**: Must adapt for Zero Hour expansion features ### TheSuperHackers Main (UPSTREAM BASELINE) -- **Location**: `references/thesuperhackers-main/` +- **Location**: `references/old-refs/thesuperhackers-main/` - **Deepwiki**: `TheSuperHackers/GeneralsGameCode` - **Purpose**: Reference copy for merge conflict resolution - **Strategy**: Merge daily, keep our platform changes, their logic changes @@ -260,8 +260,8 @@ git push origin linux-attempt **Find fighter19 approach to a file**: ```bash -find references/fighter19-dxvk-port -name "filename.cpp" -grep -rn "pattern" references/fighter19-dxvk-port/ +find references/old-refs/fighter19-dxvk-port -name "filename.cpp" +grep -rn "pattern" references/old-refs/fighter19-dxvk-port/ ``` **Check if file is in build**: @@ -271,7 +271,7 @@ grep "filename.cpp" */CMakeLists.txt **/**/CMakeLists.txt **Compare implementations**: ```bash -diff -u Core/path/file.cpp references/fighter19-dxvk-port/Core/path/file.cpp +diff -u Core/path/file.cpp references/old-refs/fighter19-dxvk-port/Core/path/file.cpp ``` --- @@ -397,7 +397,7 @@ grep "SAGE_USE_FREETYPE" build.log | grep "c++" 3. **Research FTP.cpp**: ```bash # Check fighter19 approach - find references/fighter19-dxvk-port -name "FTP.cpp" + find references/old-refs/fighter19-dxvk-port -name "FTP.cpp" # Read class definition cat Core/Libraries/Source/WWVegas/WWDownload/ftp.h | less diff --git a/docs/WORKDIR/archive/sessions/SESSION23_HANDOFF.md b/docs/WORKDIR/archive/sessions/SESSION23_HANDOFF.md index 1759e77a2f4..f84d0305f4c 100644 --- a/docs/WORKDIR/archive/sessions/SESSION23_HANDOFF.md +++ b/docs/WORKDIR/archive/sessions/SESSION23_HANDOFF.md @@ -297,13 +297,13 @@ char* _strupr(char* str) { /* implementation */ } ## 📚 REFERENCE MATERIALS ### fighter19 DXVK Port -- **Location**: `references/fighter19-dxvk-port/` +- **Location**: `references/old-refs/fighter19-dxvk-port/` - **Deepwiki**: `Fighter19/CnC_Generals_Zero_Hour` - **Key insight**: fighter19 does NOT have WWDownload directory (excluded in their port) - **Lesson**: We chose different strategy (keep WWDownload for LAN support) ### jmarshall Modern Port -- **Location**: `references/jmarshall-win64-modern/` +- **Location**: `references/old-refs/jmarshall-win64-modern/` - **Deepwiki**: `jmarshall2323/CnC_Generals_Zero_Hour` - **Coverage**: Generals base game ONLY (NOT Zero Hour) - **Note**: May have different WWDownload handling (investigate if needed) @@ -362,13 +362,13 @@ git push origin linux-attempt **Find file in references**: ```bash -find references/fighter19-dxvk-port -name "filename.cpp" -find references/jmarshall-win64-modern -name "filename.cpp" +find references/old-refs/fighter19-dxvk-port -name "filename.cpp" +find references/old-refs/jmarshall-win64-modern -name "filename.cpp" ``` **Compare implementations**: ```bash -diff -u Core/path/file.cpp references/fighter19-dxvk-port/Core/path/file.cpp +diff -u Core/path/file.cpp references/old-refs/fighter19-dxvk-port/Core/path/file.cpp ``` **Check if macro defined**: diff --git a/docs/WORKDIR/archive/sessions/SESSION_19_QUICKSTART.md b/docs/WORKDIR/archive/sessions/SESSION_19_QUICKSTART.md index 597e39a6d35..44907bec488 100644 --- a/docs/WORKDIR/archive/sessions/SESSION_19_QUICKSTART.md +++ b/docs/WORKDIR/archive/sessions/SESSION_19_QUICKSTART.md @@ -55,7 +55,7 @@ dx8wrapper.h:1304: error: 'To_Matrix4x4' was not declared **Investigation Steps**: ```bash # 1. Check fighter19's approach -cd references/fighter19-dxvk-port/ +cd references/old-refs/fighter19-dxvk-port/ grep -r "To_D3DMATRIX\|To_Matrix4x4" GeneralsMD/Code/Libraries/Source/WWVegas/WW3D2/ # 2. Check for alternative names @@ -129,7 +129,7 @@ grep -n "^#endif" GeneralsMD/Code/Libraries/Source/WWVegas/WW3D2/dx8fvf.h # Compare with fighter19 diff -u \ - references/fighter19-dxvk-port/GeneralsMD/Code/Libraries/Source/WWVegas/WW3D2/dx8fvf.h \ + references/old-refs/fighter19-dxvk-port/GeneralsMD/Code/Libraries/Source/WWVegas/WW3D2/dx8fvf.h \ GeneralsMD/Code/Libraries/Source/WWVegas/WW3D2/dx8fvf.h ``` @@ -158,7 +158,7 @@ profile_funclevel.h:154: error: expected ';' at end of member declaration grep -rn "_int64" Core/Libraries/Source/profile/ # Check fighter19's approach -grep -rn "_int64\|uint64_t" references/fighter19-dxvk-port/Core/Libraries/Source/profile/ +grep -rn "_int64\|uint64_t" references/old-refs/fighter19-dxvk-port/Core/Libraries/Source/profile/ ``` **Fix Pattern**: @@ -215,9 +215,9 @@ Core/Libraries/Source/profile/profile_funclevel.h ### Fighter19 Reference Paths ``` -references/fighter19-dxvk-port/GeneralsMD/Code/Libraries/Source/WWVegas/WW3D2/ -references/fighter19-dxvk-port/GeneralsMD/Code/CompatLib/ -references/fighter19-dxvk-port/Core/Libraries/Source/profile/ +references/old-refs/fighter19-dxvk-port/GeneralsMD/Code/Libraries/Source/WWVegas/WW3D2/ +references/old-refs/fighter19-dxvk-port/GeneralsMD/Code/CompatLib/ +references/old-refs/fighter19-dxvk-port/Core/Libraries/Source/profile/ ``` ### Build Logs diff --git a/docs/WORKDIR/archive/spikes/FIGHTER19_SDL3_DEEP_ANALYSIS.md b/docs/WORKDIR/archive/spikes/FIGHTER19_SDL3_DEEP_ANALYSIS.md index 7e8408554ae..6a626c59e65 100644 --- a/docs/WORKDIR/archive/spikes/FIGHTER19_SDL3_DEEP_ANALYSIS.md +++ b/docs/WORKDIR/archive/spikes/FIGHTER19_SDL3_DEEP_ANALYSIS.md @@ -451,9 +451,9 @@ Bool getCapsState() { ## 📌 References -- **Fighter19 SDL3Mouse**: `references/fighter19-dxvk-port/GeneralsMD/Code/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Mouse.cpp` -- **Fighter19 SDL3Keyboard**: `references/fighter19-dxvk-port/GeneralsMD/Code/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Keyboard.cpp` -- **Fighter19 SDL3GameEngine**: `references/fighter19-dxvk-port/GeneralsMD/Code/GameEngineDevice/Include/SDL3Device/Common/SDL3GameEngine.h` +- **Fighter19 SDL3Mouse**: `references/old-refs/fighter19-dxvk-port/GeneralsMD/Code/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Mouse.cpp` +- **Fighter19 SDL3Keyboard**: `references/old-refs/fighter19-dxvk-port/GeneralsMD/Code/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Keyboard.cpp` +- **Fighter19 SDL3GameEngine**: `references/old-refs/fighter19-dxvk-port/GeneralsMD/Code/GameEngineDevice/Include/SDL3Device/Common/SDL3GameEngine.h` - **Fighter19 Key Mapping**: Lines 77-131 in SDL3Keyboard.cpp (ConvertSDLKey function) --- diff --git a/docs/WORKDIR/archive/spikes/SESSION_36_UNDEFINED_REFERENCES.md b/docs/WORKDIR/archive/spikes/SESSION_36_UNDEFINED_REFERENCES.md index 6e78909e0d5..12b343edd41 100644 --- a/docs/WORKDIR/archive/spikes/SESSION_36_UNDEFINED_REFERENCES.md +++ b/docs/WORKDIR/archive/spikes/SESSION_36_UNDEFINED_REFERENCES.md @@ -62,8 +62,8 @@ void parseCommandLine(const CommandLineParam* params, int argc, char** argv); **Recommended**: Option A (minimal change, maintains Windows compatibility) ### References to Check -- `references/fighter19-dxvk-port/GeneralsMD/Code/Main/` - SDL3Main.cpp globals -- `references/jmarshall-win64-modern/Code/Main/` - Modern refactor approach +- `references/old-refs/fighter19-dxvk-port/GeneralsMD/Code/Main/` - SDL3Main.cpp globals +- `references/old-refs/jmarshall-win64-modern/Code/Main/` - Modern refactor approach --- @@ -124,8 +124,8 @@ SDL_Window* g_mainWindow = nullptr; **Recommended**: Option A (matches fighter19 working implementation) ### References to Check -- `references/fighter19-dxvk-port/GeneralsMD/Code/Main/SDL3Main.cpp` - Window creation -- `references/fighter19-dxvk-port/GeneralsMD/Code/GameEngineDevice/Source/W3DDisplay.cpp` - Usage patterns +- `references/old-refs/fighter19-dxvk-port/GeneralsMD/Code/Main/SDL3Main.cpp` - Window creation +- `references/old-refs/fighter19-dxvk-port/GeneralsMD/Code/GameEngineDevice/Source/W3DDisplay.cpp` - Usage patterns --- @@ -236,7 +236,7 @@ void OSDisplayWarningBox(AsciiString title, AsciiString msg, unsigned int, unsig **Recommended**: Start with stubs, upgrade later. ### References to Check -- `references/fighter19-dxvk-port/` - Check if implemented or stubbed +- `references/old-refs/fighter19-dxvk-port/` - Check if implemented or stubbed - SDL3 docs: `SDL_SetCursor()`, SDL3 has no native message box --- @@ -400,7 +400,7 @@ FileSystem* SDL3GameEngine::createLocalFileSystem() { ### References to Check - `Core/GameEngine/Source/Common/GameEngine.cpp` - Base implementations - `GeneralsMD/Code/GameEngine/Include/Common/GameEngine.h` - Virtual declarations -- `references/fighter19-dxvk-port/` - How fighter19 handled SDL3GameEngine +- `references/old-refs/fighter19-dxvk-port/` - How fighter19 handled SDL3GameEngine --- @@ -594,20 +594,20 @@ bool GetStringFromRegistry(std::string, std::string, std::string&); ### fighter19 DXVK Port (PRIMARY for P0-P1) ```bash -references/fighter19-dxvk-port/GeneralsMD/Code/Main/SDL3Main.cpp -references/fighter19-dxvk-port/GeneralsMD/Code/GameEngineDevice/Source/SDL3GameEngine.cpp -references/fighter19-dxvk-port/GeneralsMD/Code/GameEngine/Source/GameClient/GameText.cpp +references/old-refs/fighter19-dxvk-port/GeneralsMD/Code/Main/SDL3Main.cpp +references/old-refs/fighter19-dxvk-port/GeneralsMD/Code/GameEngineDevice/Source/SDL3GameEngine.cpp +references/old-refs/fighter19-dxvk-port/GeneralsMD/Code/GameEngine/Source/GameClient/GameText.cpp ``` ### jmarshall Win64 Port (SECONDARY for patterns) ```bash -references/jmarshall-win64-modern/Code/Main/ -references/jmarshall-win64-modern/Code/Audio/ # OpenAL patterns +references/old-refs/jmarshall-win64-modern/Code/Main/ +references/old-refs/jmarshall-win64-modern/Code/Audio/ # OpenAL patterns ``` ### TheSuperHackers Upstream (BASE TRUTH) ```bash -references/thesuperhackers-main/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp +references/old-refs/thesuperhackers-main/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp ``` --- diff --git a/docs/WORKDIR/audit/RETAIL_COMPATIBILITY.md b/docs/WORKDIR/audit/RETAIL_COMPATIBILITY.md new file mode 100644 index 00000000000..751eee80cca --- /dev/null +++ b/docs/WORKDIR/audit/RETAIL_COMPATIBILITY.md @@ -0,0 +1,68 @@ +# Retail Compatibility Macros Analysis + +This document provides an overview of the various `RETAIL_COMPATIBLE_*` macros defined in `Core/GameEngine/Include/Common/GameDefines.h`. It explains what each macro does and outlines the advantages and disadvantages of setting them to `0` (ignoring retail compatibility). + +Since the primary goal for this fork is *not* retail compatibility, it is highly recommended to disable these macros where applicable to benefit from the numerous engine fixes. + +--- + +## 1. `RETAIL_COMPATIBLE_CRC` +**Definition:** Game is expected to be CRC compatible with retail Generals 1.08, Zero Hour 1.04. + +* **What it does:** The original game relies on a deterministic frame-by-frame simulation (CRC checks) to ensure all players in a multiplayer match (and replays) stay synchronized. Many legacy bugs (like uninitialized variables, floating-point inconsistencies, and logic errors in particles/physics) were left untouched because fixing them would change the CRC and cause "Sync Errors" when playing against players using the original CD/Origin release. +* **Advantage of disabling (`0`):** Enables hundreds of bug fixes across the engine. Fixes undefined behaviors, logic errors, and makes the simulation much more robust. +* **Disadvantage of disabling:** Breaks multiplayer cross-play and replay compatibility with the original retail game. + +--- + +## 2. `RETAIL_COMPATIBLE_XFER_SAVE` +**Definition:** Game is expected to be Xfer Save compatible with retail Generals 1.08, Zero Hour 1.04. + +* **What it does:** Governs the serialization formats used for saving the game and transferring assets (e.g., when a player disconnects and their units are transferred to an ally). +* **Advantage of disabling (`0`):** Allows fixing struct alignments, memory layouts, and data serialization bugs. This improves memory safety and prevents crashes related to corrupted save states or network transfers. +* **Disadvantage of disabling:** Old retail save files will no longer load correctly, and transferring units with retail clients will crash or desync. + +--- + +## 3. `RETAIL_COMPATIBLE_PATHFINDING` +**Definition:** Toggles between the retail-compatible pathfinding with its fallback mechanics and the pure fixed pathfinding mode. + +* **What it does:** The original pathfinding system had "fallback" crutches that would trigger when units got stuck. These fallbacks were often inefficient and led to the infamous "dancing tanks" issue. +* **Advantage of disabling (`0`):** Activates a purely fixed pathfinding mode without the legacy crutches. Units move much more intelligently, reliably, and get stuck far less often. +* **Disadvantage of disabling:** Pathfinding directly affects unit movement, so disabling this will completely break replay synchronization for any retail replay. + +--- + +## 4. `RETAIL_COMPATIBLE_PATHFINDING_ALLOCATION` +**Definition:** Toggles between the retail-compatible pathfinding memory allocation and the new static allocated data mode. + +* **What it does:** Retail pathfinding constantly allocates and deallocates small chunks of memory on the heap every frame. This causes severe memory fragmentation, CPU overhead, and eventual memory leaks over long games. +* **Advantage of disabling (`0`):** Switches the pathfinder to use statically allocated memory pools. This provides a massive performance boost during heavy unit movement and eliminates related memory fragmentation/leaks. +* **Disadvantage of disabling:** Minor determinism changes in how paths are resolved could theoretically affect sync, but practically, it just breaks retail replay perfection. + +--- + +## 5. `RETAIL_COMPATIBLE_CIRCLE_FILL_ALGORITHM` +**Definition:** Uses the original circle fill algorithm, which is more efficient but less accurate. + +* **What it does:** Determines how the game calculates circular areas (used for shroud clearing, sight ranges, and area-of-effect damage). +* **Advantage of disabling (`0`):** Uses a more accurate mathematical circle algorithm. Fixes bugs where units on the very edge of an explosion wouldn't take damage, or sight ranges weren't perfectly round. +* **Disadvantage of disabling:** Slightly heavier on the CPU (negligible on modern hardware) and changes AoE/Sight determinism, breaking retail replays. + +--- + +## 6. `RETAIL_COMPATIBLE_NETWORKING` +**Definition:** Disables non-retail fixes in the networking, such as putting more data per UDP packet. + +* **What it does:** The retail netcode has a critical bug where command IDs (16-bit integers) overflow after 65,535 commands, causing the game to discard new packets and disconnect players in long matches. It also limits packet payload to 476 bytes. +* **Advantage of disabling (`0`):** Fixes the command ID overflow bug, completely eliminating the "long game disconnect" issue. It also increases the maximum UDP payload to 1100 bytes, which reduces network fragmentation and CPU overhead during intense battles. +* **Disadvantage of disabling:** Packets become incompatible with retail clients (they will drop the larger packets or fail to parse them). + +--- + +## 7. `RETAIL_COMPATIBLE_AIGROUP` +**Definition:** AIGroup logic is expected to be CRC compatible with retail. Guards against fixing memory safety issues that would alter CRC. + +* **What it does:** The `AIGroup` logic in the original game is notoriously unstable, riddled with use-after-free errors, double-frees, and memory leaks. Fixing these safely alters how AI decisions are processed. +* **Advantage of disabling (`0`):** Allows the engine to use robust memory-safety fixes (often described as "massive hacks" in the codebase) to prevent the AI from crashing the game. Vastly improves stability in Skirmish and Campaign modes. +* **Disadvantage of disabling:** AI behavior changes slightly, breaking synchronization if a human plays against a retail client with AI, or when watching retail AI replays. diff --git a/docs/WORKDIR/lessons/2026-02-LESSONS.md b/docs/WORKDIR/lessons/2026-02-LESSONS.md index 031b9e2fe37..cfc9d6a07b6 100644 --- a/docs/WORKDIR/lessons/2026-02-LESSONS.md +++ b/docs/WORKDIR/lessons/2026-02-LESSONS.md @@ -827,7 +827,7 @@ This is the same category as LESSON-50 (`long` in TGA structs) and LESSON-45 (`v **Applies To**: `SDL3Mouse::translateEvent()` — all three event types (motion, button, wheel). For wheel events use `event.wheel.mouse_x/mouse_y`, not a separate `SDL_GetMouseState()` call. -**Reference**: `references/fighter19-dxvk-port/GeneralsMD/Code/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Mouse.cpp` → `scaleMouseCoordinates()` +**Reference**: `references/old-refs/fighter19-dxvk-port/GeneralsMD/Code/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Mouse.cpp` → `scaleMouseCoordinates()` --- diff --git a/docs/WORKDIR/lessons/2026-04-LESSONS.md b/docs/WORKDIR/lessons/2026-04-LESSONS.md index 6493717ca2d..b1cd91fd6ff 100644 --- a/docs/WORKDIR/lessons/2026-04-LESSONS.md +++ b/docs/WORKDIR/lessons/2026-04-LESSONS.md @@ -140,6 +140,43 @@ - `flatpak-builder --user --build-only` now advances to SDK update/install phase (instead of failing on missing system remote refs). - Prevention: For Flatpak work, never copy `.so` files from host system paths into app runtime. Build inside SDK and ship only artifacts produced by the manifest modules. +## Session 2026-04-12 - LAN lobby visibility must be traced at render and prune points, not only at announce receipt + +- Problem: Logs could show `handleGameAnnounce` success while the user still reported that the LAN lobby list did not visibly retain the remote game. +- Code finding: The LAN lobby callback does not apply extra compatibility filtering; it forwards `m_games` directly into `LANDisplayGameList`. +- Remaining suspects: a parsed game can still fail to appear usefully if its row content is unexpected, or it can disappear shortly after due to `lastHeard`-based pruning. +- Process improvement: For LAN lobby regressions, instrument both row rendering and timeout-driven removal in addition to message receive/parse logs. + +## Session 2026-04-11 - LAN auto-IP and global broadcast can hide hosts cross-platform + +- Problem: Linux and macOS builds failed to discover each other in the LAN lobby, and same-platform behavior remained uncertain without multiple test machines per OS. +- Code finding: LAN discovery sends to a fixed global broadcast target (`INADDR_BROADCAST` / `255.255.255.255`) and LAN menu startup can auto-pick the first enumerated non-loopback IP. +- Risk: On macOS/Linux systems with VPN/Docker/virtual NICs, first-IP selection can bind sockets to a non-game interface, and global broadcast may not reach peers as expected for the active subnet. +- Process improvement: For LAN regressions, always log selected bind IP, broadcast destination, bind/send error codes, and incoming source addresses before judging protocol-level compatibility. +- Prevention: Prefer interface-aware LAN discovery (default-route NIC + subnet broadcast calculation) and keep manual IP override visible/easy in options. + +## Session 2026-04-12 - LAN join accept must stay unicast to avoid host-only false positives + +- Problem: In direct-connect tests, the host sometimes showed the remote player in the game lobby while the joining machine still timed out and never entered the lobby. +- Evidence: Fresh `[LAN86]` logs showed the host receiving the join request and then emitting `MSG_JOIN_ACCEPT` as broadcast (`255.255.255.255`) instead of sending it back directly to the joining client IP. +- Root cause: `LANAPI::handleRequestJoin()` zeroed the response target after adding the player locally, so `sendMessage()` fell through to the broadcast path for the join-accept packet. +- Fix: Keep the requester IP as the response target for join accept/deny packets; only local game-state updates should be host-local. +- Prevention: For request/response handshake packets (`REQUEST_JOIN`, `JOIN_ACCEPT`, `JOIN_DENY`), never repurpose the destination selection based on local slot mutation; log both sender and final response target during debugging. + +## Session 2026-04-12 - In-game LAN control packets need directed delivery on mixed OS networks + +- Problem: Even after direct-connect join success, clients could miss host-driven updates (game options, game start, and host leave effects), producing partial desync UX. +- Evidence: Join handshake traffic succeeded with unicast, but follow-up control behavior matched packet classes still emitted as broadcast without explicit destination. +- Fix: Prefer directed fan-out to known human slots for in-game control/state packets, keeping broadcast only as fallback when no slot target exists. +- Prevention: In cross-platform LAN paths, treat broadcast as discovery-only by default; use explicit per-slot delivery for game/session control events. + +## Session 2026-04-12 - LAN discovery should prefer subnet broadcast over global broadcast + +- Problem: LAN lobby discovery remained unreliable across macOS/Linux even when direct-connect worked. +- Evidence: Discovery still depended on global broadcast `255.255.255.255`, which may not be forwarded/handled consistently on mixed-network setups. +- Fix: In POSIX builds, collect broadcast addresses from active IPv4 interfaces matching the selected local LAN IP and send broadcast packets to those subnet addresses first; keep global broadcast as fallback. +- Prevention: For multi-platform LAN discovery, avoid single global broadcast as the only path; use interface-scoped subnet broadcast to reduce network-policy sensitivity. + ## Session 2026-04-09 - libxcb Flatpak PoC needs newer source libs, not host baseline copy ## Session 2026-04-11 - 8-player macOS crash points to AI guard-state null dereference path diff --git a/docs/WORKDIR/phases/PHASE02_LINUX_AUDIO.md b/docs/WORKDIR/phases/PHASE02_LINUX_AUDIO.md index d480f15bf96..1c4b8a87de4 100644 --- a/docs/WORKDIR/phases/PHASE02_LINUX_AUDIO.md +++ b/docs/WORKDIR/phases/PHASE02_LINUX_AUDIO.md @@ -84,13 +84,13 @@ Wire OpenAL backend to game audio system, implement audio event tracking, music - [ ] Wire `cmake/audio.cmake` into main `CMakeLists.txt` under `SAGE_USE_OPENAL` guard ### Step 2: Port `OpenALAudioFileCache` (FFmpeg decoder) -- [ ] Port `references/fighter19-dxvk-port/GeneralsMD/Code/GameEngineDevice/Source/OpenALAudioDevice/OpenALAudioCache.cpp` +- [ ] Port `references/old-refs/fighter19-dxvk-port/GeneralsMD/Code/GameEngineDevice/Source/OpenALAudioDevice/OpenALAudioCache.cpp` - [ ] Port matching header - [ ] Locate `FFmpegFile` wrapper in fighter19 and port it - [ ] Test: load one `.wav` or `.mp3` file and print duration ### Step 3: Port `OpenALAudioStream` (music streaming) -- [ ] Port `references/fighter19-dxvk-port/GeneralsMD/Code/GameEngineDevice/Source/OpenALAudioDevice/OpenALAudioStream.cpp` +- [ ] Port `references/old-refs/fighter19-dxvk-port/GeneralsMD/Code/GameEngineDevice/Source/OpenALAudioDevice/OpenALAudioStream.cpp` - [ ] Port matching header - [ ] Minimal changes expected (91 lines, no game-specific logic) @@ -113,7 +113,7 @@ Wire OpenAL backend to game audio system, implement audio event tracking, music **jmarshall Reference Analysis**: -- [ ] Study `references/jmarshall-win64-modern/Code/Audio/` structure +- [ ] Study `references/old-refs/jmarshall-win64-modern/Code/Audio/` structure - [ ] Document MusicManager implementation (streaming patterns) - [ ] Document AudioManager event system (handle tracking) - [ ] Document buffer lifecycle (creation, queueing, cleanup) @@ -380,7 +380,7 @@ Phase 2 is **COMPLETE** when: ## References ### Primary -- **jmarshall OpenAL implementation**: `references/jmarshall-win64-modern/Code/Audio/` +- **jmarshall OpenAL implementation**: `references/old-refs/jmarshall-win64-modern/Code/Audio/` - **OpenAL Programming Guide**: `docs/ETC/` (download if needed) - **Miles Sound System docs**: `docs/ETC/` (reverse-engineer if no docs available) diff --git a/docs/WORKDIR/phases/PHASE03_VIDEO_PLAYBACK.md b/docs/WORKDIR/phases/PHASE03_VIDEO_PLAYBACK.md index 73ecb580cd3..f7150af7369 100644 --- a/docs/WORKDIR/phases/PHASE03_VIDEO_PLAYBACK.md +++ b/docs/WORKDIR/phases/PHASE03_VIDEO_PLAYBACK.md @@ -351,8 +351,8 @@ Phase 3 is **COMPLETE** when the following is achieved: - [ ] Document video triggers (INI files? Hardcoded?) ### Reference Implementations -- fighter19: `references/fighter19-dxvk-port/` (check video handling) -- jmarshall: `references/jmarshall-win64-modern/` (likely still Bink) +- fighter19: `references/old-refs/fighter19-dxvk-port/` (check video handling) +- jmarshall: `references/old-refs/jmarshall-win64-modern/` (likely still Bink) - dsalzner: `references/dsalzner-linux-attempt/` (check if addressed) --- diff --git a/docs/WORKDIR/planning/FUTURE_SDL3_RENDER_MIGRATION.md b/docs/WORKDIR/planning/FUTURE_SDL3_RENDER_MIGRATION.md index 6d4a950949c..adcf9c9d1e4 100644 --- a/docs/WORKDIR/planning/FUTURE_SDL3_RENDER_MIGRATION.md +++ b/docs/WORKDIR/planning/FUTURE_SDL3_RENDER_MIGRATION.md @@ -127,7 +127,7 @@ git diff TheSuperHackers/main -- Core/GameEngineDevice/ ### 4. **Fighter19's Proven Solution** -Reference implementation (`references/fighter19-dxvk-port/`): +Reference implementation (`references/old-refs/fighter19-dxvk-port/`): - ✓ Full Linux build (native ELF) - ✓ Both Generals + Zero Hour - ✓ Gameplay tested (skirmish, campaign intros) @@ -403,7 +403,7 @@ IF macOS native becomes mandatory: - **Current Implementation**: [DXVK Graphics Pipeline](Phase1_DXVK_Graphics.md) (when written) - **SDL3 Documentation**: https://wiki.libsdl.org/ -- **Fighter19 Reference**: `references/fighter19-dxvk-port/` +- **Fighter19 Reference**: `references/old-refs/fighter19-dxvk-port/` - **DirectX8 API**: DXVK source code analysis - **Replay System**: `docs/WORKDIR/support/REPLAY_DETERMINISM.md` (future) diff --git a/docs/WORKDIR/planning/NEXT_STEPS.md b/docs/WORKDIR/planning/NEXT_STEPS.md index 71b8d6a5a3d..778ba65d60f 100644 --- a/docs/WORKDIR/planning/NEXT_STEPS.md +++ b/docs/WORKDIR/planning/NEXT_STEPS.md @@ -226,7 +226,7 @@ cd build/linux64-deploy/GeneralsMD - Known gaps (TODO Phase 2) ### fighter19 Reference Patterns -**Location**: `references/fighter19-dxvk-port/` +**Location**: `references/old-refs/fighter19-dxvk-port/` **Key files for comparison**: - `GeneralsMD/Code/Main/SDL3Main.cpp` - Entry point pattern @@ -439,10 +439,10 @@ docs/WORKDIR/planning/NEXT_STEPS.md (this file - updated) 2. **File Comparison**: ```bash # Compare structure changes - diff -r GeneralsMD/Code/GameEngineDevice/ references/fighter19-dxvk-port/GeneralsMD/Code/GameEngineDevice/ + diff -r GeneralsMD/Code/GameEngineDevice/ references/old-refs/fighter19-dxvk-port/GeneralsMD/Code/GameEngineDevice/ # Compare CMake changes - diff CMakePresets.json references/fighter19-dxvk-port/CMakePresets.json + diff CMakePresets.json references/old-refs/fighter19-dxvk-port/CMakePresets.json ``` 3. **Grep Search**: @@ -451,7 +451,7 @@ docs/WORKDIR/planning/NEXT_STEPS.md (this file - updated) grep -r "IDirect3D8" --include="*.cpp" --include="*.h" # Find DXVK integration points in fighter19 - grep -r "DXVK\|SDL3" references/fighter19-dxvk-port/ + grep -r "DXVK\|SDL3" references/old-refs/fighter19-dxvk-port/ ``` ## Docker Build Commands (Keep Mac Clean!) diff --git a/docs/WORKDIR/support/PLANO_B_WORKAROUND.md b/docs/WORKDIR/support/PLANO_B_WORKAROUND.md index 01140062390..9ef69da6df9 100644 --- a/docs/WORKDIR/support/PLANO_B_WORKAROUND.md +++ b/docs/WORKDIR/support/PLANO_B_WORKAROUND.md @@ -7,7 +7,7 @@ If `cmake --preset macos-vulkan` takes more than 20 minutes, we can use the figh ### 1. Check if fighter19 has DXVK compiled ```bash -ls -la references/fighter19-dxvk-port/GeneralsMD/Run/ +ls -la references/old-refs/fighter19-dxvk-port/GeneralsMD/Run/ # Should have libdxvk_d3d8.0.dylib + libdxvk_d3d9.0.dylib ``` @@ -17,13 +17,13 @@ mkdir -p build/macos-vulkan-backup cp -r build/macos-vulkan build/macos-vulkan-backup # copy DXVK dylibs -cp references/fighter19-dxvk-port/GeneralsMD/Run/libdxvk_*.d*.llib \ +cp references/old-refs/fighter19-dxvk-port/GeneralsMD/Run/libdxvk_*.d*.llib \ build/macos-vulkan/_run/ 2>/dev/null || echo "Not available" ``` ### 3. If fighter19 has no build, clone and compile ```bash -cd references/fighter19-dxvk-port +cd references/old-refs/fighter19-dxvk-port cmake --preset linux64-deploy # For native builds # OR cmake --preset macos-vulkan # If it has a macOS preset diff --git a/docs/WORKDIR/support/SDL3_GAPS_QUICK_REFERENCE.md b/docs/WORKDIR/support/SDL3_GAPS_QUICK_REFERENCE.md index fb63a97e0f9..9b1a00b5e4a 100644 --- a/docs/WORKDIR/support/SDL3_GAPS_QUICK_REFERENCE.md +++ b/docs/WORKDIR/support/SDL3_GAPS_QUICK_REFERENCE.md @@ -148,7 +148,7 @@ Session 42+ (Phase 2 Text Input): ``` Fighter19 Implementation: - └─ references/fighter19-dxvk-port/ + └─ references/old-refs/fighter19-dxvk-port/ ├─ GeneralsMD/Code/GameEngineDevice/ │ ├─ Include/SDL3Device/Common/SDL3GameEngine.h │ ├─ Include/SDL3Device/GameClient/SDL3Mouse.h (line 40+) diff --git a/references/.codegraphignore b/references/.codegraphignore new file mode 100644 index 00000000000..e69de29bb2d diff --git a/references/fighter19-dxvk-port b/references/old-refs/fighter19-dxvk-port similarity index 100% rename from references/fighter19-dxvk-port rename to references/old-refs/fighter19-dxvk-port diff --git a/references/jmarshall-win64-modern b/references/old-refs/jmarshall-win64-modern similarity index 100% rename from references/jmarshall-win64-modern rename to references/old-refs/jmarshall-win64-modern diff --git a/references/thesuperhackers-main b/references/old-refs/thesuperhackers-main similarity index 100% rename from references/thesuperhackers-main rename to references/old-refs/thesuperhackers-main