Skip to content

Player Join Disconnect And AntiStack Lifecycle

rayswaynl edited this page Jul 1, 2026 · 24 revisions

Player Join Disconnect And AntiStack Lifecycle

This page maps how a player enters, reconnects, leaves and gets persisted in the Chernarus source mission. It complements Lifecycle wait-chain, AntiStack database extension audit, Networking and public variables and Respawn/death lifecycle.

Scope And Source Files

Source mission: Missions/[55-2hc]warfarev2_073v48co.chernarus.

Area Source refs
Engine connection hooks initJIPCompatible.sqf:60-66 registers spawned onPlayerConnected and onPlayerDisconnected handlers.
Client join request Client/Init/Init_Client.sqf:410-456 sends/retries RequestJoin; :759-760 sends player-object registration; :960-962 sends CLIENT_INIT_READY.
PV registration Common/Init/Init_PublicVariables.sqf:16 registers RequestJoin; the client HandleSpecial path carries join answers.
Join authorization Server/PVFunctions/RequestJoin.sqf:10-89 checks stored team, launch-side and AntiStack skill before sending join-answer and storing side.
Connect bookkeeping Server/Functions/Server_OnPlayerConnected.sqf:41+ creates or updates WFBE_JIP_USER%UID, funds, wfbe_uid and wfbe_teamleader.
Disconnect cleanup Server/Functions/Server_OnPlayerDisconnected.sqf:31-175 deletes client objects, restores AI/group state, saves funds, clears commander/delegation and writes AntiStack side/score state.
Launch-side raw PV Server/Module/AntiStack/clientHasConnectedAtLaunch.sqf:1+ records WFBE_PLAYER_%UID_CONNECTED_AT_LAUNCH.
Supply player object list Server/Module/supplyMission/playerObjectsList.sqf:1-29 maintains WFBE_SE_PLAYERLIST for supply mission lookup; on current master the update index still resets inside the loop (_i = 0; at :18, the first statement in the forEach at :17) so reconnect always overwrites slot 0 — see the Patch-Ready Findings row — and disconnect cleanup still does not prune stale rows.
AFK intersection Client/FSM/updateclient.sqf:28-31,117-160, Client/Module/AFKkick/monitorAFK.sqf:24-30, Client/Module/AFKkick/handleKeys.sqf:11-15, Server/Module/afkKick/initAFKkickHandler.sqf:9-12, BattlEyeFilter/publicvariable.txt:1-2.

Identity Model

The audited server paths use UID as the durable key, player/group objects as the live team ownership surface, and owner IDs for targeted replies/locality. Player names are log labels, not authority keys, and no profileName use was found in the scoped lifecycle paths.

Identity key Main use Source evidence
UID JIP records, previous side, funds, disconnect persistence, AntiStack score/list storage. Server_OnPlayerConnected.sqf:27,42,65,70,79,93,101; Server_OnPlayerDisconnected.sqf:39,48,125,128,129,133,157,164,170,174,175
Group/object Team leader ownership, object cleanup, commander/delegation cleanup and special-request context. Server_OnPlayerConnected.sqf:45,56,64-66; Server_OnPlayerDisconnected.sqf:32,45,57,68,79,93,102,117,125,128,129
Owner Targeted ACKs, supply-value replies and locality changes. clientHasConnectedAtLaunch.sqf:4,15; Server_PV_RequestSupplyValue.sqf:4,8; Server_SetLocalityOwner.sqf:10,13

This is why reconnect hardening should re-check UID and object identity at the point of cleanup, not only at the start of Server_OnPlayerDisconnected.sqf.

Join Paths

The client derives sideJoined from side player early in Client/Init/Init_Client.sqf. If teamswap protection is active and mission time is past the early-launch grace window, the client sends RequestJoin and waits up to repeated 30-second cycles for WFBE_P_CANJOIN.

When the join request path is skipped at launch, the client instead publishes WFBE_CLIENT_HAS_CONNECTED_AT_LAUNCH = player. The server records WFBE_PLAYER_%UID_CONNECTED_AT_LAUNCH = side _player and ACKs the owner with WFBE_P_HAS_CONNECTED_AT_LAUNCH_ACK. The client ACK receiver stores the full public-variable event tuple (_this) into WFBE_P_HAS_CONNECTED_AT_LAUNCH_ACK, not only the boolean payload, so consumers that only need truthiness work but future code should not assume the variable is a plain bool (Client/Module/AntiStack/hasConnectedAtLaunchACK.sqf:1-7).

For JIP/reconnect, Server_OnPlayerConnected.sqf waits for server init, matches groups by UID in playableUnits, assigns wfbe_uid and wfbe_teamleader, and creates or updates the WFBE_JIP_USER%UID session record.

Branch Intel - B745/B747 JIP Enrollment

Current master origin/master@c4c9a6479 contains the broad B745/LoadoutManager merge, PR #85 B748 settings commit, PR #86 B749 enrollment body-fix and later B75/B750 merges. B750 does not touch the checked JIP enrollment files. The older included 4d16fad70 batch adds a primitive team-roster path for late joiners whose side wfbe_teams group-object array is slow, empty or broken on the client; that primitive path is present in maintained Vanilla too (Server_OnPlayerConnected.sqf:128-131, initJIPCompatible.sqf:228-260, GUI_VoteMenu.sqf:28-34, GUI_Commander_VoteMenu.sqf:26-31). B74.7 connect fallback diagnostics are also present in both roots (Server_OnPlayerConnected.sqf:25,38-45,51,55-65). PR #86 supersedes the prior B74.7.2 maintained-Vanilla gap: both roots now store the client-provided player body in RequestJoin.sqf:95-96, read WFBE_JIP_BODY_<uid> before the playableUnits scan in Server_OnPlayerConnected.sqf:32, and keep the nil-wfbe_side retry guard at Server_OnPlayerConnected.sqf:135-140. That retry guard is not a clean bail: when the resolver cannot find a team (Server_OnPlayerConnected.sqf:118-131) or resolves a team whose group has nil wfbe_side (:135-140), it re-arms the UID-keyed WFBE_CONNECT_RETRY_<uid> counter and re-dispatches itself up to 3 times (:125-130, :137-138), then stops. An exhausted enrollment leaves the player in permanent DEADSPAWN — no team, funds, HUD or markers. See the cmdcon30 editor-slot reaper row below for the root trigger and fixes. Current B74.2 origin/claude/b74.2-aicom@21b62b04, miksuu/master@b8389e74 and perf still do not carry the checked B745/B749 enrollment combination.

Surface Current-master source evidence What to verify before release wording
B74.7/B74.9 server enrollment lookup Both roots now keep the widened lookup budget and B74.7 fallback diagnostics at Server_OnPlayerConnected.sqf:25,38-45,51,55-65. PR #86 adds the B749 primary path in both roots: RequestJoin.sqf:95-96 stores the real networked player body in WFBE_JIP_BODY_<uid>, Server_OnPlayerConnected.sqf:32 reads that body before falling back to playableUnits and side-team scans, and Server_OnPlayerConnected.sqf:78-80 keeps the nil-side retry guard. Dedicated heavy-AI or mid-game late join should prove EAST and WEST get team membership, funds, marker feeds and vote roster state in both roots. RPT should show body-store/team-resolution diagnostics without unbounded retries; release wording still needs exact package/hash smoke even though the prior maintained-Vanilla B74.7.2 gap is source-superseded.
Server connect push Server_OnPlayerConnected.sqf:70-80 re-broadcasts side-logic wfbe_teams for the joining owner; :82-113 builds a side-keyed primitive payload [count, rows] and sends WFBE_JIP_ROSTER_%1 with publicVariableClient. Rows are [leaderName, isPlayer, funds, groupId]; WEST/EAST only because GUER has no commander-vote menu. Dedicated late-join/reconnect under heavy AI load must show the targeted roster push in server RPT and no stale/wrong-side payload on the other side.
Early client receiver initJIPCompatible.sqf:225-247 installs WEST/EAST public-variable handlers before the B56 wfbe_teams wait and before Init_Client; :248-266 polls once for a payload that arrived before handler install, then stores WFBE_JIP_ROSTER_PRIMS / WFBE_JIP_ROSTER_COUNT. Join before and after team registration; prove the receiver or poll-adopt path fires before the vote menu opens.
Client safe empty state Client/Init/Init_Client.sqf:284 defaults clientTeams to [] instead of nil so broken JIP clients avoid forEach nil in vote/marker loops and can still take the primitive-roster path. RPT should stay clean for missing WFBE_%1TEAMS; funds/markers/vote UI must recover once live groups arrive, not stay permanently primitive.
Vote UI consumer GUI_VoteMenu.sqf:10-35,75-99,119-125 and GUI_Commander_VoteMenu.sqf:10-32,69-91 render primitive names until live player-led groups exist, then rebuild from real team indexes. Smoke both vote menus; primitive rows should not blank out, should not be pruned within one tick, and should hand over to live team rows before treating selected player rows as normal vote/reassign targets.

cmdcon30 — Editor-Slot Reaper Deadspawn Root Cause + Fix (2026-06-30)

A 16-agent audit (deployed in cmdcon30) traced the JIP/mid-match "joins into DEADSPAWN — no team, HUD or markers" report to the editor-slot REAPER added by PR #122 (the qol-polish-pack), not to slow wfbe_teams replication. The fix is in current source.

Surface Source evidence Detail
Reaper root cause (now REMOVED) Init_Server.sqf:838-853 PR #122 added an editor-slot sweep in the WFBE_EDITOR_GROUPS_TAGGED block that called deleteGroup on EMPTY west/east/resistance editor player-slot groups to reclaim group-cap headroom. ~13 slots/side are "overflow" slots whose placeholder unit self-deletes at load (mission.sqm slot init="... deleteVehicle this") — leaving an EMPTY group a human can STILL JIP into. The reaper's only discriminator was count (units _x) == 0, so it deleted a JIP-selectable slot's group. A joiner landing in that already-deleteGroup'd group had no wfbe_side, the enrollment resolver re-armed 3x and exhausted (Server_OnPlayerConnected.sqf:118-131,:135-140), and the player was stranded in DEADSPAWN.
cmdcon30 fix Init_Server.sqf:838-853 The reaper was REMOVED entirely — reverted to the pre-PR122 audit-only tag sweep (_x setVariable ["wfbe_group_src", "editor-player-slot", true], no deletion). The empty overflow slots ran the whole pre-PR122 history with no group-cap problem; the only safe reap would skip them anyway since they carry wfbe_persistent, which the persistence-aware server_groupsGC honours and the reaper did not. Never delete a JIP-selectable slot group at boot.
Disconnect retry-clear (related) Server_OnPlayerDisconnected.sqf:20-27 WFBE_CONNECT_RETRY_<uid> is now cleared on disconnect, not only on enrollment SUCCESS (Server_OnPlayerConnected.sqf:143). Closes the same-UID-rejoin permanent-deadspawn hole where a 3x-exhausted player got no fresh re-arms. See the Disconnect Cleanup section above.
getPosVisualvisiblePosition (related) Init_Client.sqf:245 The PR #122 name-tag overlay used getPosVisual, which is Arma-3-only and undefined in A2-OA 1.64 (the file's "A2-safe ... no A3 commands" comment at :230 was FALSE). cmdcon30 swapped it for visiblePosition, the A2 equivalent. Client-side script change → shipped under a new pbo filename (see JIP deploy rule below).
WFBE_Client_Logic nil-guard (related) Client/Functions/Client_FNC_Special.sqf (~:268) cmdcon30 added an isNil guard around a WFBE_Client_Logic setVariable to close a JIP race.

WEST AI-Commander Founding Failure — source-integrated / runtime-pending (cmdcon31, 2026-07-01)

Separate from the deadspawn issue and pre-existing on cmdcon28/cmdcon30. WEST intermittently founds 0 commander teams (server SNAP str=0/teams=0 although the allocator shows ALLOC teams=14) while EAST founds normally. The active release branch now carries the 8de3c4a60 / local b4df22ede starved-infantry fallback candidate, packaged at codex/release-command-center-20260630@c441d6f38df103e32694791f0bca5c5a70428c12, but this remains runtime-pending.

Current source candidate: Server/AI/Commander/AI_Commander_Teams.sqf now admits the cheapest stored-type-0 infantry template when eligibility stripping leaves a side with no infantry at founding time, and logs AICOMGATE|<side>|infFallback|.... This targets Takistan WEST BIS_US infantry templates being stripped by upgrade-0 founding gates when their base team leader requires upgrade tier 1. The earlier WEST HC re-grab/registry churn path remains a telemetry hypothesis to monitor, but do not describe it as the primary fix. Runtime proof must pass the exact-build takistan-west-aicom-infantry-fallback scorer gate for Takistan WEST AICOMGATE|WEST|infFallback, plus TEAM_FOUNDED, CMDRSTAT, autonomous AICOM progress and no SQF errors; HC registry proof (groupSide=civilian, register=true, zero connect-skip) is still required as a supporting gate.

JIP Deploy Rule (reinforced, cmdcon30)

Client-side script changes MUST ship under a NEW pbo FILENAME. A2-OA caches missions by filename; clients re-download unreliably and can serve a stale pbo, which makes a correct client fix look like it did nothing. cmdcon30's client fixes (the visiblePosition name-tag fix, the Client_FNC_Special guard) shipped under a new pbo name for exactly this reason. Server-only changes (the reaper removal, the disconnect retry-clear) take effect in-place on restart. See also JIP enrollment and client data delivery.

Slot And Side Surfaces

Side identity is split across several layers. Do not treat any one of them as the whole authority model:

Surface Evidence Meaning
Playable slots mission.sqm playable unit blocks. Engine/editor side selection and slot availability.
Client side cache Client/Init/Init_Client.sqf:5-9,221-223 The client sets sideJoined / side text from side player.
Server team objects Server/Init/Init_Server.sqf:465-501 Server groups/teams carry wfbe_side, UID and teamleader state.
Join gate Server/PVFunctions/RequestJoin.sqf:17-89 Team-swap and AntiStack checks decide accepted side for JIP/reconnect.

Team Identity And Teamswap Guards

RequestJoin.sqf is the main teamswap gate. It checks:

  • WFBE_JIP_USER%UID_TEAM_JOINED, which records a previously accepted side.
  • WFBE_PLAYER_%UID_CONNECTED_AT_LAUNCH, which records the launch-side signal.
  • AntiStack skill data when AntiStack is enabled.

Successful joins store both WFBE_JIP_USER%UID_TEAM_JOINED and STORE_SIDE. The handler is mostly idempotent, but repeated client retries can still duplicate logs and database writes because the server handler does not explicitly de-duplicate in-flight requests.

Disconnect Cleanup

Server_OnPlayerDisconnected.sqf sleeps for 0.5 seconds, then:

  • Deletes WFBE_CLIENT_%UID_OBJECTS.
  • Finds the player team by wfbe_uid.
  • Restores the player unit to the original group if needed.
  • Saves funds into WFBE_JIP_USER%UID.
  • Clears wfbe_uid and wfbe_teamleader.
  • Clears AI delegation and commander state when the disconnecting team owned those roles.
  • Stores AntiStack score diff and STORE_SIDE NONE when AntiStack persistence is enabled.

The sleep gives Arma time to settle disconnect state, but it also creates a race window. A fast reconnect for the same UID can overlap with stale cleanup, so future hardening should confirm the team variables still match the disconnecting UID before clearing them.

Enrollment-retry counter is now cleared on disconnect (cmdcon30, 2026-06-30). Early in Server_OnPlayerDisconnected.sqf (after the init wait and the __SERVER__/local-player exit) the handler now clears the UID-keyed JIP enrollment retry counter: if (_uid != "") then {missionNamespace setVariable [Format ["WFBE_CONNECT_RETRY_%1", _uid], nil]} (Server_OnPlayerDisconnected.sqf:20-27). Before this, WFBE_CONNECT_RETRY_<uid> was cleared only on enrollment SUCCESS (Server_OnPlayerConnected.sqf:143). So once a JIP enrollment exhausted its 3 re-arms (see the enrollment branch-intel below), a RE-JOIN with the same UID got zero fresh re-arms — the resolver re-bailed immediately and the player was stuck in DEADSPAWN (no team / HUD / markers) until a server restart. Clearing it on disconnect restores a full 3-attempt self-heal for every same-UID rejoin. A2-OA-1.64-safe (setVariable nil); HCs skip the resolver so they never hold the key and the clear is a harmless no-op for them.

Commander disconnect is intentionally only a cleanup edge here. The commander ownership, UI reaction and no-auto-reassignment caveat live in Commander/HQ lifecycle.

AntiStack Persistence

AntiStack enabled mode stores join side, samples score frequently, flushes periodically, and persists score diff on disconnect. Disabled mode avoids the score DB save on disconnect, but first-join side-swap protection and launch-side signals still influence join decisions.

Disconnect persistence is currently fire-and-forget: Server_OnPlayerDisconnected.sqf:151-176 calls the DB wrappers but does not act on failure return codes. If the extension is slow, missing or malformed, a disconnect can look clean in mission flow while persistence silently fails.

The periodic player-list flush prefers confirmed join side from WFBE_JIP_USER%UID_TEAM_JOINED. If that does not exist but a launch-side record exists, the current code uses the launch record only as an existence check and sends current side _x, not the stored WFBE_PLAYER_%UID_CONNECTED_AT_LAUNCH value (AntiStack/flushLoop.sqf:32-40). That distinction matters for teamswap/side-audit fixes.

Use AntiStack database extension audit for the wrapper return-shape risks and external A2WaspDatabase dependency. This page owns the lifecycle edges around when those calls happen.

Raw Public Variable Edges

Several lifecycle signals use global variable names rather than UID-scoped variable names:

  • WFBE_P_HAS_CONNECTED_AT_LAUNCH_ACK
  • WFBE_CLIENT_HAS_CONNECTED_AT_LAUNCH
  • WFBE_C_PLAYER_OBJECT
  • AFKthresholdExceededName
  • CLIENT_INIT_READY

Owner-targeted sends reduce some blast radius, but the names themselves are shared. Future hardening should prefer UID/payload-scoped values where possible and validate sender/player ownership in handlers.

The launch-connect path is especially important because the client publishes a player object through WFBE_CLIENT_HAS_CONNECTED_AT_LAUNCH, and the shipped BattlEye filter only covers kickAFK. Treat launch-side storage as client-pushed until the handler validates sender/UID/side/object consistency.

BattlEye asymmetry is deliberate in the shipped tree: BattlEyeFilter/publicvariable.txt contains only 5 "kickAFK", while AFKthresholdExceededName, WFBE_CLIENT_HAS_CONNECTED_AT_LAUNCH and WFBE_P_HAS_CONNECTED_AT_LAUNCH_ACK are unfiltered mission public variables. Do not describe the included filter as broad lifecycle hardening.

AFK Kick Intersection

AFK enforcement is client-local and currently has two live paths:

  • The client FSM path reads WFBE_C_AFK_TIME, whose current Chernarus parameter default is 15 minutes (Rsc/Parameters.hpp:44-48). updateclient.sqf converts it to seconds, marks WASP_AFK, warns players during the final 10 minutes, switches to per-30-second hints while more than 120 seconds remain, then per-tick second hints under 120 seconds. When elapsed inactivity exceeds the threshold it logs locally and publishes kickAFK, which the in-repo BattlEye publicVariable rule is expected to kick (Client/FSM/updateclient.sqf:28-31,117-160; BattlEyeFilter/publicvariable.txt:1-2).
  • The older AFKkick module is also started by client init (Init_Client.sqf:256-264). It uses keypresses to reset a minute counter, publishes AFKthresholdExceededName and calls failMission "END1" after WFBE_CO_VAR_AFKkickThreshold = 30 minutes (monitorAFK.sqf:19-30). The server handler only logs the reported name (initAFKkickHandler.sqf:9-12).

For public hosting, treat AFK as client UX plus BattlEye/logging signals, not a fully server-authoritative enforcement model. The dual-path shape is also a maintenance risk: future cleanup should decide whether the old AFKthresholdExceededName/failMission path is still wanted or whether the BattlEye kickAFK path should be the single owner.

Patch-Ready Findings

Finding Evidence Patch shape
WFBE_SE_PLAYERLIST update index bug FIXED in master (a51f13ef4-era wikiwins batch): playerObjectsList.sqf:17 now places _i = 0 before the forEach block. The comment reads 'wiki-wins: was reset INSIDE the forEach'. Stale UID row removal on disconnect remains a separate open task.
Disconnect delete-then-setPos contradiction Server_OnPlayerDisconnected.sqf:102 deletes _old_unit; :122 later calls _old_unit setPos. Decide whether the unit should be deleted or relocated, then keep only one behavior. Verify in RPT because deleted-object setPos behavior may be engine-sensitive.
Stale supply player rows playerObjectsList.sqf:31-35 appends when no valid match updates; Server_OnPlayerDisconnected.sqf does not remove WFBE_SE_PLAYERLIST rows. Add UID-based disconnect cleanup and ignore/null-prune stale object refs before supply completion lookup.
Stale disconnect cleanup race Server_OnPlayerDisconnected.sqf:128-129 clears wfbe_uid / wfbe_teamleader after a 0.5-second delay. Before clearing, confirm the variables still match the disconnecting UID and old player object.
Raw lifecycle ACK names are global Lifecycle ACK/request names above are not UID-scoped. Use payload arrays or UID-scoped state; validate owner/player when Arma 2 OA gives enough context.
Launch ACK stores the event tuple hasConnectedAtLaunchACK.sqf:4-6 assigns _this to WFBE_P_HAS_CONNECTED_AT_LAUNCH_ACK, while clientHasConnectedAtLaunch.sqf:13-15 publishes boolean true as the payload. Normalize the ACK receiver to _this select 1 or document tuple truthiness wherever it is consumed.
Join and launch ACK retries are unbounded Init_Client.sqf:416-430 resends RequestJoin every 30-second warning cycle until WFBE_P_CANJOIN; :442-456 repeats WFBE_CLIENT_HAS_CONNECTED_AT_LAUNCH until WFBE_P_HAS_CONNECTED_AT_LAUNCH_ACK. Add bounded retry/backoff and a clear degraded-server message/log path so missing PV handlers do not leave clients in an endless retry loop.
Launch-connect side signal is client-pushed clientHasConnectedAtLaunch.sqf:1-15 records side from the client-published player object and owner-targets the ACK. Validate sender/UID/side/object consistency before storing WFBE_PLAYER_%UID_CONNECTED_AT_LAUNCH; log mismatches once per UID.
Player-list flush does not reuse stored launch side flushLoop.sqf:32-40 sends confirmed WFBE_JIP_USER%UID_TEAM_JOINED when present, otherwise checks for a launch record and sends current side _x. Decide whether the DB player list should represent confirmed accepted side, current engine side or stored launch side, then make the fallback explicit.
AntiStack disconnect writes are unchecked Server_OnPlayerDisconnected.sqf:151-176 calls store-side/score wrappers and ignores return codes. Log failures, consider one bounded retry and expose degraded persistence in the AntiStack audit/performance state.
AFK has two active client-local enforcement paths Init_Client.sqf:256-264 starts the older monitorAFK.sqf path, while updateclient.sqf:28-31,117-160 also runs the parameterized kickAFK BattlEye path. Pick one canonical AFK owner or deliberately document the two-stage policy. If keeping both, align thresholds, warning cadence, logging and disconnect validation.
AFK PV trusts a name string AFKthresholdExceededName carries the client-reported name and kickAFK carries a client-formatted name string. Send [player, uid, name] or derive from owner where possible; log mismatches rather than trusting the string.

Validation Checklist

  • First join with teamswap protection enabled and disabled.
  • Launch players on both sides; verify launch-side ACK is owner-specific.
  • Temporarily suppress the join/launch server reply in a test copy; verify clients get bounded diagnostics instead of an infinite retry loop.
  • Fast disconnect/reconnect with the same UID; confirm no stale cleanup clears the new team record.
  • Supply mission player-object list updates the matching UID row after reconnect; stale-entry removal is still a separate hardening task.
  • Commander disconnect clears commander state exactly once.
  • AntiStack enabled/disabled disconnect paths both avoid wrapper errors.
  • AntiStack enabled disconnect path reports DB write failure instead of silently treating it as success.
  • Launch-connect spoof attempt with mismatched object/side is ignored and logged.
  • AFK timeout logs the correct player and cannot be spoofed by another client.

Continue Reading

Previous: Lifecycle wait-chain | Next: AntiStack database extension audit

Main map: Home | Feature triage: Feature status | PV inventory: Public variable channel index

Sidebar

Clone this wiki locally