1.19.4 is the production release after 1.19.3: the testing line 1.19.3.<N> promoted as it stands (owner, 2026-11-01: "1.19.4 release checklist is finished ready for takeoff", then yes on the release card). Every change below was made, explained and built in one of those testing builds. The short version is on the updates page.
- Network protocol 66 (1.19.3 used 53: 54 power; 55-58 rocket thrust and launch speed; 56 walk teleporters; 59 winner stays; 60-61 dash charges (back to two); 62 ball; 63 no air drift; 64 the dash slide; 65 the grapple catches the ball; 66 rocket knockback doubled). A 1.19.4 game only plays with a 1.19.4 server, so update games and servers together.
- Map format 13 (1.19.3 wrote 10: 11 mode spawn sets, 12 walk teleporters, 13 a race map's default time). Older maps convert on load and on disk, originals kept in
maps/old/. - Human-made assets: 16.1% (no asset changes since 1.19.3).
Settings: batch 1, the 4s (owner, 2026-10-30: "go ahead and start working on implementing all of the settings ... score all the setting out of 4"; q ticked all 231 on the scored list)
- MORE pages. CONTROLS and VIDEO + AUDIO were full (the VIDEO page's comments already say a row more pushes SOUND into DEVICES), so each gets a MORE page (footer middle,
moreOpen_, screen paths/controls/more,/video/more,--settingstab controls/more). The rest of 1.19.4's settings go on pages like these instead of squeezing the tabs. - Mouse (#4).
sens_y(vertical as a multiple of SENSITIVITY),invert_y,zoom_sens(a multiple of the existing "same feel" zoom scaling, so x1.00 is today's behaviour). Applied inGame::turn. - Scoreboard toggle (#67).
score_toggle: the key flipsscoresOn_instead of being held. - Background cap and mute (#9).
bg_fps(default 60, 0 = FPS LIMIT as usual) capsGame::maxFps()while the window isn't focused or is minimized (new iconify callback);bg_mutesets the master volume to 0 then. Why: an alt-tabbed game running uncapped burns power and heat for nothing; focused play is untouched. - Window (#119, #1, #166). The windowed size and position are saved on every move / resize (not while maximized or fullscreen) and restored at launch if the title bar lands on a monitor that's still there; WINDOW SIZE offers 720p up to 4K that fit the monitor. A display change (DISPLAY or WINDOW SIZE) shows KEEP / REVERT with a 10 s count and reverts by itself (not in the UI test, whose clicks would race the count).
- Dedicated GPU (#113).
NvOptimusEnablementandAmdPowerXpressRequestHighPerformanceexported from the Windows game, so hybrid laptops use the dedicated GPU; VIDEO > MORE shows GL_RENDERER and hints when it looks integrated. - Shadow quality (#3).
Renderer::setShadowResreallocates the shadow map in place (1024 / 2048 / 4096, clamped to the GPU's limit) and updates both programs' texel size; it used to be cfg-only and read at startup. - HUD size (#7).
hud_sizemultiplies every HUD element's own SIZE (each element'skandhudFont). The menus already scale with the window height, so no separate menu scale was added. - Crosshair (#13).
ch_dot(0 = THICKNESS),ch_opacity,ch_outline_w,ch_outline_a,ch_wstyle_<weapon>(a weapon's own style, -1 = the normal one), and COPY / PASTE CODE: the ch_ lines as onequark-crosshair:line; pasting reads only ch_ keys, so a code can't change anything else. - Hitsounds (#14, #152).
hit_volume(0-2),hit_pack(-1 = your SOUNDS pack),headshot_sound(a higher ding over the hit on a headshot),kill_sound(was tied to HITSOUND). - Server list (#78).
browser_sort,browser_hide_full,browser_hide_empty,browser_max_ping,fav_servers(right-click a row; favourites are listed first and never filtered out),recent_servers(the last 8 joined; RECENT cycles them into the address field). The list shows 5 rows instead of 6 to make room for the filter row. - Race reset (owner, 2026-10-30: "keybind R should restart the map/reset to checkpoint in race/surf modes"). New action RACE RESET (R; bind_version 4 leaves it unbound if R was already taken) sends
racereset; the server respawns you at your last checkpoint, or withracereset full(no checkpoint yet, or the full restart) at the start with the checkpoint count and splits cleared. Nothing new on the wire: it's a player command and the existing EV_SPAWN. Tap or hold (owner, 2026-10-30: "r tapped = checkpoint, r held for 250ms = full restart"; it was a double press): the checkpoint is sent when R is let go before 250 ms, so a tap acts on release, the earliest moment a tap can be told from a hold. A separate FULL RESTART action (MORE KEYS, unbound) sendsracereset fullon press.
Settings: batch 2, the 3s (first half)
- Pages.
moreOpen_is now a page number: GAMEPLAY > MORE (1 page), CONTROLS > MORE (1), VIDEO + AUDIO > MORE (3, the footer button reads MORE 2 / MORE 3). Screen paths/gameplay/more,/video/more2,/video/more3; CONTROLS gets a third keys sub-page, MORE KEYS (/controls/keys2), because the other two key pages have no free rows. - Render scale 25-400% (#250). Was 50-200%. The renderer already capped the target at the GPU's largest texture, so 400% of a 4K window is clamped to what the driver allows rather than failing.
- Fog (#43).
fog: a multiple of the old fixed density 0.0035 (0 = none), set in both the world and model programs (Renderer::setFog; the world program keeps it across map changes). - Gamma / contrast / saturation (#29).
gamma,contrast,saturation, agrade()at the end of the world, model and sky shaders (after tonemapping and gamma encoding), so the HUD and menus aren't touched. At the default (1, 1, 1) it's one uniform compare per pixel and returns unchanged. Why in those shaders: there's no full-screen post pass at render scale 100% with AA off, and adding one would cost a full-screen draw every frame. - Video presets (#56).
video_presetcycle: LOW (no player shadows, 1024 shadows, no AA, 75% scale, effects 0.6), MEDIUM (shadows 2048, SMOOTH AA), HIGH (4096, MSAA 4x), COMPETITIVE (no shadows, no AA, no fog, effects 0.5, no kill bursts or recoil kick). It sets those settings once; anything changed afterwards stays. - Effects (#30, #49).
fx_opacityscales every explosion / spark / burst box's alpha (0 = none drawn),kill_bursts,tracers,view_recoil(the gun model's kick; also moves the muzzle point, so shots still leave the gun you see). Muzzle flash and impact sparks were already switches. - Colourblind presets (#11). Two more TEAM COLORS presets: RED-GREEN (blue / yellow-orange) and BLUE-YELLOW (cyan / red).
colorPairClashsimulates protan, deutan and tritan vision (Machado et al. 2009, full severity, linear RGB) and the PLAYER tab warns when the pair's simulated colours are under 0.4 apart (out of 1.73). Checked: cyan / red, blue / orange and both new presets pass; red / green fails for deutans (0.37), white / yellow for tritans (0.29). - Enemy model (#15).
enemy_modeldraws every non-friendly player as that model (client only; ranked already forces STANDARD for everyone and is left alone). - Sound groups (#33, #174).
vol_steps(your footsteps, jumps, landings),vol_others_steps(0-2, everyone else's),vol_weapons(every gunshot sound),vol_objective: the planted bomb now beeps from where it is, in step with the model's blink (every bomb timer / 20 s, at least 0.15 s), reusing the sticky beep a fifth higher. No UI sounds exist yet, so there's no UI volume. - Kill feed / chat (#22).
feed_time(6 s),feed_lines(6),feed_mine,chat_time(10 s),chat_lines(8),chat_opacity(not while typing). Chat history (#138): typing shows 12 lines of the 30 kept, scrolled by the wheel or Page Up / Down. - Team chat (#41). New player command
sayteam: in a team mode the server sends the EV_CHAT only to the sender's team, withb = 1so it shows "(TEAM)"; elsewhere it's an ordinary say. Old clients just show such a line without the tag. - Keys (#41, #137, #148, #178). New actions TEAM CHAT, LAST WEAPON (the weapon held before the current one), SPEC NEXT / SPEC PREV (dead or spectating; a click still cycles), HIDE HUD (
hide_hud_crosshairkeeps the crosshair; the scoreboard still works) - all unbound by default. A bind that takes a key from another action shows "<KEY> WAS ON <ACTION>: UNBOUND THERE" with UNDO for 8 s. The bomb objective line and the F3 hints name your bound key (Game::keyOf). INFO's key list said F4 for VOTE START, which has been F5 since bind version 3.testHeld_(the UI test's held keys) was 32 bits wide while actions passed 32: now 64, with a static_assert. Ready and inspect have no game system behind them yet, so they're not here. - Screen (#28, #60, #176, #25).
show_fps/show_ping(top right, frames counted over 0.5 s; hidden while F3 is up),captions(each announcer line as text under the crosshair for 2 s, through oneGame::announce),hp_scaled(the health bar against the most health seen this life, so 200-health customs and 50-health ones fill correctly; the client isn't told the mode's max, so its spawn health stands in),ch_head_color(a headshot's crosshair flash). - Controls (#18, #73, #5).
grapple_toggle(press to hook, press again to let go; reset on death),scroll_wrap/scroll_skip(a bit per weapon; number keys still select them),left_handed(both first-person models mirror sides and their tilt). Dash stays a tap (a toggle makes no sense for an impulse); CROUCH was already off every key page. A separate viewmodel FOV isn't done: the models are drawn in the world pass with the scene's projection, and their SIZE / DEPTH settings already do what a viewmodel FOV is used for. - Display (#218, #121, #112).
vrr: swap interval 1 andmaxFps()capped at refresh - 3 (from the monitor's mode, or DEVICES' REFRESH RATE in fullscreen), the usual G-SYNC / FreeSync recipe that keeps the frame out of vsync's queue.auto_iconify(GLFW_AUTO_ICONIFY).cpu_priority: PRIORITIZE GAME can ask for HIGH_PRIORITY_CLASS instead of ABOVE_NORMAL (setGamePriority(level)). - Menus (#161, #165, #193, #77).
remember_tab;confirm_leave(QUIT, DISCONNECT, STOP HOSTING arm on the first click as "SURE? ..." and act on a second within 3 s);auto_reconnect(a "connection to server lost" drop goes back to the last address after 2 s, up to 3 tries, not for a kick, a refusal, a demo or your own server) and RECONNECT next to JOIN GAME;queue_flash(a filled queue also plays the finish chime and flashes the taskbar if the game isn't focused). - Editor (#195, #196). Redo: each undo step now keeps its own op beside its inverse (
edUndoFwd_), so undo moves the step toedRedo_and Ctrl+Y / Ctrl+Shift+Z does it again; a new edit clears the redo list; drags and coalesced nudges redo to where they ended. Autosave: every 60 s with unsaved changes the map goes toeditor-autosave.qmapin the data folder (outsidemaps/, so it never shows in map lists); SAVE removes it; RESTORE AUTOSAVE appears in the panel when one is left from before.
Settings: batch 2, the 3s (second half)
- Settings search (#132).
tools/settings_index.pyreads game.cpp's settings draw functions at build time (a CMake step, like the embedded maps) and writessettings_index.h: each widget label written as a string literal, and the tab / MORE page / keys sub-page it's on, so the index can't go stale when a setting moves. The SEARCH field (top right of the settings panel) matches labels and key action names. While the field has focus, up to 14 matches replace the page body (an overlay over the page would sit on top of live widgets); a click on one opens its page, and a click anywhere else closes the list and goes to the page. Only this field's focus counts (Ui::focused(id)), nottyping(), which a slider or colour being typed into also sets. - Third bind slot (#140).
kBindSlots = 3. Binds are saved as three values per action; older games read the first two and ignore the third, so a config still works if you go back a version. Key rows are now three 82 px slots. - Console binds and aliases (#141, #142).
bind <key> <command>(cbind_<code>in the config),unbind,binds,alias <name> <commands>(;-separated,alias_<name>),unalias,exec <file>(from the data folder) andtoggle <cvar>. A command bind runs on key press, after the action binds.autoexec.cfgruns once at start (not in the UI test, which must start from defaults). - All settings export / import (#167). EXPORT puts the config as text (
quark-settings:thenkey=valuelines, keys included, your name / Discord / servers left out) on the clipboard; IMPORT applies every line it recognises and says how many. RESET ALL arms on the first click (SURE?) and keeps name, Discord name and server lists. - Demos (#143, #144, #145). The DEMOS page sorts by date / name / size / map (
demo_sort), shows sizes, and DELETE asks first.auto_record: OFF / EVERY MATCH / RANKED, started on entering the match.demo_keep(0 = all) deletes the oldest demos past N when an auto recording starts (never the one being written). - Sensitivity converter (#9). Quark's yaw is 0.022 degrees per count x sensitivity, the same as Source and Quake, so CS2 / Quake / Apex / TF2 carry over 1:1; Valorant is 0.07 and Overwatch 2 / Call of Duty 0.0066. USE <value> sets yours.
- Fullscreen resolution (#113).
fs_width/fs_heightfromglfwGetVideoModes(0 = the desktop mode). Applied when fullscreen is (re)entered. - Practice (#148). Admin command
practice savepos|loadpos|god|passive|reseton your own server, refused in ranked or when another human is connected (so it can't be used against anyone). loadpos sets position, velocity and view and sends EV_SPAWN so prediction snaps there instead of smoothing; passive strips the bots' fire buttons; god makescanHurtfalse for you. - Players menu (#156). Pause > HOST > PLAYERS: everyone on your server with KICK (not yourself), plus ADD BOT, KICK BOTS and RESTART, through the existing admin commands.
- Race ghost (#179). Your position every 50 ms on race maps; a finished run faster than the saved one is written to
ghosts/<map>.txtand drawn as a see-through player along the run, by the time since your start. Capped at 20 minutes of points. Client only. - Team panel (#17).
team_panel: teammates' names and health bars on the left edge in team modes, and a square per enemy, lit while they're alive (off by default). - Match summary (#180).
match_summary: your shots, hits, accuracy, damage dealt and best streak for the match under the end-of-match scoreboard, counted on the client. - Demo rename (#139). RENAME beside DELETE turns the row into a field; Enter renames the file (letters, digits and - kept, the rest _), Esc or a click away leaves it. Not in this batch (still 3s): a turning 3D model preview (#128) and moving the objective / notice / scoreboard boxes (#21), which come with batch 3. Viewmodel FOV: see Controls above.
Thin surfaces are wallbang (owner, 2026-10-30: "any 1/8m surface should have the wallbang property on it")
wallbangBrush(b)(map.h): the wallbang material, or any shot-stopping brush whose thinnest side is 1/8 m or less (0.126 with a rounding margin), whatever its material: a Coat (K) layer, a thin panel, trim.raycastBullet(server hit checks and every client prediction of them) and the map'shasWallbangflag use it, so a map with only thin brushes still takes the wallbang path. Movement, sight, splash and grapple rays are unchanged: a thin wall still blocks players and explosions. A bullet passes one or more wallbang brushes and takes the weapon's wallbang damage once, as before. Test:testWallbang(a 1/8 m box passes, a 1/4 m one stops).
Map picker (owner, 2026-10-30: "you shouldnt get to pick maps that are not under the playable gamemode for the map, also map previews should exist in the picker in game; list should be scrollable and 7 wide")
- Only the mode's maps. The host, vs bots and in-game pickers list a map only if its
modesinclude the chosen mode (a tourney rotation list: tourney; ranked: maps ranked for that ladder). The "NOT FOR THIS MODE" tag, its legend and the ranked-but-greyed cards are gone with them. A consolemapcommand can still start any map in any mode. - Previews. Each map gets a 192x108 picture from above, made on the CPU when its info is read (once per map file, cached with its title and modes; about a millisecond): each column the colour of its highest surface by material and the map's palette, lit by height and a light from the top left, darker where the height steps. An indoor map's roof and any slab over most of the map are left out so the rooms show; clip and far-scenery brushes too. All previews go to the GPU as one atlas, only when the map list or a file changes, and draw through the swatch image program over the 2D batch (
Renderer::preview; the swatch code now sharesdrawImageQuads). Why not a 3D render: the renderer holds one world (the menu backdrop or the match), so rendering 25 other maps would mean loading each one. - 7 wide, scrolling. Cards are 122 px wide (picture over the name, the mode it's for under it); as many rows as fit the band, the wheel scrolls the rest, and it opens scrolled to the current map. Clicking the picture picks the map too.
Settings: batch 3, the 2s (video, visuals, audio, performance, latency, network, crosshair, accessibility)
Everything lives on VIDEO + AUDIO > MORE 2 (added to), MORE 4, MORE 5 and MORE 6 (new pages), the main VIDEO + AUDIO page and CROSSHAIR. Every renderer switch is a shader uniform that is 0 at its default (so the swatch program, which never gets them, draws as before) and is only set when it changes (Renderer::setLook compares); Game::applyLive pushes the batch into the renderer, mixer and connection once a frame as compares and atomic stores. Nothing costs anything per frame at its default.
- Tone curve (#44).
tone_curve: off swaps the ACES-style filmic curve for a straight clamp (world, sky). The dynamic shader never had the curve, so players are unchanged. - Shadows (#45, #213).
shadow_filter: HARD (1 tap), SOFT (the 4 taps as before), SOFTER (16 taps, a texel apart: inside the caster rect's 2 texel margin, so no rect change).shadow_dist: the shadow map's half-width, 16 / 32 (as before) / 64 m: wider reaches farther but each texel covers more (blurrier).shadow_world/shadow_players: where dynamic shadows land (the world's / dynamic shader'suShadowOn); with neither on, the shadow pass isn't drawn at all.own_shadowoff skips your first-person body entirely (it is drawn only as a shadow caster), so no shadow-pass geometry either. - Clean surfaces (#46).
clean_surfacesdrops the palette materials' 2 m panel seams and per-panel shade noise (materials below 12). The newer textured materials are their look, so they stay. - Lighting (#47).
flat_light: the world shader ignores the lightmap (full sun and sky everywhere, as the see-through pass already does). The bake resolution and bounce count were not made settings: a bake is keyed and cached by map content and the official maps ship baked; another quality would bake every map again (minutes each) and need its own cache key. q's call. - Sky and far scenery (#48).
simple_sky: the sky shader returns the flat sky colour (no sun glare, no stars).far_sceneryoff skips the far-scenery chunks (they were already their own chunks;WorldChunk::farmarks them). - Draw distance (#209).
draw_dist150 / 250 / 400 m is the projection's far plane (was a fixed 400). The frustum's far plane already culls world chunks and dynamic objects, so a shorter distance also draws less. - Player brightness (#50) and always lit (#210).
player_emit: the dynamic shader's own-light share (0.3 as before; 1 = full colour in any shade).players_lit: players get sun visibility 1 and the per-player sun ray (the costliest thing per player) is skipped. - MSAA 8x (#53). The cycle offers 8x;
msaa=8loads. The renderer already clamped toGL_MAX_SAMPLES. - Show when not supported (#219). MSAA levels above the GPU's maximum are labelled N/A with the maximum; 10-BIT OUTPUT says what the window got; BYPASS COMPOSITOR says Linux only elsewhere; the latent sync settings say Windows only, or to pick latent sync first; AUDIO LATENCY says LOW = NORMAL on Windows.
- FOV mode (#74).
fov_mode: FOV read as horizontal on this screen (as before), vertical, or horizontal at 4:3 (Hor+).Game::hfov()turns it into the horizontal degrees every camera / zoom / sensitivity use takes (at most 170). - Outlines (#86, #211, #212). A ninth outline preset, CUSTOM, takes
outline_friendly_color/outline_enemy_color(MORE 4's pickers; the PLAYER tab's cycles list it automatically throughkOutlineColors).outline_alphamixes the outline toward the body colour (a uniform in the dynamic shader).outline_team_width/outline_enemy_width(-1 = PLAYER OUTLINE WIDTH). glTF player models have no edge data, so a real outline would need an inverted-hull pass per model (a cost): a hint says so instead. - Fixed team colours (#87).
fixed_teamswithteam_a_color/team_b_color: while spectating or playing a demo, team modes draw team A and B in these instead of "yours / the enemy" (which made team A always your friendly colour). - Tracers (#101), rope (#103), trails (#215).
tracer_modeclassic pale yellow / the shooter's team /tracer_color(each tracer carries its colour, so no lookups when drawn).rope_color(was a fixed near-black).trailsoff / short (40%) / full andtrail_opacityfor the rocket, bolt, knife, syringe and orb streaks. - Colour presets (#111). COPY / PASTE COLORS: every colour setting (team, outlines, crosshair, tracer, rope, fullbright, name) as one
quark-colors:line, only those keys read back. Three slots (color_preset_1..3) SAVE and LOAD it. - Deband and 10-bit (#204).
dither: plus or minus half an 8-bit step of interleaved gradient noise on the world, players and sky (no bands in sky and fog).out10: asks GLFW for a 10-bit window at the next start (falls back to 8 if none); when granted, the scene targets (MSAA / render scale) are RGB10_A2 too so the bits reach the window. - Accessibility (#58, #59, #61, #108).
it_palette: tag's "it" in your enemy colour and the rest in your team's (was fixed red / blue), and the IT banner too.split_palette/split_words: race splits in your team / enemy colours, and AHEAD / BEHIND after the time.reduce_flash: no muzzle flashes, the bomb and stuck stickies stop blinking, explosions at 40% opacity, the damage flash at most 0.3.safe_colors: the OK / error colours (kGreenOk/kRed, no longer const) become blue and orange, told apart under every common colour blindness. - Audio output (#34, #140, #231, #232).
channelsstereo / mono / swapped, mixed after the voices.stereo_width(the pan factor, 0.8 before),behind_muffle(scales the X8 behind-you dip and low-pass),hear_range(the 90 m cut-off, 30 - 150 m).occlusion: one map ray from your ears as each 3D sound starts; a wall between makes it about 7 dB quieter and low-passed (~1 kHz); off by default because it changes what you hear.dyn_range: CLEAN (the soft limiter at 0.08 instead of 0.3), NORMAL (as before), NIGHT (a 5 ms / 300 ms level follower pulling loud passages down about 4:1 over -20 dB, then +6 dB). HRTF was not done: it needs an HRTF data set and a convolution per voice (a real mixer cost and an asset choice). Sound when unfocused:bg_volume, next to the existing MUTE IN BACKGROUND. - Volume (#234). VOLUME goes to 150% (
volumeup to 1.5, shown in %);vol_dbshows every volume as its gain in dB;vol_perceptualmakes the gain the slider's square. - Audio latency (#116).
audio_latency: ALSA 10 / 20 (as before) / 40 ms; CoreAudio 2 / 3 / 6 buffers; WASAPI SAFE keeps twice the queue (LOW is NORMAL there: the owner found anything under one engine period worse). The output is reopened when it changes. - Sound pack, callouts and cues (#63, #65, #154, #172, #173).
force_pack: everyone's sounds in your pack (Game::soundPackOf).streak_callsall / mine / off.time_warnoff / 60 + 30 (as before) / also a count of the last 10 s (ticks, then THREE TWO ONE),time_warn_textas notices too.cue_mute/ann_mute: a bit per sound id;Audio::setMuteddrops them atplay/play3Dwith one bit test; muted announcer lines also skip their caption. - Visualize sounds (#228).
sound_viz: the mixer keeps the 3D sounds started (game thread, only while on) and they're drawn as STEPS / SHOT / BOOM / HOOK / HURT / DASH round the crosshair toward where they came from, fading over 0.8 s. - Net graph and packet loss protection (#17, #115).
net_graph: ping and loss over 12 s, snapshots in, input packets out and how many commands each carries, and the interpolation delay, at the right edge (F3 unchanged). The command rate itself stays one packet per tick (fewer would add latency).cmd_redundancy2 / 4 (as before) / 8 commands per input packet (NetClient::setCmdsPerPacket; the server already reads up to 16). - Downloaded maps (#194).
map_downloadsoff: a server's map you don't have ends the connection with a message instead of downloading.map_cache_maxkeeps the newest N in maps/downloads (pruned after each download and when lowered); the page shows the count and size, CLEAR DOWNLOADED MAPS empties it. - Light cache (#117).
light_cache_max(was a fixed 32 bakes),bake_threads(0 = all but one core, as before), the cache's bakes and size, CLEAR LIGHT BAKES (shipped bakes stay). A prebake of every map was not added: the official maps ship baked, and baking your own maps at once would hold every core for minutes. - Latent sync (#114).
latent_safety_us: the spare time before the blank (the fixed 100 us before).latent_slow: a frame that misses its blank flips now (a tear, as before) or WAITs for the next one (no tear, a refresh later). Windows only, like latent sync. - Bypass compositor (#223).
bypass_compositor: Linux on X11,_NET_WM_BYPASS_COMPOSITORon the window: GLFW's (fullscreen only), ALWAYS (a window unredirected too) or NEVER. The X11 calls are declared by hand so Xlib's macros stay out of game.cpp. - Menu cursor (#230).
cursor_sizesystem / x1 / x1.5 / x2 andcursor_colorwhite / black / yellow / team: an arrow made withglfwCreateCursor(again only when they change). - USE toggle (#149).
use_toggle: a press holds USE (plant / defuse) until the next press, like GRAPPLE TOGGLE; dying lets go. On VIDEO + AUDIO > MORE 6 (CONTROLS is another branch's this batch). - Crosshair (#145, #217).
ch_spread: a ring of dots at the held weapon's spread (its tangent over the view's), shown when it's wider than the crosshair.ch_cooldown: 32 dots round the crosshair that fill as the refire runs out (hidden when ready).ch_pixels: the crosshair's sizes as exact pixels, not scaled from 720p. All three ride in the crosshair code.
Settings: batch 3, the 2s (HUD, social, controls, menus, host, editor, launcher)
Worker 2's half of the 2-scored items. Batch-3 keys are table-driven (b3Keys in config.cpp: type, bounds, default), parsed by setBatch3 before the old keys and written by batch3Text, so each new key is one line and always clamped. Three MORE pages are new (GAMEPLAY > MORE 2 / 3 / 4, CONTROLS > MORE 2, PLAYER > MORE), each with its screen path for the UI test and its page function in tools/settings_index.py so the settings search finds every label. Nothing new runs per frame at the defaults: every feature sits behind its setting's check (a compare), and the three that track state from frame to frame (health, alerts, auto-hide) are a few compares each.
Feedback (GAMEPLAY > MORE 2)
- Damage numbers (#16).
dmg_num_color(a normal hit; headshots stay red and big hits gold so the meaning stays),dmg_num_stack(quick hits add up / a number a hit / one running total a target until it fades) anddmg_num_time(0.3 to 3 s, 0.9 as before). - Damage arcs (#23).
dmg_arcs,dmg_arc_color,dmg_arc_size(x0.5 to x2) anddmg_arc_time: the arc pointing where a hit you took came from. - Kill mark and hit flash (#24).
kill_mark,kill_mark_time,kill_mark_color;hit_flash_time(its colours stay on the CROSSHAIR tab, which is the other worker's). - Kill text (#153).
kill_text: "KILLED <NAME>" over the crosshair for 1.5 s. - Low health (#62, #92).
low_hp(the share of the mode's max health, so vampire and instagib work too),low_hp_color,low_hp_pulseandlow_hp_sound(one beep on crossing the threshold, fromtrackHealth, never repeated while you stay low). - Heal numbers (#177).
heal_nums: a green +N over your health when it rises (vampire, regen, the syringe); heals within a second add up. - Weapon kick (#55).
recoil_amountx0 to x2 multiplies the view model's kick (and the muzzle point that follows it); FOV and hand were #5. - Per-weapon model (#144).
wpn_vm_<n>=<shown>,<x>,<y>,<depth>(written only when not the default): a weapon's model hidden, or moved on top of the HUD tab's WEAPON MODEL element (same units: 720p pixels and mm, -200 to 200).weaponFrametakes the weapon, so the muzzle (tracers, beams, your projectiles' start) follows the moved model; hiding one weapon's model also hides its muzzle flash and, with YOUR SHOTS: WITH WEAPON MODEL, its tracers, as the whole-model switch does.
HUD info (GAMEPLAY > MORE 3)
- Speed meter (#69, #200, #199).
speed_unit(m/s, the game's own; km/h; ups at 39.37 a metre, as Quake counts),speed_scale(the bar's full scale) andspeed_ticks;bar_capsturns each bar's caption on or off. - Clock (#69, #158).
clock_12hfor the verbose TIME line;clock_mode(time left / elapsed) andclock_tenths(tenths in the last 10 s) for the round clock, drawn after the live line so hudclock.h stays as it is. - Verbose lines (#27).
verbose_lines: a bit per line of the verbose info block. - Auto-hide (#159).
autohide_healthalways / after a change (3 s) and when low / only when low;autohide_slotsalways / after a switch;autohide_barsalways / while one is recharging. - Objective markers (#68).
obj_size,obj_mark_opacity(notobj_opacity, which is the objective zones' opacity on VIDEO + AUDIO > MORE 7),obj_distance(the distance under the label) andobj_arrow(an arrow at the screen edge when the objective is off screen). - Scope (#200).
scope_size,scope_color,scope_line(the reticle's width). - Network warnings (#71).
net_icons: STALLED (nothing received for 0.3 s), LOSS (3% snapshot loss or more) and PING (150 ms or more) badges.NetClient::lastRecvTime()was added for the stall; not on your own server or in a demo. - Spectator HUD and feed (#202).
spec_hudfull / minimal;feed_headshotadds (HS) to headshot kills in the feed. - Stalemate warning (#175).
drain_warn: the round modes' last 10 s before health starts draining, over the crosshair, with a beep at 10 and 5.
Social (GAMEPLAY > MORE 4)
- Chat (#36, #98, #168).
team_chat_only,chat_filter(a built-in word list, matched case-blind at a word's start, the rest of the word starred),chat_one_color+chat_color,sys_color,chat_box+chat_box_opacity(a box behind the chat lines),chat_stamps(the local time before each line),mention_hl(a line naming you in gold) andmention_sound. - System messages (#242).
sys_msgsall / important (votes, kicks, ranked, errors) / off. - Name colours (#70).
other_name_colorsoff draws everyone else's name white. - Streamer mode (#79).
streamer: server addresses, invite codes and the Discord code shown as stars, other players' names as PLAYER 1, 2..., the data folder path hidden. - Scoreboard (#185).
score_sort(score, kills, deaths, ping, name; within each team) andscore_cols(a bit per column). - Alerts (#241).
alerts: with the window in the background the taskbar button flashes on a match start, a round start, a vote called or a player joining (glfwRequestWindowAttention). - On join (#243).
on_joinplay / spectate / ask, applied once when a match's first snapshot arrives. - Quips (#80, #130). Pause > PLAYERS is now open to every player (KICK, bots and restart stay the host's). QUIPS ON / OFF per player writes
quip_block(names), and their quips are not played.quip_pick(PLAYER > MORE: MY QUIP) sendsquip <n>to the server, which plays that one instead of a random one. No wheel: a picker only. - Hide ratings, remember queues (#171).
hide_ratingsdrops the RATING column from the ranked scoreboard;remember_queuessaves your ranked queues (last_queues) and queues them again at the next start (not in the UI test). Not done: fill notify / join, Discord presence and showing Discord names (each needs the Discord bot or Worker side).
Spectating and the death camera
- Death camera (#75, #155).
death_camthe killer / my body / a teammate, forkill_cam_time0 to 3 s (the client default stays 1.5; the server's limitkKillCamTimewas raised from 1.5 to 3 so a longer camera isn't cut off). - Spectating (#32).
spec_fovandspec_third(third person behind the watched player, pulled in by a ray against the map so it never goes through a wall). Free camera already existed. Demo speed and seek are not done: playback runs through the snapshot clock, so a speed needs the interpolation clock scaled with it, and seeking back needs a replay from the start with events muted; more than a setting.
Controls (CONTROLS > MORE 2, binds)
- Per weapon (#40, #142, #143).
wpn_sens_<n>(x0.1 to x5, written only when not 1),alt_toggle_weapons(a bit per weapon; -1 = the old rule, the scopes only) andswap_fire(a bit per weapon: fire and alt-fire swapped while it's held). - Mouse (#40).
mouse_accel0 to 1 (faster moves turn further, at most x4) andraw_input(GLFW raw mouse motion where supported; off: the OS's pointer motion). - Auto-hop (#146).
auto_hopoff: holding jump jumps once; let go and press again. - Console (#123).
console_key(F1 F10 F11 INSERT HOME \ '; one no action is bound to) andconsole_menus` (open it from the menus too). - Ranked (#150).
ranked_alt_toggle: the alt-fire (scope) toggle allowed in ranked; the other control scripts stay off there. - New binds (#235, #237). VOL UP / VOL DOWN / MUTE, RECORD (start / stop a demo), JOIN TEAM, SPECTATE, appended to the action list so older binds keep their slots.
- Mode profiles (#126). A controls profile can now hold
sensitivity,fov(taken through FOV MODE like the main FOV),zoom_fov_sniperandzoom_fov_rifle, applied while that mode is played; EXPORT writes them. Crosshair per mode is not done: the crosshair is the other worker's tab.
Menus (PLAYER > MORE, #52, #107, #118, #131)
menu_themedark / high contrast (buttons outlined, brighter text),menu_accent_on+menu_accent(not the team colour),text_bright,panel_dark. No light theme: the menus are drawn over the 3D backdrop and every hint colour assumes a dark panel; it needs a design pass.menu_bgclockwork / a map you pick (menu_bg_map) / a plain colour (menu_bg_color).- The data folder's path with OPEN (MOVE not done: moving a running game's data folder needs a restart and a copy step).
- Mods:
use_overrides(your own asset overrides on or off, applied at start), buttons for the overrides and mods folders, and how many overrides you have. No live reload: assets are loaded once at start.
HUD elements and preview (#21, #128, #26)
- Objective, notices and scoreboard as HUD elements (#21).
HUD_OBJECTIVE,HUD_NOTICES,HUD_SCOREBOARDare appended (hud_11 to hud_13), so older configs' hud_<n> keep their meaning, and theme.cfg getsobjective,noticesandscoreboard. They keep their own drawing code (the stacked notice column and the scoreboard's table);Renderer::uiTransformmoves and sizes what they drew, as a group, about their anchor (the objective line's top centre, the notices' top centre, the scoreboard's top centre), plus the theme's margin and opacity. At the defaults nothing runs (one compare). The objective's colour tints its line; NOTICES tints the notices and the round message; the scoreboard keeps its team colours, so it has no colour row. Hidden: the objective line, its bar and the bomb timer go; the scoreboard isn't drawn on its key. Ranked keeps the default HUD. They're on Settings > HUD's ELEMENT list; the on-screen editor doesn't list them yet (it needs each element's box, which a fixed-code draw doesn't give). - Model preview (#128). PLAYER shows the chosen model in your team colour under the outlines, turning slowly; drag to turn it. Its boxes (
playerModelParts, or a box override) are projected flat into the 2D batch (back faces culled, far faces first, one light): no render target, no extra pass, about 150 quads, and only while the tab is open. A mesh override shows a line instead. - Widget values (#26). hud.cfg widgets can show
snaprate,accuracy,damage,streak,round,vspeed,heightandweapon(appended to the value list). Per-mode widgets already work through the HUD profiles (HUD > PER MODE).
Host and votes
- Votes (#187). callvote
restart,extend(5 more minutes),shuffle(the humans dealt to the teams alternately, then a restart) andkick <player>(the same checks as moving one to spectator; the target can't vote). Text commands and vote text, so no protocol change. - Host advanced (#188, #189).
host_tickandhost_snap(cycles),host_upload(an upload cap),host_auto_start,host_bot_skill(balance bot skill): passed to the server config at host start. Tourney rotation not done: it needs the bracket to carry over between matches on the server. - Vs bots (#192). Up to 15 enemies and 15 teammates (was 7 / 7).
- Not done: a mode rules tab from the param table (#190) and the custom mode editor's extras (#191: weapon set, movement, logic file, duplicate, share code). Both change the custom mode format and the host flow; they need q's call on the share code format.
Editor (#81, #82, #197, #198)
ed_fly_binds(fly keys follow your movement binds),ed_fast_mul(was a fixed x3),ed_fly_speedanded_gridremembered,ed_grid_custom(a grid step after the built-in ones, reached with [ and ]),ed_rot_step(Q / E on a spawn; 45 as before),ed_light(new lights take the last light's values). Saved when the editor closes or test play starts, never from the widgets themselves.- Test play:
ed_test_mode(the map's or any mode),ed_test_bots(0 to 7),ed_test_at_cam(practice setpos, host alone, clamped to the map's range),ed_test_no_warmup.
Race (#182, #183)
race_split_vsbest run / best segments / last run andrace_split_time; the race stats page's map picker andrace_dateformat. No split against #1: the leaderboard has times, not splits.
Launcher (#83, #203)
- OPTIONS (in place of the changelog panel; the bottom row is now UNINSTALL | CHANNEL | OPTIONS):
auto_start(start the game when it's up to date),keep_open(PLAY doesn't close the launcher),args(launch options, passed to the game on every platform) andupdatesautomatic / ask first / at next launch (an update is held back, and the next launch installs it; never for a first install or a pinned version). Asset names unchanged. Install location picker (#84) not done: it needs native folder dialogs on three platforms and moving an install.
Skipped, with why
- #76 pause offline games when unfocused: the server's pause resets the match state and the snapshot stream; needs a server-side pause first.
- #133 practice speed: needs a timescale through prediction and the server tick.
- #170 UTF-8 chat: the font atlas is 8-bit and the server sanitises to ASCII.
- #184 leaderboard paging / around me: needs new Worker API endpoints.
- #238 instant replay buffer: needs an always-on in-memory recorder; left for a performance review.
- #127 player accent colour: other players would need it, which is a protocol change.
- #162 right-click reset on every control: sliders and colour pickers already do; toggles and cycles have no default argument, so it means changing every call site.
- #164 keyboard menu navigation: needs a focus order for every widget on every page.
Settings: batch 4, the 1s (video, visuals, audio, latency, accessibility, cosmetics, modes, movement)
Everything is on VIDEO + AUDIO > MORE 2 (added to) and two new pages, MORE 7 (colours) and MORE 8 (screenshots, demos, the window, latency, sound). As in batch 3, every renderer switch is a uniform at its old value by default and set only when it changes, and Game::applyLive pushes the lot once a frame as compares and stores; nothing costs anything per frame at its default.
- Liquids and glow (#51).
liquid_flowoff gives the world shaderuTime0, so water and lava stand still (no animation, same cost).glow_strength0 to 2 scales the map's glow colour (1 as before). - World outline colour (#57).
world_edge_color: the world shader's panel outline now mixes towardsuWorldEdge(black by default, which is what it was) instead of a constant. - Typing sounds (#64). TYPING SOUNDS flips the SND_TYPING bit in batch 3's
cue_mute, the same bit as its CUE row. - Hitsound tiers (#66).
hit_tiersoff plays the medium hitsound for every hit, whatever the damage. - FFA colours (#88).
ffa_colors: in modes without teams each other player gets one of 8 distinct colours by id (teamColor), so you can tell opponents apart; off as before (all enemy colour). - Own effects colour (#89).
own_fx/own_fx_color: your projectiles, laser and rope in your colour (theropelambda now knows whose rope it draws). - Health bars (#90).
hb_low(0 to 100 HP, 30 as before) is where bars and the HUD health number turn tohb_low_color(the old red until you pick one);hb_custom/hb_colorset the healthy bar's colour. - Name tags (#91).
tag_colorteam / white / custom (tag_custom),tag_alpha0.1 to 1,tag_size0.5x to 3x. - Effect colour (#102).
fx_color_mode: explosions in their own colour (as before), your team's (explosions don't record an owner, so "team" is your team's colour) orfx_color. Kill bursts keep theirs: they say whose kill it was. - Objectives in your palette (#104).
obj_palette: the bomb's blink and payload checkpoints not yet reached use your enemy colour (from your colour scheme, so the batch 3 palettes apply). The other objectives were already team-coloured. - Map colour overrides, high contrast (#105).
sky_override/sky_colorandglow_override/glow_colorreplace every map's sky and glow (prepareScene builds them as locals; x < 0 means the map's own).high_contrastlightens wall base colours and darkens trim. - F3 colourblind palette (#109).
f3_safe: the F3 pipeline bars use the Okabe-Ito colours (blue / orange / bluish green / vermilion ...), distinct in every common colour blindness. - Always on top (#122).
on_top:GLFW_FLOATING, set when it changes. Confine cursor was skipped: GLFW 3.3 has no confine, in play the cursor is already captured, and the menus would need per-OS code that can't be tested here. - Gun colour (#129).
gun_colormap (as before) / team / custom (gun_custom): the viewmodel's metal tint. - Objective zones (#136).
obj_opacity0 to 2 scales points zones' and the KOTH hill's fill;obj_pulseoff holds them steady. - Dash buffer (#147).
dash_bufferoff / 0.05 / 0.1 (as before) / 0.15 s: how long a dash pressed before it's ready is kept. - Smooth strength (#206).
smooth_amt0.25 to 1: the FXAA pass mixes its result with the unfiltered pixel (uEdgeOff, 1 = full as before), set only when it changes. - Lightmap filter (#208).
lightmap_nearest: the lightmap texture's filter becomes GL_NEAREST (crisp texel blocks, the old-school look), switched only when it changes. - Wallbang tint (#214).
wallbang_tint/wallbang_color: wallbang brushes in your colour instead of the dark grey. - Ultra safety margin (#220).
ultra_margin_us0 to 2000 µs (150 as before; the slider goes to 1000): how early low latency ULTRA starts each frame. More margin, fewer missed vblanks; less, a little lower latency. - NVIDIA driver profile (#222).
nv_threaded(Threaded Optimization off, setting 0x20C1221E) andnv_dxgi(Vulkan/OpenGL present method: DXGI, 0x20D690F8) write the game's NVIDIA profile through NVAPI, the same path as GPU MAX CLOCKS (now a sharednvProfileSetting). Windows only; the setting IDs are from NvApiDriverSettings.h and could not be tested on an NVIDIA card here. A failure says why and turns the toggle back off. - Quip captions (#229).
quip_captions: player quips get a caption even with CAPTIONS off. - Announcer ducking (#233).
ann_duck0 (off, as before) to 1: effects get that much quieter while an announcer line plays (the mixer checks its voices once a buffer). - Demo rate in settings (#20). DEMO RATE (
demo_full_rate, 62.5 Hz or every snapshot) is on MORE 8 as well as on DEMOS. - Screenshots (#38).
shot_formatPNG / TGA,shot_folderthe game's data folder / Pictures/Quark in your home folder,shot_hide_hudsaves the 3D view right after it's drawn, before the HUD (one frame later, no extra pass). OPEN FOLDER opens it. JPG was not added: the tree has no JPG encoder and adding one is a new third-party file. - Skipped: #19 voice chat (there is none), #85 language (every string would first have to move into one table), #224 HDR (parked).
Settings: batch 4, the 1s (HUD, menus, social, editor, practice, gameplay)
Worker 2's share of the 1-scored items, in the same batch-3 key table (b3Keys). Nothing runs per frame at the defaults beyond what ran before: the colours replace constants, the compass and radar draw only when on.
- HUD > MORE (new page; morePages / moreNav / search / uiScreenPath / settings_index know it). The HUD tab's panel ends at Y 560 to leave the live HUD preview visible, so the HUD's MORE button sits in that footer; the MORE page itself is a normal full-height page without the preview.
- Colours (#93, #94, #95, #96, #99).
bar_recharge_color(a bar while it recharges, and the dash pips),speed_low_color/speed_high_color(the speed meter blends from one to the other as before from gold to red),slot_off_color(the weapon list: a weapon not usable now, at its old 35% opacity),sticky_full_color,warn_color(the ranked and unrated notes under the score box),info_color(the grey lines: JOIN A TEAM..., the host's port and listing lines),dmg_head_color/dmg_big_color(damage numbers, 40+ for big). Each defaults to the colour it replaces. - Text outline (#216).
text_outline0 to x1.5 scales the UI shader's outline band (a uniform set only when it changes). Capped at 1.5 and kept at least 0.12 distance-field units inside the glyph edge, because the distance field's spread runs out at small text and a thicker band blotted the letters together. - Notices (#72).
notice_lines1 to 8 (was a fixed 4) andnotice_timex0.5 to x3 on each notice's time. - Compass and radar (#160).
compass: a 180-degree strip at the bottom centre with N / NE / E... every 45 and ticks every 15.radar: a 60 px disc top left, up = where you look,radar_range10 to 120 m (further ones sit on the rim): your teammates in team modes and the objective markers' positions (collected from the marker labels the world pass already draws). Never enemies, and off in ranked (the default HUD). No minimap: it needs a top-down render of each map. - REC (#201).
hide_recdrops the blinking REC while recording; the demo playback bar stays (it's the playback control's progress). - Tourney TAB (#179).
tourney_tab0 the bracket (as before), 1 the scoreboard; the bracket still shows between rounds. - HUD panel colour (#97). Not a new setting: theme.cfg's
hud-panel(background, border, opacity) already does it, and the on-screen editor edits it. - Stair smoothing (#156).
step_smoothoff skips the X7 camera ease on stair steps (the view jumps with your feet). - Typing indicator (#169).
send_typingoff: the client stops sendingtypingwhile you type, so nobody sees your "..." or hears your keys. - Clickable tiebreak (#180).
rps_click: while you choose in the hide and seek tiebreak, the cursor is freed and a click on ROCK / PAPER / SCISSORS throws it (the slot keys still work). - Fixed logo (#110).
fixed_logo: the title in the default cyan / red instead of your team colours. - Bots (#135).
bot_looks0 a mix of models (as before), 1 all STANDARD;bots/names.txt(BOT NAMES FILE makes one and opens the folder): one name a line, letters / digits / spaces / - _, up to 12 characters and 64 names, given to your server's bots as "BOT <NAME>" (ServerConfigbotNames,botLooks; text in the config, no protocol change). Hosted games only; dedicated servers keep the built-in names. - Clip box colour (#106).
clip_colorfor the editor's see-through clip boxes (their opacity unchanged). - Race / leaderboard colours (#100). Worker 1's SPLITS: PALETTE (
split_palette) now also colours the race stats' BEST row and the leaderboard's #1 in your team colour instead of gold, rather than a second setting for the same thing.
Movement: wall teleporters keep speed, clip refuses the grapple (owner, 2026-10-30: "clip should also refuse hook"; the teleporter bug from a map review)
- Wall teleporters kept only the clipped speed. A teleporter block is solid, and the touch check (pmove.cpp) ran after the slide move had already clipped the velocity against it. So whenever a step ended inside the wall rather than just short of it,
n * length(s.vel)used what was left after the clip. At 60 m/s into a wall teleporter, 4 starts out of 8 came out at under 1 m/s. Now the exit speed is the larger of the speeds before and after the slide. Floor teleporters behaved the same before; it only showed on walls, because the speed was horizontal. Test: movement, 8 start offsets at 60 m/s, all keep 60. - Clip refuses the grapple. Clip (material 9) let the hook pass through it to whatever was behind, which made the invisible lids and the clip over walls hook points nobody could see.
Map::raycasttakeshitClip(only the grapple and the bots' hook planning pass it). A hook that reaches clip hits it and lets go, like nograpple. Shots and sight still pass clip. Cost: none on the shot path (the extra test only runs on brushes that don't stop shots).
Maps: q's Surfy and Cobblestone ship (owner, 2026-10-30: "make sure all future updates get cobblestone map and surfy map, use latest version; also surfy map is default surf gamemode map")
- Shipped as hand-made maps.
maps/surfy.qmap(58 brushes,for surf, saved 2026-10-29 18:46) andmaps/cobblestone.qmap(885 brushes, noforline, saved 2026-10-29 15:59) are the newest copies from q's maps folder, byte for byte (sha256 20620ee2... and 31fbb542...)..gitignoreun-ignores both, so CMake embeds and copies them into every build like the generated maps. Ashandmademaps they run every map check but never failquark_tests. - Surfy is surf's default.
Game::bestMapFortook the first map built for a mode, alphabetically, which is Cascade. A small per-mode default (defaultMapName: surf -> surfy) now comes first when that map is there and offers the mode. The mode picker's "MAP:" line now names the mapbestMapForwould start on, instead of the last map built for the mode, so the line and the start agree (it also fixes koth and hide naming a different map than the one you got). - Race and surf on a map with no race spawn. Surfy starts players on an
anyspawn, but race and surf asked for araceone, so the game didn't count Surfy as a surf map at all and would have kept Cascade as the default. Now a map with race checkpoints and no race spawn offers race and surf, andmodeSpawnTypestarts racers on itsanyspawns. Maps with race spawns behave as before. - Hand-made maps don't fail the test run, as CLAUDE.md says: the spawn-pool check (Surfy has 1 spawn for 8 racers, Cobblestone 4 for 8 in FFA) and the save round trip's float-noise check (Quarkology's cut pieces write numbers like 1.0000048) now report a hand-made map's misses as HUMAN-MADE, like the other map checks.
- Why Cobblestone ships: it's q's test map for the cobblestone look's frame-time lows (0.1% low 800 vs 2800 fps on the older looks, parked to fix later), so every build carries the same benchmark.
Servers: testing-only install (owner, 2026-10-30: "default is both, you should have the option")
install.sh --testing-onlysets up only the testing instance (userquark-testing,/opt/quark-testing,/var/lib/quark-testing) on the production port (--port, default 27500;--testing-portoverrides). Noquarkuser or production clone is made; an existing production server is stopped and disabled with its data kept. The testing instance builds this checkout itself (there's no production build to copy) and runs its first update check during the install, as production does.- Always on. Its update unit sets
QUARK_TESTING_ALWAYS=1, andautoupdate.shthen skips the "only while newer than production" switch: noactivefile, noquark-testing-active.path, noExecCondition. It runs the newest testing build, and installs and runs the production release once that is newer (the updater already built production in that case to keep its own code current; now the server also runs it), so the box never sits empty after a release. Why: a box meant only for testing would otherwise be idle from each release until the next testing push. - No governor, normal priority when alone: the load governor and
Nice=5/CPUWeight=50exist to make room for production, which isn't there. - Remembered in
/etc/quark/install.conf(PRODUCTION=0), so re-running install.sh with no options keeps it, taking the server options from the testing unit.--bothgoes back to both (the testing port then moves to production + 1 again unless--testing-portsays otherwise);--no-testingstill means production only. The default is unchanged.
Servers: the OFFICIAL tag (owner, 2026-10-30: official servers get a tag in the server browser that can't be faked by naming a server "official")
- What. JOIN's server list draws an OFFICIAL chip (the ranked gold: gold Rubik text at hint size on a dark gold chip with a gold border; dark text was tried and the text outline swallowed it) before the name of an official server and sorts the official servers first, ahead of favourites and the chosen sort. The official servers are now one box, 75.127.7.139: production on UDP 27500, testing on 27501.
- How it can't be faked. The client has the list built in (
kOfficialServers,src/game/master.cpp;isOfficialServer(ip, port)/isOfficialServer("a.b.c.d:port"),master.h) and matches it against the address the info reply actually came from (ServerInfo::official, set once per reply inServerBrowser::poll, and for a silent internet entry from its listed address), never against the name the server reports. A spoofed UDP source would also need the reply to reach the browser's own socket for a query it sent, and joining goes to that same address. The string form is strict (four dotted parts, no leading zeros, a port required), so lookalikes such as75.127.7.139:27502,75.127.7.13:27500or075.127.7.139:27500don't match (testOfficialServers, group ranked). - Why built in, not from the Worker.
/list's format stays oneip:portper line (old clients take any short line as an address), and a flag served by the Worker would only be as trustworthy as the Worker's reply; a list in the signed game can only change with a signed release. The cost: if the official servers move,kOfficialServersmust be updated and a release shipped (docs/HANDOVER.md says so). - Cost. One bool per server, computed when its reply arrives; the draw adds a static string, a measured width, one rounded rect and one text per official row: no allocation a frame.
Modes: power (MODE_POWER 14; owner, 2026-10-30: "each team has player count * 2500 health, nobody can die, just soak damage; when total team health reaches 0 they lose")
- Rules. Team vs with one health pool per team:
poolPerPlayer(2500, a new param of the newpoolobject) x the players on it, full at the start of each round. InServer::damagePlayer, an enemy's hit (after the ruleset's multipliers, so instagib takes 101) comes off the victim's team pool while the round is live, and never off the player's own health: nobody dies of damage, and knockback lands as before. Your own hits, teammates' and the world's (acid, spike floors and the other liquid / feature damage throughtakeAcid) take nothing, so a rocket jump or a lava touch can't lose the match for your team; a fall out of the map still kills and respawns you after 2 s (modeRespawnDelay) at no cost to the pool. A pool at 0 loses the round. When the 600 s clock runs out, the pool with the larger share of its size wins (a draw starts another round). One round wins the match (rounds1). - Joins and leaves (
poolStep, every tick): a player who joins a team mid-round addspoolPerPlayerto its pool and its size; one who leaves takes their share off the size, and the pool shrinks to fit it (an emptied team's pool goes to 0 and it loses). - Wire and demos. The pools ride in ModeState fields power doesn't otherwise use (
powerPool/powerPoolMax, protocol.h: payloadDist / payloadLen = the pools, point / pointCount = the players each counts, target = hp per player), so the snapshot format and every demo stay as they were. Protocol 54 anyway: an older client has no mode 14 (demos convert with no change). - Mode logic.
tools/modes/power.qlogic(use pool,poolStep(), thepool[team](rw) andpoolMax[team]arrays) says what the native rules do; the lockstep test plays power on Bastion natively and through the interpreter at 1000 / 250 / 125 Hz and finds no difference. Custom modes can base on power. - HUD. The score box shows your pool and the enemy's (rounded up) instead of rounds; the objective line reads "TEAM POOL n / size - ENEMY n / size" with your pool's share as its bar. Main menu > INFO > MODES explains it. The mode pickers suggest Bastion (power plays on the team vs map).
- Maps. Bastion and Orbital (team vs) and Lodestone also offer power (
alsoin their builder files), so they're graded for it like their other modes. - Bots play it as team vs (no objective; they fight).
- Tests.
testPower(rules): the pools start at 2500 each; an 80 hit takes 80 from the victim's pool and none of their health; your own rocket takes nothing; a third player brings 2500 to their team; emptying team B's pool gives team A the round. The every-mode bot run plays power on Bastion; the lockstep covers it.
Map checks: the walk grid walks ramps (engine work list, 2026-10-30: "the walk check can't see ramps")
- What was wrong.
tests/walkgrid.h(the collision-checked walk behind the spawn / route checks,quark_tests maps,boundsandquark_maptool's check / route / metrics) put every solid brush into its columns as its whole box,box.mn.y .. box.mx.y. A wedge, corner, tetra or cut slope was a block as tall as its high end, so a 15 m decline was a 15 m wall. Mine's haul road (a 25 degree wedge, 15 m over 31 m) was found only by an 18.6 s detour, and Mine's lava ramp and QK7's helix (a solid wedge, then cut ramps every 5 m) weren't found at all. - Now. A slope you can walk (
WalkGrid::walkableSlope: standing on the floor,up0, or a cut box, and no steeper than pmove'sfloorFacelimit, n.y >= 0.7071 - 1e-4, tan 1.0002) goes into each column as what of it is over that column's hull footprint (columnSpan): a wedge / corner / tetra up tobrushTopOverof the footprint (where pmove's box collision rests the feet: the slope's highest point under the hull); a cut box between its planes' bounds over the footprint's corners (the top no higher than each up-facing plane's highest corner, the bottom no lower than each down-facing plane's lowest; a side plane that cuts the whole footprint away leaves the column out). That is exact on a ramp's face and errs high (more solid, never a false way through) only where two planes meet over the column. A 45 degree ramp rises 0.25 m per 0.25 m column, well inside the 0.55 m step. Steeper slopes, and slopes on their side or hung from a ceiling, stay whole boxes: stepping up a 63 degree face column by column would climb walls pmove slides you off. - Standing on a ramp. The feet rest on the slope's highest point under the hull, at its edge, so the 10 cm support check (floor within 0.3 m of the centre, 2 cm under the feet) failed on a slope. Each merged span now carries a slack, (0.4 - 0.3) x the steepness + 5 mm, and the support probe goes that much deeper there (0 on boxes: flat floors check exactly as before).
- Results. Routes (
quark_maptool route, before -> after): Mine haul road foot to top 18.6 s (round the long way) -> 3.7 s; Mine lava ramp unreachable -> 3.3 s; QK7 ground to the first helix landing (5 m) unreachable -> 7.0 s, to the top (30 m) unreachable -> 21.1 s. Standing spots: Mine 535,394 -> 543,740, QK7 264,086 -> 303,058, Datacenter 1,609,545 -> 1,626,575. The shipped maps are unchanged except Cobblestone (151,829 -> 152,465; its slopes), and all still pass. - Speed. Only slope brushes take the new path (boxes are rasterised as before). Raster lap on Datacenter (166 cut boxes) about 120 -> 165 ms and Mine 100 -> 133 ms (best of 4, threaded, a shared CPU); the whole build is unchanged within noise (Datacenter 956 -> 932 ms for the header's single-threaded build: fewer spots fail the off-centre search). A first version clipped each cut's faces per column (
cutBounds) and took the raster to 670 ms; the corner bounds give the same spots. - The maptool copy.
tools/maptool/map/walkfast.cpp(the threaded copy of the build) uses the samewalkableSlope/columnSpan/mergeSpan/RawSpanfrom the header;walkverifystill finds the two builds identical on Mine, QK7 and Datacenter. - Test.
testWalkGrid(maps group): a 4 m deck with no way up is unreachable; a 27 degree wedge, a 45 degree wedge, a cut ramp (y <= x over its box), a wedge rising along z and a corner ramp each make it reachable (and the way down walks); a 63 degree wedge and an upside-down wedge don't.
Bots: bomb attackers take every site in turn (engine work list, 2026-10-30: "bomb bots always pick the nearest site")
- What was wrong. The objective (
bots.cppand the default brain'sobjective bombslot) sent the attackers tosite(0)every round: the carrier always went for the first site in the file, and the others escorted it. On Datacenter, B is about 8 m further from the attackers, and no bot ever went there. - Now. The carrier (and any attacker before someone has the bomb) goes for site
(round + teamRank) % sites: deterministic, no randomness, so a seeded match plays the same, and with two sites each one gets every other round (more when the carrier changes rank). The escorts follow the carrier as before. The defenders are still dealt one site each by rank (teamRank % sites), so both sites are covered whenever a side has two or more. - Brain language. A new objective sensor
roundNo(int: the round, 1 = the match's first, 0 before one), in GRAMMAR.md. The default brain's line isgoto site(attacker ? roundNo + teamRank : teamRank), which stays the native code's twin (the brain lockstep tests compare them). Quarkology's bot editor (its ast.js sensor list and fixture default.qbrain) needsroundNoadded to know the new sensor. - Measured. Depot, 4 v 4 expert bots, 600 s (the new
bombsitestest group): before, 16 rounds with plants A 7, B 0; now 20 rounds, A 10, B 8, defenders sent 40 / 40. Datacenter (maps-m2's map,quark_maptool bots, 8 experts, 600 s): 12 plants, A 5, B 7.
Map format 11: mode spawn sets (q, 2026-10-30: Mine's koth hill gets its own koth spawn set, separate from the bomb spawns)
- Before. A spawn's team was
a,b,any,raceor a multi point stage (att0..def9), shared by every mode that uses that side. A map built for bomb with a hill too (Mine) could only start koth on its bomb sides. - Syntax.
spawn <mode>-<side> x y z [yaw], sidea,borany, the mode asmodeNamewrites it (lower case):spawn koth-a -10 0 10 0,spawn koth-b 10 0 -10 180,spawn ffa-any .... One word, so every tool that reads a spawn's coordinates as words 2-4 (tools/maps/prefabs.py, Quarkology's readers) keeps working;M.spawn("koth-a", x, y, z, yaw, mirror_team="koth-b")writes a pair. Refused: race and multi point (race-a,points-a: their race and staged spawns are their sets), an unknown mode or side, upper case. - Format change, by the rule. An older reader refuses an unknown spawn team (since format 2), so this is a format change:
kMapFormatVersion10 -> 11,upgradeMap10to11inkMapUpgrades(nothing to change: a format 10 map has no such spawns; only the version line moves),testModeSpawnSetsnext totestMapConverter(whose every-version loop now covers 10 -> 11).tools/maps/prefabs.pywritesversion 11. - In memory. The team is
SPAWN_MODESET + 3 x mode + side(24 .. 68, after the staged ones, map.h:modeSpawn,isModeSpawn,spawnSetMode,spawnSide);isStagedSpawnnow stops at 23.spawnTeamWord/spawnTeamFromWord(map.cpp) are the one reader and writer (the parser,serialize, the maptool's JSON names). Editor edit ops (s set ...) accept them; a race / points set is refused there too. - Who uses them.
modeSpawnTypereturns the mode's own set's side when the map has a spawn of it (thenspawnPoolpicks only those), and the normal side otherwise, so a map with no set plays exactly as before, and one side's set alone leaves the other side on the normal spawns. No other mode ever starts on them:spawnPool's last fallback (every spawn) leaves mode sets out unless they are all a map has.canSupportModecounts a mode's own set for its sides (a koth-only map withkoth-a/koth-boffers koth, not vs). - Checks. The walk check (
walkRoutesMissing) routes each mode from its own sides (WalkSpawnSets::forMode): koth from the koth set to the hill. The spawn-pool check already askedmodeSpawnTypeand the sight check too (koth sides in sight of each other are flagged); the mirror-twin check pairskoth-awithkoth-b; the 1.5 m spacing check counts a mode set as its own group.quark_maptool: check (the same code), metrics (a mode's objective routes from its set) and custom tests (team: "koth-a"). The pool check wants full / 2 = 4 spawns a side in koth, the same as any team mode: a map's koth set needs 4 a side. - Surfy and Cobblestone (the shipped hand-made maps) get
version 11in place ofversion 10, nothing else: what the converter writes. Left at 10 they converted on every load, and the converted text's sha256 no longer matched the embedded one, soisOfficialMaprefused them (ranked group, reset built-in maps). - Editor. A spawn's panel gets MODE SET (EVERY MODE, or ONLY one of the 13 modes that can have a set); TEAM (M) then cycles that set's A / B / ANY. A mode set's spawn has a white cap over its team colour.
- Test.
testModeSpawnSets(movement group): read and written back, koth starts onkoth-a/koth-band bomb / vs ona/b, pools hold only their own, the fall backs (no set; one side's set), a koth-only map,ffa-any, eight refused words, edit ops, and a koth spawn sealed in a box reported as "can't reach hill 1" with nothing else reported.
Map format 12: walk teleporters (owner, 2026-10-30: "teleporters should have toggle for carry all speed or max at walking velocity, in solar they should all be walking speed")
- What. A teleporter has a speed mode: CARRY (as before, the default) or WALK. Through a walk teleporter a player comes out of its pair at
min(speed in, run speed): the run speed iskGroundSpeed(9.5 m/s) x the custom mode's speed scale, still pointing straight out of the exit face. At 60 m/s in, out at 9.50 (8 start offsets, every one); at 6.1 m/s in, out at 6.1 (never faster than you went in). - Which teleporter decides: the one you go INTO (
Brush::tpWalkon the entry). A mapper places a teleporter per route, and a pair can then be walk one way and carry the other (a stage's arrival portal in a surf map caps the run you came in with, the way back keeps it). The exit's own flag only matters for whoever enters it. - Why the cap is the run speed and not zero. You leave the exit face moving the way it points, as with carry, so a floor exit still lifts you clear and a wall exit still puts you out of the wall; standing still in the doorway would leave you on the exit's edge.
- Projectiles keep all their speed through either kind (server.cpp, a projectile that hits one): a rocket capped at 9.5 m/s would hang in the air and fall; the toggle is about players' runs.
- Format change, by the rule. File:
teleporter x0 y0 z0 x1 y1 z1 tp <link> <face> walk(the word only when set; slopes and cut boxes the same after their box).readTprefuses any other word there, and an older reader refuses the word, sokMapFormatVersion11 -> 12,upgradeMap11to12inkMapUpgrades(nothing to change: a format 11 teleporter carries, as one without the word still does; only the version line moves) and a v11 case intestMapConverter.tools/maps/prefabs.pywritesversion 12and getsM.teleporter(x0, y0, z0, x1, y1, z1, link, face, speed="carry"|"walk"). Surfy and Cobblestone getversion 12(as at 11: left behind they would convert on every load and fail the embedded sha256). - Protocol 56. Edit ops carry it (
b set ... p <link> <face> walk), and pmove reads it on both sides (prediction: an older client would carry the speed the server caps), sokProtocolVersion55 -> 56. Demos: no record layout changed, an older demo's edits (p <link> <face>) still read as carry; the existing converter covers it. - Editor and tools. The teleporter's panel gets SPEED: CARRY / WALK under EXIT FACE.
quark_maptoolparse / serialize carry it asspeed: "walk"on the brush (left out for carry), so Quarkology keeps it through a save. Bots don't plan through teleporters, nothing to change there. - Test. movement (testFeatureBlocks): the 60 m/s wall-teleporter run through a walk one, 8 offsets, all out at 9.50 pointing out of the face; a 4 m/s run in comes out no faster than it went in; written back and read, in an edit op and applied, an unknown word refused. The carry run still keeps 60 at all 8.
Weapons: the primary rocket thrusts 16 m/s2 (owner, 2026-10-30: "rocket starting velocity 48, accel 16m/s2 so after 2 seconds it should be travelling 80m/s")
- What.
kRocketAccel4 -> 16 m/s2, launch speed 48 m/s as before: 64 m/s after 1 s (56 m covered), 80 m/s after 2 s (128 m), 128 m/s at the end of its 5 s life (440 m in all, was 290 m). Time to 10 / 20 / 40 m: 0.201 / 0.391 / 0.742 s (were 0.207 / 0.410 / 0.806 at 4 m/s2): close up it is the same rocket, and a long shot gets there much sooner. - Earlier the same day it was 24 m/s and 8 m/s2 for a few hours (PR #135, protocol 57; the lob held at 33.6 m/s by
kRocketLobSpeed1.4). Both are back: 48 m/s andkRocketLobSpeed0.7, so the lob is still 33.6 m/s. - A whole step short.
projectileExtrapolate/projectileRewindcounted their 1 ms steps as(int)(t / kMoveStep), and 1.0 / 0.001 in float is 999.99..., so a 1 s extrapolation ran 999 steps plus a straight 1 ms: a step of thrust short (0.016 m/s at 16 m/s2, 0.004 at 4, under the old test's tolerance). Now+ 1e-3before the cut (and a rest never below 0): the closed form is 64.0000 m/s at 1 s against the server's stepped 63.9997. - Protocol 58. Every copy of a rocket's path (prediction, rocket riding, the hook probe) flies with the thrust, so a 57 client would mispredict rockets. No layout changed; the demo converter covers older demos as before.
- Rocket riding flies its anchor with the same thrust, so the rocket you ride pulls away faster. Bots lead by
projectileFlightTimewith the new thrust (nothing to change). - Test.
testRocketThrust: 64 ±0.01 m/s after 1 s, 48 + 16 x 1.001 / 2 m along; the base table's 48 m/s and the lob's 33.6 m/s checked.testRocketRidinghooks your own rocket once it is 2.2 m out (past the 1.2 m release distance + 1 m), which took the real-time race out of which command crosses it.
Weapons: the primary rocket accelerates (owner, 2026-10-30: "primary fire rocket should have an acceleration of 4m/s^2 on top of its current velocity / secondary unchanged")
- What.
kRocketAccel = 4m/s² (gamedefs.h), along the rocket's velocity, from its 48 m/s launch speed: 52 m/s after 1 s, 50.0 m covered in that second (48 + 2), 68 m/s at the end of its 5 s life. The lob (gravMulkRocketLobGravity, PROJ_LOB) gets none:projectileAccel(type, gravMul)is the weapon set'srocket.accelfor the primary rocket and 0 for everything else. A rubber bounce turns the velocity and the thrust follows it (it is along the velocity, not the launch direction). - No speed cap. The most it can gain is 4 x 5 s = +20 m/s (68 m/s, still under the crossbow's 76.5), and a rocket that far out has long left any fight, so a cap would only be another number to keep in step on every copy of the path.
- One path everywhere. A step is now
projectileStepVel(the drop, then the thrust along what that leaves) in Server::stepProjectile, pmove's rocket-riding anchor and the rockets its probe flies on, and Map::flyProjectile's slow-mo steps.projectileExtrapolate/projectileRewindtake aProjFlight(drop + thrust) instead of a gravity: a thrust with no drop is a constant acceleration vector (it stays along the velocity), so the closed form is still one pass and exact (the old gravity sum with a vector), and the rewind is still its inverse. Only a weapon set that gives the primary rocket a drop as well bends the thrust; those few are stepped (the same steps, forwards and back) instead of summed. Every caller (the client's drawing and snapshot extrapolation, your rockets' predicted copies, the hook probe, the server's rewind for catching someone else's rocket) passesprojectileFlight(type, gravMul). - Wire: protocol 55. Riding a rocket, pmove flies the anchor with the rocket on both sides, so it needs the thrust: MoveState
hookAccel(and HookProbe / RocketCopy / HookCatchaccel), set by the server every tick likehookGrav, and written after it in the own movement state (kSnapRocketThrustSince; the read clamps it to the field's 0-100 range). An older client would mispredict every rocket anyway. Demos: the converter reads a 54 header without it (no thrust) and writes today's layout, so older demos convert at startup as before. - Bots. They lead a rocket by
projectileFlightTime(solving d = v t + a t² / 2) instead of distance / speed: at 40 m that's 0.81 s, not 0.83 s, about 20 cm less lead on a target running 8 m/s. The maptool's weapon range uses the same time for its aimed shots. - Weapon sets and stats. New field
rocket.accel(m/s², 0-100, default 4; 0 turns it off). The weapon stats' travel times to 10 / 20 / 40 m count it (0.207 / 0.410 / 0.806 s, were 0.208 / 0.417 / 0.833), and the maptool's stats row notes it. - Test.
testRocketThrust(weapons group): a server-stepped primary rocket is at 52 ±0.01 m/s after 1 s, still on its line, within 2 mm of projectileExtrapolate and 50.002 m along; projectileRewind brings it back (1 s and 0.2345 s); the lob keeps its horizontal speed and drops 13.5 m/s²; the stepped drop-and-thrust path matches its steps and rewinds. The snapshot round trip carrieshookAccel, and a protocol 54 header reads as no thrust.
Modes: neutral payload's clock scales with the players (owner, 2026-10-30: "npayload round timer should be 2 minutes per total players")
- What. New mode param
roundTimePerPlayer(modeparams.h, s, 0-3600, default 0 = off; common.qlogic declares it and its roundLength slot uses it). Above 0 the round clock is it times the players on teams A and B when the round goes live (at least 1), and it beatsroundTime. Neutral payload sets 120 (npayload.qlogic too): 1v1 4 min, 2v2 8, 4v4 16 (it was a flat 5 min).roundTime300 stays as the number a custom mode falls back to with the per-player clock set to 0. - Why at the round start. The clock is set once, when the freeze ends (roundLength is read there only), so someone joining mid-round doesn't move it; the next round counts them.
- Test. The neutral payload test (bots group): a 1v1 goes live with a 240 s clock.
Tests: testBotFalls plays on a lava moat again (engine work list, 2026-10-30)
- What was wrong.
testBotFalls(group botfalls) said it played experts on "Dojo (a platform in a lava moat)". That was the old Dojo; the rebuilt Dojo (a hall, tatami, garden: maps branch) has no lava, so the test would have stopped testing edges at all once it lands. - Now. The test writes its own map,
zz_lavamoat(lavaMoatMap(), the old Dojo's layout: a 24 m platform, a 4.5 m lava moat round it with its bed 1.4 m down, a ring path outside at the platform's height, walls), into the user maps folder for the run, checks it is what it says (lava under three points of the moat, none on the platform or the ring, 1v1 offered), plays 90 s of expert 1v1 on it, and removes it. The name is fixed (the bots' seed comes from it), andzz_keeps it out of the every-map checks. Foundry (tag, lava trenches) is unchanged. - Result. Lava moat: 0 falls, 0 self-kills, 6 deaths in 90 s (the old Dojo: 0 falls, 3 deaths); the limit stays at 2 falls and 1 self-kill.
Settings: the page strip, and pages that ran into their footer (owner, 2026-10-30)
- The page strip. Owner: "in the settings pages at the bottom you should be able to see all the page numbers, and on the end they should have left and right arrows, which you can also go to by pressing left and right arrow". Every tab with MORE pages (PLAYER 2, GAMEPLAY 5, CONTROLS 3, VIDEO + AUDIO 9, HUD 2; CROSSHAIR has one page and no strip) draws
< 1 2 ... N >in the footer's middle (Game::drawSettingsPages): the 376 px between BACK and the page's own right-hand button (DEVICES, RESET ALL KEYS, RESET CROSSHAIR...), cells up to 40 px wide, 4 px apart (nine pages: 30.5 px cells), so the strip takes no row from the content. The page open is a filled tab; a number goes straight to its page (Game::settingsPage, which also closes a picker and a key capture, and puts CONTROLS back on its first key page), the arrows step one. Edges: clamp, not wrap (the arrow at an end is greyed out and does nothing): a wrap from page 9 to page 1 reads as a jump, and every page is one click away on the strip anyway. Not shown on FONT, DEVICES, the controls import list, the HUD editors or while SEARCH's results replace the page. - Arrow keys. Left / Right (no Ctrl / Alt / Super) step the page in
Game::onKey, before the game's binds see the key, only whenGame::settingsPageKeys()says nothing else has the keyboard: not while a text field has focus (SEARCH, a name, a slider's or colour picker's typed value:Ui::typing), a key bind is being captured (so an arrow can still be bound), a slider is held, a colour picker or dropdown is open, chat or the console is open, or on the pages without the strip. No other settings widget takes keys (the < > option rows are clicked, they never hold focus), so the arrows had no job there before. - Replaced buttons. The single
MORE >/MORE n >button and the MORE pages' BACK-to-the-tab are gone (the strip does both). BACK is now the same on every page of a tab: it leaves settings. Esc keeps its old step (a MORE page goes to its tab's first page, then out). DEVICES >, RESET and the other right-hand buttons stay where they were. - The bug: content drawn over the footer. On Windows, VIDEO + AUDIO's left column has two rows Linux / macOS don't (GPU MAX CLOCKS, PRIORITIZE GAME, each with a two-line hint: 126 px; 166 px with VSYNC: LATENT SYNC's TEAR LINE row), which pushed FLAT MAP down until MAP COLOR (y 618-650) sat on BACK and RESET MAP COLOR on MORE (footer at y 640). Its right column ended at y 638 with EXCLUSIVE AUDIO, 2 px above the footer. GAMEPLAY's left column ran its last hint (USED WHEN SMOOTHING IS CUSTOM) to y 642 on every platform. CROSSHAIR's COPY / PASTE CODE answer (shown 3 s after a click) was a hint under that row, down to y 640 (found by the new UI test check): it's now beside the PER WEAPON header.
- The fix: rows moved, nothing shrunk. FLAT MAP (FULLBRIGHT, MAP COLOR, RESET MAP COLOR) to VIDEO + AUDIO page 2 under INTERFACE (that column ended at y 440, now 572; 602 with the integrated-GPU hint). EXCLUSIVE AUDIO (Windows) to page 6 under OUTPUT, after AUDIO LATENCY (its column: 568 to about 624 on Windows). SMOOTHING (INTERP) and SMOOTHING DELAY from GAMEPLAY's NETWORK to page 7's NETWORK, after PACKET LOSS PROTECTION, with the other network settings (that column: 458 to 572; GAMEPLAY's left column now ends at about 494, the page's lowest ink at 546). The settings search index follows by itself (
tools/settings_index.pyreads the pages at build time). - Audit. Every page at 1280x720, 1920x1080 and 1024x768 (Linux, measured): the closest is GAMEPLAY page 4 at 10 px (at 720p) above the footer, then PLAYER 12, CROSSHAIR 16. The Windows-only rows were added by hand: VIDEO + AUDIO page 1 left 526 (566 with latent sync), right 582; page 6 about 16 px clear; page 9's NVIDIA hint +14 px (ends about 400); page 7's two platform hints cancel out. The layout scales with the window height (text a little smaller at 4:3), so no resolution is tighter than 720p.
- UI test. The strip's
PREVIOUS PAGE,PAGE 1...PAGE NandNEXT PAGEare eachnavto their page (the open page and an end arrowinert), and the arrow keys are key actions doing the same, so every number, arrow and key is clicked / pressed on every page of every tab, in the menus and in game. New layout check: a settings page's content (every 2D quad drawn between the tab row and the footer, labels and hints too, not only widgets:Renderer::uiInkBottom, a pass over that frame's UI vertices, only while the test probes) must end at least 4 px (at 720p) above its footer row, at all three resolutions; the existing widget-overlap check only saw widgets, so a hint over BACK or a row on a platform's extra rows went unnoticed.
Settings: the SEARCH tab (owner, 2026-10-30)
- What q asked. "search should be its own tab, where it shows every setting in the game in a big scrollable list 3 wide, and whatever you type in the search bar should limit what is seen; you should be able to search the name of the setting, or what it does, so like typing in screen should show the devices, fullscreen setting, ...". It replaces 1.19.4's first search (a box over the tab row's right end that listed up to 14 labels as buttons jumping to their page).
- One source of truth: the pages themselves.
Game::drawSearchTabcalls every page's own draw function (PLAYER 1-2, GAMEPLAY 1-5, CONTROLS 1-3 and its two key pages, VIDEO + AUDIO 1-9 and DEVICES, CROSSHAIR, HUD 1-2; 25 in all) with aUiRelocatorset on the Ui. Every widget call (button, toggle, cycle, slider, colour picker, dropdown, text field) asks it first: it either skips the widget (returns before any text is fitted or drawn, reports nothing, takes no input) or gives it a cell, where the widget is drawn and works with the very code and config variable it has on its page (right-click to default, typing a slider's value, colour pickers, key rebinding). A setting added to a page later is in the search with nothing else to do. Considered and not done: a registry of every setting's widget as data (a rewrite of ~600 immediate-mode rows), or listing labels that jump to their page (what the old search did; q asked for working rows). - Keeping a page's other drawing out. While relocating,
Uidrops a page's labels and hints and answers no hover outside the placed widget (so e.g. PLAYER's model preview drag can't react), the renderer's 2D alpha is set to 0 andRenderer::rect / text / roundRect / ...now return at once when it is 0 (no quads; also a small saving for HUD elements at opacity 0),drawModelPreviewdoesn't run, and a page's state (tab, page, CONTROLS' key page, DEVICES) is put back after each. Left out of the list: navigation (navexpectations: FONT >, DEVICES >, EDIT ON SCREEN, the CONTROLS key-page tabs: they lead to pages that are listed themselves) and things that aren't widgets. A row that opens something of its own page (CONTROLS' IMPORT, when the clipboard holds no controls, opens its file list) takes you to that page with it open. - Matching. Every word typed (split on spaces, upper-cased once per change of the query) must occur in the widget's label, its id (a key slot's "CHAT KEY 1", a monitor's "MONITOR: ..."), its value names (a cycle's / dropdown's), its help or its keywords. Help is the hint drawn under the row, read at build time by
tools/settings_index.py(each hint attached to the row just before it;kSettingsIndexgains a help field); keywords are a small table there (KEYWORDS, by label or by an id's first part) for words neither has: "SCREEN" on DISPLAY, WINDOW SIZE, FULLSCREEN SIZE, RENDER SCALE, SHARPEN, MONITOR, REFRESH RATE, PRESENT, BRIGHTNESS, FOV MODE; "RESOLUTION", "MIC", "FIELD OF VIEW", "LAG", ... The result per widget is cached in a hash map for the query (cleared when it changes), so a frame does one lookup per widget. - The list. 3 columns of 235 px (at 720p), 9 rows on screen (a 14 px tag over each 32 px widget, 54 px apart; the last row ends at y 602, clear of the footer), only the rows on screen drawn. The tag names the page ("VIDEO 3", "CONTROLS: MOVE KEYS") and, for a widget whose label doesn't say what it is, its id ("CONTROLS: JUMP KEY 1", "PLAYER: NAME"). The count of matches is shown by the field. Wheel: 2 rows a notch; scrollbar: drag the thumb, or click the track for a page.
- Keys. Ctrl+F (Cmd+F) anywhere in settings, or "/" when nothing is being typed (its character isn't typed into the field), opens SEARCH with the field focused; not while a key bind is being captured, chat / the console is open, or in the HUD's full-screen editors. Esc with text in the field clears it (an open colour picker closes first); Esc again leaves settings. SEARCH has no page strip, so Left / Right never turn pages there.
- Cost. CPU time of the settings panel's page per frame, best 60-frame averages on this (shared, 4-core, llvmpipe) machine: SEARCH 0.3-0.5 ms (unfiltered and "screen"), VIDEO + AUDIO page 2 alone 0.2-0.3 ms, other pages 0.04 ms. Most of SEARCH's first measurement (1.6 ms of it) was VIDEO + AUDIO page 2 and DEVICES calling
glfwGetVideoMode(an XRandR round trip) every frame for WINDOW SIZE / REFRESH RATE:Game::desktopModenow keeps a windowed answer for a second (cleared when a monitor is plugged in or out), which speeds those two pages up too. Between full passes a page whose rows are all above or below the screen isn't run (its count from the last full pass stands in); every page runs when the query or scroll changes, on a click, drag or typing, with a picker open, and every 16th frame. Only the settings screen pays any of it, never a match. - UI test. New screen
settings/search(ingame/pause/settings/searchis skipped by host-ffa: the same widgets are tested on their pages in game). The field islocal()and is typed "FOV"; the scrollbar is a handle (state("searchscroll")); CTRL+F and / are key actions to it from every other settings page, ESC (CLEAR SEARCH) one while text is typed. A widget in the list is reported as<PAGE> > <ID>(two pages both have an IMPORT button, which the test mixed up) and as optional (the scrollbar's drag shows rows the test then never stops on; each is tested on its own page). Clipped labels aren't layout issues in the list (a third of a page wide; whole on their page). After the menus scenario,searchChecktypes queries and fails the run (as SETUP) unless "screen" finds FULLSCREEN SIZE, DISPLAY, RENDER SCALE and WINDOW SIZE, "fov" and "FOV" show the FOV slider on screen, "fov zoom" finds SNIPER ZOOM FOV and "volume announcer" ANNOUNCER VOLUME.config:last_tabaccepts 6 (SEARCH).
Bots: the tick and round spawns don't depend on slot order (map worker, 2026-10-30: "the team listed second scores about 3% more, on any map": Mine koth, 48 seeds)
- What depended on order. (1) In
Server::simulate()each bot thought and then ran its command in slot order, so bot 5 decided on a world bots 0-4 had already moved, shot in and killed in this tick (positions,lastFire, deaths), and the per-tick route budget (kPathsPerTick2) went to the low slots first. A team added first takes the low slots. (2)startRoundspawned players in slot order andspawnPlayerscores spawns by distance from and sight of alive enemies: the second team dodged the first team's fresh spawns, and last round's survivors still counted as alive where they stood. - Now. (1) Every bot thinks first, all on the tick-start world, then everyone's commands run, starting from a different connected player each tick (
tick % players), so no slot is always first. One extra pass over at most 32 slots and a 32-entry command array: no measurable cost. Humans' commands run in the same rotated order. (2)startRoundmarks everyone dead before the spawns and, in team modes,spawnPlayerignores enemies spawned in the same batch, so each team's spawn choice is the same whichever went first. Respawns during play are unchanged. - Numbers. The 3% didn't reproduce as a significant effect. Share of the team in the high slots:
quark_maptool botsSummit koth 180 s, 200 runs: score 0.503 +- 0.010, kills 0.502 +- 0.003; Bastion vs 120 s, 400 runs: score 0.492 +- 0.012; Mine koth 300 s, the 48 seeds both ways: score 0.506 +- 0.020 (pairwise +0.011 +- 0.044). So 3% on 48 seeds is within noise. In-process, Bastion vs 4v4 60 s, 600 seeds each way (1200 runs): before, kills 0.4925 +- 0.0050 (1.5 sigma), score 0.490 +- 0.010; with the tick fix alone 0.4920 +- 0.0051; with both fixes kills 0.5053 +- 0.0050, score 0.506 +- 0.010. Both are within noise; the fixes take away the mechanisms, so nothing depends on order by construction. - Test.
quark_tests tickorder: a hook hashes every player's position, velocity, health and alive at each bot think and checks every think in a tick sees the same world, no bot thinks twice in a tick and all 8 bots are first at some point; then Bastion vs 4v4, 40 seeds each way, the team-A score share with A listed first vs second within 4 standard errors.quark_tests orderbias [seeds] [map] [mode] [seconds] [perTeam] [firstSeed]measures it at scale (QUARK_LEGACY_ORDER=1for the old tick order). - Seeded tests. The rotated order changes every seeded bot match.
testBots's FFA-vs-experts run moved from seed 0x5EED0001 (the standing player now goes 40 s unhit, as 2 of 20 seeds did before too) to 0x5EED0003; over 20 seeds the old and new orders give the same spread (the player's deaths 34 vs 32 in total, kills 3 to 9, grapples 4 to 16).
Bomb: spawns by role, not by team (q, 2026-10-30: the recommended option, confirmed)
- Why.
modeSpawnTypestarted team A on the map'saspawns and team B onbin every round. q's timing rule for bomb maps (the attackers reach a site in about 5 s, the defenders 1 s later) is built for the side that attacks, and team A attacks the first half (bomb.qlogic:attackers = half > 0 and roundNo > half ? B : A). So after halftime team B attacked from the defence's end and team A defended from the attack's: one team had the short way in both halves. On War (115 m vs 144 m) team A won 7 of 8 matches. - Now. In bomb, the attackers start on
aand the defenders onb(or the map'sbomb-a/bomb-bset, format 11), whichever team that is:team == attackers ? a : b. Soameans "the attack's spawns" on a bomb map, as the maps were built (team A attacks first).roundSetupsets the attackers before the round's spawns, so every round, halftime included, spawns on the new sides; respawns in the warmup use the first half's attackers (team A). Custom modes based on bomb get the same throughmapSpawn. Other modes are unchanged. - Checks. The walk check already asks both sides' spawns to reach every site, and the sight check pairs
awithb, so nothing changes there:quark_maptool checkpasses all 15 checks on Depot, Datacenter and War (maps-1196). Quarry isn't in any worktree here. - Test.
testBombSpawnRoles(grouprules spawns):modeSpawnTypefor each team in each half on Depot and on a map with abomb-a/bomb-bset, vs still by team; then a real Depot match with halftime after round 1: in round 1's freeze team A stands on anaspawn and team B on abone, in round 2's team B onaand team A onb.
Glass: sloped panes are their shape, not their box (a map review, 2026-10-30: Museum's glass dome on maps-1196, four 40 x 6 m slabs floating over the roof)
- What was a box. Only the drawing and a few point tests. The game drew every pane with
translucentBox(brush.box)(plain and breakable glass, and the editor's baked view), and the see-through looks and breakable blocks withboxFaces(the box's 6 faces), so a pane on a wedge, corner, tetra or cut drew as its bounding box: a dome face's box is mostly air.Map::glassAt(which pane a shot or bolt is in),blockAtandcrumbleAttested the box grown by 3 cm: where sloped panes' boxes overlap (a dome's four faces all do), a shot on one face could break another. Shards started anywhere in the pane's box. Collision (sweepAxiswithbrushObstacle), every ray (rayBrush: shots, sight, the grapple) and the dash-through test already used the real shape. - Now.
Renderer::translucentBrushdraws a slope or cut as its faces (slopeTriangles, map.h: a fan overslopePolygons), a box as before;boxFacesgives see-through looks and breakable blocks on a slope their faces too, with the outline distancesRenderer::slopeuses.brushNear(b, p, eps)(map.h: the box and each sloped plane, grown by eps) replaces the box tests inglassAt,blockAtandcrumbleAt.glassShardstakes the pane's brush: on a slope a shard's start is drawn again (up to 12 times) until it is in the shape; a box draws exactly the same numbers as before, and the server and the clients (GlassFx.brush, new) pass the same brush, so they still make the same shards. A shattered sloped pane's glitter starts at its pieces' starts when it would be off the shape. No cost for box panes; a slope's triangles are made per frame like the box's quads (no shipped map has a sloped pane yet). - Test.
quark_tests slopedglass: a breakable wedge, a second one rising the other way in the same box, and plain glass on a cut box of the same slope. Both draw 8 triangles, 157.7 m2, every corner on the shape (the box: 208 m2); the cut is air over its low end and solid under it; a shot down at x 7 hits it at 0.50 m (the box's top: 4); a player box drops onto the slope;glassAtpicks the wedge whose shape holds the point (the box test picked the first); a shatter's 600 shards are all in the wedge (290 off it with only the box), and a box pane's shards are unchanged; breaking one wedge leaves the other standing. Checked by eye on Museum (maps-1196): the dome draws as four sloped glass faces with the rafters under them.
Race leaderboard: the race total (owner, 2026-10-31: "race leaderboard time should be combination of all times on all maps; i will complete the maps and give a default time that if you havent played the map that is your "time" on it")
- Map format 13:
racedefault <s>. A race or surf map's default time (1-3600 s;Map::raceDefault, 0 = none), read, written back byMap::serialize(so the editor keeps it) and carried by quark_maptool's JSON asracedefault(so Quarkology's editor round-trips it). Converter step 12 -> 13 changes nothing but the version line (a format 12 map has no default); the shipped hand-made maps (Cobblestone, Surfy) get just that line, as the converter writes it, so they still hash as official.testMapConverterconverts a version 12 race map and checks the line's parse, write-back and bounds. - Ranked race maps are data, not a name.
rankedRaceMaps()(ranked.h) = the official maps built for race or surf (for race,for surf; owner, 2026-10-31: "the total race time also counts the 2 surf maps"), sorted by name, read once from the embedded maps (only maps with such aforline are parsed; a time trial server reads them at start, not on its first tick). Nothing lists map names by hand: a retired map (a legacy map, no longer official, as Viaduct becomes) drops off by itself. Lodestone also offers race (itsmodesline) but is built for points: not on it. The time trial (Server::timeTrialCounts, the client's split comparison, MY RACE STATS) uses the same list instead of"viaduct". - Surf time trials. Surf already shared race's checkpoints, splits, practice and the server's /racerun post (
isRaceMode); what was missing was a way to start one and to count it. PLAY TIME TRIAL on a surf map now hosts it in surf (knife only, no dash or grapple), and a time trial counts only in the map's own mode (RaceMapInfo::mode): a surf course run in race mode with dash and grapple would post times surf can't match. - Defaults. A map's
racedefaultline, orkRaceFallbackDefaultS= 600 s until q gives it one (Solar System and Surfy have none yet; Surfy's file is q's and isn't edited). Placeholders on the new race maps (tools/maps/<name>.py, about 3x a good run, until q sets them): Particle Accelerator 225 s (a good run is about 75 s), Jungle Ruins 180 s (about 60 s), Rooftop Run 180 s (about 60 s). - Split floors on Surfy are weak: it has one checkpoint (the finish) about 5 m from the start as the crow flies, so its floor is 109 ms and only the Worker's 5 s minimum run guards it. More checkpoints on Surfy would fix that (q's map).
- Worker:
GET /racetotal[?n=25]answers "total_ms maps_run name" per line, fastest first, for every racer with a run on at least one ranked race map. The total is the sum of the defaults plus each racer's (best - default) over the maps they ran, so one grouped query over race_times (joined to a VALUES table of the maps and defaults) answers it; ties go to whoever set their latest time first. A run counts as run even if slower than the default (q's rule: "if you haven't played the map that is your time on it"). Rows on a map that is no longer ranked (Viaduct's) stay in race_times and count nowhere. No D1 schema change, so no migration./raceboard?map=Mstill answers each map's board, and/crownslists each ranked race map's top 10 as before. - The Worker's map table
RACE_MAPS(name -> defaultMs and split floors; replacesRACE_MAPS+RACE_FLOORS) is generated:build/quark_tests racefloorsprints it, and quark_tests fails if worker.js's copy differs from the maps. Chosen over the game server sending the list: a server can't change what counts, and the Worker answers the board with no server involved. - Game. LEADERBOARD's fourth column is RACE TOTAL (top 10, then YOU: your total and maps run, your time on each map, grey at its default, and "UNPLAYED MAPS COUNT AT THEIR DEFAULT TIME"); a TRIAL MAP cycle picks the map PLAY TIME TRIAL starts (
--timetrial --map NAMEtoo). MY RACE STATS lists the ranked race maps first and says what the map adds to the total (your best time trial, or its default). Your own total is worked out from your local runs (race_times.log), the board's from the Worker. - Tests.
testRaceFloorschecks every ranked race map (every official race / surf map is on it, floors, its default or the fallback, above the fastest plausible run,raceTotalMs) and the Worker table;testTimeTrialalso checks a surf map's trial counts in surf and not in race;testTimeTrialruns on the first ranked race map;tools/master/test.mjsruns the race tests on a map from the table and adds the total (defaults for unrun maps, a second map lowering a total,n, a retired map's rows ignored).
Maps: the old pool retired to maps/legacy/ (q, 2026-10-30: "every current map goes except q's two hand-made ones, surfy and cobblestone"; 2026-10-31: a "Legacy maps" setting, default off, instead of deleting them)
- Build.
tools/make_maps.pykeeps the 20 retired modules (clockwork, depot, summit, canal, harbor, bastion, foundry, temple, colosseum, furnace, gantry, anvil, viaduct, hamlet, downtown, orbital, lodestone, midtown, hightown, cascade) in a newLEGACYlist and writes them tomaps/legacy/<name>.qmap(--list-legacy; CMake makes them outputs of the map step). They are not passed totools/embed_maps.py, so they're not official: not inkOfficialMapData, never ranked, no shipped light bake (a first load bakes, Midtown ~5 s). Packages already copy all ofmaps/, somaps/legacy/ships on every platform with no packaging change. make_maps.py also deletes a generated copy of a legacy name left at the top ofmaps/by an older build (it would list as an unofficial map)..gitignoreignoresmaps/legacy/. - Listing and loading.
setLegacyMapsListed()(map.h): off,listMaps()lists the shipped and your maps folders as before; on, it also lists the shippedmaps/legacy/and your<maps>/legacy/(names deduplicated, so a current map of the same name wins).mapFilePath()looks in the legacy folders last whatever the setting says, so--map temple, an old server config, a vote from an old client, a demo or the shipped Hamlet benchmark demo keep working.isLegacyMap()tells the picker: the title gets " (legacy)". The client sets it fromlegacy_maps(GAMEPLAY > MAPS > LEGACY MAPS, default off for everyone, upgrades too) at start and on toggle (and rescans); the hosted server's map list (Server::start->listMaps()) and the map vote follow it. The dedicated server takes--legacy-maps. - Your maps folder (moveOldOfficialMaps, once per version). Older versions and launchers copied official maps into
userMapsDir(), where a copy shadows the shipped map. At the first start of a version (the game client, skipped in automated runs like the map converter; and the dedicated server), each top-level.qmapwhose first line is# generated by tools/make_maps.pyand has nohandmadeline moves to<maps>/legacy/(a taken name becomes<name>_2, ...; an identical copy is just removed), with its<name>.mapedit.json; its light caches named after it (<maps>/<name>.qlm,cache/<name>.qlm) are deleted (they rebuild). Hand-made, editor-saved and custom-named maps,maps/downloads/and every subfolder are never touched. Retired generated maps left at the top of the install's maps folder (an unzip over an old install; the launcher'sswapInstallalready drops packaged files the new version lacks) are removed, or moved intomaps/legacy/if it lacks them; a read-only install is left alone. A stamp<user>/legacy_maps.stampholds the version, so later starts don't scan (no startup cost). What moved is logged (maps: temple.qmap: moved to legacy/temple.qmap).resetOfficialMapsalso removes the retired names from the install's top level. - Defaults off the old maps. Host map (
host_map) and the dedicated server's default: bastion -> sewer (built for free for all, the default mode). Menu background: clockwork -> castle ("BACKGROUND: CASTLE"). The UI test hosts qk1 (was temple), edits a copy of sewer (harbor) and plays hide on museum (hamlet);tools/test_editor.shedits qk1;tools/server/install.sh's example is--map greenhouse. INFO > MAPS lists the new pool. A ranked server configured for an old ranked map (temple, furnace, gantry, anvil: legacy now, never ranked) logs it and plays its ladder's designed-for map instead of refusing to start, so the live servers survive the update without a config change. - Tests.
testLegacyMaps(group resetmaps): listMaps with the setting off and on (shipped and user legacy folders, a current name winning), loading by name either way, every legacy name unofficial; the move on a made-up user folder (a generated copy with sidecars moves, a name clash gets_2, the hand-made, custom, downloads and subfolder maps stay, the install's retired copy goes) and the stamp (the same version doesn't scan again, the next one does).testResetMapsruns on qk1. - Docs. MAP_STRUCTURES.md: the retired maps' sections are dropped (they're in git history), a table of the new pool and a legacy-maps note added. RELEASE_CHECKLIST.md: the mode rows name the new maps, plus rows for the legacy toggle / folder move and the ranked defaults.
- Mode coverage (q's target: 2 built for each mode, 5 playable). No map is built for duel, tourney or one-way payload (duel plays 14 maps, tourney 11 1v1 maps as q allows, payload only Railyard); bomb is playable on 4 maps, npayload 4, race 3, points 2.
Ranked: qk1-8 on every ladder, each ladder's designed-for map, fairness instead of symmetry (q, 2026-10-31: "qk1-8 can be played in instagib, vanilla, and lgvamp. lg vamp is designed for dojo, 1v1 vanilla is designed for greenhouse, instagib is designed for spire"; 2026-10-30: "Maps never have to be symmetrical")
- Ladders. qk1-qk8:
ranked vanilla vampire instagib(lgvamp = thevampireladder, laser vampire). Greenhouse getsranked vanilla, Spireranked instagib; Dojo keepsranked vampire. Theirforlines already fit (Greenhouse and Dojo 1v1, Spire ffa: ranked instagib is a free for all). - Designed-for map.
rankedDefaultMap(ladder)(gamedefs.h): greenhouse / dojo / spire. Going ranked (Server::setRanked) and the client's ranked pickers (bestRankedMap) take it first when it's ranked for that ladder; else, as before, the map ranked only for that ladder, else the first that takes it. Before, the default was whichever map was ranked for that ladder alone (temple, furnace, gantry); with qk1-8 on all three that rule would have picked qk1 for vanilla and instagib. - Fairness, not symmetry. The ranked check required every solid, spawn, ladder and light to have a point-mirrored twin and the sun overhead, which Greenhouse and Spire aren't. It now checks fairness: for a ladder played 1v1, as many A as B spawns, and on the walk grid (Dijkstra, horizontal length plus climb at run speed) the two sides' mean run to the nearest enemy spawn, and to the middle of the map, within 1 s of each other. A hard failure for generated maps (human-made: reported). Instagib's free for all draws everyone from one pool, so it needs no side check; spawn sight and spacing are testSpawnSanity's, for every map. The test's "3 maps ranked for one ladder alone" rule becomes "each ladder's designed-for map is ranked for it" (and 2+ maps per ladder).
prefabs.top_up_spawnskeeps spawns in mirrored pairs only on symmetric ranked maps (a ranked map with the sun overhead: the qk maps, Dojo), so Greenhouse's and Spire's pools top up as before.
Hide and seek: a 5 s head start (q, 2026-10-31: "reduce hider time to 5 seconds")
kHideTime30 -> 5 s (gamedefs.h): the freeze while the seeker faces the wall. It feeds the native default (ModeParams::hideTime), the mode logic'shideTimeparam default (modelogic.cpp, nowkHideTimeinstead of a literal 30) andtools/modes/hide.qlogic(param hideTime f32 = 5). A custom mode that sets hideTime keeps its own value.- No protocol bump. The client never derives the head start: the freeze is a server phase, and its end reaches clients as the phase timer in the mode state, the same as before; the "hide! (N s)" line is server text. An older client on a 1.19.6 server shows a 5 s freeze correctly, and the other way round a 30 s one.
Race and surf: no time limit (q, 2026-10-31: "in race and surf modes there should be no time limit lol why is there one")
- What changed: race and surf rounds had a 300 s round clock (sized for two loops of Viaduct), and a round with no finisher ended with "nobody finished". The mode default
roundTimefor race and surf is now 0, so there's no clock and a round runs until someone finishes (first to finish wins it, as before). A host or mod that sets a clock still gets one (tickRaceonly ends a round on time whenphaseEnd_ > 0). - Split times were worked out from the clock (
liveLength_ - (phaseEnd_ - now)). They now count from the round going live (roundStart_), which gives the same number when there is a clock. - The HUD's run time (to the hundredth) was
300 - timeLeft, which only worked while the clock existed. It's now the client's own stopwatch from the round going live, shown in every race and surf round, not only the time trial. The splits and the board still use the server's times. - The built-in mode scripts (tools/modes/race.qlogic, surf.qlogic) declare
roundTime 0too, and their tick ends a round on time only whenphaseEnd > 0(with no clock,now >= phaseEndwould have ended every round the moment it went live). - No protocol change.
Rockets: splash did no damage on servers (q, 2026-10-30: "explosions aren't registering, they do no damage on my Oracle box server"; direct hits worked, splash did nothing, no hit markers)
- Cause. Since 1.19.3 the shooter's game reports its own rocket hits (
Game::reportImpact->reportBlast,hitcommands) andServer::explodeleaves every victim but the shooter to that report. The game reported the blast at the exact point its rocket's path met the floor or wall (q.pos + d / len * hitfor the predicted copy,pr.pos + d / len * hitfor the server's copy drawn ahead).reportBlast's cover test then cast its rays from that point, on or (by rounding) a hair inside the surface, andMap::raycasthits a box at t = 0 from a point on its face, so the centre ray and both F16 rays were "blocked" and every splash victim was skipped: the report went out with no victims. The server's own blast never had this:stepProjectilesets it off 5 cm short of the surface. A direct hit's point is on the player's box, not the map, so it still counted. - Not ping. Reproduced on a local dedicated server (1v1, 250 Hz, a UDP delay proxy at 20, 80 and ~220 ms round trip, with jitter and loss) with two real games under Xvfb, the shooter aiming at the floor 1.2 m beside a standing player: 1.19.3 counted 4 of 20 blasts the server would have hit (the 4 came from the "vanished projectile" path, which reports from a point in the air), the fix 14 of 14. Any reported blast on a surface was affected, at any ping.
- Fix.
blastPoint(pmove.h,kBlastBackoff0.05 m) is where a projectile goes off: 5 cm back along its flight from where it met the surface or player.stepProjectileuses it (unchanged: it wasmax(0, best - 0.05)), andreportImpactbacks every reported rocket blast off the same way. The cover test is one shared function,blastReaches(pmove.h), used by bothServer::explodeandGame::reportBlast, so the two can't drift apart again. Where the explosion is drawn (localBoom) is unchanged. No wire change. - Test.
testClientHits(group materials): a floor blast 0.8 m from a player reaches them fromblastPoint, not from the surface point (exactly on it or 1e-5 inside), and a player under the floor stays behind cover.
Votes: no legacy maps unless the server lists them (owner, 2026-10-31: "you cant vote the old maps on the new testing server update")
- A legacy map loads by name (an old config or demo still works), so
callvote map summit kothused to pass. A map vote for a legacy map is now refused ("<map> is a legacy map (this server doesn't list the old maps)") unless the server runs with--legacy-maps. testMapVote checks the refusal and votes Glacier Summit instead, and its every-map loop still votes every listed map in every mode it supports. - A dedicated server set up for a legacy map (an old
--map, unranked, legacy maps off) now plays the listed map built for its mode instead (else the first that takes the mode), so nobody lands on a map they couldn't vote back. A ranked one already switched to its ladder's map. (It's in quark_server's start, not Server::start: there it also swapped the maps of the in-process test servers and broke ~370 weapon/rules checks.)
Servers: a new install plays unranked laser vampire on Dojo (owner, 2026-10-31: "default server position should be dojo ranked lg vamp for future updates", then "actually unranked")
tools/server/install.shwith no server options on a first install now runs--mode 1v1 --variant vampire --map dojo --public(it was--public: FFA on the default map). Re-running it still keeps an installed server's options.
Solar System: q's own stage designs (owner, 2026-10-31: "so i can design the maps, you just build them")
- q laid out each of the 15 stages (Sun to Pluto) as a list of pieces in the Solar Stage Builder. Claude drew each one top-down, and q approved it before it was built.
tools/maps/solar.pybuilds exactly those lists, in the approved layout. - Surf conventions (q):
- Every ramp has its own gap before the next, and an air piece is just an extra-big one.
- Ramps run about 3 s and flights about 2.2 s at the speed reached there.
- Left/right/top/bottom walls are kill walls standing in the flight, death floor to ceiling, covering 2/3 of the lane so you strafe to the open third.
- A speed check is a solid full-width wall, its top 1.2 m under an 85%-speed flight. Its height is an estimate for q to tune in the editor.
- Turnback legs are split by a wall you ride round.
- Geometry:
- Gaps are ballistic flights at g=24, placed for a run at 85% of the ride's speed.
- The next ramp starts as steep as the flight falls there (at most about 50°), so you land along the face.
- Faces are 56-62°, 20 m wide (10 m for narrow ramps and split lanes).
- Curves are pieces of at most 3 m and 5°.
- Walls stand 24 m each side of the line. Lower sections passing under higher ones get a kill-liquid roof.
- Jupiter's helix has a centre column, so you can't drop through and skip the spiral.
- Each stage ends on a platform with a framed gate; Pluto's is the finish room.
- Some flights are very long (Mars 218 m, Asteroids 292 m, Io 252 m), so later ledges sit high (Pluto's at 1309 m) and speeds reach about 110-130 m/s. That's for q to tune.
- Back in MAPS, and on the ranked race total (RACE_MAPS,
racefloors) withracedefault 600as a placeholder until q sets the time. About 18,300 brushes.
Weapon switch: a held scope doesn't lock fire (owner bug report, 2026-10-31: "when holding scope on rifle and switch to laser you cant shoot until you un hold right click")
- Cause: switching with fire OR alt-fire held locked fire until both were let go, and a held scope is alt-fire. So rifle (scoped) -> laser locked the laser.
- Now only the buttons that fire the new weapon lock it:
fireButtons(w)= fire, plus alt-fire where it is a shot (knife/syringe throw, shotgun bounce, rocket lob, bubble) or the sticky detonation. It doesn't include the rifle/sniper scope or the laser pull. The same rule applies on the server, in the client's mirror and in weapon prediction. - The game also ignores an alt-fire held over a switch until it's let go, so the laser shoots normally rather than as a pull. Toggle-scope already dropped it on a switch.
- A knife thrown with right click, landing back on the sniper with the button still held: the sniper is scoped and isn't locked (it was locked before). Tests: testSemiAuto (scope held rifle -> laser, then fire: fires) and weaponpredict.
Syringe: half the healing (owner, 2026-10-31: "half syringe healing")
- Self-use 10 -> 5 HP (the SYRINGE row's damage, which is its heal), still once per 1 s cooldown. The thrown heal on a teammate is 30 -> 15 HP (kSyringeHeal). The throw's cooldown resets are unchanged. A custom mode's syringe damage scale still multiplies both. testSyringe and the throw-reset test have the new numbers; the release checklist says 5 / 15.
Clean surfaces on every look (owner, 2026-10-31: "clean surfaces should apply to the custom surfaces too not just default ones")
- VIDEO > CLEAN SURFACES only took the panel seams and shade noise off the palette materials (0-11). Now every look from 12 on (ice, sand, wood, brick, the 400+ surface looks, the large patterns as their base, the see-through and feature blocks) draws as its average, flat: its albedo on tops and on sides separately (grass: green tops, earth sides), its see-through alpha and its glow. Liquids keep their motion.
- How: when it's on,
Renderer::buildCleanMeansdraws each look the map uses once, with the map's own world program and this frame's palette, as a 4 m square (64 px) on a top and on a side. It averages the pixels into a small RGBA32F texture (texture unit 5, a texel per material, 3 rows). The world shader then reads that in place of running the look. That's cheaper per pixel with it on, and unchanged with it off (one uniform branch). It's redone only when the world program, the palette or high contrast change: a few dozen 64 px draws and one readback while a map loads.
Legacy move: only copies named like a shipped map (owner, 2026-10-31: "solar system 12 doesnt load in game... i can only see up to solar system 9")
- moveOldOfficialMaps moved every untouched generated map (first line
# generated by tools/make_maps.py, nohandmade) from your maps folder to maps/legacy/, whatever its name. On q's PC that took solarsystem1-12 (live playtest copies) and solar.qmap, along with older copies of the new pool. It now also needs the file's name to be an official map's, a legacy one's or a retired one's (arena, tug, duel, sprint, crucible): those are the copies that shadow a shipped map, and anything else is yours. testLegacyMaps adds a generated map saved as solarsystem12, which stays.
Map editor: sloped and cut pieces are picked by their solid (owner, 2026-10-31: "in the editor the hitboxes for the blocks are super fuckey, with the custom cut face blocks")
- edPickAll tested every box and group part against its bounding box. A cut box (or wedge, corner, tetra) was clicked through the empty part of its bounds, so the click took it instead of what was behind, or the wrong one of two neighbours. Non-box shapes now use rayBrush, the same ray-vs-solid test shots and the grapple use (box slabs cut by the sloped planes). Plain boxes keep the bounds test and its inside-the-box rule.
- (q, 2026-10-31: "smooth faces should turn all faces into single colors") With it on, the palette materials drop their remaining detail too: the metal's brushed streaks, nograpple's stripes and the walls' darkening near the floor. Water, lava and acid become one still colour each. Every face is a single colour.
Step-up on cut boxes' upright sides (owner, 2026-10-31: "sometimes you cant stepup on cut boxes even though the height distance is still below 0.55")
- slideMove treats any face of a slope or cut box that isn't within 45 degrees of flat as a wall (
wall), and the step-up ran only!steep. So a cut box's upright side at an angle in plan (n.y = 0) blocked like a steep ramp, while an axis box of the same height was stepped onto. An upright face (|n.y| < 0.01) is now a riser. The step-up is tried, taken only if it ends higher, and it gives back the speed the face took (unless dashing). Sloped faces steeper than 45 degrees still never lift you. testSlopes: a 0.4 m cut box with a 45-degree upright side ends at 0.40 m; a 0.4 m 63-degree wedge stays at 0.
Flush faces: the thinner brush's face shows (owner, 2026-10-31: "the FACE of the SIDE OF THE STAIRS is not attached to the object itself", "basically every map does it")
- Where two boxes' faces lie in the same plane, face the same way, overlap and differ in look, the world mesh lifts one a hair off the plane so they don't z-fight. It used to lift the higher material index, whatever the shapes, so a staircase built into a thin rail (Dam's valley stair: steps x -32..-27, rail x -27.1..-27, sharing the plane x = -27) drew its steps' side faces over the rail's outer face: the stair profile showed on the outside of the rail.
- Now the face whose box reaches less far behind the plane goes on top: a decoration set flush into a wall (thinner than the wall) still shows; a box or staircase overlapping a thin wall (5 m deep behind it vs the rail's 0.1 m) stays behind the wall's face. Equal depth: the higher material, as before. Same look: nothing lifted (no z-fight to hide).
- The lift is now 4 mm a level (a face over a lifted face goes one level higher) instead of 2 mm times the material index. With 596 looks that index runs into the hundreds, so faces floated far off their boxes: Monorail's deck top (polishedconcrete, look 362, over its flush white edge bands) drew 72 cm above the real deck, hiding the cart line and the bases of things on it, and Dam's stair sides (boardformedconcrete, 397) stood 79 cm out from the stairs (owner, 2026-10-31: "the monorail platform face being lifted off of the actual surface"). Lighting bake unchanged (no rebake).
Pattern fade: every look fades to its average when it's too fine to draw (owner, 2026-10-31: "if you are far away from a patterned surface it should fade, like how lava does, because many many surfaces have the same problem lava does; depends on angle and screen resolution, so should have a setting... where lava changes now is the default for all textures")
- The world shader measures each pixel's footprint on the face in metres (fwidth of the face-plane position: grows with distance, a grazing angle and a lower resolution or render scale alike). Past PATTERN FADE's start the surface look (every material from 12 on: the 596 looks) blends to its average colour, alpha and glow; at the end it's all average.
- The averages are CLEAN SURFACES' (each used look rendered once on a 4 m top and side and averaged into uCleanTex), now built whenever pattern fade or clean surfaces is on: no cost per frame.
- Default = lava's own numbers: lava's veins fade between 0.25 and 0.5 noise cells a pixel at 0.35 x 4 cells a metre, i.e. 0.18 to 0.36 m a pixel, so every look now starts fading at 0.18 m a pixel and is flat at 0.36. FADE DISTANCE (0.25x-4x) scales the start (2x: fades from twice as far), FADE LENGTH (1.25x-8x) is the end as a multiple of the start. The liquids' and noise surfaces' own anti-aliasing (noiseFade) follows the same two numbers. PATTERN FADE off: as before.
- Speed: a fully faded pixel doesn't run its look at all (one texel fetch), so far floors and walls of expensive looks (cobblestone's Voronoi) cost less: better lows on big maps. Part-faded pixels pay the look plus two fetches.
- Config keys pattern_fade, fade_dist, fade_len.
Alt-tab: frames stop, the game doesn't (owner, 2026-10-31: "you shouldnt pause the server when hosting if you alt tab it should only pause the frame generation")
- Settings item #76 (pause offline games when unfocused) was skipped as needing a server-side pause; q's ruling replaces it: nothing pauses, only drawing stops.
bg_draw(DRAW IN BACKGROUND, off by default): while the window is unfocused or minimized,Game::frameruns the game (network, prediction, sounds, the match) and returns before the render section, and the main loop skips the latent-sync wait, the swap and the low-latency fences. The loop then runs at BACKGROUND FPS LIMIT (60 when that's "same as FPS limit"), so the network is read and commands sent as before, with no GPU work at all. On: drawn at the background limit, as before.- The hosted server was never paused by alt-tab: it runs on its own raised thread, the process priority only drops back to normal (not below), and Windows 11's background timer and power throttling were already opted out of (net.cpp). This removes the last cost of the background game, the frames nobody sees.
- Automated runs (screenshots, benches, the UI test, backdrop shots) always draw: a headless window is never focused.
1v1: winner stays, spectators rotate in (owner, 2026-11-01: "make 1v1 modes swap in specators cyclicly winner stays, even ranked")
- Which modes: arena (1v1, ranked's mode) and a duel with team size 1; never vs bots.
Server::oneVsOne(). - The line.
ServerPlayer::queueSeq(0 = not in line) from a counter (queueNext_), so join order decides. A newcomerautoTeamseats as a spectator joins it;team a/team bon a full 1v1 joins it (unranked still calls the let-in vote too; ranked, which refused before, and a ranked match in progress now queue you);team specleaves it (the only way to watch without playing next). Each gets a message saying where they are. - The rotation.
enterMatchEndkeeps the winning team (matchWinner_); when the match end screen is over, beforerulesReset,rotateWinnerStaysmoves the loser to spectators at the back of the line and seats the first in line on the loser's side. A draw (no winner) or an empty line: the same two play again. The ranked result is posted at match end as before, so each ranked match is rated between the two who played it, and the next match gets its own ticket. - Leavers. In warmup,
fillOneVsOneSeatsgives an empty seat (a player gone for good, after the reconnect grace) to the first in line. - No protocol change: the line lives on the server, the messages are ordinary events.
- Test:
quark_tests duel(testWinnerStays): join order, the loser to the back, a draw, cyclic order over three matches, leaving the line, a leaver's seat.
Winner stays mode (owner, 2026-11-01: "make a new winner stays mode, its like 1v1, but winner stays 1 round at a time first to 10 wins is the winner")
MODE_WINNER15, "winner" / WINNER STAYS; protocol 59 (an older client has no mode 15).isOneVsOneMode()= 1v1 or winner stays: everything 1v1 does (one human a side, the drain, the short round end, the rulesets, the HUD clock) winner stays does too.- Rounds, not matches, rotate. The 1v1 line (
queueSeq, from winner stays for 1v1 matches) turns every round:endRoundkeeps the round's winning team (roundWinner_), and when the round end screen is overwinnerStaysNextputs the loser at the back of the line and seats the first in line on the loser's side (a draw: the same two again), then fills a seat a leaver left. - Score = round wins. A round win is +1 to the player's own score (a kill scores nothing here,
nativeKilland winner.qlogic'sslot kill); scoreA / scoreB always show the two seated players' totals (Server::syncWinnerScores), so the match ends through the ordinary first-to-rounds check at 10 (rounds10). The scoreboard shows the score (round wins). - Mode logic: tools/modes/winner.qlogic, with a new object
lineand its stepwinnerStays()(MA_WINNER_STAYS); the native rules match it. - Maps: winner stays plays on 1v1's spawn set (
spawnSetFor, map.h), so there is no map format change; prefabs.py offers it (and tourney) on every map that offers 1v1. The menus pick a map built for 1v1. - Not ranked: the ladders stay 1v1 and FFA.
- Test:
quark_tests duel(testWinnerStaysMode): the loser to the back each round, round wins as the score, the match over at the 10th.
Tourney on any 1v1 map (owner, 2026-11-01: "tourney shouldnt have map overrides, tourney + 1v1 can be played on any 1v1 map")
- prefabs.py adds
tourneyto themodesline of every map that offers 1v1 (before, only the qk maps, Greenhouse and Orbit listed it), so the host picker, the vote and the TOURNEY MAPS rotation list every 1v1 map.
Alt-tab sound burst (owner, 2026-11-01: "when you are alt tabbed out of the game and tab back in it plays like 200 sounds at once")
- Each queued sound carries when it was played (
Voice::queued); the mixer drops a sound effect that waited over 0.2 s (an announcer line 1.5 s) instead of starting it late. A device that stalls in the background (exclusive audio gives way to another app) had kept up to 256 sounds pending and started them all together. - At most 32 new sound effects start in one mixer period (the newest); the rest of a burst is dropped. A backlog of events handled in one frame stays a few sounds.
- With the master gain at 0 (MUTE IN BACKGROUND, or volume 0) nothing is queued at all.
Accuracy: each weapon's own, averaged (owner, 2026-11-01: "it should count as percent of shots per weapon averaged not total shots because 30% lg for 1 second means 300 shots landed and 700 shots missed and will lock your acc to lg")
ServerPlayer::wpnShots/wpnHitsper weapon; ACC (PlayerSnap::acc) is the mean of the fired weapons' hit percentages. Example: laser 30% over 100 shots and rifle 80% over 10: 55% (by total shots it was 34%).- The match summary's accuracy is the server's now (it was the client's total over every shot).
- Reset: every match start (
startMatch) and every new player, as before (resetAccuracy). Test: testAccuracyPerWeapon.
Health bars in ranked (owner, 2026-11-01: "because healthbars are enabled they should be allowed in ranked")
- The ranked check on the health bars over players is gone (game.cpp): ranked draws them like any other match when HEALTH BARS OVER PLAYERS is on (the default). The server already sent visible players' health in ranked; nothing else changes.
Dash charges stay at two (owner, 2026-11-01: "also only 1 dash charge default", then "nevermind change the dash back")
- Testing build 1.19.3.23 had one charge (protocol 60);
kDashChargesis 2 again and the protocol is 61, so a 60 client (which would predict one) can't join a 61 server. The tests are back to the two-charge defaults.
Planting / defusing keeps knockback (owner, 2026-11-01: "when defusing/planting you take no knockback this is wrong")
- The planter / defuser was MF_FROZEN (the round start's freeze), which zeroes velocity every step. Now
bombRooted(protocol.h) drops the command's movement, jump, dash, grapple and fire instead, on the server (runPlayerCmd) and in the client's own prediction (NetClient::tick), so knockback, blasts and grapple pulls move them and friction slows them as usual. bomb.qlogic'sslot frozenis the freeze phase only. Pushed out of range, the plant / defuse stops (bombStep's range check, unchanged). - Test:
quark_tests bombsites(testPlantKnockback).
Votes: one VOTE builder for mode, modifiers and map (owner, 2026-11-01: "just a vote button / then you can toggle on options gamemode modifiers map / under gamemode you pick objective based or player based ... pick gamemode first, then you get to see modifiers, then maps")
- Pause > VOTE > MAP / MODE / MODIFIERS opens the builder (drawVoteSetup, game.cpp): three toggles (all on by default; the modifiers page starts at the standard rules, so leaving it as it is votes the standard rules), then each part that's on, in the order q asked for. GAMEMODE: OBJECTIVE BASED (bomb, koth, neutral payload, payload, multi point, race, surf) or PLAYER BASED (1v1, vs, ffa, tag, hide and seek, power, winner stays, duel, tourney), then the mode list filtered to that group, then the ruleset for modes that have one. MODIFIERS: steppers for damage, speed, gravity, jump (x0.25 steps), knockback, health (25 steps), and VAMPIRE / REGEN / INSTAGIB / SELF DAMAGE / DASH / GRAPPLE switches, STANDARD resets them. MAP: the live map picker (the maps offered for the chosen mode). Then a summary (MODE, MODIFIERS, MAP; a warning when the map isn't built for the mode) and CALL VOTE. The ranked server's vote page is unchanged (ladders only).
- The vote is one command:
callvote map <map> <mode> <ruleset> [mods k=v ...]. The server takes only known keys (gravity, speed, jump, knockback, damage, health; vampire, regen, selfdamage, dash, grapple, instagib as 0 / 1), finite and in range; anything else is refused with the word that's wrong.modswith nothing after it = the standard rules. - Passed modifiers become a custom mode named
voted(applyVotedRules, server.cpp):damagesets every weapon's damage multiplier, the rest are the custom mode keys of the same name. It goes to everyone through the existing EV_RULES path (no protocol change); clients say VOTED MODIFIERS ON or STANDARD RULES. A later map / mode vote without modifiers keeps them (the base mode follows the vote). - Refused: modifiers on a ranked or time trial server, on a server the host runs a custom mode on (the host's mode stays in charge), and tourney with modifiers (it plays its rulesets only).
- Not in it yet: player size (no custom mode key for it) and weapon bullet limits (weapons have no ammo yet); both need a design call.
- Test:
quark_tests votes(testVoteModifiers): bad words refused, the modifiers applied on the server and in the shared rules, kept through a mode vote, cleared bymods. The UI test walks the builder's pages (ingame/pause/vote/setup/...).
Modes: ball (MODE_BALL 16, protocol 62; owner, 2026-11-01: "theres goals and you have 10 minutes to score the most goals, it should have the same physics as a player on ice at 50% gravity, use qk8 and the highway bridge map for it")
- The ball is a 1 m box (
kBallHalf), its centre sent inModeState::bombPos(unused outside bomb, so the wire format only gains the mode number).ballMove(pmove.cpp) moves it like a player's hull with no input on an ice floor: half of pmove's gravity, the ground friction xkIceFriction(0.08) on any floor, 1 ms steps; from 10 m/s it slides 7.6 m in the first second and is at 5.7 m/s. A landing stops its fall like a player's; a wall or ceiling sends it back at half the speed (a player's hull stops dead, which would leave the ball stuck on walls). - What moves it (server,
ballStep/ballBlast/ballShot, gamemodes.cpp): a player whose box meets it sends it on at 1.3 x the speed they meet it with, and lifts it 2 m/s; a blast pushes it like an enemy (the weapon's knockback x the falloff, twice the vertical), throughblastReaches; a hitscan shot that meets it pushes it along the ray, 0.12 m/s per damage point, at most 12 m/s. Its speed is capped at 60 m/s. The grapple doesn't catch it yet. The ball is simulated by the server and drawn where the snapshot puts it (not predicted). - Goals: the map's two
siteboxes, each one the goal of the team whose spawns are nearer it. The ball's centre in one scores for the other team (teamScore; the last player to touch it is named, and gets a point if it's their own team's goal), and the ball goes back to the start: halfway between the goals, lifted out of anything solid and dropped to the floor (on QK8, on top of the block under the middle bridge). Into the void or lava: back to the start, no goal. - One 10 minute round (
roundTime 600), 3 s respawns. The host's score limit ends it sooner. When the clock runs out the most goals wins; level is a draw ("match over"). - Mode logic: tools/modes/ball.qlogic with a new object
ball(needs the sites) and its stepballStep()(MA_BALL_STEP); the native rules match it (modelockstep case on QK8). - Maps: QK8 gets a goal in each end room's 12 m door (
site -68 0 -6 -63 6 6and its mirror) and offers ball; Suspension Bridge gets one across the deck just inside each tower (x +-84..88) and offers ball, and loses its fog banks and haze sheets (owner: "remove the smoke from the highway map"), so its long sight lines are back; spilled shipping containers (2.6 and 5.2 m tall, on the old banks' footprints at x +-19..22 and +-59..63) take the banks' place as cover, so no two enemy spawns see each other (the spawn check: 11 pairs in sight without them, 0 with). A Lodestone mode test now skips ball (no goals there) and counts winner as 1v1. The sites don't make either a bomb map: theirmodeslines don't list bomb. The menus start ball on QK8. - Bots: they get behind the ball on the far side from the enemy goal, then run through it.
- Client: the goals as boxes in their team's colour (YOUR GOAL / ENEMY GOAL), the ball as three crossed boxes with a dark band that turns as it rolls, and the objective line.
- Test:
quark_tests ball(testBallMode): half gravity, the ice slide, the wall bounce, blast and shot pushes, a goal scoring for the other team and resetting the ball, the clock ending the match.
Movement: air drift 0 (protocol 63; owner, 2026-11-01: "change air drift to 0")
kAirDriftAccel8 -> 0 m/s². Below run speed, a held direction in the air used to push you along it at 8 m/s² up to run speed (so a standing jump drifted like a run-up one). Now it adds nothing; strafe acceleration and air control are untouched. Theair_driftmultiplier in custom rules multiplies 0, so it does nothing now. Protocol 63, since an older client would predict the drift. Owner asked for no tests this push; the movement tests that measure drift (standing jump + W) will need their numbers updated.
Movement: dash slide (protocol 64; owner, 2026-11-01: "if you dash down into the ground, without holding space, it should put you on ice level of sliding until you go back to regular speed, or until you press jump")
- A dash aimed down far enough for a super dash (the same
kSuperDashDowntest) that meets the floor with jump neither held nor buffered setsMoveState::dashSlide. Once the dash ends, the floor grips like ice while it lasts (friction xkIceFriction0.08, push xkIceAccel), so the dash's exit speed carries you a long way. It ends when your horizontal speed is back to run speed (kGroundSpeedx the mode's speed) or when jump is held. With jump held or buffered, the super dash happens as before. - Wire: the own movement state's opt bit 128 (the dash bounces byte) is now sent while the slide lasts too, with bit 128 of that byte meaning the slide (bounces never reach 128), so prediction replays it. No tests were run, at the owner's request.
Modes: ball tuning (owner, 2026-11-01)
- "treat every surface as if its rubber floor, bouncy":
ballMovebounces off any face atkRubberBounce(0.85) of the speed into it when that's overkRubberMin(3 m/s), and stops a slower touch (it was walls x0.5 and no floor bounce). The ice glide on the floor stays. - "it shouldnt respawn instantly": after a goal it sits in the goal, and after the void or lava it stays where it went, for 3 s (
Server::ballRespawnAt_), out of play (nothing pushes it, no second goal), then goes back to the middle. - "knife pushes too much, it should be more vertical and less horizontal": a knife swing on it gives 1.5 m/s along the swing's level aim and lifts it to 7 m/s up (it was 3.6 m/s along the aim).
- "projectiles should interact with it": rockets, lobs and bubbles go off on it (their blast pushes it), a bolt, thrown knife or syringe gives it a quarter of its speed (at most 15 m/s) and stops, and a sticky gives it that and drops off.
- "if the ball hits somebody moving more than 20m/s relative to them it should damage them and knock them back": (speed - 20) x 4 damage, at most 100, a knock of half its relative velocity (at least 4 m/s up), and the ball bounces off at half that speed. The kill goes to the last player who touched it (kill cause
kDamageBall248, "ball"). Once per player per 0.3 s. - "laser needs to do more knockback like 3x, and pull doesnt work": on the ball the laser pushes 3 x its push on a player (60 m/s per second of beam; it was damage-based, about 4.8), and the pull beam pulls it toward you as hard (it used to push).
- The ball test waits out the respawn delay.
Modes: the grapple catches the ball (protocol 65; owner, 2026-11-01: "you should be able to hook the ball")
HookProbegets the ball's box (server: where it is; client: the latest snapshot's, since the ball isn't predicted), and the hook catches it ahead of any wall:hookTarget kHookBall(kMaxPlayers + 2). You're pulled to it as you would be to a player, the server moves the anchor with it each tick, and it reels the ball toward your chest at 30 m/s² up to 20 m/s relative to you. It lets go at the usual release distance, on release, or when the ball leaves play (a goal or the void). The wire readers keep kHookBall from 65 on (older readers drop it). The ball test checks a grapple aimed at it catches it.
Weapons: rocket knockback x2 (protocol 66; owner, 2026-11-01: "rocket should do double knockback in all cases")
- The rocket's
knock(a blast's centre) goes from 11 to 22 m/s. It covers your own rocket jumps (x1.3 as before), enemies (vertical x2 as before), the lob, and the ball's blast push. Stickies and bubbles stay at 11. A weapon set's ownknockstill overrides it. Protocol 66, since rocket jumps are predicted.
Modes: one knife push on the ball per swing (owner, 2026-11-01: "okay so the knife got even crazier lol")
- A swing that hits no player keeps cutting every tick for
kKnifeSwingTime(0.12 s, so 120 hitscans at 1000 Hz), and each one pushed the ball, up to the 60 m/s cap. Now a player's knife pushes the ball at most once per 0.9 x its refire (Server::ballKnifeAt_): 1.5 m/s forward, 7 m/s up. - Ball damage (owner, 2026-11-01: "ball should only do 1 damage per m/s total"): the relative speed itself (21 to 60), no longer (speed - 20) x 4 up to 100. It still needs over 20 m/s.
Modes: the ball goes up ramps (owner, 2026-11-01: "balls should also try to go up ramps that are more than 45 and launch up still")
- In
ballMove, a slope's face (a wedge, corner or cut box, at any angle) is no longer a rubber wall. The ball turns along the face keeping its speed, carries the rest of the step along it, and flies off the top. A hard landing on a face (overkRubberMininto it) bounces like rubber about the face. Box faces stay rubber. The ball test rolls it at 20 m/s into a 63° 4 m wedge: it goes up to 8.6 m.
Website on quark.casa (owner, 2026-11-01: "help me link the website to the shit i have setup already")
- The website (GitHub Pages, published from the
sitebranch togh-pages) gets the custom domain quark.casa: aCNAMEfile on thesitebranch, which the pages job copies ontogh-pages(the release-testing job keeps whatever gh-pages has, so it stays too). Once the domain is set, GitHub forwards every floration.github.io/quark URL to the same path on quark.casa, so links in older builds, Discord posts and the launcher keep working. - Linux and macOS HTTP requests (system curl) now follow redirects (
-L), but only to https (--proto-redir =https). Why: before this, curl handed back GitHub's 301 page instead of the file, so the signed master.txt lookup at the old address would silently fail once it forwards. Windows (WinHTTP) already followed them. No effect today: master.txt names the built-in ranking address and has no signature, so every build falls back to that address anyway. play.quark.casanames the official game-server VPS (production port 27500, testing 27501): players can type it in Connect instead of the IP.
Fullscreen size warning (owner, 2026-11-01: "the game should have a warning next to the fullscreen resolution setting")
- VIDEO + AUDIO > MORE 3: a line in the WARNING colour under FULLSCREEN SIZE, "WARNING: DON'T TOUCH THIS UNLESS YOU KNOW WHAT YOU'RE DOING". A wrong exclusive mode can leave the screen black or stretched, so most players should leave it on DESKTOP.