CNA-BUG-169: NetworkSession::GamerJoined reports a gamer added with AddLocalGamer twice to a handler subscribed between that call and the next Update
Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit 009d40f5; unless a sentence says otherwise, nothing here was built or executed. Nothing on this page was executed unless the Evidence section says so.
AddLocalGamer inserts the new gamer into AllGamers at once and queues its GamerJoin event for the next Update, while subscribing replays GamerJoined for every gamer already in AllGamers, so a handler subscribed in that window is called twice for the new gamer.
- Identifier
CNA-BUG-169- Category
- Bug
- Subsystem
- Networking & gamer services
- Status
- Open
- Verified against
- CNA
009d40f5(009d40f5dd085c4e674d3479675fac84b12b3e0a) - Severity
- Low (a triage suggestion, not a project priority)
- Evidence basis
- Source-verified: read at TARGET, not executed
- Tests touching this area
- Yes: see Current tests
- Affected contract
- Microsoft::Xna::Framework::Net::NetworkSession::GamerJoined (subscription replay) with NetworkSession::AddLocalGamer and the transport's AddRemoteGamer
Expected behaviour
Each subscriber sees one GamerJoined per gamer: subscribing replays the gamers already present, later joins are raised once. XNA's AddLocalGamer is asynchronous, so the gamer enters the collections and its event is raised inside one Update call and user code cannot subscribe between the two. CNA's constructor comment states the same intent, avoiding a queued event that would double-fire a replayed join.
Actual behaviour at TARGET
AddLocalGamer adds the gamer to LocalGamers and AllGamers immediately and pushes a GamerJoin event that Update raises later. The replay hook installed by the constructor calls each new handler once for every gamer in allGamers_ at subscription time, so a handler subscribed after AddLocalGamer and before the next Update receives the new gamer's join twice. The AddRemoteGamer path (used by the transport inside Update) inserts and queues the same way, but a handler can subscribe in that window only from inside another GamerJoined handler, which XNA's own add accessor and ProcessGamerJoined reproduce, so it is not part of this entry.
Source locations
modules/net/src/Xna/NetworkSession.cpp— constructor SetReplayHook and its comment; AddLocalGamer; AddRemoteGamer; Update's GamerJoin branchmodules/net/tests/Microsoft/Xna/Framework/Net/NetworkSessionTests.cpp— AddLocalGamerRaisesGamerJoinedForAnAlreadySubscribedHandler, AddRemoteGamerJoinsRostersAndRaisesGamerJoined, GamerJoinedReplaysForALateSubscriber
Evidence
Checked by reading NetworkSession.cpp and NetworkSessionTests.cpp at 009d40f5; not executed. The replay semantics (the hook is invoked with the new handler on every Add, before the handler is stored) were read in System/EventHandler.hpp of a sibling Sharp Runtime checkout (next at 41b918c9, not pinned by TARGET). The existing tests subscribe before the add, or replay only construction-time gamers, so none opens the window.
Independent re-verification: Checked by reading NetworkSession.cpp (constructor replay hook, AddLocalGamer, AddRemoteGamer, Update) and the decompiled XNA NetworkSession (GamerJoined add accessor, ProcessGamerJoined, Update, RaiseUpdateEvents, AddLocalGamer); not executed. The replay semantics were read in an unpinned sibling Sharp Runtime checkout.
Focused reproduction
// Illustrative; not compiled or run for this entry. One signed-in gamer, room for two.
NetworkSession* session = NetworkSession::Create(NetworkSessionType::Local, 2, 8);
session->AddLocalGamer(secondSignedInGamer);
int joins = 0;
session->GamerJoined += [&](System::Object*, const GamerJoinedEventArgs&) { ++joins; };
// joins == 2: the replay reported both gamers
session->Update();
// joins == 3: the queued GamerJoin reported the second gamer again
Current tests
The three tests named above; none subscribes between an add and Update.
Regression test
The reproduction as a test expecting two calls, one per gamer; fixed by not replaying a gamer whose join is still queued, or by replaying from the queue's point of view.
Blast radius
Handlers that allocate per-gamer state in GamerJoined (UI slots, maps keyed by gamer) get a duplicate for split-screen players added before subscription, and for remote joins in the nested-subscription case. Handlers subscribed before any add, or after the next Update, are unaffected.
Workaround
Subscribe to GamerJoined immediately after Create or Join, before adding local gamers, and keep handlers idempotent per gamer pointer.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- Internals
- Network session internals: the pump
- Deep dives
- Network sessions: who owns what
- Known issues
- Bug index