Client/server authority model

The client can only act on state delivered to it, derived locally, or exposed through an owned request contract. Rendering a value does not make the view authoritative, and a replicated property is authoritative only for the scope and delivery guarantees declared by its entity or service contract.

State classTypical ownerSafe interpretationMain risk
Server-authoritative gameplay/account stateServer plus native request/result pathSubmit intent; accept the response and later synchronized state as separate observationsOptimistic UI, ambiguous timeout, or retrying an operation that already succeeded
Replicated entity stateServer entity contract, scoped by area of interest and detail levelUse while the entity/component and arena generation remain currentMissing, delayed, observer-dependent, replay-filtered, or destroyed data
Client-derived stateBattle controller, cache, projection, or local ledgerUseful for presentation when its inputs and freshness are explicitMistaking an estimate, cached snapshot, or partial event stream for final truth
View/frontend stateWulf, Gameface, Scaleform, or browser instanceDescribes what that concrete view currently displaysStale route, destroyed native owner, or asynchronous response after teardown

Information the client may not have

Do not infer hidden participants, undisclosed server decisions, final settlement fields, or data outside the current entity/detail/replay contract. Spectator and observer contexts can expose a different controlled vehicle and different presentation data than a live player; replays reproduce recorded client-visible data, not a live account session or a complete server history.

Stale asynchronous results

Capture the account, arena, entity, route, and local generation needed by the request. Before applying a response, confirm both identity and owner lifetime. A technically successful result can still be irrelevant to the view that initiated it.

Readiness gates

A robust request normally needs several gates, not one broad “lobby ready” flag.

  1. The current player is the expected Account or PlayerAvatar for the verified client source.
  2. The owning feature or mode is active.
  3. The dependency container can resolve the controller or service.
  4. The relevant cache has completed synchronization.
  5. The target item, request, or state still exists and is actionable.
  6. No request for the same logical action is already in flight.
  7. The account, event, arena, or view generation still matches the one that scheduled the work.

Optional modules should gate only their own feature. Keep imports, dependency descriptors, class definitions, and initialization for that feature behind the same boundary so a missing optional package cannot prevent unrelated code from loading.

Prefer native processors and action factories

When the client already wraps a server request in a processor or action object, use that layer. It usually owns current validation, request formatting, result messages, and synchronization behavior.

Directly calling a lower-level inventory command can skip validators, produce incomplete messages, or rely on a signature that is less stable than the client’s own action layer.

# Illustrative pattern. Verify the processor and callback contract
# in the current client build.

def request_action(target_id):
    if not _can_request(target_id):
        return

    _busy[target_id] = True
    processor = _create_native_processor(target_id)
    processor.request(_on_action_result)


def _on_action_result(result):
    target_id = result.targetID
    _busy[target_id] = False

    if not _context_is_current(result.contextToken):
        return
    if not result.success:
        _show_native_failure(result)
        return

    _begin_confirmation(result)

With @adisp_process-style APIs, yield the native processor and keep all continuation logic in the same owned flow. A timed delay can be part of that flow when it allows a synchronized cache to catch up, but it should remain bounded and cancellable.

Bulk requests and per-item invariants

Native action APIs sometimes accept a total amount plus a list of targets rather than an explicit amount per target. If your feature must preserve a per-target reserve, cap, or safety invariant, do not assume the server will consume from the target list in the order your UI displayed.

  • Recompute eligibility immediately before submission.
  • Round requested values to the native action's valid step size before sending.
  • Split the operation into sequential single-target native requests when the native bulk contract cannot express the per-target invariant.
  • Stop the sequence on rejection, context change, cache mismatch, or exhausted retry budget.
  • Show the user that multiple native confirmations or result messages may appear when the safe path uses several independent requests.

Busy flags should match independent operations

Use one in-flight guard per independent request domain. Two independent server-changing request domains can complete separately and should not share one global lock.

  • Set the guard immediately before submitting.
  • Clear it before processing success or failure so an exception in the callback cannot permanently disable the feature.
  • Bind target IDs and item objects into callback closures; never rely on the loop variable still containing the intended value.
  • Do not blindly retry a state-changing request after an ambiguous result.
for target in pending_targets:
    if _busy_by_type.get(target.kind):
        continue
    _busy_by_type[target.kind] = True
    _submit(target, lambda result, target=target: _finished(target, result))

React to synchronized state, not request bursts

For repeated claims, openings, or activations, submit one request and continue from its response or the system’s synchronized update event. Re-read the count and availability after a short delay before deciding whether another operation remains.

Stop the chain when the count reaches zero, the owning feature is disabled, the account context changes, the user leaves the relevant screen, the retry budget is exhausted, or a request fails.

Response success versus cache confirmation

A server response can report success before the client cache exposes the resulting inventory change. When the user-facing message claims that an exact item was received or consumed, verify the observable state separately.

  1. Snapshot the exact target item and quantity before submission.
  2. Preserve the server-reported item identifier and count.
  3. After success, perform bounded checks of the synchronized cache.
  4. Show the success confirmation only after the expected change is observed.
  5. If confirmation times out, log the mismatch and do not repeat the same request automatically. The operation may have succeeded and the cache may simply be late.