Live battle events, immediate result notifications, full result payloads, replay playback, and replay time warp expose overlapping but non-identical data and lifecycles.
Result stages
| Stage | Characteristics |
|---|---|
| Live battle state | Incomplete, mutable and tied to current entities/controllers |
| Battle end notification | Signals completion but may not contain full statistics |
| Full result payload | Server-confirmed records available later or through a cache/request |
| Stored result | Historical data with its own retention and availability rules |
| Replay playback | Recorded client-visible state without a live server session |
| Replay time warp | A separate UI and entity rebuild boundary inside the same replay |
Deduplicate by arena identity
Reconnects and repeated notifications can deliver the same battle more than once. Track processing state by a stable result or arena identifier and keep requested, received, validated, and persisted states distinct.
_processed_results = set()
def handle_full_result(result):
battle_id = extract_battle_id(result)
if battle_id is None or battle_id in _processed_results:
return
validated = validate_result(result)
if validated is None:
return
store_result(validated)
_processed_results.add(battle_id)
Replay differences
- No live server method should be expected to work.
- Some services may be absent or expose playback-specific state.
- Seeking can recreate avatars, controllers, entities, view injectors, GUI roots, and replay-delivered properties.
- Recorded data can omit information that was never exposed to the client.
- Features should declare whether they fully support, partially support, or disable themselves in replays.
Time warp is a lifecycle boundary
Replay seek is not merely a change in playback time. It can invalidate battle-side objects while the replay remains active. Treat time-warp start and finish as their own teardown and recovery boundary.
- A replay event hub can expose start and finish events. Their exact module path and dispatch order are version-sensitive.
- The replay controller can remain in playing state while time warp is active, which makes playback state a useful guard for deferred recovery.
- Do not assume normal battle exit, avatar transition, arena-period, or GUI-space events fire in the same order during every seek.
- Reconnect into an in-progress live battle is a separate lifecycle case and may need its own avatar-entry recovery kick.
Classify the state owner
Recovery must occur at the boundary that owns the stale state.
| Feature state | Typical owner | Recovery action |
|---|---|---|
| Scaleform child view | Top-level injector or container alias | Destroy and recreate the injector or owned child, then republish state. |
Raw GUI roots |
Battle/controller instance or explicit root registry | Remove every owned root and stop the shared refresh callback. |
| Gameface/Wulf overlay | Battle shared page or current main window | Destroy before the stale parent disappears; recreate after the current page is populated. |
| Recurring callback loop | Controller or mod-wide runtime | Cancel the stored callback ID and start exactly one replacement. |
| Python cache | Arena, avatar, vehicle, or replay generation | Clear only the transient scope and rebuild from authoritative playback state. |
| Monkey patch | Mod-wide installation | Normally retain it; restore and reinstall only when the patched owner is actually replaced. |
Idempotent rewind handling
_rewind_generation = 0
_recover_callback = None
def on_time_warp_start():
global _rewind_generation
_rewind_generation += 1
cancel_recovery_callback()
stop_owned_loops()
destroy_battle_ui()
clear_transient_replay_state()
def on_time_warp_finish():
token = _rewind_generation
def recover():
if token != _rewind_generation:
return
if not is_replay_playing():
return
recreate_battle_ui_from_authoritative_state()
schedule_zero_delay(recover)
- Retain the exact handler objects used for replay-event removal.
- At start, stop callbacks and animations before destroying UI and clearing transient state.
- At finish, defer recreation by one client callback when the owning containers need to settle.
- Guard every delayed recovery with a generation token.
- Make repeated start/finish cycles converge to one view, one root set, one callback loop, and one hook layer.
Scaleform recovery
A surviving top-level injector can retain a stale child after time warp. When evidence shows this, destroy the view by its registered alias, clear the mod's own loaded flags, then recreate after the container settles.
- Destroy a child alias separately only when it is an independently loaded top-level view. Normal children should be released by their injector.
- Re-publish configuration, data, and visibility from authoritative state in the recreated view's populate phase.
- Do not force-show a feature that was disabled or hidden before the seek.
- Restore only wrappers still owned by the mod and clear hook flags in
finally.
Raw roots and callback registries
Raw GUI components can outlive the controller that created them when normal teardown is skipped. Maintain an explicit owner registry when several instances contribute roots to one shared refresh loop.
_roots_by_owner = {}
_refresh_callback = None
def register_root(owner_id, component):
_roots_by_owner.setdefault(owner_id, []).append(component)
GUI.addRoot(component)
def clear_all_roots():
global _refresh_callback
cancel_callback(_refresh_callback)
_refresh_callback = None
for roots in _roots_by_owner.values():
for component in roots:
safe_remove_root(component)
_roots_by_owner.clear()
Preserve user state
- Creation handlers must check the feature toggle before recreating anything.
- Recreated views should apply stored visibility rather than defaulting to visible.
- Retain durable settings, but clear arena-, avatar-, entity-, or replay-generation state.
- Do not treat time warp as a new account session or a completed battle.
Optional replay support
Replay integrations should fail closed. Guard version-sensitive event and UI imports so their absence disables only replay recovery rather than preventing the mod from loading in live play.
def install_replay_support():
try:
from ReplayEvents import g_replayEvents
except ImportError:
return False
g_replayEvents.onTimeWarpStart += on_time_warp_start
g_replayEvents.onTimeWarpFinish += on_time_warp_finish
return True
Result schemas
Participant, vehicle, team, and personal records can use different keys. Validate optional fields, distinguish zero from missing data, and do not reuse live entity references while processing delayed results.
Result view models are presentation
Battle-results presenters and generated view models are useful places to repaint local presentation, but they are not the source of truth for rewards, economy, achievements, or account state. Treat presenter hooks as view refresh boundaries unless the current source proves that the presenter delegates to a native server-changing action.
- Read final values from the result payload or the owning cache before deriving display-only totals.
- Keep local-only result flags scoped by arena or result identity so the next result opens cleanly.
- Do not let a repaint hook submit a reward, multiplier, currency, or inventory request as a side effect.
- If a button is made local-only, mark that state locally and repaint the model; do not reuse the server apply handler accidentally.
Recording gates, tags, and support
Replay eligibility, replay metadata tags, and complete playback support are separate contracts. The current replay-disable range is mutable and can be populated by feature registration. Avatar entry consults that range and can disable and delete the partial native recording for a matching GUI type.
- Inspect the live range before assuming the gate is active; current readable personality source does not prove that any feature populated it.
- A registered replay mode tag is metadata written to the recording. It does not populate the disable range or prove recording eligibility.
- Keeping the recorder enabled does not prove that feature entities, controllers, GUI, serialization, settings, and playback branches reconstruct a usable replay.
- A missing tag does not by itself prove playback is unsupported. Trace the feature's explicit replay branches and verify it at runtime.
Replay settings snapshots
The recorder snapshots selected server configuration into arena metadata. The key set combines typed schemas registered for replay with a mutable feature configuration collection. During playback, feature helpers can fall back from unavailable live settings to that snapshot.
Registration order is part of determinism: register every required key before the account snapshot is taken, and explicitly forward later changes when playback needs their final value. Keeping recording enabled cannot recover a configuration that was never captured.
Common mistakes
- Treating the battle-end signal as a complete result.
- Saving the same result twice after reconnect or repeated delivery.
- Using live entity references while processing delayed results.
- Sending network requests during replay playback.
- Assuming every mode uses the standard result schema.
- Treating a generated result view model as proof that the server state changed.
- Repainting stale replay UI without rebuilding the owner that became invalid.
- Recreating during time-warp start, before containers and entities have settled.
- Force-enabling a feature that the user had hidden or disabled.
Verification matrix
| Test | Expected invariant |
|---|---|
| Normal live battle | No replay-only handler changes live behavior. |
| Normal replay playback | Exactly one UI/root/callback set exists. |
| One rewind | Stale owners disappear and one replacement is created. |
| Several consecutive rewinds | Counts remain stable; no duplicate wrappers, roots, or loops accumulate. |
| Rewind while disabled | The feature remains disabled and creates no visible UI. |
| Replay exit | All replay-owned callbacks, roots, views, and subscriptions are released. |
| Live reconnect | Recovery uses the live avatar/page boundary rather than replay assumptions. |
During diagnosis, log time-warp start and finish, generation, active view/root counts, callback IDs, and enabled/visible state. Visual success alone does not prove an old timer, hook, or root was released.
Live values, projections, and final authority
Battle feedback can omit values known only in final results. Label live calculations as
known-so-far or projected, then use the final result for authoritative reconciliation. Choose
one source of truth for each metric and deduplicate final processing with
arenaUniqueID.
See Battle Data Pipelines for baselines, derived metrics, cache fallbacks, respawn, and estimate labeling.
Result revisions and accounting
Posting, cached reopening and additional-XP refresh are different result-service paths. A post can repeat for one arena, while retained result data can change without another post. Persistent analytics must replace a prior arena contribution when supported totals change. See Battle Results and Accounting for revisions, aggregate-first rows, currency/sign handling and limits on reconstructing hit history.