Selected vehicle and inventory identity

The hangar's selected vehicle is an inventory object, not a battle entity. Persist its compact descriptor or other stable semantic key when the feature must survive UI reconstruction.

Prebattle systems

Platoons, event queues, and other prebattle contexts can use different controllers and identifiers. Treat joining, updating, leaving, and disconnecting as separate transitions.

Account-session state

Data intended to survive entering a battle should remain owned by the account session. Clear it on actual disconnect, account replacement, explicit completion, or user cancellation.

Do not treat an early cache miss as deletion

During reconnects and fast transitions, the account GUI can appear before the relevant cache has finished filling. A lookup that returns nothing at that moment means not ready yet, not necessarily the item no longer exists.

  • Keep previously confirmed pending state while the account and cache are still being restored.
  • Retry for a short, bounded period and stop if the account, lobby, or feature context changes.
  • Validate inventory only when an action actually requires it, such as activating or replacing an item.
  • Do not let a notification refresh or an early lobby render delete unresolved state.
  • Clear preserved state only after a confirmed account change, explicit cancellation, or an authoritative lookup proves it is no longer valid.
Practical rule

Empty before synchronization is an unknown result. Empty after the correct synchronization event can be treated as real.

Account diffs and cache notification

The Account synchronizes its owned components and caches before emitting broad player update events. Listeners normally see new cache state; the raw diff is not an old-state snapshot. Keep an old value beforehand when a comparison is required.

Initial/full synchronization is special. GUI personality updates selected lobby owners but skips normal items/events-cache refresh and ClientUpdateManager dispatch. Initialize from the authoritative cache after its own readiness event, then use update-manager paths for later changes.

  • Normal account diffs await items and events caches, update lobby context, dispatch path callbacks, and only then update the offers provider.
  • Broad player events are safe fan-out, not an awaited transaction across async listeners.
  • Path callbacks receive a matched raw fragment, not the reconstructed cache value; replacement and deletion markers have distinct meaning.
  • Handlers are strongly retained. Default owner removal is deferred and safer during active dispatch than force removal from a snapshot.

Account commands and streamed responses

Typed Account command helpers allocate a request ID, retain a callback strongly, and dispatch through a matching base-proxy signature. The Python 2 proxy accepts byte str values, not arbitrary Unicode, and unsupported argument shapes fail before sending.

  • The native layer has no public per-command timeout or cancellation. Validate account readiness and give every local busy flag an independent teardown path.
  • Normal and extended callbacks are one-shot; streamed commands add a second completion phase under the same request ID. Transport completion does not prove decompression or payload validity.
  • On account exit, pending command callbacks receive a terminal non-player result and dynamic stream callbacks receive failure. They must clear local state and must not start replacements.
  • A local async timeout does not remove the native callback or stop the server action.

Local teardown and remote outcome are separate. In the current Account implementation, __cancelCommands() notifies pending callbacks and clears their maps; it sends no cancellation or undo request. A terminal RES_NON_PLAYER therefore does not establish server rejection or rollback. Reconcile a mutation with authoritative state when a valid account context is available before considering a retry.

Callback lookup also comes after response preprocessing: onCmdResponseExt() unpickles its payload and calls shop.synchronize() for RES_SHOP_DESYNC before popping the callback. An unmatched request ID therefore does not skip those earlier operations. Shop synchronization has its own guards, including an ignore flag set on account exit, so reaching that method does not prove another request was sent. These are version-sensitive Python control-flow findings; delivery to an old Account after transition was not runtime-tested.

Source excerpt (version-sensitive): the relevant body of PlayerAccount.onCmdResponseExt() shows the boundary directly:

ext = pickle.loads(ext)
if resultID == AccountCommands.RES_SHOP_DESYNC:
    self.shop.synchronize(ext.get('shopRev', None))
callback = self.__onCmdResponse.pop(requestID, None)
if callback is not None:
    callback(requestID, resultID, errorStr, ext)

The presence check protects callback delivery, while decoding and the synchronization entry point precede it. Inspect the linked owner and its guards before reusing this flow; this excerpt is not a replacement command handler.