From fdef41134c0644c07667b3e683ad0b765a8aed14 Mon Sep 17 00:00:00 2001 From: Mewt0 <36929260+Mewt0@users.noreply.github.com> Date: Mon, 7 Sep 2026 14:10:14 +0300 Subject: [PATCH 1/6] Add SmartReconnect diagnostic v0.1 scaffold --- .gitignore | 6 + README.md | 88 +++++++++++++- build.sh | 43 +++++++ docs/ARCHITECTURE.md | 61 ++++++++++ meta.xml | 6 + .../client/gui/mods/mod_smartReconnect.py | 16 +++ .../client/gui/mods/smartReconnect/Config.py | 9 ++ .../mods/smartReconnect/ConnectionMonitor.py | 113 ++++++++++++++++++ .../smartReconnect/ReconnectController.py | 40 +++++++ .../gui/mods/smartReconnect/SmartReconnect.py | 77 ++++++++++++ .../gui/mods/smartReconnect/__init__.py | 0 11 files changed, 458 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100755 build.sh create mode 100644 docs/ARCHITECTURE.md create mode 100644 meta.xml create mode 100644 res/scripts/client/gui/mods/mod_smartReconnect.py create mode 100644 res/scripts/client/gui/mods/smartReconnect/Config.py create mode 100644 res/scripts/client/gui/mods/smartReconnect/ConnectionMonitor.py create mode 100644 res/scripts/client/gui/mods/smartReconnect/ReconnectController.py create mode 100644 res/scripts/client/gui/mods/smartReconnect/SmartReconnect.py create mode 100644 res/scripts/client/gui/mods/smartReconnect/__init__.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ac5a2cf --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +build/ +*.wotmod +*.mtmod +*.pyc +__pycache__/ +.DS_Store diff --git a/README.md b/README.md index 79d8fe8..7ad8c13 100644 --- a/README.md +++ b/README.md @@ -1 +1,87 @@ -# SmartReconnect \ No newline at end of file +# SmartReconnect + +SmartReconnect is an experimental World of Tanks client mod intended to shorten recovery from a dead battle connection. + +## Current milestone + +**v0.1 is diagnostic-only. It does not disconnect or reconnect automatically.** + +The first build validates the same lag signal used by WoT's battle debug panel before any connection-changing action is enabled. + +It polls: + +- `BigWorld.statLagDetected()` +- `BigWorld.statPing()` + +Default behavior: + +1. Normal connection -> monitor remains idle. +2. Lag indicator becomes red -> start a timer. +3. Indicator returns to normal -> reset the timer. +4. Indicator remains red for 3 seconds -> write `WOULD RECONNECT` to `python.log`. +5. No network action is performed in diagnostic mode. + +`Ctrl+K` is also detected as a manual reconnect request, but in diagnostic mode it only logs `WOULD RECONNECT`. + +## Planned reconnect path + +`RED -> grace period -> goToLoginByDisconnectRQ() -> LOGIN state -> WGC login -> ongoing battle` + +The reconnect controller will use WoT's own gameplay and login services rather than terminating the process or manipulating sockets directly. + +## Project layout + +```text +SmartReconnect/ +├── README.md +├── .gitignore +├── meta.xml +├── build.sh +├── docs/ +│ └── ARCHITECTURE.md +└── res/scripts/client/gui/mods/ + ├── mod_smartReconnect.py + └── smartReconnect/ + ├── __init__.py + ├── Config.py + ├── ConnectionMonitor.py + ├── ReconnectController.py + └── SmartReconnect.py +``` + +## Build + +Requires a Python 2 environment compatible with the WoT client mod pipeline plus `zip` and a POSIX shell. + +```bash +./build.sh -v 0.1.0 +``` + +The result is written to the repository root as: + +```text +mewt0.smartReconnect_0.1.0.wotmod +``` + +Copy it into: + +```text +World_of_Tanks/mods// +``` + +Then inspect `python.log` for lines beginning with `[SmartReconnect]`. + +## Diagnostic test + +A successful first test should look roughly like this: + +```text +[SmartReconnect] battle monitor started +[SmartReconnect] RED started ping=... +[SmartReconnect] RED elapsed=1.0s ping=... +[SmartReconnect] RED elapsed=2.0s ping=... +[SmartReconnect] RED threshold reached elapsed=3.0s +[SmartReconnect] WOULD RECONNECT reason=auto-lag +``` + +If the indicator returns to normal before the threshold, the timer resets and no reconnect request is produced. diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..025a438 --- /dev/null +++ b/build.sh @@ -0,0 +1,43 @@ +#!/bin/bash +set -e + +MOD_NAME="mewt0.smartReconnect" +VERSION="" + +while getopts "v:" flag; do + case "${flag}" in + v) VERSION=${OPTARG} ;; + esac +done + +if [ -z "$VERSION" ]; then + echo "Usage: ./build.sh -v " + exit 1 +fi + +rm -rf ./build +mkdir -p ./build +cp -r ./res ./build/ + +CONFIG_PATH="./build/res/scripts/client/gui/mods/smartReconnect/Config.py" +perl -i -pe "s/\{\{VERSION\}\}/$VERSION/g" "$CONFIG_PATH" + +python2 -m compileall ./build/res + +META=$( ./meta.xml + +OUTPUT="${MOD_NAME}_${VERSION}.wotmod" +rm -f "$OUTPUT" + +zip -r -0 -X "$OUTPUT" res -i "*.pyc" +zip -r -0 -X "$OUTPUT" meta.xml + +cd .. +cp "./build/$OUTPUT" "./$OUTPUT" +rm -rf ./build + +echo "Built $OUTPUT" diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..0edd604 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,61 @@ +# SmartReconnect architecture + +## Problem + +During a dead World of Tanks battle connection the client can show the red network/lag indicator while the battle appears frozen, then wait for its normal timeout before returning the player to login. + +SmartReconnect aims to shorten that recovery path without patching the executable, manipulating packets, or replacing the game's connection stack. + +## Detector + +WoT's own battle debug controller reads: + +```python +isLaggingNow = BigWorld.statLagDetected() +ping = BigWorld.statPing() +``` + +SmartReconnect deliberately uses the same native lag signal instead of external ICMP/HTTP probes. + +Polling interval: `0.2s`. + +## State model + +```text +IDLE + -> MONITORING + -> LAG_SUSPECTED + -> RECONNECT_REQUESTED +``` + +v0.1 stops at `RECONNECT_REQUESTED` and writes `WOULD RECONNECT` to the log. + +Future reconnect flow: + +```text +RECONNECT_REQUESTED + -> goToLoginByDisconnectRQ() + -> wait for LOGIN gameplay state + -> WGC login on the selected server + -> WoT rejoins the ongoing arena using its normal reconnect path +``` + +## False-positive protection + +Automatic reconnect must not fire on a brief lag spike. The detector therefore requires a continuous lag state for `LAG_GRACE_PERIOD` seconds. Any healthy sample resets the timer. + +The first live validation build intentionally has all network-changing actions disabled. + +## Manual fallback + +`Ctrl+K` produces a manual reconnect request. In v0.1 it is also diagnostic-only. + +## Next milestone + +After validating logs on the target client: + +1. add a guarded real disconnect through `IGameplayLogic.goToLoginByDisconnectRQ()`; +2. observe `GameplayStateID.LOGIN` rather than sleeping for a guessed delay; +3. start WGC login through the existing login manager; +4. add single-flight protection so one outage cannot start multiple reconnects; +5. stop automatic retries after a failed login until behavior is measured. diff --git a/meta.xml b/meta.xml new file mode 100644 index 0000000..75b2449 --- /dev/null +++ b/meta.xml @@ -0,0 +1,6 @@ + + mewt0.smartReconnect + {{VERSION}} + SmartReconnect + Fast reconnect helper for World of Tanks by Mewt0 + diff --git a/res/scripts/client/gui/mods/mod_smartReconnect.py b/res/scripts/client/gui/mods/mod_smartReconnect.py new file mode 100644 index 0000000..e703be8 --- /dev/null +++ b/res/scripts/client/gui/mods/mod_smartReconnect.py @@ -0,0 +1,16 @@ +from .smartReconnect.SmartReconnect import SmartReconnect + +_instance = None + + +def init(): + global _instance + if _instance is None: + _instance = SmartReconnect() + + +def fini(): + global _instance + if _instance is not None: + _instance.dispose() + _instance = None diff --git a/res/scripts/client/gui/mods/smartReconnect/Config.py b/res/scripts/client/gui/mods/smartReconnect/Config.py new file mode 100644 index 0000000..201838a --- /dev/null +++ b/res/scripts/client/gui/mods/smartReconnect/Config.py @@ -0,0 +1,9 @@ +VERSION = '{{VERSION}}' + +# Keep the first live build safe: detector only, no connection-changing action. +DIAGNOSTIC_MODE = True +AUTO_RECONNECT_ENABLED = False +MANUAL_HOTKEY_ENABLED = True + +POLL_INTERVAL = 0.2 +LAG_GRACE_PERIOD = 3.0 diff --git a/res/scripts/client/gui/mods/smartReconnect/ConnectionMonitor.py b/res/scripts/client/gui/mods/smartReconnect/ConnectionMonitor.py new file mode 100644 index 0000000..c3fe9bf --- /dev/null +++ b/res/scripts/client/gui/mods/smartReconnect/ConnectionMonitor.py @@ -0,0 +1,113 @@ +import logging + +import BigWorld +import BattleReplay + +from .Config import POLL_INTERVAL, LAG_GRACE_PERIOD + +_logger = logging.getLogger('SmartReconnect') + + +class ConnectionMonitor(object): + + def __init__(self, reconnectCallback): + self._reconnectCallback = reconnectCallback + self._callbackID = None + self._running = False + self._lagSince = None + self._triggered = False + self._lastLoggedSecond = -1 + + def start(self): + if self._running: + return + self._running = True + self._resetLagState() + _logger.info('[SmartReconnect] battle monitor started') + self._schedule(0.0) + + def stop(self): + self._running = False + if self._callbackID is not None: + try: + BigWorld.cancelCallback(self._callbackID) + except Exception: + _logger.exception('[SmartReconnect] failed to cancel monitor callback') + self._callbackID = None + self._resetLagState() + _logger.info('[SmartReconnect] battle monitor stopped') + + def _schedule(self, delay=None): + if not self._running: + return + if delay is None: + delay = POLL_INTERVAL + self._callbackID = BigWorld.callback(delay, self._tick) + + def _tick(self): + self._callbackID = None + if not self._running: + return + + try: + if BattleReplay.isPlaying(): + self._resetLagState() + return + + player = BigWorld.player() + if player is None or not hasattr(player, 'arena') or player.arena is None: + self._resetLagState() + return + + isLagging = bool(BigWorld.statLagDetected()) + ping = BigWorld.statPing() + now = BigWorld.timeExact() + + if isLagging: + self._handleLag(now, ping) + else: + self._handleHealthy(ping) + except Exception: + _logger.exception('[SmartReconnect] monitor tick failed') + finally: + self._schedule() + + def _handleLag(self, now, ping): + if self._lagSince is None: + self._lagSince = now + self._triggered = False + self._lastLoggedSecond = -1 + _logger.warning('[SmartReconnect] RED started ping=%s', str(ping)) + + elapsed = max(0.0, now - self._lagSince) + wholeSecond = int(elapsed) + if wholeSecond != self._lastLoggedSecond: + self._lastLoggedSecond = wholeSecond + _logger.warning( + '[SmartReconnect] RED elapsed=%.1fs ping=%s', + elapsed, + str(ping) + ) + + if elapsed >= LAG_GRACE_PERIOD and not self._triggered: + self._triggered = True + _logger.warning( + '[SmartReconnect] RED threshold reached elapsed=%.1fs', + elapsed + ) + self._reconnectCallback('auto-lag', elapsed, ping) + + def _handleHealthy(self, ping): + if self._lagSince is not None: + elapsed = max(0.0, BigWorld.timeExact() - self._lagSince) + _logger.info( + '[SmartReconnect] GREEN recovered after %.1fs ping=%s; timer reset', + elapsed, + str(ping) + ) + self._resetLagState() + + def _resetLagState(self): + self._lagSince = None + self._triggered = False + self._lastLoggedSecond = -1 diff --git a/res/scripts/client/gui/mods/smartReconnect/ReconnectController.py b/res/scripts/client/gui/mods/smartReconnect/ReconnectController.py new file mode 100644 index 0000000..931e949 --- /dev/null +++ b/res/scripts/client/gui/mods/smartReconnect/ReconnectController.py @@ -0,0 +1,40 @@ +import logging + +from .Config import DIAGNOSTIC_MODE, AUTO_RECONNECT_ENABLED + +_logger = logging.getLogger('SmartReconnect') + + +class ReconnectController(object): + + def __init__(self): + self._busy = False + + @property + def busy(self): + return self._busy + + def requestReconnect(self, reason, elapsed=None, ping=None): + if self._busy: + _logger.info( + '[SmartReconnect] reconnect request ignored; already busy reason=%s', + str(reason) + ) + return False + + if DIAGNOSTIC_MODE or not AUTO_RECONNECT_ENABLED: + _logger.warning( + '[SmartReconnect] WOULD RECONNECT reason=%s elapsed=%s ping=%s', + str(reason), + str(elapsed), + str(ping) + ) + return False + + # Intentionally not implemented in v0.1. The first live build must prove + # the detector before any connection-changing action is enabled. + _logger.error( + '[SmartReconnect] real reconnect path is disabled in v0.1 reason=%s', + str(reason) + ) + return False diff --git a/res/scripts/client/gui/mods/smartReconnect/SmartReconnect.py b/res/scripts/client/gui/mods/smartReconnect/SmartReconnect.py new file mode 100644 index 0000000..fdbce1f --- /dev/null +++ b/res/scripts/client/gui/mods/smartReconnect/SmartReconnect.py @@ -0,0 +1,77 @@ +import logging + +import BigWorld +import BattleReplay +import Keys + +from gui import InputHandler +from PlayerEvents import g_playerEvents + +from .Config import VERSION, MANUAL_HOTKEY_ENABLED +from .ConnectionMonitor import ConnectionMonitor +from .ReconnectController import ReconnectController + +_logger = logging.getLogger('SmartReconnect') + + +class SmartReconnect(object): + + def __init__(self): + self._controller = ReconnectController() + self._monitor = ConnectionMonitor(self._onReconnectRequested) + self._inBattle = False + + g_playerEvents.onAvatarBecomePlayer += self._onAvatarBecomePlayer + g_playerEvents.onAvatarBecomeNonPlayer += self._onAvatarBecomeNonPlayer + InputHandler.g_instance.onKeyUp += self._onKeyUp + + _logger.info('[SmartReconnect] loaded version=%s diagnostic=true', VERSION) + + # Defensive support for script reloads while already inside an arena. + player = BigWorld.player() + if player is not None and hasattr(player, 'arena') and player.arena is not None: + self._onAvatarBecomePlayer() + + def dispose(self): + self._monitor.stop() + + try: + g_playerEvents.onAvatarBecomePlayer -= self._onAvatarBecomePlayer + except Exception: + pass + try: + g_playerEvents.onAvatarBecomeNonPlayer -= self._onAvatarBecomeNonPlayer + except Exception: + pass + try: + InputHandler.g_instance.onKeyUp -= self._onKeyUp + except Exception: + pass + + _logger.info('[SmartReconnect] disposed') + + def _onAvatarBecomePlayer(self, *args, **kwargs): + if BattleReplay.isPlaying(): + return + self._inBattle = True + self._monitor.start() + + def _onAvatarBecomeNonPlayer(self, *args, **kwargs): + self._inBattle = False + self._monitor.stop() + + def _onReconnectRequested(self, reason, elapsed=None, ping=None): + self._controller.requestReconnect(reason, elapsed, ping) + + def _onKeyUp(self, event): + if not MANUAL_HOTKEY_ENABLED: + return + if BattleReplay.isPlaying(): + return + if event.key != Keys.KEY_K: + return + if not event.isCtrlDown(): + return + + _logger.warning('[SmartReconnect] Ctrl+K manual reconnect requested') + self._controller.requestReconnect('manual-hotkey') diff --git a/res/scripts/client/gui/mods/smartReconnect/__init__.py b/res/scripts/client/gui/mods/smartReconnect/__init__.py new file mode 100644 index 0000000..e69de29 From ae1e7495ae368bd9868b3d51f48e709ab764e6b1 Mon Sep 17 00:00:00 2001 From: Mewt0 <36929260+Mewt0@users.noreply.github.com> Date: Mon, 7 Sep 2026 14:13:02 +0300 Subject: [PATCH 2/6] Document WoT modding research references --- docs/REFERENCES.md | 136 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 docs/REFERENCES.md diff --git a/docs/REFERENCES.md b/docs/REFERENCES.md new file mode 100644 index 0000000..134ed29 --- /dev/null +++ b/docs/REFERENCES.md @@ -0,0 +1,136 @@ +# SmartReconnect research references + +This document is the working source map for SmartReconnect. Prefer these sources over guesses when changing client-facing logic. + +## 1. Decompiled WoT client — primary technical reference + +Repository: https://github.com/izeberg/wot-src + +Primary files currently used by SmartReconnect research: + +- `sources/res/scripts/client/gui/battle_control/controllers/debug_ctrl.py` + - WoT battle debug panel polling loop. + - Reads `BigWorld.statLagDetected()` and `BigWorld.statPing()` every 0.2 seconds. + - This is the primary evidence for how the in-battle lag/red-lamp state is computed. + +- `sources/res/scripts/client/gui/Scaleform/daapi/view/battle/shared/debug_panel.py` + - Python-side debug panel state. + - Tracks `_isLaggingNow` and forwards changes to Scaleform. + +- `sources-as3/gui_battle/scripts/net/wg/gui/battle/views/debugPanel/DebugPanel.as` + - Scaleform implementation of the red/green network indicator. + - Toggles `lagOnlineSpr` / `lagOfflineSpr` from the lag boolean. + +- `sources/res/scripts/client/gameplay/delegator.py` + - Current implementation of `IGameplayLogic`. + - `goToLoginByDisconnectRQ()` prepares WGC login, then performs the normal request-disconnect path. + +- `sources/res/scripts/client/skeletons/gameplay.py` + - Interface and gameplay state IDs. + - Includes `GameplayStateID.LOGIN` and `goToLoginByDisconnectRQ()`. + +- `sources/res/scripts/client/connection_mgr.py` + - Current client connection manager. + - Provides `disconnect()`, `isConnected()`, `isDisconnected()` and connection events. + - Also contains WoT's login retry/backoff implementation. + +- `sources/res/scripts/client/skeletons/connection_mgr.py` + - Public client-side interface for connection state and events. + +- `sources/res/scripts/client/gui/login/Manager.py` + - WGC/token login flow. + - Provides `tryPrepareWGCLogin()` and `tryWgcLogin(serverName=None)`. + +- `sources/res/scripts/client/gui/login/Servers.py` + - Login server selection state. + - Exposes the selected server data used by the login flow. + +- `sources/res/scripts/client/gui/app_loader/observers.py` + - Gameplay-state-driven UI transitions. + - Useful for waiting for the real `LOGIN` state instead of using arbitrary sleeps. + +- `sources/version.xml` + - Always check this before trusting findings as version-current. + +### Version rule + +Treat `izeberg/wot-src` as highly useful but not guaranteed byte-identical to the user's installed client. Re-check `sources/version.xml` and validate critical behavior in `python.log` on the exact target client before enabling destructive or connection-changing actions. + +## 2. WoT modding documentation + +Community documentation: https://wgmods.dev/docs/wot + +Use primarily for: + +- `.wotmod` package layout +- `meta.xml` +- Python client-mod packaging conventions +- general mod-loading behavior + +This is a secondary reference for internal APIs; the decompiled client remains primary for version-specific behavior. + +## 3. Open-source WoT mods — implementation references + +### FastReconnect + +Repository: https://github.com/Pruszko/FastReconnect + +Use for: + +- historical proof that a client mod can intentionally trigger the normal disconnect/login transition +- manual `Ctrl+K` reconnect UX reference + +Do not copy old API calls blindly; verify them against the current client first. + +### WotStat mods + +Repositories: + +- https://github.com/wotstat/wotstat-vegetation +- https://github.com/wotstat/wotstat-positions + +Use for: + +- modern `.wotmod` project layout +- `mod_*.py` entrypoint style (`init()` / `fini()`) +- build scripts and Python 2 `.pyc` packaging +- current event/hotkey patterns + +## 4. Official Wargaming public API + +Developer portal: https://developers.wargaming.net/ + +This is a web/public-data API, not the internal WoT client mod API. It is useful for account, vehicle, clan and related external data, but is not currently required for SmartReconnect's network recovery logic. + +## 5. Fair Play / prohibited-mod rules + +Official policy: https://worldoftanks.eu/en/content/guide/fair-play/prohibited-mods/ + +SmartReconnect should remain a connection-recovery utility only. It must not expose hidden battle information, automate combat decisions, alter aiming/shooting behavior, or provide prohibited gameplay advantages. + +## 6. Current SmartReconnect API map + +| Purpose | Client API / class | Status | +|---|---|---| +| Detect WoT's lag/red-lamp state | `BigWorld.statLagDetected()` | Confirmed in current client source | +| Read displayed battle ping | `BigWorld.statPing()` | Confirmed in current client source | +| Poll cadence reference | `DebugController` (`0.2s`) | Confirmed | +| Check connection state | `IConnectionManager.isConnected()` | Confirmed | +| Normal disconnect | `IConnectionManager.disconnect()` | Confirmed | +| Request disconnect + return toward login | `IGameplayLogic.goToLoginByDisconnectRQ()` | Confirmed | +| Wait for actual login state | `GameplayStateID.LOGIN` + gameplay observer | Confirmed | +| Prepare WGC login | `ILoginManager.tryPrepareWGCLogin()` | Confirmed | +| Start WGC login | `ILoginManager.tryWgcLogin()` | Confirmed | +| Recover ongoing arena after relogin | Stock WoT reconnect flow | Confirmed to exist; live SmartReconnect integration still pending | + +## 7. Development rule for this repository + +For every connection-changing feature: + +1. Find the current client implementation in `izeberg/wot-src`. +2. Find at least one stock WoT call site using the same API when practical. +3. Keep the first implementation diagnostic-only. +4. Validate behavior on the exact client build through `python.log`. +5. Only then enable the real reconnect action. + +This rule is intentionally conservative because a false reconnect in a live battle is worse than waiting for the stock timeout. From d1d65d19c39e438913045e5c547360a014468c45 Mon Sep 17 00:00:00 2001 From: Mewt0 <36929260+Mewt0@users.noreply.github.com> Date: Mon, 7 Sep 2026 14:20:21 +0300 Subject: [PATCH 3/6] Improve diagnostic connection telemetry --- .../mods/smartReconnect/ConnectionMonitor.py | 46 ++++++++++++++----- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/res/scripts/client/gui/mods/smartReconnect/ConnectionMonitor.py b/res/scripts/client/gui/mods/smartReconnect/ConnectionMonitor.py index c3fe9bf..d868c22 100644 --- a/res/scripts/client/gui/mods/smartReconnect/ConnectionMonitor.py +++ b/res/scripts/client/gui/mods/smartReconnect/ConnectionMonitor.py @@ -3,6 +3,9 @@ import BigWorld import BattleReplay +from helpers import dependency +from skeletons.connection_mgr import IConnectionManager + from .Config import POLL_INTERVAL, LAG_GRACE_PERIOD _logger = logging.getLogger('SmartReconnect') @@ -12,6 +15,7 @@ class ConnectionMonitor(object): def __init__(self, reconnectCallback): self._reconnectCallback = reconnectCallback + self._connectionMgr = dependency.instance(IConnectionManager) self._callbackID = None self._running = False self._lagSince = None @@ -62,48 +66,68 @@ def _tick(self): isLagging = bool(BigWorld.statLagDetected()) ping = BigWorld.statPing() now = BigWorld.timeExact() + connected = self._readConnectedState() + arenaPeriod = getattr(player.arena, 'period', None) if isLagging: - self._handleLag(now, ping) + self._handleLag(now, ping, connected, arenaPeriod) else: - self._handleHealthy(ping) + self._handleHealthy(ping, connected, arenaPeriod) except Exception: _logger.exception('[SmartReconnect] monitor tick failed') finally: self._schedule() - def _handleLag(self, now, ping): + def _readConnectedState(self): + try: + return bool(self._connectionMgr.isConnected()) + except Exception: + _logger.exception('[SmartReconnect] failed to read connection manager state') + return None + + def _handleLag(self, now, ping, connected, arenaPeriod): if self._lagSince is None: self._lagSince = now self._triggered = False self._lastLoggedSecond = -1 - _logger.warning('[SmartReconnect] RED started ping=%s', str(ping)) + _logger.warning( + '[SmartReconnect] RED started ping=%s connected=%s arenaPeriod=%s', + str(ping), + str(connected), + str(arenaPeriod) + ) elapsed = max(0.0, now - self._lagSince) wholeSecond = int(elapsed) if wholeSecond != self._lastLoggedSecond: self._lastLoggedSecond = wholeSecond _logger.warning( - '[SmartReconnect] RED elapsed=%.1fs ping=%s', + '[SmartReconnect] RED elapsed=%.1fs ping=%s connected=%s arenaPeriod=%s', elapsed, - str(ping) + str(ping), + str(connected), + str(arenaPeriod) ) if elapsed >= LAG_GRACE_PERIOD and not self._triggered: self._triggered = True _logger.warning( - '[SmartReconnect] RED threshold reached elapsed=%.1fs', - elapsed + '[SmartReconnect] RED threshold reached elapsed=%.1fs connected=%s arenaPeriod=%s', + elapsed, + str(connected), + str(arenaPeriod) ) self._reconnectCallback('auto-lag', elapsed, ping) - def _handleHealthy(self, ping): + def _handleHealthy(self, ping, connected, arenaPeriod): if self._lagSince is not None: elapsed = max(0.0, BigWorld.timeExact() - self._lagSince) _logger.info( - '[SmartReconnect] GREEN recovered after %.1fs ping=%s; timer reset', + '[SmartReconnect] GREEN recovered after %.1fs ping=%s connected=%s arenaPeriod=%s; timer reset', elapsed, - str(ping) + str(ping), + str(connected), + str(arenaPeriod) ) self._resetLagState() From 746ccf3ae6eaca5ffd8619571a868e1a1be75626 Mon Sep 17 00:00:00 2001 From: Mewt0 <36929260+Mewt0@users.noreply.github.com> Date: Mon, 7 Sep 2026 14:21:12 +0300 Subject: [PATCH 4/6] Add diagnostic live-test matrix --- docs/TESTING.md | 96 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 docs/TESTING.md diff --git a/docs/TESTING.md b/docs/TESTING.md new file mode 100644 index 0000000..2b9852a --- /dev/null +++ b/docs/TESTING.md @@ -0,0 +1,96 @@ +# SmartReconnect testing plan + +This document defines the gate between diagnostic v0.1 and any real reconnect action. + +## Principle + +v0.1 must never intentionally disconnect the client. Its only job is to prove that the detector identifies the same sustained outage the player sees as the red network indicator. + +Do not enable real reconnect until the acceptance criteria below are met on the exact target WoT client build. + +## Expected log fields + +During a lag episode, `python.log` should include: + +- `ping` +- `connected` from `IConnectionManager.isConnected()` +- `arenaPeriod` +- elapsed RED duration +- the final `WOULD RECONNECT` decision + +Example: + +```text +[SmartReconnect] RED started ping=... connected=True arenaPeriod=... +[SmartReconnect] RED elapsed=1.0s ping=... connected=True arenaPeriod=... +[SmartReconnect] RED elapsed=2.0s ping=... connected=True arenaPeriod=... +[SmartReconnect] RED threshold reached elapsed=3.0s connected=True arenaPeriod=... +[SmartReconnect] WOULD RECONNECT reason=auto-lag elapsed=... ping=... +``` + +The most important observation is whether the red-lamp signal becomes sustained while `IConnectionManager` still reports `connected=True`. That is the window SmartReconnect is intended to shorten. + +## Test matrix + +| ID | Scenario | Expected result | +|---|---|---| +| D01 | Start client and enter a normal battle/training arena | Mod loads; monitor starts once; no exceptions | +| D02 | Stable connection for several minutes | No `WOULD RECONNECT` | +| D03 | Very short RED/lag spike below 3 seconds | `RED started`, then `GREEN recovered`; timer resets; no reconnect request | +| D04 | Sustained RED longer than 3 seconds | Exactly one `RED threshold reached` and one `WOULD RECONNECT` for the outage | +| D05 | RED remains active for 10+ seconds | No repeated `WOULD RECONNECT` for the same continuous outage | +| D06 | RED -> GREEN -> RED again | First episode resets; second episode starts a fresh timer | +| D07 | Leave battle normally | Monitor stops and callback is cancelled | +| D08 | Enter another battle | Monitor starts cleanly with no stale RED state | +| D09 | Play a replay | Detector and Ctrl+K reconnect logic remain inactive | +| D10 | Press Ctrl+K outside replay | Diagnostic manual request is logged; no network action occurs | +| D11 | ConnectionManager becomes disconnected before the 3-second gate | Log captures `connected=False`; v0.1 still performs no network action | +| D12 | Client shutdown/mod fini | No callback or event-unsubscribe exceptions | + +## Controlled outage test + +Prefer a training environment when possible so testing does not affect a normal match. + +For detector validation, briefly interrupt only the test machine's own network connection long enough for WoT's red indicator to remain active beyond the configured 3-second threshold, then restore it. The v0.1 mod must only log the decision and must not alter the connection itself. + +Record the relevant `python.log` section from a few seconds before RED starts until the connection recovers or the stock client times out. + +## Acceptance criteria for v0.1 + +All of the following must be true before work moves to an enabled reconnect controller: + +1. No import/init exceptions on the target client. +2. The monitor starts and stops with the battle lifecycle without duplicate callbacks. +3. Short RED spikes below the threshold never generate `WOULD RECONNECT`. +4. Sustained RED generates exactly one `WOULD RECONNECT` per continuous outage. +5. The log demonstrates the relationship between `statLagDetected()` and `IConnectionManager.isConnected()` during a real outage. +6. Replay behavior is inert. +7. Ctrl+K produces only a diagnostic request in v0.1. +8. No network-changing code path exists in the installed diagnostic build. + +## Evidence to save + +For every live-test session keep: + +- WoT client version/build +- SmartReconnect version +- relevant `python.log` excerpt +- whether the visible indicator was red/green +- approximate duration of the outage +- whether WoT recovered by itself or reached the stock timeout + +These observations will be used to choose the final grace-period and guards for v0.3 auto reconnect. + +## Next gate: v0.2 manual reconnect + +After v0.1 passes, implement only the manually initiated reconnect path first: + +```text +Ctrl+K + -> validate state + -> single-flight guard + -> goToLoginByDisconnectRQ() + -> observe real LOGIN state +``` + +Automatic RED-triggered disconnect remains disabled during this milestone. This isolates reconnect-controller correctness from detector correctness. From e5b567b402d9b62f836f1f56ca58b5ac3931807c Mon Sep 17 00:00:00 2001 From: Mewt0 <36929260+Mewt0@users.noreply.github.com> Date: Mon, 7 Sep 2026 14:23:35 +0300 Subject: [PATCH 5/6] Add implementation roadmap --- docs/ROADMAP.md | 179 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 docs/ROADMAP.md diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md new file mode 100644 index 0000000..656cde8 --- /dev/null +++ b/docs/ROADMAP.md @@ -0,0 +1,179 @@ +# SmartReconnect roadmap + +This roadmap is the execution order for the project. Each enabled reconnect milestone is blocked by evidence from the previous one. + +## Milestone 0.1 — Diagnostic detector + +Tracking issue: #2 + +Goal: prove the detector on the exact WoT client without changing the connection. + +Implementation: + +- use the same native signals as the WoT battle debug panel: + - `BigWorld.statLagDetected()` + - `BigWorld.statPing()` +- poll every 0.2 seconds +- require a continuous 3-second RED window before producing a decision +- log `IConnectionManager.isConnected()` and `arena.period` +- emit exactly one `WOULD RECONNECT` per continuous outage +- keep all real disconnect/relogin actions disabled + +Gate to continue: + +- complete `docs/TESTING.md` +- attach exact client build and `python.log` evidence to #2 +- prove short RED spikes do not trigger +- prove sustained RED triggers once +- determine whether the useful window is `lag=True` while ConnectionManager still reports connected + +## Milestone 0.2 — Manual stock disconnect + +Tracking issue: #3 + +Goal: validate the reconnect controller independently from automatic detection. + +Implementation: + +- Ctrl+K only +- resolve `IGameplayLogic` +- add single-flight/busy guard +- reject replay +- validate current client state +- call `goToLoginByDisconnectRQ()` +- log all guards and state transitions +- automatic RED trigger remains diagnostic-only + +Gate to continue: + +- repeated Ctrl+K cannot start duplicate disconnects +- stock cleanup reaches the expected login transition +- no replay action +- no executable/socket manipulation + +## Milestone 0.3 — LOGIN observer + WGC relogin + +Tracking issue: #4 + +Goal: complete a manual end-to-end reconnect. + +Implementation: + +- capture the selected/current server before disconnect +- observe `GameplayStateID.LOGIN` using `IGameplayLogic.addOneshotObserver()` +- no fixed sleeps/magic delays +- invoke supported `ILoginManager` WGC login flow +- handle WGC unavailable, rejected login and expired token as terminal/non-looping outcomes +- reset controller state after success/failure + +Gate to continue: + +- Ctrl+K can disconnect, reach real LOGIN state and initiate exactly one WGC login attempt +- successful login returns through WoT's normal ongoing-battle reconnect path +- failed login leaves the user in a safe login state without an infinite loop + +## Milestone 0.4 — Automatic sustained-RED reconnect + +Tracking issue: #5 + +Goal: connect the validated detector to the validated reconnect controller. + +Implementation: + +- RED must remain continuous beyond the configured grace period +- immediately re-check `IConnectionManager.isConnected()` before forced disconnect +- reject replay and non-reconnectable client states +- one outage -> one automatic attempt +- preserve Ctrl+K manual fallback +- conservative cooldown / retry policy + +Recommended initial defaults: + +```text +autoReconnect = true +lagGracePeriod = 3.0s (subject to v0.1 telemetry) +pollInterval = 0.2s +maxAutomaticAttemptsPerOutage = 1 +manualHotkey = Ctrl+K +``` + +Gate to continue: + +- brief RED spikes never disconnect +- sustained RED triggers once +- stock timeout race does not double-disconnect +- long internet outage does not loop + +## Milestone 0.5 — Hardening + +Tracking issue: #6 + +Goal: make all transitions bounded and predictable. + +Target controller state machine: + +```text +IDLE + -> MONITORING + -> LAG_SUSPECTED + -> RECONNECTING + -> WAITING_LOGIN + -> CONNECTING + -> MONITORING + +Terminal/error paths: + -> LOGIN_IDLE + -> COOLDOWN + -> IDLE +``` + +Required edge cases: + +- threshold-boundary RED->GREEN race +- WoT disconnects itself before SmartReconnect acts +- battle ends during reconnect +- arena changes +- prebattle/loading/afterbattle +- postmortem/spectator +- repeated outages +- WGC unavailable or token rejected +- long local internet outage +- server outage/restart +- clean client shutdown/mod fini + +Every transition must be reconstructable from `python.log`. + +## Milestone 1.0 — Release engineering + +Tracking issue: #7 + +Goal: ship a reproducible and clearly versioned `.wotmod`. + +Implementation: + +- reproducible Python 2-compatible build +- CI syntax/build check +- tag -> release artifact +- exact WoT build compatibility table +- installation/configuration/troubleshooting docs +- release notes +- Fair Play review before release + +## Architecture rules + +1. Prefer current `izeberg/wot-src` call sites over guessed APIs. +2. Verify version-sensitive behavior against `sources/version.xml`. +3. Use stock WoT state/events instead of external ping probes or magic delays. +4. First implementation of any connection-changing behavior must be manually triggered or diagnostic-only. +5. No executable patching, packet manipulation, hidden battle data, combat automation or Fair Play bypasses. +6. One PR should represent one coherent milestone/behavior change where practical. + +## Current status + +- PR #1: diagnostic v0.1 scaffold +- #2: first live-validation gate +- #3: manual reconnect +- #4: LOGIN/WGC relogin +- #5: automatic reconnect +- #6: hardening +- #7: release pipeline From 851041fd37fbbbf1173e7608072ea96c5da077ff Mon Sep 17 00:00:00 2001 From: Mewt0 <36929260+Mewt0@users.noreply.github.com> Date: Mon, 7 Sep 2026 14:24:06 +0300 Subject: [PATCH 6/6] Add Python 2 diagnostic build workflow --- .github/workflows/diagnostic-build.yml | 86 ++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 .github/workflows/diagnostic-build.yml diff --git a/.github/workflows/diagnostic-build.yml b/.github/workflows/diagnostic-build.yml new file mode 100644 index 0000000..5c1425c --- /dev/null +++ b/.github/workflows/diagnostic-build.yml @@ -0,0 +1,86 @@ +name: Diagnostic build + +on: + pull_request: + workflow_dispatch: + +jobs: + build-wotmod: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Prepare diagnostic build tree + shell: bash + run: | + set -euo pipefail + VERSION="0.1.0-diagnostic-${GITHUB_SHA::7}" + echo "SMARTRECONNECT_VERSION=$VERSION" >> "$GITHUB_ENV" + + rm -rf build + mkdir -p build + cp -r res build/ + + perl -i -pe "s/\{\{VERSION\}\}/$VERSION/g" \ + build/res/scripts/client/gui/mods/smartReconnect/Config.py + + META=$( build/meta.xml + + - name: Compile WoT mod with Python 2.7 + shell: bash + run: | + set -euo pipefail + docker run --rm \ + -v "$PWD:/work" \ + -w /work \ + python:2.7 \ + python -m compileall ./build/res + + - name: Verify compiled payload + shell: bash + run: | + set -euo pipefail + test -f build/res/scripts/client/gui/mods/mod_smartReconnect.pyc + test -f build/res/scripts/client/gui/mods/smartReconnect/SmartReconnect.pyc + test -f build/res/scripts/client/gui/mods/smartReconnect/ConnectionMonitor.pyc + test -f build/res/scripts/client/gui/mods/smartReconnect/ReconnectController.pyc + test -f build/res/scripts/client/gui/mods/smartReconnect/Config.pyc + + if grep -R "{{VERSION}}" build/res build/meta.xml; then + echo "Unresolved version placeholder found" + exit 1 + fi + + - name: Package .wotmod + shell: bash + run: | + set -euo pipefail + OUTPUT="mewt0.smartReconnect_${SMARTRECONNECT_VERSION}.wotmod" + cd build + zip -r -0 -X "$OUTPUT" res -i '*.pyc' + zip -r -0 -X "$OUTPUT" meta.xml + mv "$OUTPUT" ../ + cd .. + echo "SMARTRECONNECT_ARTIFACT=$OUTPUT" >> "$GITHUB_ENV" + + - name: Inspect archive + shell: bash + run: | + set -euo pipefail + unzip -l "$SMARTRECONNECT_ARTIFACT" + + if unzip -l "$SMARTRECONNECT_ARTIFACT" | grep -E '\.py($| )'; then + echo "Source .py file unexpectedly packaged" + exit 1 + fi + + - name: Upload diagnostic .wotmod + uses: actions/upload-artifact@v4 + with: + name: SmartReconnect-diagnostic-${{ github.sha }} + path: ${{ env.SMARTRECONNECT_ARTIFACT }} + if-no-files-found: error