feat(network): Self-healing NAT state#3010
Conversation
|
| Filename | Overview |
|---|---|
| Core/GameEngine/Source/GameNetwork/FirewallHelper.cpp | Centralizes the in-memory detection lifecycle, performs a complete socket/state reset on refresh, and preserves completed classifications. |
| Core/GameEngine/Source/GameNetwork/NAT.cpp | Retains the shared helper across connection setup and requests a fresh classification after negotiation failure. |
| Core/GameEngine/Source/Common/OptionPreferences.cpp | Invalidates the active NAT classification when the selected LAN or online address changes. |
| Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLWelcomeMenu.cpp | Advances background detection without deleting its session-lived result. |
| GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLWelcomeMenu.cpp | Mirrors the persistent background-detection lifecycle for Zero Hour. |
| Generals/Code/GameEngine/Source/GameNetwork/GameSpy.cpp | Continues firewall probing from the GameSpy update loop while retaining the resulting helper state. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Enter online lobby] --> B{FirewallHelper exists?}
B -- No --> C[Create helper and start detection]
B -- Yes --> D[Reuse session helper]
C --> E[Advance probe during updates]
D --> E
E --> F[Cache NAT behavior in memory]
F --> G[Game setup and quick match consume behavior]
H[Selected IP changes] --> I[Reset probe state and sockets]
J[NAT negotiation fails] --> I
I --> E
Reviews (5): Last reviewed commit: "fix(network): Preserve FirewallHelper li..." | Re-trigger Greptile
c5806f1 to
79e5c8e
Compare
### What this code did: The `ButtonFirewallRefresh` button in `OptionsMenu.cpp` and `FirewallNeedToRefresh` in `FirewallHelper.cpp` / `OptionPreferences.cpp`: 1. Saved/read `FirewallNeedToRefresh` and `LastFirewallIP` boolean flags to/from `Options.ini`. 2. Required players to manually click a "Refresh NAT" button in the Options GUI when changing network interfaces or encountering P2P negotiation failures. 3. Contaminated `GlobalData` (`TheWritableGlobalData->m_firewallBehavior`) with disk-persisted firewall state across process restarts. ### How the new code works: 1. **Automated In-Engine RAM Lifecycle**: NAT state classification (`m_behavior`) is managed 100% in memory within `FirewallHelperClass`. 2. **Transparent Background Probing**: Non-blocking STUN probing runs in the background during online lobby entry (`WOLWelcomeMenu`), caching the classified result in RAM for the remainder of the game session. 3. **Self-Healing Re-Detection**: If a peer connection times out (`NAT.cpp`) or the user selects a new IP address in Options (`OptionPreferences.cpp`), `TheFirewallHelper->flagNeedToRefresh(TRUE)` resets `m_behavior = FIREWALL_TYPE_UNKNOWN` in RAM to seamlessly trigger a fresh background probe. 4. **Clean GUI Deprecation**: Hides `OptionsMenu.wnd:ButtonFirewallRefresh` via `winHide(TRUE)` in C++ without breaking custom or legacy `.wnd` layout files. ### Background & Reason for Removal: - **Obsolete Manual Workaround**: Manual NAT refresh buttons are a legacy 2003 workaround; modern network stacks handle NAT re-detection automatically in-engine. - **Elimination of Disk I/O**: Completely removes `FirewallNeedToRefresh` and `LastFirewallIP` from `Options.ini`, preventing stale or corrupted flags from persisting across game crashes or process restarts. - **Architectural Decoupling**: Fully encapsulates STUN probing state inside `FirewallHelperClass` in memory, removing direct mutations to `TheWritableGlobalData->m_firewallBehavior`.
79e5c8e to
8113c09
Compare
| FirewallHelperClass(); | ||
| virtual ~FirewallHelperClass(); | ||
| Bool detectFirewall(); | ||
| void detectFirewallBehavior(/*Bool &canRecord*/); |
| if ((*this)["IPAddress"].compareNoCase(IP) != 0) | ||
| { | ||
| (*this)["IPAddress"] = IP; | ||
| if (TheFirewallHelper != nullptr) |
There was a problem hiding this comment.
nit: Assignment of IP and resetting refresh flag of firewallhelper are two non-related commands. An empty line between the assignment and the if statement would have my preference for higher readability.
| void OptionPreferences::setOnlineIPAddress(AsciiString IP) | ||
| { | ||
| (*this)["GameSpyIPAddress"] = IP; | ||
| if ((*this)["GameSpyIPAddress"].compareNoCase(IP) != 0) |
There was a problem hiding this comment.
This code is a duplication of setLanIPAddress(). IMO it is better to have a private function with this logic and the two public functions calling the private function, i.e.:
void OptionPreferences::setLANIPAddress(AsciiString IP)
{
setIpAddress(IP);
}
void OptionPreferences::setOnlineIPAddress(AsciiString IP)
{
setIpAddress(IP);
}
void setIpAddress(AsciiString IP)
{
if ((*this)["IPAddress"].compareNoCase(IP) != 0)
{
(*this)["IPAddress"] = IP;
if (TheFirewallHelper != nullptr)
TheFirewallHelper->flagNeedToRefresh(TRUE);
}
}| void OptionPreferences::setLANIPAddress(AsciiString IP) | ||
| { | ||
| (*this)["IPAddress"] = IP; | ||
| if ((*this)["IPAddress"].compareNoCase(IP) != 0) |
There was a problem hiding this comment.
Rather than putting all the logic within an if statement, you could tap out early
if ((*this)["IPAddress"].compareNoCase(IP) == 0)
return;
(*this) ....This increases readability and maintainability.
|
|
||
| void OptionPreferences::setOnlineIPAddress(UnsignedInt IP) | ||
| { | ||
| AsciiString tmp; |
There was a problem hiding this comment.
As this code is touched, maybe also change the naming of the parameter tmp as it is a poor name that doesn't reflect its purpose.
| delete TheFirewallHelper; | ||
| TheFirewallHelper = nullptr; | ||
| } | ||
| FirewallHelperClass *helper = NEW FirewallHelperClass(); |
There was a problem hiding this comment.
nit: add a whiteline after the closing bracket of an if-statement for higher readability
| FirewallHelperClass * createFirewallHelper() | ||
| { | ||
| return NEW FirewallHelperClass(); | ||
| if (TheFirewallHelper) |
There was a problem hiding this comment.
I am confused by the logic here. createFirewallHelper creates a new FirewallHelperClass and assigns it to a local variable, then returns that variable.
But first it deletes the instance (if any) of the global TheFirewallHelper parameter.
This reads like this function is not adhering to single responsibility.
What this code did:
The
ButtonFirewallRefreshbutton inOptionsMenu.cppandFirewallNeedToRefreshinFirewallHelper.cpp/OptionPreferences.cpp:FirewallNeedToRefreshandLastFirewallIPboolean flags to/fromOptions.ini.GlobalData(TheWritableGlobalData->m_firewallBehavior) with disk-persisted firewall state across process restarts.How the new code works:
m_behavior) is managed 100% in memory withinFirewallHelperClass.WOLWelcomeMenu), caching the classified result in RAM for the remainder of the game session.NAT.cpp) or the user selects a new IP address in Options (OptionPreferences.cpp),TheFirewallHelper->flagNeedToRefresh(TRUE)resetsm_behavior = FIREWALL_TYPE_UNKNOWNin RAM to seamlessly trigger a fresh background probe.OptionsMenu.wnd:ButtonFirewallRefreshviawinHide(TRUE)in C++ without breaking custom or legacy.wndlayout files.Background & Reason for Removal:
FirewallNeedToRefreshandLastFirewallIPfromOptions.ini, preventing stale or corrupted flags from persisting across game crashes or process restarts.FirewallHelperClassin memory, removing direct mutations toTheWritableGlobalData->m_firewallBehavior.