1.17.0 is the biggest update so far, and most of it is fixes. Two reviews went through the whole project: a second adversarial review of 1.15.1 (over a hundred findings: crashes, cheats, latency bugs, broken mode rules, an unsafe update path) and a final review of everything going into this release. The owner then made the design calls the reviews raised, and those are in too: weapon balance, overtime, the syringe and throws, hide and seek scoring, tag, ranked rules. On top of that: rebuilt bots, a new font, signed updates, your files in your own user folder, and a new testing / production release process.
- Network protocol 42 (1.16 used 27; demos from earlier 1.17.0 builds still play). A 1.17.0 game only plays with a 1.17.0 server, so update games and servers together.
- Ranked needs the new ranking server, deployed with this release. Older games can't report ranked results to it.
- Demos from 1.11 on keep playing: they are converted the first time you start 1.17.0.
- Your own maps keep working: they are converted to the new map format at startup, and the originals are kept.
This page is the complete record: every change, however small, with what it was before, what it is now, and why. The short version is on the updates page.
Some words used below:
- Tick: one step of the server's simulation. Servers run at 1000 ticks a second and step down to 500, 250 or 125 if the machine can't keep up.
- Snapshot: the packet the server sends your game each tick, with where everyone is.
- Lag compensation (rewind): when you fire, the server checks your shot against where players were on your screen, not where they are now.
- Prediction: your game moves you at once and corrects itself when the server's answer arrives.
Codes in brackets point to the project's review and decision documents: (NET-1), (CLI-2), (WEB-1), (PERF-9), (MAP-3) and similar are findings in docs/REVIEW.md; (review 2.1) and similar are rows in FINAL_REVIEW.md; (owner 5.1) is an answer in docs/OWNER_ANSWERS.md; (B1-B10) are in docs/OWNER_DECISIONS_2.md and (C1-C16) in docs/JUDGMENT_CALLS.md. Quotes are the owner's own words.
Gameplay and weapons
Weapon balance (owner B7)
| Weapon | Setting | 1.16 | 1.17.0 | Time to kill from 100 HP, perfect aim |
|---|---|---|---|---|
| Rifle | Body damage | 5.95 | 4.95 | 17 shots, 2.4 s → 21 shots, 3.0 s |
| Rifle | Head damage | 8.5 | 7.5 | 12 shots, 1.7 s → 14 shots, 2.0 s |
| Laser | Range | 32 m | 24 m | unchanged: 2.5 s (0.04 damage every 1 ms) |
| Sniper | Body damage | 34 | 50 | 3 shots, 3.0 s → 2 shots, 1.5 s |
| Sniper | Head damage | 50 | 50 | 2 shots, 1.5 s |
| Syringe | Healing yourself | 1 HP every 0.1 s | 10 HP a use, 1 s cooldown | |
| Rocket | Gravity | 12 m/s² | 0 (flies straight); lob 13.5 m/s² (was 36) | unchanged |
- Why the rifle and laser changed. The review measured a flat time-to-kill band (1.5-3.0 s for every weapon) and a rifle that won everywhere past close range: hitscan to 300 m, no spread, no falloff. The owner's call: "decrease the rifle damage by 1 per shot overall", "i dont want any spread", and "make the laser 24m instead of 32… so the lg has more dps, but is shorter range, unless you can headshot enough then rifle overpowers it".
- Against the laser's 40 damage a second, the rifle now does 32.7 on the body (0.82 of the laser) and 49.5 on the head (1.24). So inside 24 m the laser wins unless you land headshots, and past 24 m only the rifle reaches.
- The rifle's fire rate (a shot every 0.152 s), its range (300 m) and its lack of spread and falloff are unchanged.
- Shotgun knockback doubled: each pellet that hits pushes 0.30 m/s instead of 0.15, so a full 20-pellet blast shoves a player 6 m/s (was 3), ricochets included. The owner's call: a point-blank shotgun blast should move people.
- Scoped rifle fire rate: 25% slower -> 10% slower (a shot every 0.168 s instead of 0.202 s while scoped). The owner's call: aiming down sights should cost a little, not a quarter of your damage output.
- Why the sniper changed. The review found it strictly worse than the rifle: 3 shots and 3.0 s to kill, and a headshot never saved a shot. The owner: "sniper should do a flat 50 damage". It's now 50 on the head or the body: 2 shots, 1.5 s.
- Rockets fly straight; the lob arcs gently (owner). The primary rocket had gravity 12 m/s² in 1.16 (briefly 10.8 during 1.17 testing) and now has none: it goes where you aim, like a Quake rocket, which makes direct hits and rocket jumps predictable. The alt-fire lob used 3x the rocket's gravity (36 m/s² in 1.16) and fell steeply; it now drops at 13.5 m/s², the 10.8 primary plus 25%, at 70% speed, so it's a usable arc over cover rather than a grenade at your feet. Damage, splash and speed are unchanged.
- Stickies inherit only forward / back and vertical speed (owner). A sticky took 90% of your whole velocity, so strafing threw it sideways off your aim and made it hard to place. It now takes 90% of the part along where you face (running forward throws it further, backpedalling shorter) and of your vertical speed, never the sideways part. Aiming straight up or down, it takes only the vertical part.
- The syringe now heals 10 HP a use on a 1 s cooldown, instead of 1 HP every 0.1 s (owner B4: "change the self use syringe to 10hp, with a 1 second cooldown"). It's the same 10 HP a second at most, in steps. Which modes have it at all changed too (see Game modes).
- The website's weapon table is generated from the game's own table, so it shows the new numbers.
Headshots
- The head is the head, not the shoulders (owner 5.5). The head zone was the top 20% of the hitbox at its full 0.8 m width, so a shot at the shoulder counted as a headshot. Now it is a head-sized box: the top 20% of the height and 45% of the width (0.36 m), centred. A shot is a headshot when its line passes through that box. This applies to the rifle, shotgun pellets, the sniper (including shots through a first player into a second) and crossbow bolts, and the box grows and shrinks with size modifiers.
- Headshot damage numbers are red (owner 5.6: "headshot damage should be red instead of white"). The server now sends a headshot hit on its own, at once, instead of folding it into the batched white number. Big body hits (40 or more) are still gold, the rest white, and quick hits only merge with hits of the same kind.
Weapon spread is a fixed pattern (NET-9)
- Before: the shotgun's pellet spread (and the bubble gun's) was seeded from a number the client chose, the command's sequence number. A modified client could pick the best numbers. At 15 m an honest shot landed 5.8 of 20 pellets on average, the best of 16 picks 9.4, the best of 256 picks 11.7: about twice the damage.
- Now: every shotgun blast uses the same layout: one pellet dead centre, 6 in a ring at 0.45 of the spread and 13 at 0.82. Each pellet covers an equal share of the spread circle, so the density is the same as the old random spread. Spread sizes are unchanged.
- The bubble gun's single shots walk a fixed 16-step spiral, counted by the server, which starts over when you let go of fire or switch weapons.
- Your tracers now show exactly where the pellets go. They used the same shared function as the server; before, local tracers and enemy shotgun tracers were drawn at random.
Knives, bolts and syringes stay where they land
- What: a thrown knife, a crossbow bolt or a thrown syringe that misses stays stuck in the wall or floor until the match is over. One that hits a player stays stuck in them, moving with them, until they die, respawn or leave (owner, before release). It's only for show: damage, heals, the thrown knife's 4 s return and every cooldown are as before, and a prop never blocks anything.
- How, cheaply: the server sends one small reliable event when such a projectile ends: where it is and which way it flew, or which player it's in and where on them, on the same grid stickies use. The game keeps the props itself. Nothing rides in the snapshots, so a match full of them costs no bandwidth. The newest 256 are kept and older ones dropped.
- Clearing: the server clears them at match end, match start (leaving warmup),
restart, a map change and edit mode, and a player's own props go when that player dies, respawns, leaves or spectates. Late joiners get the newest 48 world props, and demos record them. - Hidden information: props in the world go to everyone (they show roughly where someone threw from). Props in a player are only sent once that player is in your sight, so a hit out of sight is never revealed. Spectators see everything.
- Frame time: props past 50 m or off screen aren't drawn, past 10 m only their main box, and they cast no shadow. Shadows were most of the cost: props spread over the map widened the shadow pass for every pixel. Measured with 256 props under software GL: 0.1 ms in the prop drawing itself; whole frames lost 3 to 5 fps in the worst case (all 256 within 11 m), which should be far smaller on a real GPU. To check on real hardware with F3 (1% and 0.1% lows).
- No protocol change: it's a new event type, which the event format already allows. A
--pulsedebug flag (fire released every other 0.3 s) was added for the screenshots.
The knife's slash
- The slash comes out of the knife you hold (owner, before release), like every other weapon's shot comes out of its muzzle: the blade-width trail starts at the knife model's tip instead of a spot by your eye, and the small box drawn at the far end of the slash is gone. Other players' slashes start from their knife hand as before.
Knife and syringe throws (owner B4)
- Before (1.16): a knife throw had no cooldown, and the weapon you switched to could fire at once, so a thrown knife plus a crossbow headshot was an instant 105 damage (GAME-8). A syringe throw had its own 2 s cooldown.
- During this update's development a shared 2 s throw cooldown and a 0.75 s delay on the next weapon were tried. The owner replaced them: "there is no special shared cooldown, just normal cooldowns with weapon disables".
- Now, knife throw: the throw triggers the knife's normal swing cooldown, 0.75 s, during which no weapon can be used. The knife is then disabled for 4 s: you switch to another weapon and can't draw the knife until it's back. The weapon list shows it greyed with the seconds left.
- Now, syringe throw: the throw triggers the syringe's normal cooldown, now 1 s, during which no weapon can be used, and the syringe is disabled for 4 s the same way.
- A teammate's thrown syringe still heals you 30 HP at once and resets your weapon, dash and grapple cooldowns. It now also brings back a thrown knife or syringe at once (owner 5.3: "cooldown reduction timers should also work for these, which it previously didnt").
- Bots know a thrown syringe is away, as they already did for the knife. The INFO screen says the syringe heals 10 HP once a second and that a thrown knife or syringe is gone for 4 s.
Projectiles fly in fixed 1 ms steps (C8)
- Before: rockets, stickies, knives, bolts, syringes and bubbles moved once per server tick, so the same shot followed a slightly different arc at 125 Hz than at 1000 Hz (8-283 mm apart in the new test). Player movement already ran in fixed 1 ms steps.
- Now: projectiles also move in 1 ms steps, so every one lands on exactly the same spot at 125, 250, 500 and 1000 Hz. A projectile's lifetime is checked at each step's own time, and a spent one stays where it hit.
- Your screen draws them along the same arc, gravity included. Before, the game drew a projectile between snapshots in a straight line, ignoring its drop (COR-8). The server sends whether a rocket is a lob or a sticky is stuck, in a byte it already sent.
- Cost: with 100 projectiles in flight, a 1000 Hz tick went from 9.6 to 6.3 µs (players' hitboxes are now worked out once a tick, not once per projectile). At 125 Hz a tick runs 8 steps (about 50 µs), the same cost per second of play.
Movement
- Movement retuned against other arena games (the owner's seven decisions, after a side-by-side comparison with VQ3, CPMA, Warfork and Cocaine Diesel scaled to a 1.8 m player):
| before | now | |
|---|---|---|
| run speed | 8 m/s | 9.5 m/s |
| time to 90% / full speed | 67 / 77 ms | 72 / 83 ms (ground acceleration 16.8 → 16) |
| running jump, flat | 5.3 m | 6.27 m |
| standing jump holding W, speed at landing | 2.3 m/s | 7.27 m/s |
| ladder up / sideways | 4.5 / 3 m/s | 6.75 / 4.5 m/s (the kick-off is unchanged) |
| chained dashes from standing | 11, 14, 17... | 11, 12, 13, 14 (each keeps +1) |
| W-only air control | 229°/s at any speed | 229°/s up to 12.65 m/s; 181 at 16, 145 at 20, 91 at 32 |
| A/D strafe push | x1.0 | x1.0 at 12, x1.043 at 14, x1.1 from 15.8 m/s |
| strafe-jumping for 4 s | 8 → 22.4 m/s | 9.5 → 23.9 m/s |
- Run 8 → 9.5 m/s, ground acceleration 16.8 → 16: quark ran slowest of the games compared (VQ3 / CPMA 10.3, Warfork 9.0, Cocaine Diesel 11.25 m/s). The lower acceleration keeps start-up about the same at the higher top speed (90% in 72 ms, full in 83).
- Air drift, 8 m/s² below run speed: a standing jump could only drift 2.3 m/s, against about 6–7 elsewhere. Drift only applies below run speed and is clamped so it never pushes you past it, so it can't be used to gain speed.
- W-only air control falls off above 9.5 m/s (run speed), and a strafe boost of x1.1 (ramping in from 12.65 to 15.8 m/s): at high speed, holding W alone steered as well as real air-strafing. Now A/D strafing wins from 9.5 m/s up, which is the skill the movement is built around. The crossover was first 12.65 m/s; the owner moved it to 9.5 before release "by lowering w stafing power not increasing ad strafing", so W-only steering now turns 4 x 9.5 / speed rad/s (181°/s at 12 m/s, 136 at 16, against A/D's 242 and 199) and the A/D boost is unchanged. Bots switch to strafe-key turns above 9.5 m/s too.
- Ladders 4.5 → 6.75 up, 3 → 4.5 sideways: quark's ladders were the slowest compared (Warfork 7.0, Cocaine Diesel 8.4 m/s). Coming over a ladder top now carries you about 5.6 m (was 2.0), so five maps got a stop at a narrow ladder top: Bastion (a tall stone on the outer wall top: its ladder threw you out of the map), Temple (a jagged back-wall piece on the tower stub), Furnace (a damper housing on the stack's far edge, mirrored), Orbital (the gallery ladder now climbs a strut facing the hull) and Railyard (a filler pipe on the tank). Hightown's stairwell lamps hung at head height, so jumping up the stairs bonked on them; they moved 4 m out.
- Dash keep 3 → 1 m/s (the dash itself is still 11–32 m/s): chained dashes built +3 m/s each, up to 32, too quickly.
- Two new custom-mode values,
air_driftandstrafe_boost, with sliders on the MOVEMENT page; air control and the strafe crossover use the mode's air acceleration, so custom modes scale together.
- Two dash charges, on top of the 1.2 s dash cooldown (owner: "i didnt want the cooldown to be removed... i wanted charges on top of the cooldown"). A dash needs a charge left and the cooldown run out. The cooldown is timed from when the previous dash ends (its burst running out, or a dash jump or super dash ending it), so two dashes are always at least the dash plus 1.2 s apart. A dash spends one charge. Holding jump refills only up to one charge (owner, before release: "when holding space you should only be able to charge 1 dash"): bunny hopping keeps one ready but never the second; landing without jump held refills both. Ending a step on the floor refills both; a bunny-hop landing gives one back. Pressing dash during a dash does nothing (it isn't saved for later), so two presses in a row use both charges, one after the other. A dash jump leaves both charges; a super dash leaves exactly one, and a wall bounce leaves exactly one. There's no refill during a dash's own steps (so the super dash's floor touch doesn't count), on a ladder, or while grappling or being pulled. The duel and blade modes' quick dash is a third charge, and a syringe heal refills them. The HUD shows a pip per charge. Custom modes have both
dash_cooldown(a multiplier on 1.2 s, as before) anddash_charges(a multiplier on 2, at least 1); an older file'sdash_cooldownworks again. The HUD's dash pips fill grey while the cooldown runs. Network protocol 42: the charge count and the cooldown (whole milliseconds, so prediction matches the server exactly) are sent when charges aren't full or the cooldown is running. Demos from protocol 41 (charges only) and older (the old cooldown) still play. During development the charges briefly replaced the cooldown (protocol 41); the owner restored the cooldown before release.- With charges alone, a grounded player could dash every 0.12 s and one super dash per bunny hop never ran out. The owner chose the cooldown to balance both, rather than separate fixes: now it's a dash every 1.2 s on the ground, and chained super dashes come at least 1.3 s apart. Bots dash a little less for the same reason. The quick-dash modifier (duel, blades) keeps its third charge with the same cooldown.
- Bots for the new movement: above 12.65 m/s a hopping bot that needs a sharp turn (over 69°) now pushes square to its momentum like a strafing player instead of steering with W, because W-only steering is weaker at speed. Expert bots' kills per 40 s in FFA: 5.8 before the movement change, 4.6 after it, 5.7 with this fix. Their dash checks use "a charge left and no dash running".
- The grapple no longer slows you down when it catches (owner 5.4: "grapple should NOT brake on catch, other than how it tries to bring you towards the connection point").
- Before: while hooked, your whole speed was clamped to the grapple's top speed of 24 m/s every step, so catching it at 40 m/s dropped you to 24 at once.
- Now: the pull only adds speed up to 24 m/s. If you were already faster, it bends your path toward the anchor and you keep your speed. Below 24 m/s nothing changes.
- Size modifiers scale your collision box (owner 5.3, GAME-9).
- Before: "double size", "giant snipers" and "tiny players" scaled your hitbox and camera, but you still collided as a normal 1.8 m player. A giant's camera (3.2 m up) went inside low ceilings; a tiny player's sat at 0.96 m.
- Now: the collision box scales with you (2x or 0.6x): walls, ceilings, the ground check, ladders, and where the grapple fires from and pulls to. Step height isn't scaled. Your hitbox is the same box. Prediction knows your size, so a giant doesn't rubber-band.
- A giant never spawns inside a wall (review 6.4). If no spawn had room for a giant, the game picked one anyway; on Anvil, 24 test giants spawned inside the geometry. Now it picks, in order: a spawn where a giant can stand, one where it fits crouched (it stands when there's room), the nearest clear spot up to 1.5 m aside or 0.6 m up (never through a wall), and only then normal size for that life. The same test now puts 0 giants in walls.
- Kept as they are, by the owner's choice: auto bunny-hop, unlimited air-strafing speed and the dash's speed limit (owner 5.4: "intentional"), and unlimited knockback (owner 5.3: "knoickback should be unlimited its funny").
Health
- Heals stop at your life's starting health (GAME-2a). Syringe doses and throws, vampire healing and regeneration all stopped at 100, whatever the life started with. So in duel, "1 HP" and "glass cannon" lives healed back up to 100, and "tanks" (250) couldn't heal past 100. Now every heal stops at the health that life spawned with.
- Regeneration waits 3 seconds, measured in seconds. It counted ticks before, so the wait depended on the server's tick rate.
Health bars on for everyone, once
- Health bars over players are on by default in 1.17. Players who had them off from an older default get them switched on once with this update (owner); anyone who turns them off again keeps them off. The config remembers it was done (
health_bars_117=1).
The grapple sticks to enemies
- What: when the tip of your grapple hits an enemy it latches on, deals 10 damage (15 if the hook passes through the head), and pulls the two of you together until you meet. The pull works like the normal hook: momentum kept, half gravity, the same speed limit, and the victim is pulled toward you just as hard, so you meet in the middle.
- Why: the owner's call ("if your grapple hits a player, it should stick to them, do 10 damage, 15 if head, and then pull both players together in the middle"): the hook becomes a close-range opener as well as movement.
- It lets go when you release, when you meet (1.2 m), when either of you dies, disconnects or respawns, when a wall comes between you, beyond the grapple's 45 m range, or after 3 s; the usual 1 s cooldown follows. A hooked player sees HOOKED and can break free with a dash or by firing their own grapple (which can latch you back).
- Who: enemies only; the hook passes through teammates to the wall behind, never hooks you, and passes through a player someone else already holds. No hooking in race, during a freeze or round end.
- Modes: wherever the grapple exists. In instagib the hook only pulls (otherwise it would be a 45 m one-shot weapon); in hide and seek and tag it only pulls, between "it" and the others. The grapple-only duel modifier allows it.
- Netcode: the hit is lag compensated like hitscan (you hook where you saw them). The victim's pull runs in their own movement prediction from the server's pull point, so it's predicted rather than corrected (measured corrections: 1-2 mm). A rope to a player you can't see isn't sent (it would reveal them). It counts in the kill feed as "grapple" (a damage source of its own, not a weapon slot).
- Bots: advanced and expert bots with a close-range weapon out sometimes hook you at 7-36 m, and dash sideways out of a hook when they want range.
Stickies stick to enemies
- What: a sticky that hits an enemy sticks to them (2 cm inside where it hit) instead of dropping at their feet, rides along on them, and goes off right on them when you set it off (alt-fire or another blast): the full blast (29.8 of 29.75 in the test). No contact damage, as before.
- Why: the owner's call ("the sticky should stick to players if it hits them"). It's drawn glued to their model on every screen; the victim gets a beep and a blinking STICKY ON YOU.
- Teammates and the shooter aren't stuck to (it drops, as before). If the player dies, respawns or leaves, it falls and sticks where they were. The 6-per-player cap includes stickies on players. A sticky on a player you can't see is hidden from you too, so it can't track them through walls. Bots set off stickies that are on an enemy.
Dash tech (from Celeste)
- Sounds for the techs (owner): a super (dash jump) plays a bright rising whoosh, a hyper (super dash) a lower, heavier one with an impact under it. They're new sounds in every kit (SUPER, HYPER), replaceable like the rest. The game tells them from an ordinary dash's end by the dash being cut short by a jump, and a hyper by the dash aimed down and the single charge it leaves.
- Dash jump: jumping during a dash while on the floor (or up to 0.1 s before it) ends the dash and keeps its full burst speed (24 m/s, or more if you were faster, up to 32) through a normal jump. Before, the dash swallowed the jump. From a standstill that's a 15.8 m leap at the normal 1.21 m height (11 m/s without the tech).
- Super dash: a dash aimed down (14.5° or more below level) that meets the floor with jump held turns into 1.25x the dash speed (up to 36 m/s) with a half-height hop (0.31 m). These are Celeste's hyper-to-super ratios. A bhop never bounces off a dash's own push into the floor.
- Wall bounce: a dash that hits a wall less than 45° from head-on reflects off it (like a mirror; vertical motion kept), finishes the dash the other way and leaves at full dash speed; at most 2 bounces per dash (a third wall slides). Glancing hits (45° or more) slide as before.
- Why: the owner's calls, after Celeste: dashes should carry momentum into jumps and off walls instead of stopping dead.
- All three are tunable in custom modes (dash jump window, super dash boost / hop / cap, bounce angle). The map report's movement model now includes them (32 and 36 m/s tiers, standing reach 13.5 -> 16 m); every map still scores 7+, nothing reaches out of bounds, and no race leg can be cut. Expert bots dash jump on long straight routes.
Name colour
- Pick the colour others see your name in (owner): SETTINGS > NAME COLOR, with R / G / B sliders (moving one turns it on). It shows on the scoreboard, in chat and in the kill feed for everyone. Name tags over teammates keep the team colour, so teams stay readable (owner's call); in FFA there are no name tags over heads.
- Sent with your model and sound pack, as RGB565 in the player info event's spare 16-bit field, so it needed no change to the network format; a server before 1.17 just ignores it. A late joiner and a demo get it too. A colour too dark to read on the HUD panels is lifted toward white on everyone's screen (a minimum brightness), so nobody can hide their name.
Game modes
Overtime (owner 5.1; GAME-3, GAME-4)
- Before: payload and neutral payload had no overtime, so a cart being pushed at 0:00 lost. Multi point had overtime only while the point was contested, so attackers alone on the point at 0:00 lost, and the best defence at 0:00 was to step off the point. In neutral payload, whoever was a metre ahead at time-out won, so teams could take a small lead and turtle.
- Now: in payload, neutral payload, multi point and bomb, when the clock runs out, overtime starts if the side that needs time is on the objective or touched it in the last 5 s. Each touch resets a 5 s timer, and overtime ends 5 s after their last touch. The owner: "overtime should happen whenever the attacker is on the point at all when timer hits 0, it should also apply when someone had touched it within the last 5 seconds, overtime timer is 5 seconds; this applies for all gamemodes with a site or cart".
- Payload: attackers at the cart, contested or not.
- Multi point: attackers on the current point.
- Bomb: an attacker on a bomb site, before the plant. A planted bomb's fuse isn't extended.
- Neutral payload: the team that would lose at time-out; both teams if the cart is dead centre.
- The HUD shows "OVERTIME 0:05" counting down. Free for all's existing tie overtime now reads "OVERTIME - NEXT KILL WINS" on the HUD. King of the hill has no clock (first to the score wins), so nothing changed there.
- Fixed: checkpoint and point time bonuses during overtime (review 6.3). Reaching a payload checkpoint or taking a point adds time, but in overtime it was added to a deadline already in the past: "+60 s" reached 20 s into overtime gave 40 s, and after 60 s of overtime nothing. Bonuses now count from now.
The syringe only where the dead respawn (owner 5.2; GAME-2b)
- Why: in round modes the syringe out-healed VS's stalemate drain (10 HP a second against 5), so a cornered last player holding it never died and the round never ended. The owner: "syringe should only exist in respawn based gamemodes, unless otherwise limited".
- Now: it's in free for all, king of the hill, payload, neutral payload, multi point and race, and gone from VS, bomb, duel, 1v1 and tourney (hide and seek and tag are knife-only anyway). A custom mode keeps it only if its weapon list names it; "all weapons" isn't enough. If you had it selected where it's gone, you spawn with the first allowed weapon.
Hide and seek (owner 5.6, B8)
- Everyone seeks exactly once. A match had
clamp(players, 3, 6)rounds in slot order, so with 2 players one sought twice and with 7 or more some never sought. Now a match has as many rounds as players, and someone who joins mid-match gets a turn too. - New scoring (owner B8: "if a hider survives the whole time they get 1 point", the seeker gets "1 per person found"):
- A hider still unfound when the round ends scores 1 (also when the round ends because the seeker left).
- The seeker scores 1 for each hider found.
- A found hider scores 0. A hider who dies from a fall scores 0, and the seeker gets nothing for them.
- This replaces 1.16's +2 for every survivor, +3 for a seeker who found everyone and +1 per find from the kill. (A time-share system, splitting 4 points per hider by how long they stayed hidden, was built during development and replaced by the owner's rule before release.)
- Rock paper scissors tiebreak (owner B8: it "only happens if once every round is played there is a tie of points", "best of 3, if more than 2, everyone who loses is out, unless everybody loses then nobody does").
- Only players tied for first take part, after the last round.
- Two players: best of 3; a drawn throw is thrown again.
- Three or more: everyone throws at once. If exactly two shapes show, everyone with the losing shape is out. If one shape or all three show, nobody is out and they throw again. When two remain, it's best of 3; if a throw leaves one clear winner, they win outright.
- Pick with 1 / 2 / 3 within 6 s; no pick means a random one. Each throw is shown for 3 s. Bots throw at random. Picks are hidden until the reveal, and anyone joining mid-tiebreak is sent the current state.
- A panel shows everyone's card (choosing, ready, their shape, best-of-3 pips, out), with sounds for a new throw, the reveal, a throw won and being knocked out. The winner wins the match.
- The seeker is seen holding the knife (owner B8: "the seeker holds it, and the tagger only holds it"). Others now see the seeker's arm and real knife (or locator) model, aimed where they look and chopping on a swing, instead of a generic gun box; hiders hold nothing. Players who aren't the seeker no longer get a local knife swing, slash trail or sound.
- Hiders are no longer shown where the seeker is (owner B8: "in HNS the hiders should not always be aware of the seeker location"). The "SEEKER" label over the seeker was drawn through walls; it now needs a clear line of sight.
- The seeker gets nothing about the hiders during the head start. The head-start blackout was only a black rectangle on the seeker's screen, while hiders' positions and footsteps still arrived (NET-10). Now the server sends the seeker nothing about them until the round goes live (see Hidden information).
The locator is a weapon (owner B8)
- Before: the seeker's or tagger's locator was an ability on the use key.
- Now: it's a weapon on key 0 with its own model: a handheld tracker with a radar screen and an antenna, whose screen dims while it recharges and pulses when ready. Others see it in the seeker's or tagger's hand.
- Fire it to see one random hider or runner through walls for 1 s; it then recharges for 5 s on its own clock, so switching back to the knife is instant. It does no damage. Only the seeker and the tagger hold it, and the seeker or tagger spawns (or is tagged) with the knife out.
- It exists only in hide and seek and tag: every other mode's weapon pool strips it, custom modes and 1v1 rulesets included, the HUD's weapon list only shows weapons the mode has, and the custom-mode editor doesn't offer it. The old use-key path is removed.
- Bot seekers and taggers fire it when nobody is in sight and head for what it finds.
- It has its own sound (owner, before release): a sonar-like ping with an echo, the new LOCATOR sound in every kit. It borrowed a pitched-up grapple sound before.
Tag
- Runners always see where the tagger is (owner B8: "in tag mode all runners should always be aware of the tagger location"). The server never hides the tagger from anyone, and runners and spectators get a red marker over the tagger with the distance ("TAGGER 31M") through walls, or an arrow at the screen edge when the tagger is off screen or behind.
- Going to spectator or leaving during a match eliminates you (GAME-1). Before, the tagger could switch to spectator for a moment: the game handed "it" to a random runner, who was out when time ran out, and the original came back with nothing on their record.
- Now, during a match, spectating or leaving is elimination. If the tagger does it, the round ends exactly as if time ran out.
- Nobody can join a side mid-match; a player who connects mid-match watches and joins at the next match.
- An elimination is kept if the player reconnects.
- Late joiners are placed properly (review 6.2). Everyone who waited out a tag match was put on team A at the next match, skipping the 1v1 and duel seat limits and team balance: a vote to 1v1 could leave three players on one side. They're now placed like any joining player.
- A bot added mid-match plays the next match (review 6.5). An admin's
addbotduring a live tag match put the bot straight on a side, past the lock every human respects; and a new bot reusing an eliminated bot's name was marked out at once. Now the bot watches and joins at the next match, and eliminations are remembered for humans only.
Bomb (GAME-5)
- A dropped bomb falls to the floor. A carrier killed in mid-air left the bomb hanging where they died, and an enemy's blast can throw you about 11 m up. Picking it up needs your feet within 1.2 m, so it could end up out of reach (bots included) and the attack timed out. Now it drops straight down to the floor below; over the void or lava it goes where the carrier last stood, as before.
- An abandoned bomb goes back to the attack: if nobody picks it up for 15 s, it goes to a random living attacker, with a message saying who.
Other mode fixes
- Balance bots stay until the round ends (GAME-10). When a human joined, the balance bot on the other team was removed within a second, even mid-round when it was its team's last player alive, which lost the round on the spot. In round modes a balance bot that's alive now stays until it dies or the round ends.
- Colosseum's tourney uses its opposite-end spawns (MAP-7). The game only used tourney spawns if the map's mode list offered VS; Colosseum's doesn't, so both finalists spawned at free-for-all spawns, maybe side by side. It now checks whether the map has the spawns. The same applies to race and staged multi point spawns.
- The server's tick rate only changes between rounds (GAME-11). It used to step up or down mid-round, against what the code itself promised, and race times and regeneration depend on it. Now it changes in warmup, at round end or at match end. A server that falls behind mid-round steps down at the next break; one that is truly overloaded (busy over 90% of the time) still steps down at once, because falling behind is worse for everyone.
- A failed step up is undone at once (review 6.1). The change above also removed the quick undo of a step up that didn't hold, so a raise made in a break could run late ticks for a whole round (frame-time spikes). A step down within 3 measuring windows of the server's own raise now happens in any phase.
- Tourneys are seeded by rating (owner 5.6: "seeding should be based on average of the 3 ranked modes (200 if not ranked)"; kept as is in C14). Seeding was random. Now each player's seed is the average of their three ladder ratings, counting 200 for a ladder with no games or a player with no ranked id. Ratings are fetched in the background when ranked players join a tourney server. Brackets use the standard order (1st plays last), so the top seeds get the byes; equal seeds are shuffled.
- Rating lookups are merged (review 2.8): every ranked join queued a lookup per ladder, so rotating keys could queue about 150 a second. Now new players join the lookup already waiting for that ladder, so at most one waits per ladder.
- A disconnect in a tourney is no longer an instant walkover (see Reconnecting).
- Fresh random numbers every start (GAME-12).
rand()was never seeded, so the duel modifier order, tourney seeding, the first bomb carrier and the first tagger came out the same on every fresh server start. Every thread now has its own generator seeded from the system's random source and the clock, and bots seed from it too. - King of the hill is unchanged (B5): any enemy on the hill still freezes the clock for both teams.
Custom modes
- A host's custom mode no longer leaks into its own game (COR-6). The custom rules lived in globals shared by the hosted server's thread and your game's thread, and stopping the server reset them under your game. Each thread now has its own copy, and the server applies its rules on its own thread.
- Blank map names are dropped from a custom mode's map list.
maps= ,templekept an empty name, and a host whose list started with it switched its own map to a map with no name at start. Found by the new fuzz tests. - Values that aren't real numbers are refused (see Security).
Bots
Rebuilt from scratch (owner 5.6)
The owner: "bots need to be redone". The review had found they had perfect information and a difficulty that depended on the tick rate.
- What they know. Before, bots scanned 360° with exact positions, aimed with the target's exact velocity and forgot a target the moment it broke line of sight. Now they only know what a player could:
- Sight: a view cone of about 89° / 97° / 106° / 115° by level, with a line-of-sight check from the eye, looked at 20 times a second.
- Hearing: gunfire within 45-75 m and footsteps or dashes within 12-24 m, with a guessed position off by 10-30% of the distance.
- Memory: a last-seen position that lasts 1.5-5 s. They hunt it and aim where you should be by now, but don't fire at it. Between fights they turn toward fresh sounds and glance around.
- In hide and seek and tag, runners only flee from a seeker or tagger they've seen or heard.
- How they aim (beginner / intermediate / advanced / expert):
- Reaction time 450 / 300 / 200 / 150 ms, up to 1.5x slower at the edge of their vision and shorter for a target lost a moment ago.
- They see you as you were 160 / 120 / 90 / 60 ms ago and extrapolate from a speed they perceive, which lags behind jukes and misjudges it.
- Turning has a top speed and acceleration; flicks overshoot and then settle; tracking gets worse the faster you cross their view; projectile leads and drop are misjudged, re-rolled every 0.7-1.5 s.
- Levels are clearly apart. Rifle damage a second against a target strafing and juking 14 m away, averaged over 1000, 250 and 125 Hz:
| Level | Old bots | New bots | Share of perfect aim | Owner's target (C11) |
|---|---|---|---|---|
| Beginner | 3.3 | 3.0 | 8% | 3 |
| Intermediate | 7.3 | 6.1 | 15% | 6 |
| Advanced | 8.1 | 10.5 | 27% | 10.5 |
| Expert | 18.1 | 18.0 | 46% | 18 |
- The old intermediate and advanced levels were barely apart. A later check measured experts at 16.7 (42-43% of perfect aim): strong but beatable. The tests now keep experts between 35% and 60% of perfect aim.
- Difficulty no longer depends on the tick rate. Bots used to decide per tick: an expert's dash roll came within about 0.1 s at 1000 Hz but 0.8 s at 125 Hz, and lower rates hit up to 50% more. Now everything runs on game time in 1 ms steps, and each command carries the view at the moment the server tests the shot. Each level hits about the same at every rate.
- How they move: beginners stand and shoot, intermediates strafe, advanced and expert bots bunny-hop with real air-strafing, dash in fights and on long straights, and grapple.
- Fixed: bots that never fired. A bot holding fire through a weapon switch kept its trigger locked, and some bots never shot at all.
- Navigation is built in the background (PERF-10). The route graph was built the first time a bot thought, on the server's tick (about 4.6 ms, a visible hitch when the first bot spawned). It's now built on a background thread at map load (the slowest, Lodestone and Midtown, take 24 ms) and rebuilt in the background after map edits. Route search uses A* with reused memory (2-6 µs a search, was 4.6-10.5), routes are kept between waypoints, and at most 2 searches run per tick. A test checks the fast build gives exactly the same graph as checking every pair, on every map.
- Thinking costs more per bot (0.52 → about 1 µs a tick; 24 bots ≈ 26 µs of a 1 ms tick), and the worst ticks are as good or better, because the graph-build spike is gone.
Bots keep off the edges
- Measured: a new playtest ran every map in its own mode at advanced and expert level and sorted deaths by cause. Bots fell into the void or lava 56 times in 120 s per map and level, mostly while grappling.
- Why they fell, and the fixes:
- Grapple pulls across pits. A hook was taken whenever it caught near the route, though the pull is a curve the bot can't steer, it lets go at full speed and skids on, and it catches some time after it was planned. Now the whole pull is simulated first, from the bot's real position and speed (pull, top speed, gravity, sliding along walls), plus the skid after letting go. Every point must be over ground that's safe to land on, the anchor's near side must be ground, and pulls stay under 22 m on maps with pits. It's checked when planned and again when the hook catches. While hooked, the bot follows the pull, doesn't air-strafe, and holds on through a fight instead of letting go at full speed.
- Hops and jumps off edges: a bunny hop at a drop the bot had stopped at, a jump at speed after a grapple, the unstick hop on a narrow ledge. The brake now works on momentum alone too, looks 0.6 s ahead when fast, and the bot never hops or air-strafes toward a drop.
- Lava on a floor counted as ground. It doesn't any more.
- Fights at the rim: bots look for edges at 1.5, 3 and 5 m; within 3 m they never strafe toward one and step away; within 5 m they drift away and don't jump or dash (a laser or blast push in the air there is a fall).
- Self-kills: no rocket into a wall inside its own blast; stickies only set off when the bot is clear of them.
- Falls in 120 s per map (advanced + expert), before → after (falls caused by an enemy's hit in brackets):
| Map | Before | After |
|---|---|---|
| Dojo | 17 | 2 (2) |
| Bastion | 10 | 0 |
| Foundry | 9 | 1 |
| Canal | 6 | 1 (1) |
| Gantry | 4 | 0 |
| Summit | 3 | 0 |
| Clockwork | 3 | 0 |
| Temple, Hightown, Viaduct | 1 each | 0 |
| Orbital | 1 | 1 |
| All others | 0 | 0 |
| Total | 56 | 5 (3) |
- Bots check objectives with their real size (review 6.6). They tested bomb sites, hills and points with a normal standing box while the server used their real one, so a giant, tiny or crouching bot could disagree with the server about being on the objective.
- Hide and seek and tag bots no longer pick spots from the previous map (review 6.6). After a map change, the old route graph stays until the new one is ready, and a hiding spot is kept for 90 s, so a bot could walk toward a spot from the last map. Goals now come only from a graph that matches the current map.
Netcode and servers
Reconnecting (owner 5.6)
- 10 s reconnect grace in every game (the owner: "all games should have a 10 second recoonect grace"). If your connection drops or you disconnect, your slot, team, score and state are kept for 10 s (you stand still). Reconnecting gives it all back. You're matched by your proven ranked id, or else by a reconnect token the server gives your game when you join. The same key reconnecting while the old session is still up takes the slot over. Players who are away don't count in votes and get no snapshots.
- In a tourney a drop is no longer an instant walkover: it only counts after the grace.
- A reconnect can't take over someone else's ranked id (review 2.11). A reconnect matched by token but proving a different ranked key (or none) got the slot back, replaced its key and kept the old ranked id. Now the token only restores a slot whose ranked id this connection proves; otherwise it's a new session and the old slot runs out its grace.
Lag compensation and prediction
- How far back a hit is checked is capped by your real ping (NET-5). Your game chose how far back the server rewound for your shots, up to 500 ms, including the knife (which decides tags and finds). Now the rewind is at most your measured ping + an interpolation allowance + 30 ms, never over 500 ms.
- Players with a high interpolation setting hit what they see (review 2.5). The cap above didn't cover
interp_mssettings up to 200 ms, so those players' shots were clamped short and landed behind moving players. Your game now tells the server its fixed interpolation delay, and the cap covers it, up to 200 ms. In a test, a player with interp 200 aiming at someone moving at 10 m/s hits 17 of 17 rifle shots, against 1 of 17 when the server didn't know the delay. - High-ping players no longer rubber-band (PERF-6). The game kept the last 256 commands for prediction: 256 ms at 1000 Hz. Above that round trip, every correction replayed from the wrong state. It now keeps 1024 (over a second), about 110 KB, and replays no more than before at normal pings.
- Fixed: a lasting input backlog (PERF-1). The server refilled its command tokens at exactly the rate commands arrive, so after a stall on the network or server side, a backlog stayed queued for good: 3-7 commands measured (a few ms of extra input lag at 1000 Hz, up to about 96 ms at 125 Hz), invisible on F3. A queue deeper than 2 for 32 ticks in a row now runs one extra command a tick from a small allowance, so a backlog clears in about 45 ticks. A client sending commands faster than real time gains at most about 1%.
- Input still goes out every tick, on purpose (PERF-12). Sending input once per frame was tried to save network calls. It was reverted because each command then rode in 2 packets instead of 4 (less protection against packet loss) and could wait up to a tick. Latency comes first.
Hidden information (NET-10, C12)
- Before: players you couldn't see still sent your game their health, weapon, firing, crouch and whether they carried the bomb (which in tag also marks the tagger). Ranked hid health bars on screen, yet every game knew every enemy's exact health. Every projectile went to everyone, and dead players received everything.
- Now:
- Players out of your sight reach your game with only their scoreboard fields: no health, weapon, firing, crouch or bomb. The tag and hide-and-seek "it" flag stays, because those modes need it. The mode state also hides a bomb carrier, planter or defuser you can't see.
- Projectiles get the same line-of-sight checks as players. One you never saw isn't sent; your own always are. One that leaves your sight is sent as an id only, so your game doesn't take it for an explosion. One that explodes out of sight is shown at that spot for 50 ms, so its explosion still sounds and flashes where it happened.
- While you're dead you get what the player you watch sees (your killer first, then whoever you pick). In team modes that's your living teammates' views; watching an enemy doesn't count. Free spectators still get everything.
- In tag, the tagger is never hidden (see Tag). In hide and seek, the seeker gets nothing about the hiders during the head start.
- Hidden players' typing and quip sounds no longer play from the middle of the map, and the game no longer shows a fake explosion when a projectile leaves sight.
- See Known issues for the 20 m radius in which everyone is still sent.
Clocks
- Fixed: the laser did about 25x damage on servers that had been up for hours. The server's game clock was a float counting seconds since it started. After a few hours it could no longer tell 1 ms steps apart, and the laser's 1 ms refire broke: a long-running server fired a burst of laser shots every command. The same rounding hit every timer: refire, cooldowns, respawns, votes (about 8 ms off after a day, 62 ms after a week).
- The clock was first made a double, then a 64-bit count of milliseconds (NET-13, PERF-5). It never loses precision and never wraps (the old 32-bit count wrapped after 49.7 days).
- The wire format didn't change: times still travel as 32 bits, and each side unwraps them against its own clock. Tests run the laser on a server 5 days up and play across the 2^32 ms wrap (17 of 17 rifle hits on a moving player).
- Your game's clock too (review 2.6). The game's render time was also float seconds of the server's uptime: 8 ms off after 18 hours, 31 ms after 3 days, so movement on screen stalled and jumped and the time your shots were checked at drifted. It's a double now.
Votes, admin and commands
- Admin is the player hosting the game, nobody else (NET-8, owner 4.1: "b alone is fine"). Admin was "the packet comes from 127.x", so behind any local relay or tunnel every remote player became admin. Now only the in-process host's own session is admin, plus a dedicated server's console.
kicktakes a player's name or id (review 4.1). The console completed/kickwith names, but the server read a number: "Bob" read as 0, so/kick Bobkicked slot 0, usually the host. Now it takes an id, an exact name (any case, exact case first) or the start of exactly one name; nothing or several matches kick nobody and say why.- Kicking can't be voted (owner B10: "server OR hoster can kick people from server with command, but that shouldnt be votable").
- New vote: move a player to spectator (owner B10: "there should be a vote for remove player, like if they go afk or are being an ass or something, it will move them to spectator").
callvote spec <name>, or the vote menu's MOVE A PLAYER TO SPECTATOR.- It needs at least 3 connected human players, so never in a 1v1. The player it's about can't vote on it. Normal vote rules and the 30 s timer apply.
- If it passes, they're moved to spectator and can't rejoin play until the next map, not by switching teams or reconnecting either.
- Each caller can target a given player once per map.
addbottakes skill names (review 4.2). Completion offered skill names first, but the server readteam skill-number, so/addbot EXPERTadded a default bot. It now takesaorb, then a skill as 0-3 or its name in any case, or a skill alone; an unknown skill adds nobody.- Vote, chat and command limits that stop abuse are under Security.
Hosting from the game
- The pause menu shows whether friends can reach you. Under "HOSTING ON UDP PORT": the UPnP result, now green (mapped) or red (failed, no router), and a reachability line: green "reachable from the internet - friends can join", red "not reachable - forward UDP N on your router to this PC", or grey while checking. CHECK AGAIN re-checks; it also re-checks every 5 minutes and when UPnP finishes.
- How: the game asks a public quark server from the list (preferring one on another address) to send one packet back to your public address, from a separate socket so your router has no mapping toward it. If your game port receives it within 3 s, you're reachable.
- Every server answers these probes, statelessly and rate-limited (once per address per 0.5 s, 50 a second in all), with one 9-byte reply to the asking address only, so it can't be used to flood anyone.
- It runs for games others can join, not for vs-bots, time trials or private map tests.
- The Host Game screen's old hint ("IF UPNP FAILS, FORWARD THAT UDP PORT ON YOUR ROUTER") now points to the pause menu, where the result is.
- Leaving or stopping a hosted game no longer freezes the screen for up to 15 s (review 2.10). Stopping waited on the UI thread for the reachability check, the tourney rating lookups, the server-list announcer and UPnP. They now finish on a background thread; a late one never undoes a newer game's port mapping or listing.
- The host screen says when ranked ids need
--own-addr(C9). See Ranked.
Dedicated servers
- Auto-update follows signed releases only (SUP-8, owner 2.4). Servers built whatever was on the branch, as root, with no signature check; a failed merge silently stopped all updates; and a hard-coded platform list froze every server on 1.14.1 for hours when a release had no macOS build. Now
tools/server/autoupdate.sh:- takes only releases of the server's channel (production releases for the production server; see the next section for the testing server), checks the release's signed manifest and Linux package against the built-in key, builds exactly the signed commit, and never downgrades;
- works out which platforms to wait for from the release itself;
- restarts the server only when no match is being played;
- logs failed or stuck updates at critical priority (
journalctl -p crit -t quark-update) and can post them to a Discord webhook (QUARK_ALERT_WEBHOOKin/etc/quark/update.env).
- Servers run as their own user, sandboxed.
tools/server/install.shnow installs a dedicatedquarkuser, the code in/opt/quark, data in/var/lib/quark, and systemd sandboxing, so the server can't rewrite the script that updates it. Re-running it upgrades an old install in place, keeping options and data.- Fixed in the final review: an old install that updated its script would fail every minute with no alert (review 1.2); it now logs "run install.sh once" and waits. Re-running
install.shstopped the old server before building, so a failed build left it down (review 1.3); it now stops the old one only after the new build succeeds.
- Fixed in the final review: an old install that updated its script would fail every minute with no alert (review 1.2); it now logs "run install.sh once" and waits. Re-running
- Dedicated servers handle ranking-server replies (review 2.3). Replies were only read in a loop dedicated servers don't use: failed race tickets never cleared, "no match ticket" and leaderboard messages never reached players, and the reply queue grew forever. They're read every frame now, and at most the newest 64 are kept.
--data-dir DIR(orQUARK_DATA_DIR) sets where a server keeps its data and logs (see Demos, files and updates).--own-addr host:portis explained in the README (see Ranked).
A production and a testing server on every box
- Why: new work now goes out as testing builds first (see Release process), and the owner wanted testers to have servers to play on, without taking capacity from production players or letting unfinished builds touch the ranked ladders.
- Every server box now runs two servers.
tools/server/install.shturns the existing install into the production server (same unit, port, user and data, now started with--channel prod) and adds a testing server: its own userquark-testing, code in/opt/quark-testing, data in/var/lib/quark-testing, on the production port + 1 (--testing-port Nto choose another), named "<name> [TESTING]".--no-testingleaves it out, and the choice is remembered in/etc/quark/install.conf. Re-runninginstall.shwith no options keeps the current settings. - The testing server runs at a lower CPU priority (systemd
Nice=5,CPUWeight=50), so the operating system already favours production. - Each server follows its own channel. The production server takes the latest production release, as before. The testing server takes the newest testing prerelease every 2 minutes; neither ever downgrades.
- The testing server runs only while there's something to test (owner: "if the testing branch and main branch are on the same update, you dont need to run a testing server"). When no testing build is newer than production, for example right after a release, it stops once its current match ends and leaves the box to production. It starts again by itself when a newer testing build comes out: the updater makes or removes a switch file,
/var/lib/quark-testing/active, which a systemd path unit and the service's start condition follow. - The signed release manifest must now name the release's channel, tag, version and commit, and a testing build is built as exactly the signed version (with its
-test.Nsuffix), checked against the commit'sVERSION. - The two updaters share one GitHub request a minute for the release list (unauthenticated GitHub allows 60 an hour per address; an optional read-only
GITHUB_TOKENin/etc/quark/update.envraises that) and take turns compiling (a lock, so two builds never run at once; the update unit may now run up to 4 hours). - The update script is read in full before it runs, since updating the checkout replaces it.
- Alerts say which server's update failed.
- The testing server runs only while there's something to test (owner: "if the testing branch and main branch are on the same update, you dont need to run a testing server"). When no testing build is newer than production, for example right after a release, it stops once its current match ends and leaves the box to production. It starts again by itself when a newer testing build comes out: the updater makes or removes a switch file,
The load governor
- Why: two servers on one machine can overload it, and a server that falls behind its ticks hurts everyone on it (frame-time spikes are what the 1% and 0.1% lows measure). Production players come first, and a live ranked match comes before casual play.
- How it decides: each server writes a small status file about once a second to a shared folder (
--governor-dir,/var/lib/quark-governor): its players, whether a ranked match is live and how many play in it, its tick rate and its own limit, and whether it's overloaded now. Overloaded means the server's existing check failed in its last window: it fell behind its ticks, its sends failed, or a player reported more than 2% snapshot loss; severe means it was more than 90% busy. Production makes the decision for both (it's the one server that never closes) and testing follows it. - What it does:
| Situation | What happens | How fast |
|---|---|---|
| The machine is overloaded | 1. The server nobody is on is slowed, one step at a time (1000 → 500 → 250 → 125 Hz) | one step every 6 s |
| Still overloaded, or neither server is empty | 2. The server not playing a ranked match is slowed (a ranked match of 0-1 players doesn't count; if both are alike, testing goes first) | one step every 6 s |
| Still overloaded | 3. The other server is slowed too | one step every 6 s |
| Both at 125 Hz and production still overloaded | 4. Testing closes: its players are told why and dropped, it leaves the server list, refuses connections and idles at about 0 CPU. Production never closes. | after 30 s in a row |
| Mid-round | A lower limit applies at once on an empty server; otherwise at the next break, as tick-rate changes always do. While one waits for a break, nothing else is lowered. | at once if the overload has lasted 20 s or is severe |
| Calm again | Limits rise one step at a time, the most important server first (a live ranked match or players, then production) | after 60 s calm; the wait doubles, up to 15 min, if a raise brought the overload back |
| Testing closed | Testing reopens at 125 Hz | after production has been calm for 5 min; doubles, up to 1 h, if it has to close again within 30 min |
| Production restarts (an update) | A closed testing server stays closed, and production picks its last decision back up from its own status file | up to 60 s |
- Cost on the server's tick: none measurable. The files are read and written, and the decision made, on the governor's own thread once a second; the tick hands over a small status copy a few times a second and reads the decision from atomic variables, with no memory allocation or disk access per tick.
- Nothing changes elsewhere: without
--governor-dir(a game hosted from the menu, a plainquark_server) servers manage their own tick rate exactly as before, and so does each of the pair if the other's status is more than 5 s old. - In a test with two servers under simulated overload, testing reached 125 Hz at 4.1 s, before production's first step down at 4.5 s; testing then closed and refused a connection with the reason, reopened later, and production climbed back first.
Ranked on testing builds is never rated
- Why: testing builds are unfinished, and their results must not move anyone's rating or race times.
- What: a server started with
--channel testing, or running any-test.build, plays ranked exactly as usual (queue and match lock included) but never opens a ticket, never asks players to sign, and never sends a result, a race run or the end-of-match ranked result, so players' games have nothing to report. A testing build hosting ranked from the menu is unrated too. - What players see: "TESTING SERVER - RANKED NOT RATED" on joining, at the start and at the end of a match; "[RANKED … - NOT RATED]" in the server list; "RANKED - NOT RATED" as the HUD tag; and a testing build's host menu says ranked isn't rated.
- The ranking server refuses them too: tickets, results and race runs from a server whose announced version is a testing build (it contains "-") get "testing server: ranked not rated", and so does a player's report under such a server's ticket.
Ranked and the ranking server
Your ranked id is proven by your key (NET-12, owner 3.1)
- Before: a ranked id was a name your game claimed (
rankid), first come first served, and ids were shown to everyone, so anyone could play under someone else's id. - Now: your id is derived from a signing key made from your
quark.keysecret, and your game proves it by signing the server's connection challenge. A wrong signature joins with no ranked id. The oldrankidcommand is gone. Signature checks are rate-limited (50 a second) so a flood just waits. - What you sign names the server you dialled (review 2.2). A malicious server could pass your connection challenge on to a real ranked server, have your game sign it, and play there under your id. Anything the real server puts in its challenge, a relay can forward; the one thing it can't is the address you chose to dial. So your game signs that address, and a server only accepts signatures for its own address: its port on its own network interfaces, its public IP, the address UPnP reports, or loopback from the same machine.
- Server owners: if players reach your server at an address it can't see itself (a router forwarding a different external port, a container or VM), start it with
--own-addr host:port, or those players join without a ranked id. The server log says so and names the address to use; the README explains it; and a game hosted from the menu shows it on the pause screen (C9).
- Server owners: if players reach your server at an address it can't see itself (a router forwarding a different external port, a container or VM), start it with
- Your ratings move to your new id automatically. The first time the ranking server sees your secret (your game sends it once at start-up), it moves your old id's ratings, account, name, Discord link, lifetime stats and race times to the new id.
- Fixed in the final review (review 3.6): if a match was rated under the new id before that, the move dropped the old rating. Now both are merged (the rating from the row with more games, the games added up). Your game also retries until the ranking server confirms (10 s, doubling up to 15 minutes), then never sends it again.
Match tickets (WEB-1, WEB-2, WEB-3)
- Why: the ranking server couldn't tell whether a match had happened. In a local test, 160 fake reports took a fresh account to #1. Anyone who knew a match id could void it, and losers could void their own losses. The host decided results, and players' games just forwarded them.
- Now:
- A ranked match only counts if the game server opened it with the ranking server (a ticket) when it went live, with its list token. Servers that aren't public register hidden; LAN-only servers can't get tickets, so their matches aren't rated.
- Each player's game signs the ticket's match id, so a server can only ticket players who are really in the match.
- Reports are accepted only from the ticket's players, once each, with the same ladder and player set.
- Your game checks the host's result against what it saw (who played, sides, scores and kills). If they differ it reports a dispute instead of agreeing. Any dispute leaves the match unrated for good, and it's logged (owner: "just logged and unrated"). The owner can read the log, but nobody has to settle anything, so a match can't sit waiting for a decision. A host who lies can at most get a match unrated, never a wrong result rated.
- A server can open at most one match ticket a minute.
- New accounts are provisional for 5 ticketed matches. Until then they're left off the leaderboards and crowns, and an established player's rating doesn't move in a match against them (their own rating still settles). This makes farming with fresh accounts pointless.
- One placement threshold everywhere: 5 games. The game said 5 and the ranking server said 3 (GAME-7).
Leaving a ranked match (owner 3.1)
- The first player to leave a live ranked match loses it. The owner: "ensure that only the first leaver gets punished with a loss, the one who stayed, who then has to leave should also not be punished". Before, leaving was free: only connected players were counted, so a losing player could leave and the match went unrated.
- The first player gone past the 10 s grace takes the loss. A 1v1 ends right there, and the player who stayed can leave freely.
- A leaver's slot isn't given to anyone else until the result is out.
- Every way of dropping out counts (review 2.1). A leaver was only recorded when the reconnect grace ran out. Two other paths dropped a player without it: a new connection from the same address, and the cap on unanswered reliable messages. Now any drop of a live ranked player counts as leaving (except the server shutting down), each player once. New connections during a live ranked match watch as spectators, so a leaver coming back isn't counted twice and can't spoil the result.
- Leaving in the first minute counts too (review 3.5). Results in a match's first 60 s were voided, so leaving early escaped the loss. An early result that names a leaver now counts, and players' early reports are kept.
- A free-for-all leaver who rejoins counts once, as the leaver (review 2.11).
Live ranked matches are locked (owner 3.4, C15)
- Once a ranked match is live, nobody can vote, change the map or mode, turn ranked off, restart, start editing, force a start or add bots, and nobody can kick a player of the match or switch teams, the host and console included, until it's over. A vote already running is cancelled. The owner: "when a 1v1 match has been started votes, gamemode changes, map changes, disabling ranked, etc should be disallowed until the match is over".
- Why: in 1v1 a vote passed at timeout if yes beat no, so the caller's own yes could change the map or turn ranked off mid-fight unless the opponent pressed F2 (GAME-7).
- It applies to exactly the three rated ladders: 1v1 vanilla, 1v1 laser vampire and free-for-all instagib (C15: "are the only locked down ones"). The Viaduct race isn't locked: it has no rating, only best times, and quitting submits nothing.
Race times (WEB-4)
- Before: players' games reported their own race times, and the only checks were "at least 5 s" and increasing splits.
- Now: when a time trial on the official Viaduct goes live, the server opens a race ticket and posts each finished run itself, with its splits. Your game no longer sends race times; the old way answers "update the game".
- Impossible splits are refused by the server and again by the ranking server. No stretch between checkpoints may beat a minimum time: the straight-line gap between the checkpoints at 48 m/s (1.5x the fastest dash exit speed). A run longer than its ticket is old (plus 30 s) is refused too.
- Race tickets carry the racer's signature (review 3.2). Any listed server, even a hidden one, could open a race ticket for any id and post a run with a name: the name was saved before checking the run was faster, so it could rename any player on the boards and plant times under their id. Now the racer's game signs the ticket, the ranking server checks it like a match ticket, and a run's name is saved only when the run is kept.
Discord links (WEB-9)
- Before:
/linkaccepted any name, the bot also matched by nickname, so a member could take the crown role of a linked player who wasn't in the Discord, and queue fills pinged every linked member. - Now: run
/linkin Discord; the bot shows you a one-time 6-digit code, valid for 10 minutes. Type it in SETTINGS > DISCORD CODE and press BACK. Wrong codes are rate-limited. Crown roles and queue pings go only to verified links. - Linking shows what becomes public: the Discord field reads "PUBLIC", and a notice says your Discord name will show next to your game name and ranked id.
Spectators of ranked matches see everything
- In a ranked match, health bars, crowns, typing indicators, chosen player models and HUD extras were hidden for everyone in the game. Now they're hidden only for the players in the match; spectators see them again.
The ranking server (Cloudflare Worker)
- Its address is built into the game (WEB-11, owner 3.3). The game fetched it from the website's
master.txt, which any push redeployed, and whoever controlled that address would receive every player's ranked secret. Now the address is pinned in the game. Amaster.txton the website overrides it only with a valid release-key signature over the file plus the prefixquark-master-v1(so no other signed file can pass as one). The Discord bot uses the same built-in address and never readsmaster.txt. - The server list can't be taken over (WEB-5). One address could announce unlimited ports and push every real server off the 200-entry list; an IPv6 caller could remove or list any address; and writes were plain GET links any web page could trigger.
- At most 8 fresh servers per address.
- A server gets a token on its first announce and must show it to announce again or be removed;
?ip=is only used over IPv6 with that token. - Writes are POST only; a write from a web page (an
Originheader) is refused; only reads allow other sites.
- Rate limits and caching (WEB-6, WEB-10). The free plan allows 100,000 requests a day, and a modest flood could take every online feature down until midnight UTC.
- Per address: any request 240 at once then 6 a second; writes 60 at once then 0.5 a second (sized for players sharing one address).
- Per player id: 10 writes at once, then one every 10 s.
- The Discord bot, with its key, has its own bucket (200, then 5 a second), so one "Queue for all" (16 requests) no longer uses up its address's limit (review 3.1).
- Lifetime stats take at most 1200 kills and 1200 deaths per id per hour (they took 300 kills every 20 s, forever).
- Leaderboards and other reads are cached for 5-60 s, and reading the queue no longer deletes anything.
- Ratings and queue fills are written atomically (WEB-7, WEB-8). Two matches for one player at once could overwrite each other and erase a loss, and a queue could fill twice, in about half of simulated races. Each write now checks a version number and retries on conflict.
- No match stays pending forever (review 3.3). A match whose players never reported, or whose server never posted a result, stayed pending and was never cleaned up; the clean-up job could keep re-picking the same stuck rows; one failing match stopped the rest. A scheduled job (every 15 minutes) now rates or voids expired matches oldest first, keeps going past one that fails, and prunes old rows.
- A server's next ticket only voids its last one if that has no result (review 3.4); before, a late dispute on a finished match was silently dropped.
- Hardening (review 3.9): keys are compared in constant time; the owner's dispute log is read by POST with the key in the body (no key in URLs); the request size limit counts the bytes actually read; bad bodies no longer cause errors; a guard keeps non-numbers out of rating maths.
- Your ranked secret stays off the command line (WEB-13). On Linux and macOS it was passed to
curlas an argument, visible to other users' process lists. It now goes through standard input.quark.keyis written with owner-only permissions (0600) and atomically. - Decided against (owner 3.4): no rating decay, and no rating-banded queues: "anybody can play anybody".
- For the ranking server's operator: run the full SQL list in
tools/master/README.mdbefore deployingworker.js, add the cron trigger*/15 * * * *and anADMIN_KEYsecret, and deploy together with the game release.
Security
Attacks over the network
- Joining does a quick address check (NET-1). The handshake had no proof that a connect packet came from where it claimed. One tiny forged packet could open a session that streamed hundreds of snapshots a second at a victim, keep it alive, or fill every slot; a map request returned about 72 times its size.
- Now a connect without a valid cookie gets one 13-byte challenge and nothing else; the server keeps no state. The cookie is a hash of a random server secret, your address and port, your session number and a 10 s time window.
- Your game echoes it straight back, so joining feels the same.
- Map download requests are limited to 400 a second per player (a download asks about 128).
- Forged packets can't kick a player (NET-2). A connect from a connected player's address with a different session number dropped that player before any check. Now a new session only replaces an old one after passing the cookie check.
- Server-list queries can't be used for amplification (NET-4). A 5-byte info request got about 300 bytes back. Requests must now be padded to 160 bytes, and the reply (server name cut to 96 characters, map name to 40) is never bigger. The server browser and the Discord bot send padded requests.
- Version-mismatch replies aren't bigger than the request (review 2.9). The 34-byte "version mismatch" answered an 11-byte forgeable packet before the cookie check. The reply now uses the longest wording that fits in the request: real games still learn the server's version, a bare probe gets nothing.
- Chat is filtered and rate-limited (NET-7). Chat had no limit and no filtering: one player could flood everyone's reliable messages (pushing out real game events) and put control and escape codes into the server console and log. Now: 5 messages at once, then one a second; printable characters only.
- Votes check map names and have a cooldown (NET-6). Map and mode votes read and parsed map files on the server tick, and there was no cooldown. Map facts now come from a cache filled in the background, tourney map lists are capped at 32 names, and a player whose vote failed waits 5 s before calling another.
- Fixed in the final review (review 2.4): the cache stored an entry for any name a vote mentioned, so
callvote map <random>grew it without limit; names with no file aren't cached now and the cache is capped. It also checked the shipped map file while the game loads your maps folder first, so your own maps never refreshed and an edited copy of an official map kept the official map's ranked flags.
- Fixed in the final review (review 2.4): the cache stored an entry for any name a vote mentioned, so
- Command budget per tick. Up to 16 text commands a packet ran with no limit. Now at most 8 commands a tick per player run (chat, votes, commands), plus up to 16 map edits within a 0.5 ms budget per tick for everyone; the rest wait and the game resends them, so nothing is lost.
- Too many unanswered reliable messages ends the connection with a reason (NET-11). The server silently dropped reliable messages past 256 unacknowledged, so a lost "ranked result" or custom-rules message left a match unrated or the game predicting with the wrong rules. Now the client is disconnected with "too many unacknowledged reliable messages (connection too lossy)".
Hostile servers, maps and demos
- A server or demo can't write outside the game's folders (CLI-2). A server's map name became a file path for the download cache (creating folders), the lighting cache and demo recording, and a shared demo set the map name with no check at all; watching one could write a file outside
maps/. Map names are now 1-40 letters, digits,-and_, and not a Windows device name (con,nul,com1and so on, which open the device instead of a file). A server sending a bad name is disconnected before anything touches the disk. - A server or demo can't crash your game with player ids (CLI-1). A snapshot's player id was never range-checked, and the game wrote into a 32-entry array with it every frame (a crash from a 370-byte demo). Kill events, tourney brackets and the mode byte had the same problem. Every id, weapon and mode is now checked when a packet is read, and bad ones are refused.
- A map can't crash or freeze your game or the server (CLI-3, NET-3, NET-15). Map loading had no limits: a 42-byte map asked for a 240 GB grid, a box at the edge of the float range loaded a map whose floor wasn't solid, and more than 65,535 boxes dropped out of collision. Edits could build boxes about 800 km out and crash the server, or produce a map that wouldn't load, which an edit session then saved over the real file.
- One validator now runs on every load, edit and download: coordinates finite and within ±4096 m; at most 60,000 boxes including generated ones; 256 spawns, ladders and groups; 1024 parts per group; 512 lights; ladders and groups at most 64 m tall; capped counts for sites, points, checkpoints and the cart path. The collision grid is refused over 16 million entries (32 MB).
- Map loading never throws; a bad map is an error message.
- A failed edit leaves the map exactly as it was. Every real box can no longer be deleted, and turning a custom group into a room clears its parts.
- An edit session keeps
<name>.qmap.bakon its first save and checks the map loads before every save.
- A demo can't freeze your game (CLI-4). A crafted record length wrapped around and the length scan never ended (a 19-byte file froze the game). Lengths are checked safely now, and demos over 512 MB are refused.
- A server or demo can't stall prediction (CLI-5). Prediction replayed every command since the server's acknowledgement, so a server or demo acknowledging something far behind cost millions of steps per snapshot. It's capped at the command ring. And starting a demo didn't reset prediction, so your own old commands were replayed onto the recorded player.
- Settings that aren't real numbers fall back to defaults (CLI-6). A server's custom rules could set gravity, speed or weapon values to NaN (not a number), and a mode name of spaces made an active mode with a blank name.
quark.cfgaccepted NaN for sensitivity and field of view, any shadow resolution, and file paths as HUD names. Non-numbers now take the default, values are clamped, and HUD and control profile names must be plain file names. On Windows, "nan" and "inf" now read the same as on Linux. - The queue menu can't crash your game (COR-1, CLI-7). When a ranked queue filled with a Discord user in it, every game that was queued or had the queue menu open crashed within 10 s (a Discord name was read as hex, which threw). The reply is now parsed safely.
- Nothing on these paths can end the game any more (ENG-2). The code had no exception handling at all, so anything thrown on untrusted input ended the process. Opening and playing demos, packets on both sides, ranking-server and server-list replies, UPnP and the launcher's package reader now catch errors and report them. A test makes every memory allocation fail in turn while a demo plays and a server takes connections: always an error or a dropped packet, never a crash.
Your computer
- No shell commands (SUP-9). On Linux and macOS, downloads, opening links and restarting were shell commands built from paths. Programs are now started directly with an argument list; links starting with
-are refused. - Secure connections: TLS 1.2 and 1.3 (SUP-10). Windows connections allowed the old TLS 1.1 and never offered 1.3. Now 1.2 and 1.3, falling back to 1.2 alone where Windows refuses 1.3 (older than Windows 10 1903; Windows 7 needs the explicit 1.2 setting).
- Background programs no longer hold each other's pipes (review 2.7). Two downloads at once could deadlock, because each child program inherited the other's pipe ends. Pipes are now closed for children, and the broken-pipe signal is ignored once at start instead of being swapped around every write (which raced between threads).
- Settings, keys and profiles are saved safely (COR-2).
quark.cfg, HUD and control profiles,hud.cfg,quark.keyand custom modes were truncated and then written, so a crash or a full disk lost every bind, or your ranked identity. They're now written to a temporary file and renamed over the old one.
Maps and the map format
Attackers and defenders reach every objective together (owner B2)
- Why: maps were designed for defenders to reach a bomb site or capture point 2-10 s first (3-10 s for bomb), and graded on the average over all sites or points, so one badly staged point could hide behind good ones. The owner: "the game is very fast paced, so really they should probably reach the site nearly at the same time, as if the defenders take over the site its hard for the attackers to ever plant the bomb", "target between -1 and +1", for "all maps", and "graded by the worst part".
- Now: at every bomb site and capture point, the attackers' arrival minus the defenders' must be between −1 and +1 s (positive means defenders first). Maps are graded at their worst site or point, and a site or point outside the band, or one a side can't walk to, fails the map. The map report lists every timing.
- Re-staged maps (attackers' arrival minus defenders', seconds):
| Map | Objective | Before | After |
|---|---|---|---|
| Midtown | points A / B / C / D / E | +3.1 / +10.0 / +7.0 / +13.4 / +2.9 | −0.2 / +0.5 / 0.0 / +0.7 / +0.1 |
| Lodestone | points A / B / C / D / E | +2.8 / +3.3 / +6.7 / +4.7 / +4.9 | +0.3 / +0.7 / 0.0 / −0.3 / −0.5 |
| Lodestone | bomb sites 1 / 2 | −10.8 / +5.4 | +0.7 / −0.6 |
| Depot | bomb sites 1 / 2 | +7.2 / +8.1 | −0.4 / +0.1 |
- Midtown: every stage's spawns were re-staged. (The review had measured D at +12.7 s and E at +0.9 s; measured again just before the change they read +13.4 and +2.9.)
- Lodestone: the points' spawns were re-staged, and the bomb sites moved from D and E (one in each team's cavern, so attackers reached D 10.8 s first) to the two places both caverns reach in the same second: the loading bay and the pillar hall under the pithead.
- Depot: a redesign. The sites moved to the middle of each flank: the west one among the warehouse racks, the east one on the dock between the containers, with a stair up each end of the dock. Defenders now spawn in the corners of the north yard, behind a screen wall.
- The band is symmetric, so it still holds in bomb's second half, when the sides swap spawns along with their roles. Payload and neutral payload have no site to arrive at, so the band doesn't apply to them; neutral payload is graded on cart fairness instead.
Stricter map grading (C1, MAP-4)
- Why: the review found the grading saturated, gameable or wrong, so the "every map scores 7 or more" bar proved little. The owner's rule (C1): fix the metrics strictly, never loosen a metric or the bar to make a map pass, and fix the map instead. The bar of 7 is unchanged.
- What changed in
tests/mapreport.cpp(documented indocs/MAP_METRICS.md):- Only reachable floor counts. The collision-checked walk grid is now the playable floor for every measure (area, levels, height range, samples, footprint), so floor nobody can walk to earns nothing.
- Loops count real ways round an obstacle on the walk grid (two-way moves only, averaged over four grid placements so an obstacle scores the same wherever it stands). The old count picked up fragments of floor: Dojo, a flat platform, had 112 loops, and 19 of 20 maps got full marks. Counts went from hundreds or thousands to 0-262 (Dojo 112 → 0, Viaduct 216 → 0, Midtown 3689 → 262). Full marks are now 3 per 1000 m² (rooms about 18 m across with four ways out).
- Vertical play comes from the height range of the real floor levels (12 m for full marks), plus levels, jumps and ladders. It was 10 on every map but Viaduct; Dojo had 10 with a 0 m height range and now has 1.5. Scores now range 1.5-10.
- Balance is per team, for every objective of every team mode the map offers, graded at the worst one (bomb sites and points on the B2 band). The report names the worst objective.
- The worst objective, not the average: bomb is graded per site and multi point per point, and cover on a point or the hill is sampled every 1 m on it (the sparse samples could read a point as having no cover).
- Ways onto the hill or a point are counted on the walk grid by direction and height; Downtown's plaza read 110 before.
- Unreachable gets no credit: a race leg the run can't complete scores 0 (it read as no shortcut at all). Lodestone's race legs that read "−1 m" were a sampling gap and now have real lengths.
- The long sightline band ends at the laser's reach, read from the weapon table: 24 m now, not a fixed 32.
- Smaller fixes: decoration buried in solid on every side doesn't count; materials aren't clamped (trim was counted as lava); lethal drops and water or lava are measured on the map (water or lava needs 40 m², not a fountain); dead ends use floor you can stand on and reach; free for all uses its own spawn pool; out of bounds is measured against the playable surfaces' bounding box.
- No exemption by name: Dojo's exemption (its layout, movement and creativity are graded but not barred, since it is a single platform on purpose) now comes from a line in its own map file, not from its name in the test code.
- Maps fixed in the generator to get back over 7 (the score that dropped under the new metrics in brackets):
- Harbor: dusk lights (decor 6.5).
- Viaduct: crowd barriers (layout 6.3).
- Hamlet: head-high field walls (hiding spots 13% → 21% of the floor), a second ladder on the water tower, steps out of the creek at both ends (layout 6.9, creativity 6.8).
- Colosseum: a ladder on every arched wall and piers in the ambulatory (layout 6.5, mode fit 6.9).
- Furnace and Temple: a second ladder to each roof walk; Temple also a pagoda balcony and more cover in the court (layout 7.0 and 6.8).
- Depot: a second warehouse ladder and more yard clutter. Railyard: more clutter.
- Foundry: catwalk ladders and more tool cribs (tag fit 6.0).
- Midtown: street furniture, skips, vans and crates down the avenues and on every block's pavements and yards, and plant on every office roof: cover 30% → 45% of the floor, loops 148 → 262, layout 5.8 → 7.5.
- Lodestone: much denser cover, on the 12 m ledges too, with rock falls and ore wagons on the cavern floors: cover 34% → 47%, loops 128 → 206, layout 6.2 → 7.5.
- Midtown and Lodestone grew from about 9,200 to 11,500 and from 8,000 to 10,600 boxes (the limit is 60,000). Every timing above is unchanged by the extra cover.
- Scores per map: old metrics on the old maps → the new metrics on the old maps → final. One number means it didn't change. Every map is at 7 or more in every graded category except Dojo's exempt ones.
| Map | Decor | Layout | Movement | Creativity | Mode fit | Balance |
|---|---|---|---|---|---|---|
| Anvil | 9.1 → 8.0 → 8.0 | 7.4 → 7.2 → 7.2 | 9.1 → 8.7 → 8.7 | 7.5 | 8.4 → 8.5 → 8.5 | 10 |
| Bastion | 7.6 → 7.1 → 7.1 | 8.0 → 7.7 → 7.7 | 8.9 → 8.8 → 8.8 | 8.2 | 7.5 | 10 |
| Canal | 8.3 → 7.4 → 7.4 | 7.5 → 7.1 → 7.1 | 9.0 → 8.6 → 8.6 | 7.5 | 9.0 | 10 |
| Clockwork | 8.5 | 8.8 → 8.5 → 8.5 | 8.4 → 8.0 → 8.0 | 7.2 | 10 | 10 |
| Colosseum | 7.4 → 7.1 → 7.1 | 7.4 → 6.5 → 7.1 | 8.3 → 7.9 → 7.9 | 7.5 | 7.5 → 6.9 → 7.4 | – → 10 → 10 |
| Depot | 8.2 → 8.6 → 8.8 | 7.1 → 6.3 → 7.1 | 8.7 → 8.5 → 8.8 | 7.5 | 10 → 4.5 → 9.1 | – → 0 → 10 |
| Dojo (exempt layout, movement, creativity) | 8.8 → 8.4 → 8.4 | 3.8 → 1.7 → 1.7 | 6.8 → 5.4 → 5.4 | 4.5 | 9.7 → 7.7 → 7.7 | 10 |
| Downtown | 9.5 | 9.4 → 9.1 → 9.1 | 8.4 → 8.3 → 8.3 | 7.5 | 8.1 → 7.6 → 7.6 | 10 |
| Foundry | 7.5 → 7.4 → 7.7 | 7.6 → 6.9 → 7.3 | 8.5 → 7.8 → 8.0 | 7.5 → 7.5 → 8.2 | 7.1 → 6.0 → 7.1 | – |
| Furnace | 8.4 → 8.1 → 8.2 | 7.2 → 7.0 → 7.4 | 9.3 → 9.0 → 9.0 | 7.5 | 8.4 → 8.6 → 9.0 | 10 |
| Gantry | 8.9 → 8.6 → 8.6 | 8.0 → 7.6 → 7.6 | 9.1 | 7.5 | 7.7 → 7.6 → 7.6 | – |
| Hamlet | 7.5 → 7.4 → 7.2 | 7.1 → 6.9 → 7.3 | 8.5 → 8.3 → 8.3 | 8.8 → 6.8 → 8.0 | 8.8 → 7.8 → 8.6 | – |
| Harbor | 7.2 → 6.5 → 7.5 | 7.8 → 7.5 → 7.5 | 8.7 → 8.4 → 8.4 | 7.5 | 7.4 | – |
| Hightown | 8.1 | 8.4 → 7.6 → 7.6 | 8.6 → 8.5 → 8.5 | 8.0 | 10 → 9.1 → 9.1 | – |
| Lodestone | 7.5 → 7.0 → 7.4 | 7.3 → 5.8 → 7.5 | 9.4 → 7.7 → 9.4 | 8.8 → 8.0 → 8.0 | 9.2 → 5.6 → 8.6 | 9.4 → 0 → 8.7 |
| Midtown | 7.1 → 7.0 → 7.4 | 7.0 → 5.7 → 7.5 | 8.0 → 8.1 → 8.1 | 8.8 → 8.0 → 8.0 | 8.5 → 5.7 → 8.7 | – → 0 → 10 |
| Orbital | 8.9 | 8.7 → 8.4 → 8.4 | 8.4 → 8.3 → 8.3 | 7.2 | 9.7 → 9.8 → 9.8 | 10 |
| Railyard | 7.4 → 7.8 → 8.0 | 7.5 → 6.7 → 7.3 | 8.7 → 8.3 → 8.3 | 8.2 → 7.5 → 7.5 | 9.0 → 9.3 → 9.6 | – |
| Summit | 7.6 → 7.3 → 7.3 | 8.2 → 7.9 → 7.9 | 9.7 → 9.3 → 9.3 | 7.5 | 9.9 → 10 → 10 | 10 |
| Temple | 7.5 → 7.1 → 7.2 | 7.2 → 6.8 → 7.2 | 8.6 | 7.5 | 7.1 → 7.0 → 7.1 | 10 |
| Viaduct | 9.4 → 9.3 → 9.3 | 8.6 → 6.9 → 7.1 | 8.3 → 8.6 → 8.6 | 8.8 | 7.3 → 7.5 → 7.5 | – |
- Bold marks a score fixed back over 7. A dash means the map had no balance grade before (bomb, multi point, tourney and free-for-all maps weren't graded for it).
- Several maps clear 7 by about 0.1: Colosseum, Depot and Viaduct layout, Temple's mode fit (7.1) and Foundry's tag fit (7.1).
Lodestone in every mode (owner B1)
- Why: Lodestone offers every mode but was graded only for multi point; in bomb, attackers reached site D 10.8 s first, so every round was an uncontested plant (MAP-6). The owner chose to keep every mode and fix the map for each.
- Now it is graded in every mode it offers (the report lists each mode's fit under it), and its timing and balance are held to the bar in all of them. Final mode fits: multi point 8.6, bomb 9.5, king of the hill 8.4, payload 9.2, neutral payload 8.8, free for all 9.1, hide and seek 9.3, tag 8.9, race 10.0.
- Bomb: the sites moved (see the timing table above).
- Race: it scored 0, because the leg to the shaft station's 12 m ledge could be cut 2.7x with movement tech (66 m leaping up against 179 m round by the stairs). The course now ends on the cage landing; the worst leg is cut 1.21x.
- Free for all: 12 spawns spread over the whole mine (it had the 8 every pool is topped up to): fit 6.7 → 9.1.
- Payload and neutral payload: sleeper stacks, crates and cribs on both sides of the ore-cart line, a step off its path: cover beside the track 5% → 27%.
- Lodestone is no longer offered for team VS, duel, 1v1 or tourney. Those modes want a 300-3,000 m² arena; the mine is about 83,000 m², so they scored 4.5 of 10 on mode fit and no spawn placement could reach 7. The owner chose to stop offering them rather than ship a known-poor fit (they can still be started there, with the menus' "may not work" warning). Every mode it is offered in passes the bar, including its timing and balance.
The generator is split into one file per map (C3, MAP-14)
- Why:
tools/make_maps.pywas one file in which Midtown used shapes defined in Lodestone's section through shared globals, so changing one map could quietly change another. - Now:
tools/maps/prefabs.pyholds the map builder and the shapes several maps use, and each map istools/maps/<name>.py.make_maps.pyis still what the build runs, with a new--map NAMEto make one map alone, and editing any map file regenerates the maps. The output is byte-for-byte the same as before the split, for all 21 maps together and each alone.
Maps are built from the generator
- Maps are a build artifact (MAP-9, owner 6: "generator output becomes a build artifact"). The game's maps are written by the generator (
tools/make_maps.py, now with one file per map intools/maps/) when the game is built, and the.qmapfiles are no longer stored in the repository. Before, the editor and edit sessions saved over the same files the generator owned, so the two overwrote each other. The shipped maps themselves didn't change from this. - Only real maps ship (review 5.3). Any stray
.qmapfile inmaps/was embedded as an official (even ranked) map, and maps the generator dropped stayed in builds forever. Now only generated maps, plus hand-made maps the repository declares, are embedded and copied. The retired map Crucible is removed from installs. - Maps are identified by the SHA-256 of their content (C2, MAP-13). They were identified by name plus a CRC32 checksum. Now the ranked map list matches by content alone, whatever the file is called; lighting caches are
cache/<sha256>.qlm(the newest 32 kept); downloads aremaps/downloads/<sha256>.qmap; and an edit session chains the id with each edit. A downloaded or edited map reusing an official name no longer evicts the official map's lighting cache (up to 24 s of re-bake, CLI-8). Nothing about this is visible in play.
Map file format, version 2 (C7; MAP-10, MAP-11)
- Maps start with a
versionline. A file without one is version 1. - Staircases and the map's setting are real keywords (
stairs,setting). They were comments, and the editor drops comments when it saves, so one editor-saved map lost all its stairs (6-42 per map) and its setting, and then failed the map tests. - A map from a newer version still loads. An unknown keyword, an unknown mode in
modesorfor, an unknown ranked ladder or palette colour is skipped with a warning instead of refusing the map, so an older game can still join a server on a newer map. - Gameplay keys stay strict, and two silent fallbacks are now errors: an unknown spawn team (it became "any") and a second hill (it replaced the first).
- Every format change comes with a converter (the owner's rule for every future change). Each version has one upgrade step to the next; older maps are converted when they load, so the parser only reads the current format. Your own map files are converted on disk once at startup (game and dedicated server), with the originals kept in
maps/old/v<N>/and aconvert.log. A map that doesn't load after converting is left as it is and not retried. - Saved maps keep every digit an edit carries (review 5.5). Saves wrote 6 significant digits while edits carried 9: an edit at x = 1000.125 saved as 1000.12, so a player who downloaded the saved map collided differently from one who applied the edit. Every number is now saved with the fewest digits (6 to 9) that read back exactly; shipped maps still save "12.5". Spawn directions are no longer rounded to whole degrees.
- Loading a map starts from a clean slate (review 5.6). A map that didn't set its title, sun, palette, ceiling, void or hold-fire kept them from an earlier map or a failed load. One title rule now covers files and edits: at most 40 printable characters, no
#. - One set of map limits and one map-name rule everywhere (review 5.2, 5.7); a light with intensity 0 is refused everywhere (it lights nothing).
Hand-made maps (the owner's rule)
- The owner will hand-make every map later. Hand-made maps don't have to pass the automated map checks, but every check still runs on them and their scores are listed in the map report, marked HUMAN-MADE, for balancing. Generated maps keep the strict bar.
- A map counts as hand-made if it has a
handmadeline (the in-game editor writes one on every save), wasn't written by the generator, or is an owner replacement inassets/overrides/maps/. A hand-made map ships by being declared in.gitignore(!/maps/<name>.qmap).
Light and sight pass through invisible walls (MAP-1)
- Before: the rays for the baked sun and bounced light, the server's line-of-sight checks and bots' sight all stopped at clip (the invisible walls). Every outer wall carries 80 m of clip, so most floors on 18 of 21 maps were in shadow: Dojo 85% shadowed by clip alone and 11% sunlit, Anvil 1% sunlit, Viaduct 0%. Players behind clip "glass" (Hightown's and Orbital's windows) were also hidden from each other.
- Now: light and sight pass through clip. Bots still feel it when moving (clip blocks players). Lighting caches re-bake once.
Spawns
- A spawn for every player in every mode a map offers (MAP-2). Free for all draws only from "any" spawns, and eight maps that offer it had 2: Anvil (which hosts the ranked 4-player instagib FFA, so players spawned inside each other), Canal, Clockwork, Depot, Furnace, Railyard, Summit and Temple. Dojo, Viaduct, Midtown and Lodestone had none and fell back to every spawn.
- The generator now tops pools up to a full 8-player server: spread-out "any" spawns on open, reachable floor (mirrored pairs on ranked maps), and race start grids. 16 maps got more spawns.
- By hand: Downtown has 4 spawns a side for king of the hill (was 3); Viaduct has an 8-racer start grid and two turn spawns.
- A test checks the spawn pool the server really uses, for every mode, side and stage: at least 8 for free for all, hide and seek, tag, race and tourney warmup; 4 a side in team modes; 2 in duel; 1 in 1v1; at least 1.5 m apart.
- Spawns face open space and don't see each other (MAP-16). Every spawn now has 2 m clear ahead at eye height (Summit's two faced rock 0.5 m away; Foundry 0.9 m, Harbor 1.1 m, Orbital 1.0-1.6 m, Midtown 1.5 m). Spawns used together are at least 1.5 m apart. In team modes no spawn sees an enemy spawn at any range. Dojo is exempt, since its hold-fire start puts both sides face to face on purpose. Sightlines closed:
- Canal (78 m down the causeway): spawn screens, rows moved back 1.2 m.
- Depot (55 m): the gatehouse's front walled.
- Downtown (44 m down the avenue): a bus shelter in front of each row, a parked car moved 3 m.
- Railyard (87 m down the track): a screen in front of the attackers' row, moved back 1.2 m.
- Lodestone: a pit prop in the tunnel mouth (128 m) and a spoil wall on the surface (150 m).
- Midtown: four staged spawn pads moved off the avenues' long lines.
- Ranked maps are checked for point symmetry and have the sun overhead (MAP-17). The check compared spawn counts only. Now every spawn (with its direction), ladder, light, bomb site, point and checkpoint must have its mirrored twin. The sun on Anvil, Dojo, Furnace, Gantry and Temple is now straight overhead, because any other sun bakes the two halves' shadows differently. This is a visible lighting change.
Routes and the payload track
- Map tests check that every route can really be walked (MAP-3). Reachability used the bots' route graph, which links box tops up to 2.6 m apart without checking for walls, so a spawn sealed inside a box read as reachable, and VS, duel, 1v1, free for all and tourney had no checks at all. Now a walk grid every 0.25 m with the real player box and real collision (step-ups of 0.55 m, jumps of 1.15 m, 2.5 m gaps, ladders, crouching) checks every route each offered mode needs. A missing route fails the map and scores 0 for mode fit. All 21 maps pass.
- The payload cart's track is kept clear (MAP-5). The cart drove through solid geometry and nothing tested it. The generator now has a keep-clear list checked against every box: generated decoration that lands on it is dropped, and a hand-placed box stops the build. A test sweeps the cart's box along the track every 10 cm.
- Railyard: the 3 x 4 m box on the track at 102-106 m moved 3.1 m beside it.
- Lodestone: the cart ran through the shaft station's landing and two corner posts; the line now runs 2.5 m south, beside the landing, still through the tunnel. (Moving the station instead opened 440 m² of out-of-bounds roof.)
- Viaduct: a decorative pillar beside leg 2's third slalom opening left a gap the player couldn't pass, so leg 2 of the race was blocked; the pillar is gone.
Grading against what movement can really do (owner 5.4)
The review found the map grading ignored bunny-hops, strafing, dash-ups and the grapple, so it under-reported shortcuts and ways out of bounds. The owner: "also yeah fix map metrics".
- Before: the map report assumed a dash-jump of 16 m across and 3.5 m up, and nothing more.
- Now: it measures the movement envelope with the game's own movement code each run: the speed a good player builds from a standstill with bunny-hops, strafing and dashes (12 m/s after 4 m, 20 after 22 m, 28 after 58 m), every jump-plus-dash arc from each speed (standing: 13.5 m across or 4.2 m up; at 28 m/s: 61 m across or 12.5 m up), and what a grapple pull leaves you with (it keeps 28 m/s in and out, since it no longer brakes).
- New check: nobody can get out of bounds. Any floor outside the playable area this movement can reach fails the map. It found that a running jump cleared Dojo's 4.5 m moat, so Dojo has an invisible wall at the moat's far edge.
- Score effects: Viaduct's race challenge dropped from 8.2 to 7.3 (fast players leap the beams), still a pass.
Other map changes
- Hightown has a colour palette (office concrete, glass blue, cool strip lights); it was the only map without one.
- Score changes from the fixes above, at the time, under the old metrics (the final scores are in the table under Stricter map grading): Canal decor 8.1 → 8.3, layout 7.2 → 7.5, mode fit 7.8 → 9.0; Depot decor 8.1 → 8.2, layout 7.0 → 7.1; Downtown layout 9.2 → 9.4; Railyard mode fit 8.8 → 9.0; Lodestone layout 7.2 → 7.3, mode fit 9.0 → 9.2; Anvil movement 8.9 → 9.1; Furnace decor 8.5 → 8.4, layout 7.3 → 7.2; Midtown layout 7.1 → 7.0. Every map stayed at 7 or more in every graded category.
- Map documents brought up to date (MAP-18): the map report regenerated (with a Hightown row and the grapple's real speed), shot sheets for Dojo, Hightown and Midtown (there were none) and redone for the changed maps;
docs/MAP_STRUCTURES.mdfixed (Gantry is ranked free for all, not 1v1; Temple is 7,000 m², not 5,700; Lodestone's "neither side is closer" doesn't hold for its bomb sites). The 1.14.0 changelog now notes that its run speed of 12 m/s was reverted in 1.14.1.
Editor
- The grid goes down to 1/8 m (owner, before release): 1/8, 1/4, 1/2, 1, 2 and 4 m, and the GRID button says them as fractions (it said 0.25 M and 0.5 M).
[and]step through all six; new sessions start at 1/2 m as before. Map files are unchanged: coordinates were never limited to the old grid. - Fixed: the crash when opening a map. The OPEN button handed the editor a map name that the editor's own map rescan freed before reading it.
- Fixed: maps saving as "mymap" whatever you typed.
- Characters that can't be in a file name (spaces, dots) stayed in the name field, and every save was then refused. Now a space becomes
_as you type and other invalid characters are dropped. - A map that failed to open was silently renamed "mymap".
- Edit sessions had no name field, so a session started from NEW always saved as "mymap". The name field now shows in sessions too: the host gets SAVE AS (the new
editmode saveas NAMEcommand), others get SAVE A COPY AS.
- Characters that can't be in a file name (spaces, dots) stayed in the name field, and every save was then refused. Now a space becomes
- A broken or unnamed map opens under a free name (review 4.7). A 40-character map that won't load opened as
NAME_newcut back to 40 characters, which was NAME again, so saving overwrote the file it meant to protect; an empty name became "mymap" and overwrote yours. Now it picks the first unused name:mymap2,NAME_new2and so on, kept within 40 characters. - Editing a built-in map keeps your edits only as a new map, when you say so (B3). The owner: "edits on official shipped maps are not saved globally only locally" and "you should just ask the user if they want to keep a local save of the edits they made in a new map".
- Before, edits to a built-in map were silently saved as a copy under its name, which then hid the real map, including its future updates.
- Now saving or leaving a built-in map you edited (Ctrl+S, SAVE, TEST, HOST EDIT SESSION, OPEN, NEW, EXIT, Esc) asks whether to keep your edits as a new map, with a suggested free name (
temple_edit,temple_edit2...). Yes saves it in your maps folder, No drops the edits, Cancel goes back. - Edit sessions on a built-in map are never autosaved, and
editmode saveasrefuses built-in names; after a save-as the map is yours and is autosaved. - The in-game editor stays available to everyone (the owner: "the in game editor should still stay in future updates").
- Trim and clip boxes can be edited (review 5.1). Every shipped map has trim (decoration that isn't solid) and clip (invisible walls) boxes, but edits only took the 8 ordinary materials: nudging a trim or clip box was refused, a deleted one couldn't be undone (lost for good), and the material key turned them into ordinary boxes. Now every edit, undo and group takes all 10 materials; the material cycle includes TRIM (NOT SOLID) and CLIP (INVISIBLE WALL); and clip boxes are drawn see-through violet. Grouping a selection no longer turns trim, clip, water and lava into ordinary "far" material.
- Clip boxes can be selected from anywhere.
- From inside a box, every click selected that box: Viaduct's editor view starts inside its invisible ceiling, so nothing else could be picked. A box you're inside now comes last.
- Something behind another object couldn't be picked at all. Now clicking the same spot again steps to the next thing along your view.
- With nothing selected, FIND CLIP BOXES (INVISIBLE WALLS): N selects each clip box in turn and moves the camera to it.
- Move and resize handles on all six faces, drawn over the world so they can be seen and grabbed through other objects. Only three showed before (the far ones were hidden), and move arrows only pointed one way per axis. Dragging a face now moves only that side.
- When handles overlap, the nearest one is grabbed (review 4.8). Looking down an axis, the far face's handle won the click even though the near one was drawn on top.
- An edit with several parts applies whole or not at all, undo included (COR-4). A step's parts were applied one by one with the undo entry pushed first, so a failure part-way left half a step applied and an undo for something that never happened. Now a multi-part step is applied to a copy and kept only if every part succeeds, and the undo entry is added only then.
- Edits apply in place, and big group edits sync 4-5x faster (MAP-12); see Performance for the numbers.
Interface, settings and HUD
Hitsounds in four tiers
- What changed: one hitsound pitched lower for bigger hits -> four different sounds by the damage of the hit: under 10 a light tick, 10-24 a deeper knock, 25-50 a heavy crunch, over 50 a brutal crack with a sub drop and a tail. Each tier is a step louder (0.7 / 0.85 / 1.0 / 1.15 of the old tick, measured as heard loudness, never clipping).
- Why: the owner wanted the sound to tell you how hard you hit, the way Quake Live's tiered hitsounds do. A sniper body shot (50) is heavy; a crossbow headshot (75) or a direct rocket is brutal.
- Every sound pack (DEFAULT, RETRO, HEAVY METAL, MAGICAL, MOUTH) has its own four, in its style. Hits that land within 40 ms of each other make one sound, the heaviest (a shotgun blast on two players); laser ticks, which the server already batches every 50 ms, stay a light tick. Every tier plays at its natural pitch, so replacement files play as recorded.
- All four can be replaced in the Asset Studio (HIT, HIT_MED, HIT_HEAVY, HIT_BRUTAL). Sound startup also got a little faster (synthesis stops each layer once it has faded).
- Tests: a test group no longer starts with the failure count of the groups before it (a failing group used to make later groups report failures with no FAIL line).
Health bars and the sniper scope
- Health bars over other players are on by default (was off). The owner's call: seeing how hurt an enemy is matters in a fast game, and most new players never found the setting. They're still never shown in ranked matches (to the players), and SETTINGS turns them off. Existing configs keep the value they have saved.
- Turning the sniper scope off now leaves your crosshair on. With SCOPE OVERLAY at 0 the sniper still zoomed but drew neither the scope's reticle nor your crosshair, so you aimed blind. Now, with the overlay off, the zoom shows your normal crosshair, exactly as the rifle's and crossbow's zoom do; with it on, the scope's reticle replaces it as before.
New font: Work Sans Bold
- The 5x7 pixel font is replaced by Work Sans Bold (SIL Open Font License), with real lowercase, proper letter widths and kerning. Player names now show as typed, in lowercase too.
- It's drawn from one distance-field texture: one texture read per pixel (the old outline took up to 10), with the edge and the black outline smoothed per screen pixel, so it's sharp at every size. Frame rate measured the same within noise, the new font slightly ahead.
- The smallest text size is 1.3 (at 1, small labels were about 5 pixels tall and HOOK read as "HOCK").
- Text that doesn't fit shrinks to the smallest size and then clips with "..." instead of overflowing (review 4.6). Buttons, sliders, cyclers, leaderboard and time-trial names are clipped by measured width, and the launcher's changelog wraps by width.
Console and chat
- The console is monospaced: every character takes the same width (wide letters like M and W are narrowed).
- Tab no longer opens the scoreboard while chat or the console is open.
- Tab completes everything: every command, every setting (and its current value), and the server commands' arguments: teams, vote yes / no, vote types, maps, modes, ladders, bot skills, on / off, and player names. One match fills it in; several fill the common part and always list them all.
- A real text caret in chat, the console, text fields and slider number entry: Left / Right / Home / End / Delete move and edit at the caret, which is a thin bar placed by measured width. Before, you could only add or delete at the end.
- Toggle chat and Disable chat (owner B9: "if there is no disable chat keybind there should be (toggle) and a disable chat option"). A TOGGLE CHAT key (unbound by default; set it in Controls) hides or shows other players' chat with a short notice ("CHAT OFF - <key> TO SHOW IT AGAIN"), and SETTINGS has DISABLE CHAT, which is the same saved setting. While chat is off, other players' lines, typing dots and key clicks are hidden; server messages still show.
- Dead players' chat is unchanged (B9): everyone keeps seeing everyone's chat. The owner: "this doesnt matter because ranked is either 1v1 or ffa instagib so everybody can see everybodies chat".
- A chat key bound to a letter no longer types that letter. With chat on T, pressing T opened chat and typed "t" into it (and more with key repeat). The key that opens chat or the console now swallows its own character and repeats until released.
Display and controls
- Alt+Enter switches between borderless fullscreen and windowed, saved to your settings. Holding it no longer also presses Enter (review 4.4): key repeats fell through as a plain Enter, sending chat or pressing the focused button.
- Crosshair hit flash: SETTINGS > CROSSHAIR has HIT FLASH on / off and HIT R / G / B colour sliders. It always flashed white before; white stays the default. The preview shows the crosshair as it is and as a hit flashes it.
- Outline thickness: PLAYER OUTLINE and WORLD OUTLINE sliders, 0-4 px in quarter steps (0 = no outline), for the outlines around players and around world edges. The default 1.0 is exactly the old look. No measurable frame-rate cost.
- Your vs-bots mode is remembered (COR-3). Picking duel, tourney, tag, hide and seek or multi point came back as free for all after a restart (the setting was clamped to the first 6 modes of 13).
HUD and menus
- The free-for-all scoreboard shows frags (M6). It sorted by frags but showed damage/10 plus objective time as SCORE, so the list looked out of order and the number shown wasn't what wins. The number shown and the sort are now the same everywhere: gibs in instagib, frags in free for all, wins in race, the score in hide and seek and tag, damage/10 plus objective time elsewhere as before.
- Long names no longer run into the next column (review 4.9, 4.5). Scoreboard names are clipped where the next column starts, with " (DEAD)" always shown; the server list's scrolling names are sized by their real width (it measured in "M" widths, but "W" is wider); and the Discord setting's label shrinks to its half column, with "(PUBLIC)" always shown.
- The weapon list greys a thrown knife or syringe with its timer, and shows only weapons the mode has.
Performance
Your game
- Big maps can be joined again, and loading never says "Not Responding" (PERF-2, PERF-3).
- Before: preparing a map's lighting ran three passes that grew with the square of the map's size on the main thread, before even checking the cache. Midtown took 13-14 s with a cached lighting file and 24 s without, and the server drops a silent player after 10 s: Midtown couldn't be joined at all. Windows showed "Not Responding", because window events weren't processed.
- The edge passes now sort faces into buckets (by axis, side, material, plane and 8 m cell), with identical output: building the Midtown world went from 18.4 s to 3.7 s, Lodestone 12.1 → 3.2 s, Hightown 229 → 145 ms, Temple 110 → 47 ms.
- The bake now runs on a worker thread with its own copy of the map. The game keeps running: window events, the network and a loading bar at about 60 fps. The new world is swapped in when it's done; a newer map cancels and restarts it; leaving cancels it. Tested: joining a remote Midtown server with an empty cache held the connection through a 200 s bake on a slow software renderer.
- The lighting atlas fits your graphics card (PERF-13). The atlas was 2048 pixels wide and as tall as needed (Midtown 2048 x 4096), and the card's maximum texture size was never asked; a card stopping at a smaller size got a black world. It now packs wider first, then with fewer texels per metre, until it fits. Atlases that already fitted are unchanged, so caches stay valid.
- The default "low latency" setting no longer costs a third of your frame rate (PERF-4).
low_latency=1waited for the GPU to finish the frame it had just submitted, exactly like "ultra", for no latency gain. It now allows one frame in flight. On Bastion against bots (RTX 3090 Ti), average / 1% low / 0.1% low: 2708 / 962 / 770 fps → 4130 / 1041 / 760. - A frame cap no longer stutters on Windows (PERF-14). The cap slept in 1 ms steps, but the Windows build's sleep took about 15 ms, so any cap above about 60 fps ran at about 66 fps with 15 ms spikes, exactly what the 1% and 0.1% lows measure. It now sleeps with Windows' own
Sleep()(about 1.9 ms per 1 ms sleep, measured). Uncapped play wasn't affected. - Players and projectiles no longer vanish for a frame at the screen edge (PERF-7). They were culled against the previous frame's camera, so they disappeared for a frame on fast turns, after spawning and when switching who you spectate.
- Maps load 4-6x faster. Reading a map built a text stream per line; a hand-written reader gives identical results (checked on 60,000 fuzzed maps): Midtown 11.7 → 2.2-3.1 ms, Lodestone 10.6 → 1.9-2.5 ms, Bastion 1.47 → 0.25 ms on the test machine (Midtown took about 100 ms and Bastion 13 ms on the owner's machine before).
- Fewer hitches (PERF-11):
- Opening the host or vs-bots menus, the editor or the in-game map picker re-read every map file: 12.7 ms for the 21 shipped maps, more with your own. Map info is now cached until a file changes: 0.1-0.2 ms.
- Refreshing the server list while the previous fetch was still loading froze the game until it finished or timed out: 6 s measured. Now 0.1 ms.
- Changing the announcer voice copied every line under the lock the audio mixer uses, stalling the mixer: 24-48 ms → 17-20 ms, worst mixer stall 8.1 → 4.1 ms.
- Recording a demo wrote and flushed every snapshot from the frame loop, so a slow disk (antivirus, a network folder) stalled frames. A writer thread does it now.
- Less work per frame (PERF-12): file lookups that checked up to 5 places on disk are remembered (2.1-2.7 µs → 0.05 µs a lookup); interpolated player positions are worked out once a frame instead of 3-4 times; the console's word list is cached instead of rebuilt from the whole config every frame.
- The editor draws big maps faster (PERF-12). The unbaked editor view streamed every map box to the GPU every frame (about 21 MB a frame on Midtown). The boxes are now one static batch, rebuilt only when the map changes. Midtown frame time 62.7 / 64.7 ms → 40.3 / 47.0 ms (on a software renderer), pixel-identical. After each edit, a redundant full rebuild is also gone (0.4-0.6 ms per nudge on Midtown).
- Map edits apply in place (MAP-12, owner 1.4). Every edit copied the whole map, rebuilt its collision grid and re-saved it to text: about 10.7 ms per edit on Midtown, on the server's tick and on every player's frame. Now an edit changes one box and updates the grid in place (identical to a full rebuild, checked after 1500 random edits). On Midtown: moving a box 2.8 µs (30 µs at the edge), inserting or deleting one 23-37 µs, a group part about 39 µs. The text is only made when needed (downloads, saves, demos). A 200-part group syncs in 134 ms instead of 602 ms.
Servers
- No more tick spikes from line-of-sight checks (PERF-9). Every viewer's line of sight to every player was re-checked all at once 20 times a second: one tick in 50 cast about 31,000 rays on Midtown with 32 players (10.7-13.5 ms). Now the checks are spread evenly over every tick, each pair still about every 50 ms, with a cap per tick. Midtown with 32 players: worst tick 0.79 ms. Hamlet with 24 bots and 8 players: 99th-percentile tick 5.3 → 0.89 ms, 99.9th 8.5-9.6 → 1.6-1.8 ms.
- Sending snapshots allocates no memory (PERF-8). Each client's snapshot was copied about 3 times and its events copied too, with 32 memory allocations per send at 32 players; finding the base snapshot scanned up to 256. Now each snapshot is built straight into the slot that keeps it, events go out by reference, and bases are found directly: 0 allocations per send.
- Hosted edit sessions no longer hitch every 5 s. The autosave serialized and checked the map on the tick (12.6 ms on Midtown); now the tick only copies it (0.1 ms) and a background thread does the rest.
- Votes and ranked switches no longer load maps on the tick (NET-6): switching a server to ranked parsed and hashed map files until it found the ladder's map (about 140 ms on the tick).
- Bot navigation and projectile costs are under Bots and Gameplay.
Demos, files and updates
- Your data lives in your user folder (SUP-11, owner 2.5). Settings, your ranked key, demos, HUDs, controls, editor groups, custom modes, your maps, downloaded maps, lighting caches and logs lived in the install folder, and uninstalling deleted them, ranked identity included.
- Now: Windows
%APPDATA%\quark, Linux~/.local/share/quark(or$XDG_DATA_HOME/quark), macOS~/Library/Application Support/quark.--data-dirorQUARK_DATA_DIRchoose another folder. - The first start copies your old files over: each one checked byte for byte, nothing overwritten, the originals kept. Shipped maps you never edited aren't copied, so they can't hide future map updates; lighting caches are rebuilt instead.
- Shipped maps stay read-only in the install; your maps folder is searched first, and the editor's MAPS FOLDER button opens yours.
- Uninstalling never deletes your data (owner 2.5: it "should not be deleted via the uninstall button on launcher"). Files an old version kept in the game folder are copied to your user folder first. On Windows the folders are recorded under
HKCU\Software\quark(DataDir,InstallDir), so cleaning programs can find them.
- Now: Windows
- Folders with non-ASCII names work (COR-5). On Windows, a user folder like
C:\Users\ZoëorC:\Users\Дмитрийbroke the data folder, settings, maps, lighting cache, demos, logs and keys, because the game opened files with the ANSI code page. Every file the game opens now goes through Unicode paths, and the game takes its arguments and its own location as Unicode. (Untested on real Windows; see Known issues.) - Updates delete only retired game files (C13). The owner: "delete retired game files, dont delete configs, custom maps, demos, etc".
- Before: every old file the new package lacked was copied into the new install, so retired files lived forever.
- Now every install records
installed-files.txt(each file and its SHA-256), and every package carries the same list. An update deletes only files on that list that are unchanged, missing from the new package, and not player files (settings, keys, demos, screenshots, logs, lighting caches, downloaded maps, and your demos, screenshots, groups, custom modes, HUDs, controls and logs folders). When in doubt, a file is kept. Installs from before 1.17 have no list, so nothing is deleted from them.
- Old demos are converted (C13: "convert it to the new update, and leave an old original copy also"). At the first start after an update, demos recorded with an older protocol are converted in the background (1.11, protocol 24, and later record the same way, so they convert cleanly). Each original is kept in
demos/old/<version>/. A demo that can't be converted is left as it is, logged indemos/old/convert.log, not retried, and says "recorded with version X" instead of failing. The game notes which version used which protocol (versions.txt) so later versions can name it. - Lighting caches are rebuilt once, because light now passes through clip and caches are named by content (see Maps).
- Replacement assets: sounds, voices, models, the font and the icon can be replaced by files in
overrides/in your user folder (see Asset Studio).
Launcher and signing
Antivirus-proof installs, REPAIR and CHECK FILES
- The report: one player's PC removed quark.exe right after installing 1.15.1 and 1.16.0, while every other build, and those builds on every other PC (about ten players, dozens of updates), were fine. Their folder still had quark_server.exe and version.txt from the same install, written at the same minute.
- The cause: not the launcher. The launcher code was identical from 1.14.2 to 1.16.0, writes each file in place and never deletes game files; the packages of both versions contain quark.exe. The pattern (one file, two exact builds, one PC) is an antivirus false positive: Windows Defender's machine-learning detections sometimes quarantine a particular unsigned build.
- What the launcher does now: every file of a new version is unpacked into
game.new, read back and checked against the package (size and SHA-256) before the swap. A file that was removed, changed or blocked stops the update with your installed version untouched, names the file, and explains how to restore it (Windows Security > Protection history > Restore / Allow on device) and how to exclude the quark folder, with OPEN FOLDER and RETRY. - Before every start the installed files are checked against
installed-files.txt(which now records sizes too: existence and size, cheap). A missing or changed quark.exe is never started; you get the same explanation and REPAIR, which downloads your version again, verifies it and swaps it in. CHECK FILES runs the full SHA-256 check on demand. A Windows "virus" refusal when starting the game shows the same screen. - Updates retry their folder renames for 3 s (an antivirus scanning new files holds them open briefly), never drop a player file they can't carry over, and put back a swap that was cut off halfway at the next start.
- A Windows copy bug fixed: MinGW's
copy_filecan't overwrite an existing file on Windows ("File exists"). Because of it an update dropped your old quark.cfg in favour of a packaged one, and a newer downloaded launcher didn't replace an older installed one. The launcher, the server's log backup and the user-folder migration now remove the old file first. - Fewer false positives: quark.exe, quark_server.exe and quark_launcher.exe now carry Windows version information and an application manifest instead of being anonymous binaries, which antivirus heuristics flag most. Code signing is what would really prevent them (docs/SIGNING.md).
Everything published is signed (SUP-1, owner 2.1, B6)
- Why: the launcher installed any download named
quark-<platform>.qpakwith no check at all, so a leaked token or a compromised account would run code on every player's machine. The owner: "yes, any update starting with 1.17.0 must be signed, including tests, site, launcher". - Now: releases are signed with an Ed25519 key (using the Monocypher library, vendored). Signed: every game package, the launcher inside each package, the standalone launcher downloads, the game zips and tarball, a release manifest (
quark-release.txt) andSHA256SUMS, and the website's file list. - The launcher refuses any update whose signature doesn't check out: a missing, failed or wrong signature deletes the download and says "refused version X". It verifies before extracting anything.
- A signed package can't pass as another release (review 1.6). A package's signature only proved the release key signed some package. Now the signed manifest lists every package's SHA-256, and the launcher installs a package only when the manifest verifies and names this release's tag, version, channel and the downloaded file's hash.
- The launcher only replaces itself with a signed launcher: the game's launcher update and the launcher's self-update check the packaged launcher's signature, on the very copy that goes in place.
- Versions from before signing (1.16 and older) are listed and install like any other, with no warning (B6: "you do not need to display in the launcher to regular users the old versions are unsigned"). An unsigned release claiming to be 1.17.0 or later is never listed and is refused at install.
- Anyone can check a download by hand:
quark_sign verify <file>(the website says how).
Channels, versions and updates (owner 1.1, 2.6)
- A CHANNEL button switches between PRODUCTION and TESTING builds, and you can pick any version in either (owner 1.1: "add a button that will toggle between testing branch and production branch where you can pick versions"). Your choice and any pinned version are remembered.
- Every release stays listed (review 1.4). The launcher read 50 releases (then 300 during development). It now reads every page until the list ends, both channels, and stops safely if a page repeats or fails.
- Pins on old versions are kept (review 1.5). An old
launcher.cfgpinned to a pre-1.17 version counted as "latest", so the launcher silently moved you to the newest version. Now the pin is kept: if it's installed you play it; a pinned version installs on its own only when nothing or an older version is installed, otherwise it waits for INSTALL. - It never downgrades on its own (SUP-6). Prereleases counted as "latest", and any version different from the installed one was installed, so a newest release without your platform meant an automatic downgrade. Now startup only installs something newer; an older version only when you pick it. PLAY stays available next to INSTALL.
- The launcher updates itself, and the game brings every older launcher up to date (owner 2.6: "upgrade from all previous launcher iterations to the new one… therefore nobody has to download a new launcher to update it or the game"). Each package carries the launcher. At start, the game replaces every older launcher it finds (from 0.1 on, under their various names and folders), installs the launcher where it belongs if missing, and repoints desktop and menu shortcuts. On Windows the running launcher renames itself aside and restarts once (never in a loop). On macOS the Applications app now starts the installed launcher instead of holding a stale copy.
- Quitting the game can't leave a half-replaced launcher (review 4.3). The update thread was detached, so quitting quickly could stop it between two renames and leave no launcher on Windows. Each swap now finishes as one step, and the game waits for the thread (at most 2 s) at exit.
- Safe package extraction (SUP-5). On Windows the extractor accepted absolute paths, drive and network paths, alternate data streams, device names and trailing dots, so a hostile package could write outside the install; a bad entry size or a non-UTF-8 name crashed the launcher; and quitting mid-install left a half-updated game. Now only plain relative names are accepted (fuzzing also found
.,a/.anddir/slipping through, now refused), sizes are checked before anything is allocated, and the update unpacks intogame.newand is swapped in, with the old install restored if the swap fails. - Downloads go through TLS 1.2 / 1.3 and start programs without a shell (see Security).
Discord bot
- No more mass pings from server names (BOT-1). Server info was posted with no mention settings, and the bot's invite granted Mention Everyone, so a game server named "@everyone" pinged the whole Discord, once per new port. Mentions are now off by default; only the bot's own role pings, queue fills and the welcome opt in.
- Names can't inject links or formatting (BOT-2). Server, map, mode and player names were inserted as-is, so masked phishing links rendered in ping messages and leaderboard names could break out of their code block. Names are now made inert: one line, links replaced with
[link],@defused, markdown escaped. - At most 5 live servers get channels (BOT-3, owner 7: "limit discord listed servers to 5"). One text and one voice channel per listed server, up to 200, while a category holds 50, and one failed channel creation stopped every update. Now at most 5 are listed (listed ones keep their place; free slots go to the busiest new servers after 120 s of answers), and each server is handled on its own: a failure is logged and retried.
- Testing builds get their own channel and role (owner 2.1). Prereleases are announced in #testing-updates with a "Testing Updates" ping role (a toggle in #get-pings); production stays in #updates. A prerelease is never announced as production.
/changelogunderstands testing builds (review 3.8). Testing builds are taggedtest-1.17.0-test.N;/changelog 1.17.0-test.3now finds that build (else the base version's changelog), marked "(testing)".- #updates posts and
/changeloglink this website's full changelog page. - The bot only manages its own roles (BOT-5). Manage Roles is used only on its six configured roles (four ping roles, #1 and Top 10), and it refuses any role granting more than @everyone or sitting above its own. Roles it creates have no permissions. The invite needs fewer permissions (268528656, no Mention Everyone).
- Secrets stay off command lines (BOT-4). The installer passed the bot token and key as command arguments (visible in process lists and sudo logs). Now they go in
/etc/quark-discord.env, mode 600, owned by the service user; the service runs with basic systemd hardening; and the installer reminds you to rotate both. - Pinned dependencies: installed with exact hashes from a lock file, and the unhashed pip self-upgrade is gone.
- The bot uses the game's built-in ranking-server address and never follows
master.txt, so its key can't be sent elsewhere; if the ranking server ever moves, setmaster_urlin its config. It has its own rate-limit bucket on the ranking server. - Ready to move to its own branch (owner 7): no paths into the game repository, and
tools/discordbot/MOVING.mdhas the steps. Its test suite grew from 19 tests to over 30. - Discord links now use one-time codes (see Ranked), and server-list queries are padded (see Security).
Website
- An updates page:
/updates/lists every version's short changelog, newest first, and each version has its own page with its full changelog (this page, for 1.17.0 on; older versions show their short notes). The home page links to them. - Signed file list: the website publishes a signed
SHA256SUMSof every site file, and says how to check downloads. - Credits for the Work Sans font.
- No
eval()in the website build (SUP-4). The step that reads the weapon table ran Python'seval()on game source text with a write token available; it now does the arithmetic safely. - The website is only redeployed by a production release (testing builds only replace the testing channel's asset data, below), and owner replacements for website pictures are copied in at publish time.
- Assets and Sounds pages (
/assets/,/sounds/, linked from the menu and the home page). What: every model you can see in 3D (the 5 player models, 10 weapon viewmodels, 7 projectiles, the crown and all 21 maps; spin, zoom, pan, team colour), and every sound you can hear (150 kit sounds, 210 announcer lines, each with a waveform and a play button), with a PRODUCTION / TESTING switch. Each has a make your own mode: a box builder that exports a .glb (boxes marked team colour go out white in ateammaterial, so the game tints them) and a small sfxr-style synth that saves a 48 kHz mono .wav, both saying where the file goes (overrides/in your user folder, orassets/overrides/in a checkout). Why: players asked what the kits and models look and sound like before picking one, and the override folders were only described in docs/ASSETS.md; now anyone can try a replacement in a few minutes without tools. How: the data is the Asset Studio's own export (quark_tests exportassets,tools/studio/build.py), cut to models, maps, sounds and voices bytools/site_assets.py(about 33 MB: the site's pictures, the font and the icon aren't repeated); the viewer, map reader and waveform code are the studio's. The Linux build exports it; the pages job publishes it asassets/data/production/and keeps the testing data already on the site, and each testing build replacesassets/data/testing/on gh-pages, re-signsSHA256SUMSover the whole site and leaves gh-pages one commit, so the branch doesn't grow ~33 MB per testing build. Both jobs rewriting gh-pages run one at a time. Files are decoded only when their card scrolls into view, and only three.js (0.147.0, already used by the studio) is loaded, on the assets page alone. A channel with no data yet says so instead of breaking. Generated data is never committed.
Release process and tests
Testing and production releases (owner 1.1, 2.2; SUP-2, SUP-3)
- Why: every push to any branch published a release marked "latest" and redeployed the website, with no concurrency control. v0.26.1 and v0.26.3 shipped files built from other commits, and a stray push of an older branch would have downgraded every launcher.
- Now:
- New work goes to a
testingbranch. Each push there publishes a signed prerelease taggedtest-<VERSION>-test.<N>, built as version<VERSION>-test.<N>(never "latest"; the website isn't touched). The tag never starts withvand a digit, or older launchers (0.12-1.16) would install it. macOS is optional there and added when it builds (owner 2.4: "testing shouldnt have this requirement"). - Production comes only from a tag
v<VERSION>on the protected default branch, which only takes pull requests fromtesting. It needs Windows, Linux and macOS all green (owner 2.4: "release version should always force all of the os options"), fails if the release already exists, and needs both changelogs. - Other branches only build and test.
- New work goes to a
- CI hardening (SUP-3): read-only tokens by default, write access only for the release and website jobs; no credentials left in checkouts; every GitHub Action pinned by commit hash and kept current by Dependabot; one run at a time per branch; separate production and testing environments.
- A hand-test checklist (
docs/RELEASE_CHECKLIST.md) the owner goes through on each testing build before it goes to production. - Every update now writes two changelogs: the short notes and this full one; a production tag without either fails.
Tests
- Tests run on Windows in CI (ENG-1, owner 2.7). The suite couldn't pass on Windows, the main player platform: the 15 ms sleep (see Performance) made it time out with 26 failures. Fixed with the sleep fix, a 16 MB test stack, unbuffered output and a real-time test clock on Windows. The test job gets the generated maps too (review 1.1, a release blocker: without them every map test failed and production could never publish).
- Sanitizers and fuzzing (ENG-3). New build options run the whole suite under AddressSanitizer and UndefinedBehaviorSanitizer, and five fuzzers throw random data at packet decoding, map loading, custom modes, the package reader and a live server. A CI job runs them on every work-branch push. They found two bugs, both fixed: package entry names that name a folder, and blank names in a custom mode's map list.
- Many new tests for what's in this update: ranked rules and the ranking server (
test.mjs), hidden information, hide and seek scoring and the tiebreak, overtime, throws, spread, projectile arcs at every tick rate, hull scaling, map limits, spawn pools, walkable routes, the cart sweep, the launcher (with a GitHub releases fixture), signatures, demo conversion, map conversion, Unicode paths, allocation failures and more. - Tests checking the code against itself: the queue sizes, protocol numbers, mode names, the ranking server's address and race split floors must match across the game, the ranking server and the bot.
- CI runs every test group on its own machine. The release waits for the tests, and Windows can only run them one group after another (no fork), so a release would have waited roughly an hour for the Windows tests. Now each of the 25 groups runs on its own Linux runner and its own Windows runner at the same time (
quark_tests groupslists them;quark_tests group NAMEruns exactly one). The slowest group sets the time instead of the sum of all of them. The timing-sensitive groups (netcode, tick rate, governor) also get a machine to themselves, which is what they need to measure reliably. The list comes from the source and the Linux build checks it matches the binary, so a new group can't be skipped. A full local run now prints each group's time. The asset tests also find the source tree on the Windows runner now (they used the build machine's path, which doesn't exist there). 1.17.0 itself was released without the CI tests, on a local test run of every group except the bot and map groups (both the owner's one-off calls for this release: bots and maps are being replaced; every map was instead loaded and played headless). From 1.17.1 on, every release waits for all of them in CI. - Steadier tests (ENG-5): each run and each test group gets its own data folder and its own block of ports (outside the system's temporary port range), fixed waits became "wait until done", and a flaky ranked-vote check waits for the map switch.
The click-every-button UI test
quark --uitestclicks every button in the game, in every situation it shows up in, and checks each one does what it says (owner: "click every button"). Every widget call now takes an expectation, a required argument: go to this screen, change this setting, send this command, edit the map (and undo must put it back exactly), capture a key, open a link, or nothing. A widget can't be added without one, and the test fails on a widget nobody described. In normal play it costs one null check.- It walks every reachable screen from 11 starting points (the menus with the offline editor, hosting FFA, VS, ranked and tourney, a spectating client, a demo, an online edit session as host and as guest, and hide and seek as seeker and in the rock-paper-scissors tiebreak), activates every widget the way a player would (a slider is dragged, then a number typed, then right-clicked back to its default), checks Esc from every screen goes back the way you came, and checks the layout at 1280x720, 1920x1080 and 1024x768: nothing off screen, overlapping or cut off.
- The last full run: 2,259 widgets on 109 screens, 113 Esc checks, all passing, in about 16 minutes. It runs with its own data folder and the network off, so your settings are never touched.
- What it found, all fixed:
- The HUD widget editor opened from the main menu crashed the game: it read the newest snapshot when there was none.
- Unbinding the knife, syringe or quip key (or chat on T / Y) came back after a restart, because the old-config migrations ran on every load. They now run once (
bind_version=2). - Editor:
- ROTATE and MIRROR ran off the bottom of the panel at 720p, and MIRROR Y's label was cut off.
- CTRL+ARROW on a box thinner than the grid grew it.
- Rotating or mirroring several objects took an undo step per object.
- GROUP on a map's only box failed.
- In an online session the first edit lost its undo step.
- Settings labels were cut off at 720p (HITSOUND, VOICE, PLAYER SHADOWS, the HUD rule MODE, and the custom mode's MOVEMENT and WEAPONS sliders); those grids use the screen width now.
- PRESET from custom colours skipped the first preset, and CHECK AGAIN (reachability) showed nothing while re-checking.
- Esc on "save your copy?" did nothing.
- Buttons that can't do anything are now greyed out: CLEAR ALL, RESET, UNBIND, QUEUE FOR ALL / LEAVE ALL, GROUP and EXPORT GROUP with nothing to group, DUPLICATE on the hill, and others.
- In CI the UI test is the
linux-uitestjob, which every release waits for from 1.17.1 on (1.17.0 ships on the local run).
Settings from the UI work
- Type a number into any slider: click its number and type, on every slider in the game, including the custom-mode MOVEMENT page (owner). A typed value isn't snapped to the slider's steps, only kept in range.
- Right-click a slider to reset it to its default (owner): the default settings for settings, the standard rules for a custom mode.
- Damage-number size (owner), the damage flash at the screen's edges has a strength setting and is off by default (the owner's call before release; turn it on in VISUALS), zoom FOV sliders for the sniper's and the rifle's alt-fire (owner), and the fullbright map colour when flat fullbright textures are on (owner).
Build
- The MSVC build is gone (ENG-4, owner 2.7). It didn't compile; releases are built with MinGW, which is now the only Windows toolchain.
- A version bump recompiles one file instead of everything (the version lives in a generated file).
- The 20 MB announcer header is generated at build time from the recorded voice files instead of being stored in the repository. The sound-effects header stays committed (C6: building it would need ffmpeg).
- Precise floating point everywhere: MSVC's fast-math was dropped and compilers may not fuse multiply-adds, which would make Apple ARM machines compute movement differently.
- CTest support, stricter compiler warnings (
-Wextra), and an LTO option, left off (C5: it measured no better here and "needs a real pc to test"). - Two old compiler warnings fixed: an unused variable, and a map-report text buffer that could cut race lines short.
Project documents
- New documents in the repository record this update's work: the second review (
docs/REVIEW.md) and its fix plan and log (docs/FIXES_HANDOFF.md), the owner's decisions (docs/OWNER_DECISIONS.md,docs/OWNER_ANSWERS.md,docs/OWNER_DECISIONS_2.md,docs/JUDGMENT_CALLS.md), the final review with each finding's status (FINAL_REVIEW.md), what's left (REMAINING.md), a handover (docs/HANDOVER.md), signing (docs/SIGNING.md), assets (docs/ASSETS.md) and the release checklist.
Asset Studio
The owner is replacing AI-made assets with human-made ones through the Asset Studio page.
- Every default asset can be exported:
quark_tests exportassets <dir>writes 135 sound-kit sounds, 210 announcer lines (lines a voice hasn't recorded are exported as short silence, marked "not recorded yet"), 24 glTF models (players, weapon viewmodels including the new locator, projectiles, the crown), every shipped map, the font, the icon and the website's pictures, with a manifest. The game's box models moved into one file that both the game and the export use; the game draws them pixel-identically. - Replacements: a file at an asset's path in
assets/overrides/(shipped, built into the game) or inoverrides/in your user folder (read at start, wins) replaces it.- Sounds and voices:
.wav,.ogg,.flac,.mp3. Leading silence is trimmed and each plays as loud as the sound it replaces. - Models:
.glbor.gltf, drawn in place of the box model (up to 20,000 triangles; materials namedteam,glow,metalorbasefollow team or map colours). - The font (a static
.ttfor.otf): the text atlas is rebuilt at start only when it's replaced. - The icon, maps (they must still pass the map checks; a per-user map replacement isn't accepted for ranked) and website pictures (same file name and type).
- With no replacements, nothing is read or drawn differently.
- Sounds and voices:
- Decoders for these formats are vendored public-domain libraries (stb, dr_libs). Everything is documented in
docs/ASSETS.md. - The studio page's font upload takes
.ttf/.otfonly (it mentioned a pixel-font PNG, which no longer exists). - No owner replacements were waiting at release, so 1.17.0 ships the default assets. The studio page is rebuilt and republished after the release with the new defaults (the locator model, the new map hashes).
Known issues / not in this version
- Hidden information has a 20 m radius: players within 20 m are always sent, without a line-of-sight check, so a hide-and-seek seeker is still sent hiders behind a wall within 20 m (NET-10). Whether hide and seek gets a smaller radius is open.
- Midtown and Lodestone have about 25-30% more boxes (cover added to pass the stricter grading); their frame times haven't been checked in game yet.
- The two-servers-per-box setup hasn't run on a real server yet: the systemd units, the shared governor folder's permissions and the CPU weighting were tested only by script, not under systemd. And one player reporting heavy snapshot loss counts as overload, as it already did for the tick-rate fallback (closing testing still needs production itself to be overloaded).
- Knives-only modes: a thrown knife is disabled for 4 s, so in a knives-only mode you can't swing for 4 s after throwing (before, you could). This is the owner's rule read literally; it may change.
- Expert bots measure 16.7 rifle damage a second against a strafing target, a little under the 18 target.
- Bots still fall about 3 times per full playtest run when an enemy's laser or rocket pushes them (mostly Dojo), occasionally fall off Hightown's roof, kill themselves slightly more often (8 → 10 over 42 runs, mostly rockets), and grapple less on Clockwork (0-9 times per 40 s, was 14-27) now that every pull is checked.
- Unicode folder names are fixed for the game but untested on real Windows, and the dedicated server still reads its own command-line arguments in the ANSI code page.
- Giants: the server's line-of-sight rays aim at a normal-size player, not a giant's bigger box.
- Old launchers (1.16 and older) only read the newest 50 releases. Production 1.17.0 must ship before 50 testing builds pile up; after that, the game keeps launchers current.
- Hosting reachability needs at least one public server running 1.17.0 to answer the check; until then hosts may see "not reachable" even with an open port.
- The ranking server's read cache does nothing on its current
workers.devaddress; it needs a custom domain. - Generating the maps takes about 6 s instead of 0.7 s, because of the spawn-spot search.
- Not in this version, planned: the owner's own advanced map editor (a separate developer build, uploading maps to a
map-testsbranch; B3), one GameMode interface per mode (C4), LTO after measuring on a real machine (C5), moving the launcher, website and Discord bot to their own branches, a map size budget and an entity system for objective markers. - No licence file yet, by the owner's choice.