Documentation

Aero Client Docs

How Aero is built, the window.__AERO__ script API, and everything you need to write userscripts that run cleanly inside the client.

Overview & Architecture

Aero Client is a Tauri (Rust + WebView2 / WKWebView) wrapper around kirka.io. Before any page JavaScript runs, the client injects a single initialization script into the game window. That script:

  • Creates the global window.__AERO__ object (settings, helpers, UI toolkit, script-settings registry).
  • Sets up the resource swapper, SPA navigation detection, window.open routing, keyboard shortcuts, FPS cap, and raw mouse input.
  • Defines the Menu class (the in-game Mod Menu) and the game preload logic.

Userscripts are not part of this init script. They are loaded separately at runtime: the game preload calls a backend command to read the scripts, then evals the ones that are enabled. Enabling or disabling a script does not require rebuilding the client.

Two kinds of scripts

  • Imported scripts.js files the user drops into a folder on their computer.
  • Bundled "Base" scripts — scripts shipped inside the client (compiled in). They always appear and cannot be deleted by the user.

Both kinds behave identically at runtime and use the same APIs.

Folder Layout

All user data lives under the OS Documents directory:

paths
Documents/AeroClient/
├─ settings.json      # client settings (source of window.__AERO__.settings)
├─ scripts/          # imported userscripts (.js) — one file = one script
├─ CSS/              # imported stylesheets (.css), managed by the CSS tab
└─ swapper/assets/   # resource-swapper replacement files

The scripts and CSS folders are created automatically on launch. The Mod Menu has buttons to open these folders directly.

Installing a Script

  1. Open the Mod Menu and click Open Scripts Folder (or browse to Documents/AeroClient/scripts/).
  2. Drop your .js file in. One file is one script.
  3. Back in the Mod Menu, the script appears as a card — click its toggle to Enable it. It runs immediately, no refresh needed.
Base scripts ship with the client and always appear under the Base filter. Imported files show under Imported.

File Format & Metadata Header

A script is a plain .js file. The loader reads an optional metadata header from the first 15 lines using simple line-prefix matching:

  • // @name <text> — display label on the Mod Menu card. If omitted, the filename without .js is used.
  • // @category <text> — grouping label. If omitted, defaults to Imported. Only the value HUD is special-cased by the menu filters; any other category is stored but not specially filtered.
  • // @author <text> — script author. If omitted, it will say that the author is unknown.

Other common userscript headers (// @version, // @description, // @match, // @run-at) are ignored by the loader — they're conventional/cosmetic only. The client decides when and where scripts run, not the @match/@run-at directives. A // ==UserScript==// ==/UserScript== block is conventional but not required.

Filename is the identity. The filename — including the .js extension and exact spacing/casing (e.g. Custom Skin Link.js) — is the stable identifier used everywhere: in the enabled/favorite/seen lists, and as the key for registering a settings panel.

Lifecycle, Timing & Execution Context

  • When: scripts run after DOMContentLoaded in the game window. The init script (and window.__AERO__) is already fully available.
  • How: each enabled script's source runs via indirect eval in the global scope. Wrap your script in an IIFE. 'use strict' is fine.
  • Which: only scripts whose filename is in the enabled_scripts setting are executed. Disabled scripts never run.
  • Live enable: toggling a script ON runs it immediately (no refresh) by dispatching an event carrying the script's source.
  • Live disable caveat: toggling OFF only removes it from the enabled list — code that already ran stays active until refresh (F5). Scripts needing clean teardown should no-op when disabled or instruct the user to refresh.
  • Host allowlist: the preload only initializes on kirka.io, snipers.io, ask101math.com, fpsiogame.com, cloudconverts.com.
  • Error isolation: an exception while running a script is caught and logged to the console (prefixed [Aero]); it won't crash the client.
No DOMContentLoaded listeners. Because scripts run at/after that event, listeners attached to it never fire. kirka.io is a Vue SPA, so most in-game elements appear later — use a MutationObserver or polling to wait for the game DOM.

Your First Script

Wrap everything in an IIFE and wait for the SPA DOM with a MutationObserver instead of DOMContentLoaded:

javascript
// @name        My First Script
// @category    HUD

(function () {
  'use strict';

  // Scripts run after DOMContentLoaded — game DOM may not exist yet.
  const obs = new MutationObserver(() => {
    const hud = document.querySelector('#app .game-hud');
    if (!hud) return;
    obs.disconnect();

    // build your feature here
    const tag = document.createElement('div');
    tag.textContent = 'Hello from Aero';
    hud.appendChild(tag);
  });
  obs.observe(document.body, { childList: true, subtree: true });
})();

Base vs Imported Scripts & Precedence

The backend returns a combined list: bundled built-ins first, then imported folder scripts.

  • Precedence: if a folder file has the same filename as a bundled built-in, the folder file is skipped — the shipped version always wins. Users can't shadow a base feature by dropping a same-named file.
  • Built-ins are flagged builtin: true, show a small BASE badge, and appear under the Base filter. Imported scripts appear under Imported.

Currently shipped base scripts

  • Custom Skin Link.js — recolor the default skin or supply a custom image URL/base64.
  • Kirka0ads.js — per-weapon ADS zoom power (0ads) with images.
  • gun_scale_modifier.js — resize the view-model (gun) with keybinds.
  • CustomServerPreset.js — save/apply/build custom room-creation presets.

Per-script entry the menu receives

  • name — the filename (stable key).
  • label — display name (from @name or filename).
  • content — the script source.
  • category — from @category or Imported.
  • builtintrue for shipped base scripts, false for folder scripts.

The window.__AERO__ Global

Properties and methods available to every script:

  • settings — live object of the client's current settings. Read for client state (e.g. base_url, menu_keybind, feature toggles); kept in sync as the user changes settings.
  • swapper — the resource-swapper URL→local map.
  • version — the client version string.
  • logo — the Aero logo as a base64 data URI (the menu is injected into a remote page, so local asset paths can't be used).
  • menuHTML, menuCSS — raw markup/styles for the Mod Menu (used internally).
  • invoke(command, args) — thin wrapper over the Tauri invoke bridge; calls a Rust backend command and returns a Promise. See Backend Commands.
  • scriptSettings — internal registry mapping a script filename to its settings-panel builder.
  • registerScriptSettings(name, builder) — register a settings panel for a script.
  • ui — the UI toolkit for building native-looking settings panels.

Adding a Settings Panel

A script can add a settings panel that opens from the gear icon on its Mod Menu card. Call window.__AERO__.registerScriptSettings(name, builder):

  • name — must exactly equal the script's filename, including .js and any spaces/casing (e.g. gun_scale_modifier.js). This is how the menu matches a panel to a card.
  • builder — a function called as builder(panel, ui) each time the user opens the gear.

Always guard the call so the script also works outside the client:

javascript
(function () {
  const KEY = 'myscript_glow';

  function build(panel, ui) {
    ui.note(panel, 'Configure My First Script.');
    ui.toggle(
      panel,
      'Enable glow',
      localStorage.getItem(KEY) === '1',
      (on) => localStorage.setItem(KEY, on ? '1' : '0'),
      'Adds a glow to the HUD.'
    );
  }

  if (window.__AERO__ && window.__AERO__.registerScriptSettings) {
    window.__AERO__.registerScriptSettings('My First Script.js', build);
  }
})();

Behavior of the gear/settings view

  • The settings view is in-place inside the Mod Menu (a body element with a back arrow), not a popup.
  • panel is that body element; it is cleared before each open, and builder runs fresh every time. Don't cache DOM nodes across opens.
  • ui is the same object as window.__AERO__.ui.
  • If the script is currently disabled, the menu shows "Enable this script to configure it." instead of calling your builder.
  • If a script has no registered panel, the menu shows a fallback "how to edit this script" guide with an "Open Scripts Folder" button.
  • If your builder throws, the menu shows "Failed to load this script's settings." and logs the error.

Scripts persist their own state in localStorage — the UI toolkit does not auto-persist; your onChange callbacks decide what to store.

The ui Toolkit

All builders append to a parent element and return the created element (or a small object). Styling piggybacks on the client's menu CSS so panels look native.

  • note(parent, text) — appends a small muted sub-label. Returns the span.
  • row(parent, label, control, sub) — a standard setting row: left-side label (with optional sub description) and a right-side control. Returns the row. This is the building block the others use; use it directly for custom controls. (Insert your own element before the row's .setting-info to add e.g. an icon/thumbnail on the left.)
  • toggle(parent, label, checked, onChange, sub) — a square checkbox row. onChange(newChecked). Returns the input.
  • text(parent, label, value, onChange, opts) — a text input row. onChange(value). opts: type (default text), placeholder, sub, image: true (renders next to a live image preview for URL/base64 fields). Returns the input.
  • imagePreview(url) — returns { wrap, set(url) }: wrap is the preview element to insert; set('') clears it; invalid/empty states show "No image"/"Invalid image".
  • slider(parent, label, value, onChange, opts) — a range slider row. onChange(value) fires on input. opts: min, max, step, sub. Returns the input.
  • select(parent, label, options, value, onChange, sub) — a dropdown. options is an array of strings, or [value, label] pairs. Returns the select.
  • button(parent, label, onClick) — a full-width client-style button. Returns the button (override inline width/padding to make it inline-sized).
  • keybind(parent, label, value, onChange, sub) — a "press a key" capture button. Captures event.key (e.g. a, Space, ArrowUp) — different from the client's own hotkeys, which store event.code. Returns the button.

Most helpers return the underlying control element so you can further tweak styles/attributes after creation.

Persisting Data & Events

Persisting script state

  • Scripts manage their own persistence with localStorage — the UI toolkit saves nothing automatically.
  • Namespace your keys with a script-specific prefix to avoid collisions (e.g. csl_…, gsc_…, kirka_ads_…, kirka_room_presets).
  • localStorage persists across sessions and refreshes.

Client settings

  • Read client settings via window.__AERO__.settings.
  • Change a client setting with invoke('update_setting', { key, value }) (for client settings — most scripts just use localStorage for their own state).

Events

Dispatched on document:

  • aero-settings-changed — a client setting changed; detail is { setting, value }.
  • aero-run-script — a script was enabled live; detail is { name, content }.
  • aero-css-folder-changed — the active folder CSS changed; detail.content is the new CSS (or empty).

Dispatched on window:

  • aero-url-change — SPA navigation; detail is { url }. React to entering lobby/game/servers/profile/market/friends.
javascript
window.addEventListener('aero-url-change', (e) => {
  if (e.detail.url.includes('/profile')) {
    // re-apply your feature for this view
  }
});

Scripts may also define and dispatch their own CustomEvents to coordinate between a settings panel and an in-page panel.

Backend Commands (invoke)

Call via window.__AERO__.invoke(command, args) → Promise.

  • Settings: get_settings, update_setting ({ key, value }), reset_settings.
  • Scripts/CSS: get_scripts, get_scripts_path, open_scripts_folder, get_css_files, open_css_folder, import_css ({ name, content }).
  • Swapper: open_swapper_folder.
  • Window/app: toggle_devtools, toggle_fullscreen, navigate ({ url }), close_app, restart_app, launch_game, check_for_updates.
  • Clipboard: get_clipboard_text, set_clipboard_text ({ text }).
  • System: open_external ({ url }), set_autostart ({ enable }), update_rpc_state ({ url }).
javascript
const settings = await window.__AERO__.invoke('get_settings');

window.__AERO__.invoke('update_setting', {
  key: 'menu_keybind',
  value: 'KeyP'
});

Most scripts only ever need update_setting/get_settings (and rarely open_scripts_folder). Everything else is used by the client's own menu.

CSS Classes & Variables

When building a settings panel (or any element injected into the menu), these classes are available (all scoped under the menu root). Use them so your UI matches the client:

  • Layout/rows: setting, setting.column, setting-info, setting-header, setting-sub.
  • Controls: checkbox (square toggle), text-input, range (slider), change-keybind, aero-button (and aero-button.large).
  • Image fields: config-image-row, image-preview, image-preview-empty.
  • Cards: card, card-top, card-name, card-icon, card-bottom, card-settings, card-toggle, card-fav.

Color custom properties are stored as RGB triplets and used via rgba(var(--name), alpha):

  • --cyan — accent/active color.
  • --light — primary text.   --lgray — muted text.
  • --dgray — input/control background.   --panel — panel background.   --border — border color.
  • --en-bg, --en-text — "enabled" background/text (good for active toggle states).
  • --close-bg — red/destructive tint.   --refresh-text, --white — extra accents.
Practical tips: text-input is capped at ~45% width — set flex: 1; max-width: none to fill a row. aero-button is width: 100% — set width: auto + padding for inline size. For a compact multi-select, style small toggle "chips" using --dgray/--lgray for off and --en-bg/--en-text for on.

Mod Menu Behavior

The Mod Menu lists feature cards. For scripts, each card shows:

  • The script label, with a NEW badge if unseen and a BASE badge if bundled.
  • A favorite (heart) icon (scripts only) — toggles membership in the favorites list.
  • A scroll icon.
  • A gear (settings) icon — opens the in-place settings view (registered panel, or the "how to edit" fallback).
  • A toggle button showing Enabled/Disabled.

Enable/disable

  • Clicking the toggle flips membership in the enabled list and re-renders the card.
  • Enabling runs the script live (dispatches aero-run-script); disabling removes it from the list (running code persists until refresh).
  • The gear and the toggle are independent: the gear opens settings, the toggle never navigates.

Filters

  • All — everything.   New — scripts not yet seen (the NEW badge clears after first appearance).
  • HUD — cards whose category is HUD.   Base — bundled built-ins.   Imported — user folder scripts.
  • A favorites-only filter and a grid/list layout toggle are available. Search filters cards by name.

Relevant settings lists (arrays of filenames): enabled_scripts, favorite_scripts, seen_scripts (and for CSS: enabled_css, favorite_css).

Gotchas & Best Practices

  • Filename = identity. The name passed to registerScriptSettings must exactly match the filename (with .js, spaces, and case).
  • No DOMContentLoaded. Scripts run after it. Set up immediately and use observers/intervals for late-loading SPA DOM.
  • Wrap in an IIFE. Scripts share global scope; avoid leaking globals and name collisions.
  • The settings builder re-runs and the panel is wiped each open. Rebuild your UI every call; don't hold references between opens.
  • Don't throw in the builder — it's caught, but your panel won't render.
  • Disabling doesn't tear down. If your script patches globals or adds observers, guard behavior behind a live localStorage/setting check, or tell users to refresh after disabling.
  • Persist with localStorage, namespaced per script.
  • Keybind capture uses event.key, not event.code.
  • Built-ins win over same-named folder files.
  • Categories: only HUD is special in filters; pick it if your script is a HUD feature.

Bundling a Base Script (maintainers)

For maintainers shipping a script with the client rather than as a user import:

  1. Place the .js file in the client's in-tree scripts source folder (src/scripts/).
  2. Register it in the backend's built-in scripts table (the list compiled into the binary), keyed by its exact filename.
  3. It will then always appear under the Base filter, take precedence over any same-named folder file, and behave exactly like an imported script otherwise.
Questions or want to publish a script? Join the Discord.