Download

← All updates

Update

quark 1.19.3

Everything that changed in this version, and why.

1.19.3 is the production release after 1.19.1: the testing lines 1.19.1.<N> and 1.19.2.<N> (1.19.2 never shipped on its own; owner, 2026-10-29: "push all into testing for 1.19.3") promoted as they stand. Every change below was made, explained and built in one of those testing builds. The short version is on the updates page.

  • Network protocol 53 (1.19.1 used 47: 48 wedges and corners, materials 20-35, glass and blown-open brick; 49 plain vs breakable brick / cobblestone / glass and materials 36-138; 50 materials 139-471; 51 liquids; 52 see-through looks; 53 breakable and feature blocks). A 1.19.3 game only plays with a 1.19.3 server, so update games and servers together.
  • Map format 10 (1.19.1 wrote format 4: 5 wedges, corners, tetras and cut boxes; 6 plain vs breakable materials; 7 materials 139-471, new keywords only, so the 6 to 7 step changes nothing; 8 liquids and large patterns; 9 see-through looks; 10 breakable and feature blocks, a teleporter's tp <link> <face>). Older maps convert on load, and players' own map files are converted on disk with the original kept in maps/old/v<N>/.
  • Human-made assets: 16.1% (no asset changes since 1.19.1).

Performance

  • PRIORITIZE GAME (Settings > VIDEO > LATENCY, Windows, prioritize=1 by default). Why: the owner's F3 with YouTube and Electron apps open (2026-10-28, desktop, Hamlet, 2560x1440, 240 Hz): about 400 us a frame, of which the game's own CPU work was about 100 us (build 42, draw calls 50, swap 5) and its GPU work 102 us, but it waited 294 us for the GPU (LOW LATENCY ON): about 190 us of every frame went to other programs' GPU work queued ahead of the game's. What: while the window has focus the process's GPU scheduling class is raised to HIGH with gdi32's D3DKMTSetProcessSchedulingPriorityClass (ABOVE_NORMAL if Windows refuses HIGH; REALTIME, which needs administrator, is never asked for), so Windows' GPU scheduler runs the game's work ahead of a NORMAL process's; and the CPU priority class goes to ABOVE_NORMAL (not HIGH: at thousands of frames a second a HIGH process can starve input, voice chat and the launcher; the hosted server and audio threads are raised on their own already). Losing focus (alt-tab, minimise) or turning it off puts both back to NORMAL. The call is skipped when nothing changes, so focus changes cost nothing. Trade-off: other programs' drawing (a video, an overlay) can stutter while the game is in front; the setting turns it off. Works best with Windows' "Hardware-accelerated GPU scheduling" on. Not in the UI test (it would change the test machine's priorities). Linux and macOS: no setting, nothing changes. Follow-up (owner, 1.19.1.2: "it takes cpu priority not gpu"; 1% lows 4000 with a YouTube video paused, 1200 playing, single-monitor exclusive fullscreen): Windows shows only the CPU class, and the game ignored the GPU call's result, so F3 now has a line with the GPU scheduling class Windows reports for the game (D3DKMTGetProcessSchedulingPriorityClass), the refusal's NTSTATUS if the last set was refused (also printed to the log), and the CPU class; and the focus state is checked again twice a second while F3 / F4 runs (a window already focused when its focus callback was set never got the call).
  • Hole grid for blown-open walls (renderer.cpp setHoles, kWorldFS inHole). Why: owner, 1.19.1.7, 2026-10-29: "when you break a brick wall and all of the bricks go away, the fps is still cut in like 1/10"; F3 looking into a brick / cobblestone room: 120 fps, GPU 7414 us (CPU world build 112 us), elsewhere 1770 fps, GPU 314 us. Every fragment of thin brick (20) and cobblestone (24) looped over all of Map::holes (up to 512, two texel fetches each), and holes stay listed until the round resets, including a fallen wall's (its whole-wall entry hides the slab, and the cell holes punched earlier stay too), so the cost grew with every shot for the rest of the round. What: setHoles also builds a grid over the holes' xz footprints (cells of at least 1 m, at most 64 x 64; RG32F texture: start and count per cell, unit 3) and a list of hole indices (R32F 256 x 64, unit 4); the shader looks up its own cell and tests only those holes. A fragment far from any hole now does one fetch and stops; one at a hole tests the few holes around it. If the references would pass 16384 (huge holes over a huge map), it falls back to one cell listing every hole (the old cost). Same hole test as before, so what's cut is unchanged. Trade-off: two more small textures (32 KB + 64 KB) and a CPU rebuild of the grid when the holes change (at most 512 holes: microseconds).

Maps: wedges and corners (map format 5, protocol 48)

  • Owner, 2026-10-28: "would it be possible to have set angle pieces like a 45 degree wedge? ... the wedge piece should just be the angle that is defined by its bounding box so you can have any angle, also ... a corner piece (1 0 0 0, instead of 1 1 0 0)". Brush gains shape (box / wedge / corner) and dir; file lines wedge|corner <material> <dir> x0 y0 z0 x1 y1 z1 (dir: a wedge rises toward +x, -x, +z, -z as stairs do; a corner's apex is above corner -x-z, +x-z, +x+z, -x+z). Both are heightfields inside their box: top = y0 + h * g, g the fraction toward the high side (a corner: the smaller of its two fractions), so any angle the box gives. Format 4 -> 5 converter (no change: a version 4 file has none), so every old map loads as it was; an older build skips the lines with a warning. Edit ops carry them (b ... w|c <dir>); quark_maptool's JSON has shape / dir on a brushes entry.
  • Collision (map.h brushSolidAbove): what of a slope is solid above a height is a box too (the heightfield falls away from the high side), so sideways sweeps clip against that box at the player's feet, falling stops on the slope's highest point under the feet (feet are flat), and rising hits its flat bottom. boxSolid, every raycast (rayBrush: the box's slabs cut by the one or two top planes, with the plane's normal) and the light bake's inside test follow the real shape.
  • Movement (pmove): walking up a slope is the existing step-up (each tick's rise is a sliver of the 0.55 m step), and walking down is the existing step-down snap, so a slope no steeper than 45 degrees (rise <= run) is floor with no hops; tested at 27 degrees up and down without leaving the ground. Steeper than 45 degrees is a wall: never stepped up, never floor; landing on one turns the fall it stops into a slide downhill along it and keeps only the velocity along the face (tested at 76 degrees). The dash wall bounce sees a steep slope as a wall. The 45 degree line is our call (owner can move it).
  • Bots (bots.cpp): a walkable slope is floor cut into strips (a corner into cells) at most 0.25 m of rise each, each a surface at its highest point, the way stairs are; as a blocker only the part above a step counts. Steeper ones aren't floor.
  • Light and outlines (lightmap.cpp): a slope's faces are convex polygons (its sloped top or two triangles, triangle sides, its bottom and its full-height side as ordinary rectangles), each with its own rectangle of the atlas; each polygon edge's outline distance is a linear function over the face, so the existing four-distance vertex attribute draws a triangle's or quad's edges exactly. Polygons that share a whole edge in one plane (wedges side by side, same group or material by the seam rule) have no line there. Slopes never cull other faces (only boxes bury faces). The editor's unbaked view draws them the same way (Renderer::slope).
  • Upside down and on its side (owner, 2026-10-28: "you should be able to Y flip edges, corners, and be able to have the slope go from x > x instead of only y > y"): Brush::up 0-5 turns the slope's own frame (u, h, v): 0 floor (x, y, z), 1 hanging from the ceiling (h mirrored), 2 / 3 on its side rising toward +x / -x (y, x, z), 4 / 5 toward +z / -z (x, z, y); dir keeps its meaning in that frame. Stored in the same file / edit-op number, dir + 4 * up (0-23: old 0-3 lines unchanged, still format 5: no version has shipped it), JSON up. Everything works on the own frame and turns the result back (map.h slopeFrame, slopeLocal, brushObstacle for the sweep, planes, polygons, the light bake's faces), so collision stays exact for any of them. The editor's rotate (X / Y / Z) and mirror (X / Y / Z) now turn slopes any way: slopeTurned tries the 24 dir / up pairs on 27 sample points off every slope surface and takes the first whose solid matches. Bots stand on an upside-down one's flat top; one on its side is no floor to them.
  • Wall faces and dashes (owner, 2026-10-28: "if you dash into a slope that is oriented where the slope pushes you upwards, it should push you up it, even if the angle is >45 / same with downwards slopes, and corners"): a slope face is a floor up to 45 degrees, otherwise a wall (steep, on its side, facing down). pmove finds the face it touched from the face's plane (slopeContact, the box's deepest corner on it within 1 cm), not from the brush, so landing on an upside-down slope's flat top is floor. A wall face no longer blocks the axis: what it stops goes on along it (on the axes still to come), and the velocity loses only its part into the face, never gaining height from it (running at a steep slope doesn't climb it). A dash turns its direction along the face, keeping its speed: up a slope facing you, down one facing down, along a sideways wall; it doesn't wall-bounce off a slope.
  • Editor: N cycles a selected box through box, wedge x4, corner x4; rotate and mirror turn it with the piece (any axis, see above). Wedges and corners go into groups too (below).
  • Protocol 48: every pmove now collides with slopes, so an older client would mispredict on a map with them; the map text and the edit ops carry them. Official maps now say version 5 (tools/maps/prefabs.py): their text, and so their ids, change.

Maps: outlines (seams)

  • Partly covered edges (owner, 2026-10-28, "connected faces of the same material not having an edge line through it seems to work very rarely"): the bake hid an edge's outline only when all 7 samples just past it landed on a flush neighbour, so an edge only partly against one (a taller stair step beside a shorter one, a wall meeting a longer wall, a T-junction) kept its line along its whole length, drawn across the neighbour. Each face is now split where its flush neighbours' edges cross its border, into a grid of pieces (at most 1024 a face, else whole edges as before), and each border piece keeps or drops its outline on its own; edges between a face's own pieces have none. Lighting is unchanged (the pieces share the face's lightmap rect), and the light cache holds only the atlas, so no bake-version bump. Flush now means within 1 mm (was 0.1 mm): a group's parts are stored as fractions of its box and land a hair off each other's plane. Cost: Sewer goes from 2445 quads to 3631, drawn in the same chunks.
  • Seams follow groups in hand-made maps (owner, same day: "the only time a seam should appear, is when things are part of different groups; even if they are different materials, if they are the same group no seam, if they are not grouped, seams"): in a handmade map, flush faces join only when both are parts of the same group, whatever their materials; ungrouped boxes and different groups keep their outlines. Generated maps (make_maps.py) have no groups, so they keep joining same-material faces (else every multi-box wall on the official maps would get lines). Brush::group records the group a generated part came from (not saved, no format change).
  • Slopes in groups (owner, 2026-10-28: "you should be able to group slopes and blocks for seam removal purposes"): a custom group's part can be a wedge or corner: part <material> <6 fractions> wedge|corner <dir + 4 * up> in the file (still format 5: nothing released reads 5 yet), g part ... w|c <n> as an edit, shape / dir / up on a maptool JSON part, w|c <n> after a group file's line. The slope sits in its part's box (fractions of the group's), so stretching the group stretches it; rotating or mirroring the group turns each slope part the way a lone one turns (slopeTurned, about the box's middle). Grouping a selection keeps its slopes (they were turned into boxes before: a bug); ungrouping gives them back as slopes.
  • Slope seams (lightmap.cpp): a slope's polygon that lies in an axis plane (a side triangle or trapezoid) is now found with the box faces in that plane, so in a group a box face's edge against a slope's side and the slope side's edge against the box both lose their outline; a polygon's edge is now off when five points just outside it all lie in joining faces (box faces or other slopes' polygons, flush in its plane), not only when another polygon has exactly that edge, so wedges of different lengths side by side join along the part they share. The crease where a slope meets a flat face keeps its line (not one plane). Tested: quark_tests groupslopes.

Maps: 25 liquids (472-496; map format 8, protocol 51; owner, 2026-10-29: round 6 picks, "all 25 liquids")

  • Why: q picked all 25 liquids on the special blocks page. They need behaviour, not just a look, so each is a LiquidDef (map.h kLiquids): speed and jump multipliers, grip (oil 0.08), gravity (mercury 1.6, quicksand 0.5, deep ocean 0.6), drag per second (tar 4, quicksand 5, wet cement 3, honey 2, swamp 1), a current along +x (white rapids 12 m/s), damage or healing per second as a share of max health (toxic 5%, plasma 20%, hot spring heals 5%), kills (molten gold, magma, void: they join the lava list via lavaMaterial), and bounce (pink slime).
  • Not solid (solidMaterial leaves them out): you wade in like water. Map::rebuildFluids (called by buildGrid and setBrush) keeps the lava, acid and liquid lists; Map::liquidAt is the first liquid a box touches.
  • pmove: speed and jump scale while in one; grip scales the ground friction and push; gravity scales the fall; at the end of the step drag (exp(-drag dt)), the current (kLiquidCurrentAccel 60 m/s2 toward its speed: against the floor's friction a player standing in it settles at about 2/3 of it, 8.6 m/s), the bounce (a fall over 2 m/s comes back at 85%) and damage / healing through acidTaken (the server's takeAcid heals when it's negative, up to max health). So all of it is predicted like acid.
  • Map format 8 (the converter 7 to 8 is empty: only new keywords), protocol 51 (MoveState's meaning changes with the new materials).
  • Editor: a LIQUIDS tab; each liquid's flat colour from its preview render.
  • Tests: testLiquids (group movement): honey halves your speed (4.7 vs 9.5 m/s), oil keeps you sliding over twice as fast, the rapids carry you over 7 m in a second, slime bounces a fall, a hot spring heals and plasma takes 20% a second, molten gold kills, and you can wade into them.

Map editor: the side panel fits with a selection

  • With something selected, the MODES chips (four rows) fold into one line, "MODES: N OF 14 (CLEAR THE SELECTION TO CHANGE)", and come back with nothing selected. A selected box's rows (material, ROTATE, MIRROR, SPLIT / COMBINE / SUBTRACT) ran off the bottom of the screen at every size since SPLIT / COMBINE / SUBTRACT were added; the UI test's layout pass caught it (24 off-screen buttons, now 0).

HUD: F3 cycles four stages (owner, 2026-10-29: "f3 should cycle between just fps values, fps + ping + server values, full graph, none")

  • perfStage_ replaces the on/off showPerf_. 1 is the AVG / 1% LOW / 0.1% LOW line alone; 2 adds WORST / PING / 3D size and the version / SERVER HZ / LOSS line; 3 is the old F3 (input-to-frame latency, the PRIORITIZE GAME line on Windows, the mouse-to-photon pipeline graph); the next press turns it off. Only stage 3 (or F4) runs the pipeline graph's and input-to-frame measuring (perfOn(): GPU timestamps and fences each frame), so stages 1 and 2 cost only their text, which matters because the overlay is how 1% and 0.1% lows get judged. The frame-time percentiles still come from the same buffer at every stage. F4 is unchanged.
  • UI test: the PERF OVERLAY key checks the stage changes on each press (perfstage).

Chat over the in-game settings menu (owner, 2026-10-29: "show chat/allow chatting while in settings menu")

  • With the settings open in a game (every tab, the HUD tab's preview too) the chat's recent lines are drawn over the menu at the HUD chat's place, on a dark box so the menu's text doesn't show through (drawMenuChat, chatOverMenu_). The chat key (Enter) opens the SAY line there as in play: Enter sends, Esc closes the chat and leaves the menu open. While typing, the HUD tab's on-screen editor ignores the keys. Pause menu keys are unchanged (it still swallows the chat key outside the settings).

Movement: gravity 22 -> 24 (owner, 2026-10-29: "increase base gravity to 24")

  • kGravity 24 (was 22), so falls and arcs are about 9% heavier and less floaty. kJumpSpeed 7.34 -> 7.67 m/s to keep the 1.22 m standing jump (h = v^2 / 2g), because every map's ledges, crates and stairs are measured against it. The trade-off is that time in the air drops about 4%: a standing jump is 632 ms (was ~660), a running jump reaches 6.0 m (was ~6.3), a dash jump 15.1 m (was ~16), and a straight-down super dash hops 2.04 m (was 2.23). The grapple, dashes and air control are unchanged.
  • Bots' jump and gap planning, the bomb's fall, the test walk grid and quark_maptool's reach checks and physics table now read the base gravity and jump (moveBaseGravity / moveBaseJump) instead of copies of 22 and 7.34. Glass shards and client debris keep their own 22 (looks only).
  • Tests: the jump, dash-jump and super-dash ranges were moved to the new arcs; the rubber drop counts the bounce from the first rising tick (at 24 the landing tick can end above 5 cm).

Modes: free for all takes the 1v1 rulesets (owner, 2026-10-29: "FFA should have rulesets like 1v1s")

  • modeHasRuleset (1v1, tourney, free for all): the mode pickers (host, vs bots, in game) go on to the ruleset step for free for all too, a map change or vote sends the ruleset, and the vote reads "PLAY <map> - FREE FOR ALL (CLASSIC)". The server applies the ruleset's weapons and AR_* rules (Server::rulesetActive) in free for all unless it's vanilla: vanilla free for all stays exactly as before (every weapon, the syringe too, which the 1v1 vanilla set leaves out), so nothing changes for anyone who doesn't pick one. Ranked free for all (instagib) is unchanged. The HUD title says "FREE FOR ALL - <RULESET>" for a non-vanilla one.
  • Test (group syringe): free for all on vanilla keeps the syringe and no rules; on classic only rocket, laser and knife; on moon shotgun the shotgun and low gravity.

Glass: one pane's shards hurt a player 50 at most (owner, 2026-10-29: "limit damage per broken glass, 50 per glass")

  • kGlassPaneMaxDamage 50: each flying shard remembers its pane, and the server counts what each pane's shards did to each player (paneTaken_, cleared with the rest of the destruction). A shard that would go over 50 does only what's left; once a player has taken 50 from a pane, its other shards break on them harmlessly. Both modes (SHARD's 10s, SHATTER's 1s). Dashing or being pulled through a pane still costs its flat 10 on top. Test (group materials): eight more of a pane's 10-damage shards into a player who already took 50 from it do nothing.

Movement: A/D strafing eases off more gently at speed, turns harder (owner, 2026-10-29)

  • "in surf gamemode you should lessen the speed limit curve so you can go very fast, also in game you should decrease the penalty on a/d strafing on high speeds, but keep the cap the same, maybe a curve". Strafing adds about the same v squared each tick at the best angle whatever your speed, so speed grows as 1 / speed. Above kStrafeEaseFrom 15 m/s the A/D acceleration is now x (speed / 15)^kStrafeEasePower 0.35, so it falls off as 1 / speed^0.65; in surf (MOVE_RUN_ONLY) the power is kStrafeEasePowerSurf 0.75 (1 / speed^0.25). The cap (2.53, the window of angles that gain) is unchanged, and so is everything under 15 m/s. A perfect strafer in the air from 15 m/s (test strafe ease): 41.4 -> 46.7 m/s after 5 s and 71.2 -> 88.2 after 15 s; surf 56.0 and 123.0.
  • "increase ad strafing air control, but without increasing the velocity acceleration": an A/D push now also turns your momentum by kAirControlAD 0.5 of the angle its sideways part makes (x the mode's air control / 4), in place: the speed the push gives is the same, only the direction goes further. Not while overstrafing (a push against your momentum), so turning far too fast still costs what it did (720 deg/s: 6.7 m/s after 2 s, was 6.3). At a fixed, slow mouse turn your momentum now keeps up with your view better, so you gain a little less there (90 deg/s for 4 s: 23.6 m/s, was 25.3) and must turn faster to gain; at 360 deg/s, 13.7 (was 10.7).

Maps: 10 feature blocks (586-595; map format 10, protocol 53; owner, 2026-10-29: round 6's 9 feature picks, then "also add a teleporter surface")

  • Underfoot (pmove's floor probe; maps with one probe the floor like ice): the healing tile gives back kHealTilePerSec 10% of max health a second and the spike floor takes kSpikePerSec 20%. Both go through acid's path (MoveState::acidTaken: predicted at once, taken by the server, the death the server's), so they're the same at every tick rate. The spikes hurt continuously rather than in once-a-second hits, so prediction and the server always agree.
  • Fields (not solid, drawn in the see-through pass; Map::fields, asked only on a map with any): the low gravity field multiplies gravity by kLowGravField 0.5 (the test's standing jump: 1.22 m, 2.45 m in it). The gravity flip tile is solid; the column kGravFlipHeight 64 m above it turns gravity upside down. You fall up, the ceiling becomes your floor (ground, friction, jumps go down), and walking out of the column drops you back. The camera stays upright. The slow-mo field: projectiles in it take half steps (Map::projectileRate, in Server::stepProjectile, rocket riding's anchor and every client extrapolation through Map::flyProjectile, which uses the closed form when the map has no slow-mo field, so other maps cost nothing).
  • Checkpoint gate: Map::raceCheckpoints is the map's checkpoints, then the gate brushes in brush order. The race, bots, ranked and the HUD use it; the file and the editor's checkpoint list keep only the map's own. It's rebuilt with the grid and on checkpoint edits. A map with gates and a race spawn supports race. Taking a gate throws green sparks through it.
  • Light block: glows in its look's cycling colour and is a light in the bake (8 m plus twice its size, shadowed by the world up to the block's own face; bake version 13). The lightmap holds brightness only, so the room's light is white; the block itself carries the colour. A colour picked per block in the editor needs a per-brush colour field, which isn't built.
  • Shield wall: in the collision grid, but stopsPlayers and stopsSight say no and stopsShots yes. Bullets, rockets and grapples stop on it; players and line of sight go through.
  • Crumble platform: drawn from the breakable blocks' own mesh. The server starts it when someone stands on its top (DESTRUCT_SHAKE: clients shake dust off its underside, with a creak), drops it kCrumbleS 1 s later (a DESTRUCT_GLASS: chunks), and brings it back kCrumbleBackS 5 s after that, once nobody's inside it (DESTRUCT_RESTORE, Map::restoreBrush). Joiners get fallen ones in the destruction list; a round reset brings all back.
  • Teleporter (solid): each brush has a link 0-255 and an exit face (+X, -X, +Y, -Y, +Z, -Z). In the file it goes after the box, written only for a teleporter: teleporter x0 y0 z0 x1 y1 z1 tp <link> <face> (face 0-5 in that order; slopes and cut boxes put it after their box too), and in an edit: p <link> <face> after the shape. Players (in pmove, so predicted) and projectiles (on the server) that touch one leave the next teleporter with the same link in brush order (round to the first) from the middle of its exit face, just outside it, at their speed, all of it pointing straight out of that face. A player whose spot there is blocked doesn't go. The editor: LINK slider and EXIT FACE cycle under the material. The map editor's helper (quark_maptool parse / serialize) carries them as link and exitFace on a teleporter's brush, so Quarkology keeps them through a save.
  • Tests (groups movement, materials): heal and spikes over 1 s, a jump twice as high in low gravity, standing on the ceiling over a flip tile, the shield's flags and a ray stopping on it, a gate making race playable, slow-mo halving a rocket's flight on the server and in flyProjectile, a player and a rocket through a teleporter pair, the link and face through a save, a load and an edit, and the crumble platform falling and coming back on the server's clock.

Maps: 24 breakable blocks (562-585; map format 10, protocol 53; owner, 2026-10-29: round 6 picks, "24 breakable")

  • Each has hp and how much each kind of hit counts (map.h kBreaks: hp, bullet, blast, touch, debris colour). A hitscan shot that stops on one adds its damage x bullet (Server::blockFromRay, in hitscan after the glass test). A blast adds the weapon's damage, falling off to 0 at its splash radius, x blast, unless a wall is in the way (blastBreaks). At hp it breaks (breakBlock). The numbers are set against the weapons: a rocket next to it does about 30, the rifle 6 a shot, the sniper 40. So a crate (80, blast x3) goes after one rocket or 14 rifle shots, thin concrete and hay (bullet 0) only to blasts, cinder block (400) and the tyre stack (300, bullet x0.25) after a lot, and a mirror (12) after 2 rifle shots.
  • Contact (blocksTouched, every player command): BREAK_TOUCH (paper screen, cardboard, sugar glass) breaks when a player's box, grown 5 cm, touches it. BREAK_DASH (plywood, drywall, picket fence) breaks when they're dashing (dashTimer > 0). BREAK_WEIGHT (rotten planks) breaks after you've stood on its top for kWeightBreakS 0.3 s.
  • Server-authoritative: a break is an EV_DESTRUCT like a pane's (Map::breakGlass takes blocks too; joiners get them in the list), so every client removes the same brush and its collision. Damage taken is per brush (blockDmg_) and cleared with the rest each round (resetDestruction). Stickies on it drop.
  • Drawing: not baked (bakedMaterial), so a broken one leaves no lightmap or shadow behind. Renderer::uploadBlocks builds a mesh of the standing ones, drawn right after the world chunks with their looks (uAlphaPass 2: opaque, full sun and sky), and rebuilds it whenever the map's destruction serial changes. A break throws 30-300 chunks (by volume) in its colour from the hit, twice as fast from a blast, with a low thud.
  • Not done: the descriptions' extras (drywall letting shots through, a tyre stack bouncing a rocket back, pole-by-pole and block-by-block crumbling). Each block breaks whole.
  • Tests (group materials): the crate breaks on the second 50-damage shot, the concrete panel ignores five 100s and breaks to two rockets, sugar glass on touch, plywood only to a dash, rotten planks after 0.3 s standing on them, and all come back on reset.

Maps: 25 see-through looks (537-561; map format 9, protocol 52; owner, 2026-10-29: round 6 picks, "all 25 semi transparent")

  • What each stops (map.h kSemi: players, shots, sight). Glass-like ones (13, plus rain window, prism, jelly cube, ghost glass): players and shots, like plain glass. Frosted glass also blocks sight ("light through, no view through"). Hex force field: players only. Chain-link mesh and cargo net: players only (shots go through the holes). Red laser barrier: shots only. Smoke wall: sight only. Hologram, soap film, water curtain and scanner beam: nothing. Why: the page's descriptions.
  • How: a see-through brush is in the collision grid when it stops anything (solidMaterial). Map::forBox (player collision, boxSolid) skips what doesn't stop players (stopsPlayers), raycast / raycastBullet skip what doesn't stop shots (stopsShots, clip as before), and the sight cast skips what doesn't stop sight (stopsSight, glass as before). Bots' nav skips what they can walk through.
  • Drawing: not in the bake (bakedMaterial), casts no shadow (litMaterial, the bake's inside). Renderer::uploadSeeThrough makes each one's box faces into a mesh of its own, drawn after the opaque world and before the other see-through things, blended, no depth writes, both sides. It uses the world shader with uAlphaPass 1: the look sets a global alpha (the round 6 GLSL), the lightmap is replaced by full sun and sky, and fragments under 1% alpha are discarded. A map's shader carries their looks only when it has them.
  • Looks are keyed by material now (blockLook(mat, ...); lookMat maps kSurfaceLooks' index to its material), since the see-through looks follow the large patterns, which have no looks of their own. Ghost glass's preview GLSL divided by zero (normalize(vPos - vPos)); it now fades in at grazing angles and within 4 m.
  • Not done: the scanner beam's "marks you on the enemy's HUD" (it's drawn, and you pass through). Sorting: boxes aren't sorted back to front, so two see-through boxes in line can blend in the wrong order.
  • Test: testSeeThrough (group movement): glass, force field, laser barrier, smoke, water curtain each stop exactly what they should. Format 8 converts to 9 unchanged (upgradeMap8to9).

Glass: SHATTER and SHARD (owner, 2026-10-29: "shatter = 25-100 pieces per sqm 1 damage each add 250-1000 particles that do 0 damage; shard = 2-5 pieces per sqm 10 damage each add 100-250 particles at impact site that do 0 damage")

  • glassShards picks the mode from how it broke: SHARD for a shot / the knife / a bolt / a grapple pulled back (glassCrater), SHATTER for a blast, a dash or a pull through. The rate a square metre comes from the seed (2-5 or 25-100), so the server and every client make the same pieces; at least 2 / 8, at most kGlassMaxPieces 600 (a 6 m2 pane at 100 a m2). Each piece carries its damage (GlassShard::damage, FlyingShard::damage): kGlassShardDamage 10 or kGlassShatterDamage 1. Sizes follow their share of the pane (half-width 0.5 sqrt(area / n), 2-45 cm), so shatter pieces are small and shards big. Directions and the throw distances (blast 3x, dash 2x, the rest 1.5x) are as before.
  • Glitter (client only, no damage): 100-250 from the hit along the shot (SHARD), 250-1000 off the whole pane with its pieces (SHATTER). It falls with drag and fades in 1.5 s with no collision tests, so a thousand costs a few hundred microseconds of box drawing. The on-screen cap went 3000 -> kMaxVisShards 6000.
  • Server cost: a shattered pane's 600 pieces each test 8 players every 10 ms step (they land within 3 s).
  • Test (group materials): a 4 m2 pane gives 8-20 shards at 10 and 100-400 pieces at 1.

Maps: 40 large patterns (497-536; owner, 2026-10-29: "add some LARGE patterns 2-4x current size per piece of the surface, especially stone bricks, cobbles, wood")

  • Each is another material's look with the face's plane divided by its scale (map.h kLarge: base, scale), so the pieces, joints and grain are all 2-4 times as big. Base materials wood, brick, cobblestone, tile and stone get a 2x and a 4x; 30 brick, tile, stone, sett, flagstone and wood looks from 39-471 get one (2x for the big-block ones, 3x for small bricks, tiles and setts).
  • Shader: Renderer::compileWorld replaces // @LARGE@ (just after the face's plane q is made) with a remap: material 497 + i becomes kLarge[i].base and q /= its scale. Anti-aliasing follows, since footprint() is taken from the scaled q. Classic brick (20) now uses q too (it rebuilt the same plane from vPos). A map's shader carries a large one's base look (setWorldLooks), so they cost what their base costs.
  • Same map format 8 / protocol 51 as the liquids (one push). Editor: a LARGE tab; the unbaked colour is the base's.

Map editor: full-window picker, SPLIT / COMBINE / SUBTRACT (owner, 2026-10-29: "make the previews twice as big, this menu should take up the entire screen"; "split, combine, and subtract buttons")

  • The picker is a full-window panel with 200 x 200 swatches (at 720p; 400 px at 1440p), and it's modal: nothing under it takes a click. Swatches are now a cache of 40 slots of 400 px rendered when first shown (an atlas of every material at that size would be 320 MB; the old one was 128 px for all of them): the least recently drawn slot is reused.
  • SPLIT (the selected boxes' longest side, at the selection's middle on the grid, or its exact middle when the grid line is an edge), COMBINE (boxes of one material whose volumes add up to their bounds with no overlap become that box; anything else goes to GROUP's MAKE, as in Quarkology) and SUBTRACT (the first box selected is the cutter: each other box it overlaps leaves up to 6 boxes round the cut, then the cutter goes). Slopes and cut boxes are left out of split and subtract. Each is one undo step; one row under MIRROR, so the panel still ends on screen at 720p. Edscript add / boxop test them headless.
  • Not done: "1 = select" applies to Quarkology's map editor (which has tools); the game's editor has no tools, a click always selects.

Maps: materials 139-471 and the picker (owner, 2026-10-29: "where's the other several hundred materials in the editor? there's only like 138"; "the material preview image should be bigger... like 100x100 pixels each")

  • The picks of preview rounds 1-4 are materials 139-471, in the pages' order: 77 of round 1 (25 nature, 21 buildings, 15 industrial, 8 sci-fi, 8 decor), 100 nature (round 2), 86 industrial (round 3), 70 wasteland (round 4). Keyword = the name in lower case letters only (one clash: round 4's Salt flat is saltflatwasteland). Editor flat colour = the mean of the preview render's front face. All solid, lit, plain (never break); looks that glow (lava rock, sci-fi panels) write emit, animated ones use uTime.
  • Each map's world shader carries only its own looks (Renderer::setWorldLooks, on every world upload): the looks moved out of the shader text into client/surface_looks.h (433 GLSL bodies, 39-471) and worldLooks() builds blockLook() from the materials in the baked world's vertices, switches of at most 50 cases (llvmpipe drops cases past about 76 in one chain). A map with none of them gets an empty function, so the stock maps' world shader is now smaller than 1.19.2.2's (which carried all 100 block looks). A recompile happens only when the set changes, while the map loads. The editor's swatches use a second program with every look, built the first time the picker opens (swatchProg_); the swatch atlas is 128 px a material, 16 across.
  • Light bake: seam buckets keyed the material on 6 bits (& 63), so materials past 63 shared buckets (correct, but slower). The key now holds the whole material, and each face is also in an "any material" bucket: the two passes that looked once per material (hand-made maps' group seams, the z-fight lift) now look once. With 472 materials the old loops would have been 472 hash lookups per face per cell.
  • Picker: tabs (IN THIS MAP, RECENT, BASIC = 0-38, BRICK, CONCRETE, TILE, STONE, BUILDINGS, NATURE, INDUSTRIAL, SCI-FI / DECOR, WASTELAND, ALL; kMatTabs in editor.cpp), SEARCH (every typed word must be in the name), 100 × 100 swatches with names, as many rows as fit above the key bar and the wheel scrolls the rest (edMatScroll_, a row a notch).

Maps: materials 12-19

  • Owner, 2026-10-28: "ice is slippery ... sand wood plastic grass act same as regular just different surfaces generated not textures; rubber, is extra bouncy; ideally water and lava and acid would have noise maps ...; make metal look more like metal ... add like a steel, and a diamond plate"; then "on rubber you have more acceleration for ground movement and quicker breaking". kMapMaterials 12 -> 20: 12 ice, 13 sand, 14 wood, 15 plastic, 16 grass, 17 rubber, 18 steel, 19 diamond plate (file keywords ice ... diamondplate, map.h kMapMaterialWords, shared by map files, group files and maptool). All solid like base (solidMaterial) and lit (litMaterial). Still map format 5 (unreleased). Glass (20) comes separately.
  • Ice (pmove): standing on it, ground friction x 0.08 and ground acceleration x 0.2: let go at 8 m/s and you still have 6 after half a second (0 on base).
  • Rubber (pmove): landing on it faster than 3 m/s sends you back up at 0.85 of the landing speed (holding jump: the higher of that and the bhop); a wall or ceiling hit faster than 3 m/s reflects that part of your velocity at 0.85. Standing on it, friction and acceleration x 1.6. The floor is asked (one short sweep) only on maps that have ice or rubber (Map::hasIce, hasRubber), so other maps cost nothing.
  • Looks (world shader, renderer.cpp): value noise and a 3-octave fbm in the shader, no textures. Ice: pale blue, cloudy, fine white cracks, glossy. Sand: fine grain and wind ripples on tops. Wood: 0.25 m planks with joints, grain and per-plank shade. Plastic: the map's accent colour, smooth, a tight shine. Grass: green blotches and blades on tops, earth with mossy streaks on sides. Rubber: near black, pebbled dots. Steel: light grey, long brushed streaks, a sharp highlight. Diamond plate: raised diamonds lit on one side. Metal (3): brushed streaks and a soft sheen, no longer a flat dark box. Highlights use a new uSunDir uniform and the bake's sun visibility. Water: two layers of drifting ripples with bright crests; lava: a dark crust broken by glowing veins that churn slowly; acid: drifting foam. Liquids flow across tops and down sides. Cost: only fragments of those materials run the noise.
  • Projectiles and the bomb (owner, 2026-10-28: "ice is slippery for players, bomb, stickies, not grapple"; "players, bomb, stickies, rocket, are bouncy against rubber"): a rocket or sticky hitting rubber (not a player first) reflects off it keeping 0.85 of the speed into it (kProjRubberBounce, server stepProjectile; your own predicted rocket copy bounces the same way, so no false blast on your screen). A sticky landing on ice slides along it (what goes into the face is gone, 1.5 m/s2 friction) and sticks once slower than 0.5 m/s. A bomb dropped over ice or rubber falls from where its carrier was with their velocity and moves on (gamemodes.cpp bombMove: 1 ms steps, gravity, ice slides with friction, rubber bounces at 0.85, anything else stops it; void or lava: back to where the carrier last stood); over anything else it drops straight to the floor as before. Its position already went out every snapshot. The grapple is unchanged on both. Test: quark_tests materials (projectiles).
  • Editor: a new box is one grid cell, the cell under the crosshair (owner, 2026-10-28: "when you place an object it should be the same scale as your grid size"); it was always 2 x 1 x 2 m.
  • Seam buckets key materials on 5 bits (were 4). The editor shows each new material in its own flat colour; Quarkology's map editor lists them.

Maps: materials 20-35, glass, blown-open walls (owner, 2026-10-28)

  • New materials (map.h, kMapMaterials 21 -> 36; keywords in kMapMaterialWords, so the file format, edit ops, Quarkology and maptool take them; still map format 5: nothing released reads 5 yet): 20 brick, 21 glass, 22 asphalt, 23 mud, 24 cobblestone, 25 concrete, 26 tile, 27 marble, 28 corrugated, 29 hazard, 30 snow, 31 stone (ashlar), 32 crystal, 33 circuit, 34 rust, 35 terracotta. Which ones: the owner's picks from 20 rendered surface ideas ("all of these are awesome, except carpet and tile, redo tile, remove mesh grate, remove hex panels, remove conveyor, speed, jump"). Tile was redone: glazed cream 0.25 m squares in grey grout with a bevel and shine (it was a black and white checker). Asphalt dropped the preview's lane line (every asphalt top would have had one). All solid; every look is the world shader's (no textures). The light bake's face key keeps 6 bits of material (was 5).
  • Grass edge: grass sides are green down the top 1/4 m (a ragged edge) only on a face piece at the side's top with nothing solid on the block or flush above it (the bake samples just above the edge and tags the vertex material 0.25 over when it's covered).
  • Asphalt and mud (pmove; owner: "asphalt should increase top ground speed, increase accel and breaking", "mud should be sticky"): asphalt: run speed x 1.15, ground friction and acceleration x 1.4. Mud: run speed x 0.6, ground friction x 2.5 (you stop dead), jumps x 0.75. The floor is probed once a step, only on maps with ice, rubber, asphalt or mud (Map::hasGripMats).
  • Ice dash (owner: "when you dash on ice it should only work 25% because its slippery"): a dash started standing on ice has no burst; it moves your velocity a quarter of the way to what the dash would leave you with (kIceDash 0.25), and still spends a charge. Tested: from standing, 0.17 s later 0.86 m on ice vs 3.34 m on base.
  • Glass (kMatGlass 21; owner's rules, 2026-10-28): solid to players, projectiles and shots; sight, bot vision, the server's visibility culling, blast line-of-sight and the light bake pass through it (skipsSight; the bake leaves it out of the static mesh and casts no shadow from it). Panes are drawn each frame as see-through very light grey boxes (kGlassTint), so a broken one simply isn't drawn. What breaks it and how (GlassHow, gamedefs.h; shards from glassShards, glass.h, the same on server and clients from the event's seed):
    • a hitscan shot or the knife (Server::hitscan, before players: the pane stops that shot): a crater and 10-20 shards along the shot, 10 damage each;
    • a crossbow bolt (shatters it) or a thrown knife (a crater) breaks it and flies on;
    • a blast (rocket, sticky, anything with splash except the bubble: "bubble will not damage windows") shatters every pane within its splash radius not behind a wall: 100-300 pieces (about 120 a square metre) flying away from the blast's side and out from under it, 1 damage each ("each shard does 1 damage");
    • a dash goes through a pane (pmove: sweepAxis lets the dash pass and names the pane, MoveState::glassBrush), which shatters along the dash; the dasher takes a flat 10 and none of its pieces;
    • the grapple: fired at a pane while standing or moving away from it (closing slower than 1 m/s) it catches nothing and the pane breaks as if shot from the other side (shards at you); moving toward it, it latches, and when the pull lets go next to the pane it shatters on away from you, a flat 10, its pieces spare you;
    • stickies stick to it; when it breaks, stickies resting on it drop. A shard flying (10 ms steps on the server, at most 4000) hurts the first player its path meets, once; one that hits the world is harmless. Credit goes to whoever broke the pane (in hide and seek, tag and race nobody else's: no tags by glass); kill cause "glass" (kDamageGlass 249). Clients draw the shards falling and lying where they land, and drop each once it's out of view ("disappear from your screen once its no longer in your fov"), at most 3000. Your own shot, knife, dash or grapple break is predicted (shown and passable at once); one the server doesn't confirm within 1 s is put back. Trade-off: 10-20 shards at 10 each kill someone standing right behind a pane that's shot (the test: 100 -> 0 at 1 m).
  • Blown-open brick and cobblestone (owner: "if the brick is 1/8m or less thick it can be exploded ... 2.5mx2.5m not a circle but kind of square circular ish, as if the bricks got broken; bricks cannot be broken any other way besides sticky or rocket ... 1m away ... a 1.5m hole"): a rocket or sticky blast cuts a hole in every brick / cobblestone box no thicker than 0.125 m (kBreakableThick) near it (Map::punchHole): kHoleSize 2.5 m across, less the blast's distance to the wall's plane, shaped |x|^4 + |y|^4 < 1 with a per-brick nudge so the edge is whole bricks (holeTakes, 0.21 x 0.075 m bricks in running bond; cobblestone 0.16 x 0.14 m cells). Collision: the box is replaced by generated pieces (whole rows above and below, each cut row's kept runs), so players, shots and projectiles go through exactly where the bricks are gone; another hole cuts the pieces again. Drawing: the static mesh stays; thin brick / cobblestone faces are tagged by the bake and the world shader discards the fragments of broken-out bricks, reading the holes from a small float texture (Renderer::setHoles, at most 512 holes a map). Chunks fly out of a fresh hole. A 2.5 m hole is about 90 pieces.
  • The same everywhere, back each round: every break is a reliable event (EV_DESTRUCT: glass with its seed, or the blast point of a hole: each client cuts the same holes in the same order). A player joining gets what's broken as lists (glass panes, hole blasts in order, DESTRUCT_*_LIST), applied once their map is loaded. Everything comes back at each round's start (freeze), a new match, a new map, edit mode and before any map edit (Server::resetDestruction; the owner's pick, "Each round (and map restart)"); FFA keeps it until then. Nothing is saved: a map's text and id never change. Tests: quark_tests materials (destruction).
  • Shimmer (owner: lava "if you have a big long line of lava and stand at one end it gets fucky because of the angle"): the shader's noise octaves fade to their average once a noise cell is under about 2 pixels (footprint: the longer side of the pixel's footprint, so a grazing angle counts like distance); lava's thin veins fade to their average coverage; brick mortar, tile grout and stone joints fade the same way (they made moire rings across a wall a few metres off).

Map editor: FIX MAP / FIX GAPS (owner, 2026-10-29)

  • Why: "a fix map button which combines objects of the same surface that are next to one another ... also delete any object that you can never see"; "a gap fixer button ... if there is a gap less than 1/8 between 2 object, expand whichever one is not on grid, to the one that is on grid". What (mapFixRedundant, mapFixGaps in map.cpp; Game::edFix): FIX MAP repeatedly joins two plain boxes of one material whose faces match on two axes and touch or overlap on the third (and drops one inside another of its material), then drops any editable brush wholly inside solid opaque boxes (checked exactly: the hidden box is cut along every covering box's faces and each piece's middle must be inside one; over 64 cuts an axis it's kept). Glass, water, lava, acid, trim, clip, wallbang (shots go through it) and thin breakable brick / cobblestone never hide anything, and glass and brick / cobblestone are never merged (a bigger pane or wall would break differently). FIX GAPS: two plain boxes facing each other (overlapping across the other two axes) with a gap under 1/8 m: the side off the 1/8 m grid stretches to the on-grid one; both off: both to the grid line between them if there is one, else left; two on-grid sides are left (a deliberate gap); never stretched into a third box. Each button is one undo step (box sets, then deletes from the last down) and says what it did. Neighbours come from a 4 m cell lookup: Midtown (11 700 boxes) takes about 0.1 s, against 2 s pair by pair. On the official maps FIX MAP would merge 0-870 boxes and remove 10-1457 hidden ones (e.g. ladders built into door frames).

Maps: faces behind breakable brick

  • PointGrid::buried (lightmap.cpp) culled a face wholly inside other baked boxes, thin brick / cobblestone (thinnest side <= kBreakableThick) included. Why it's wrong: a rocket or sticky can blow that brick open (Map::punchHole, or the whole wall falls at 80%), and the face behind it was gone, so the hole showed the sky through the wall (owner, 2026-10-29: "in game there are gaps in walls that dont exist in editor"; the editor draws every box whole, so it never showed this). What: thin breakable boxes no longer hide faces. Cost: a few more faces where brick is laid flush on a wall (a coat), drawn only where the brick is gone or behind it (depth-tested). The face behind keeps the bake's shadow from the brick (it's dark until lit by the hole), as it would be inside a wall.

Maps: cobblestone joints

  • Owner, 2026-10-29: "in game there are gaps in walls that dont exist in editor" ... "cobblestone". The joints came from F2 - F1 of a 3 x 3 Worley search, which isn't a distance to the stone's edge: it stays small in long wedges out of every corner where three stones meet, so dark smears ran across the stones and read as cracks through the wall; and the stone shade was hash2(floor(q * 3)), a 1/3 m checker over the stones. What: stones() in kWorldFS finds the nearest stone (3 x 3), then the exact distance to its border against each neighbour's bisector (5 x 5 around it), and returns that stone's own number for its shade. Joints use smoothstep(0.015, 0.06) of that distance (about the old width). Cost: 34 hash lookups a cobblestone pixel instead of 9; only cobblestone pays it. The editor's plain unbaked view never showed the pattern, which is why the gaps were only in game.

Map editor: material picker (owner, 2026-10-29)

  • Why: "in the map editor it should give you a pick list with images (like 20x20 grid or whatever) when you select a material; and also have a seperate list for materials already in the map, and the last 10 used surfaces". What: the MATERIAL rows (NEW ONES, and a selected box's or room's / stairs') are buttons that open a picker over the 3D view (drawEditorMatMenu): IN THIS MAP (every material its boxes and groups use), RECENT (the last 10 picked, newest first, saved in editor-recent-materials.txt in the user folder) and ALL, 12 a row, the current one ringed. A pick sets new boxes' material, or every selected box / room / stairs in one undo step (edSetMaterial); ESC or CLOSE closes it. The swatches (Renderer::swatch) are each material on a 2 m square drawn by the world shader itself into a 96 px cell of an atlas (one draw per material into an FBO, full sun and sky from a one-texel lightmap, no shadows or holes), so they match the game and the map's palette exactly; the atlas is redrawn only when the palette or brightness changes, and drawn over the 2D batch by a small textured-quad program. Cost: nothing unless the picker is open; one 768 x 480 RGBA texture once it has been. FIX MAP / FIX GAPS (above GRID) now show only with nothing selected: with a selection their row pushed the MIRROR row off the panel (UI test: off screen at 1024x768 to 1920x1080).

Maps: plain vs breakable brick, cobblestone and glass (map format 6, protocol 49; owner, 2026-10-29)

  • Owner: "you should have separate destructable vs non destructable glass, brick, cobblestone, etc so it doesnt cost nearly as much". Before, every glass pane was breakable and every brick or cobblestone box 1/8 m thick or less could be blown open, whether the mapper wanted that or not.
  • Now brick (20), glass (21) and cobblestone (24) never break, and breakable brick (36), breakable glass (37) and breakable cobblestone (38) are their own materials (breakablebrick, breakableglass, breakablecobblestone). They look the same (lookMaterial: the bake writes the plain one's id into the vertex, the swatch too).
  • What a plain one saves: no hole test in the world shader (only faces tagged thin + breakable run it, and only once a hole exists); in the light bake, faces behind it can be culled (thin breakable brick keeps them, so a blown hole doesn't show the sky); FIX MAP merges it and lets it hide what's inside; plain glass isn't in Map::glass, so the per-shot and per-dash pane scans (glassAt, sweepAxis's pass-through, the crossbow / knife break, the grapple's shards) skip it. Plain glass is listed in Map::windows and drawn see-through each frame like a standing pane; bots and sight see through it, shots and players don't pass it.
  • Old maps (format 5 and before) convert on load and on disk (upgradeMap5to6): all glass becomes breakable glass (box lines, slopes and cut boxes by number, group materials, parts); brick and cobblestone boxes no thicker than kBreakableThick become breakable, as do custom-group box parts that thin once scaled to the group and rooms whose walls come out that thin (min(0.25, smallest side / 4)). Slopes and thicker boxes never broke and stay plain. Numbers are kept as written; only the material word or number changes.
  • Protocol 49: an older client would let a plain pane break in its own pmove and wouldn't know materials past 35.
  • Tests: format 6 plain vs breakable (breakGlass, punchHole, sight through plain glass), the 5 to 6 converter on every kind of line (group "materials"); the other tests' unversioned maps convert, so the destruction tests run on the breakable kinds as before.

Maps: 100 brick / concrete / tile / stone materials (39-138; owner, 2026-10-29: "yes to everything")

  • The owner ticked all 100 looks of the fifth surface round (https://claude.ai/artifact/QKuSiQMiV4SrJN3HM17HhA). They're materials 39-138 in the page's order: 30 bricks, 20 concretes, 25 tiles, 25 stone walls. Solid, lit, never break. File keywords are the names in lower case without spaces (londonstock, flemishbond, pennyroundtile ...: map.h kMapMaterialWords).
  • Drawn by blockLook in the world shader, called for material 39 and up: two switches of 50 cases (one long chain lost cases on Mesa's llvmpipe in the previews). Helpers: grid, bond (running bond), hexc (hexagons), cells (Voronoi, 3 x 3).
  • Thin joints: each pattern's mortar ramp is widened to at least a pixel (aa = 1.5 x the pixel's width in metres) around the same middle, so a joint keeps its share of the wall far away instead of shimmering, and bricks don't wash out toward the mortar colour at a distance (the first try, widening only the outer edge, did).
  • Cost: a material picks its own case, so a map that doesn't use them pays nothing. Measured: the shipped Hamlet benchmark under llvmpipe, interleaved 2 runs each, 40.6 / 40.2 fps before vs 40.0 / 41.1 after (1% lows 30.3 / 29.5 vs 29.2 / 31.6): no change beyond noise.
  • Editor: a colour per material for the unbaked view (each one's mean colour in its render) and a label each. The material picker picks its column count (12 up to 32) so all 139 fit above the editor bar, about 20 across on 1080p; names show under swatches only while they're at least 72 px, otherwise the one under the mouse (or the current one) is named next to CLOSE.

Weapons

All four are the owner's numbers (2026-10-28, right after 1.19.1), in src/game/gamedefs.h's standard table; custom modes scale them as before (weaponset.cpp reads the same constants). No protocol change: like earlier tuning (rifle 4.95 -> 5.0, sniper 40), only the numbers moved, and testing servers and games update together.

  • Rifle +1 a shot: body 5 -> 6, kRifleHeadDamage 7.5 -> 8.5. Refire unchanged (0.1515 s). Time to kill from 100 hp: 17 body shots, 2.42 s (was 20, 2.88 s); 12 headshots, 1.67 s (was 14, 1.97 s). DPS against the laser's 40: 0.99 body (was 0.82), 1.40 head (was 1.24): the rifle now matches the laser inside its 24 m on body shots.
  • Bubble fire rate +20%: refire 1/5 -> 1/6 s, so 60 damage a second (was 50); 10 pops kill in 1.5 s (was 1.8 s).
  • Bubble stream up to 25: kBubbleStreamMax 15 -> 25 (at 30 m or more; kBubbleStreamMin stays 5 within 5 m, the count still grows evenly in between; reload 1.8 s unchanged). At full range a stream is 250 damage if every bubble lands. Trade-off: a stream is 10 more live projectiles; the server holds 128 at once, so in a crowded fight a later stream can come out shorter.
  • Knockback +33%: kShotgunKnock 0.30 -> 0.40 m/s per pellet's worth of damage (a full 20 pellet blast 6 -> 8 m/s); kCrossbowKnock 16 -> 21.28 m/s along the bolt.
  • Sniper knockback +33% (owner, 2026-10-28, after the two above): the sniper had no knockback of its own, the hitscan default (1 m/s along the shot, WeaponExtra::knock 1); new kSniperKnock 1.33 m/s (weaponset.cpp sets it, so custom modes scale it like the others). The rifle keeps the default 1.

Movement

  • Dash up share 40% -> 50% (kDashVerticalUp 0.4 -> 0.5; owner, 2026-10-28). Looking straight up, the sine of the dash's direction is 0.5: 12 m/s of the 24 m/s burst goes up. Down stays 90%. A custom mode's dash vertical multiplier scales both, as before.
  • Air dashes scale with how far below the top you are (owner, 2026-10-28: "if you dash at 10 m/s it should let you keep a lot more than if you dash at 25 m/s"). A dash that starts in the air leaves with your speed + kDashAirGap (0.35) x (32 - your speed), never less than the old + 1 (kDashKeep), at least 11 and at most 32 as before: 0 -> 11.2, 10 -> 17.7, 20 -> 24.2, 25 -> 27.5, 30 -> 31.0 (+1). Before: max(11, speed + 1) everywhere. Chained air dashes from a standstill: 11.2, 18.5, 23.2, 26.3 (were 11, 12, 13, 14). Why a share of the gap: it gives a lot at low speed and fades to nothing at the cap, so dashing can't push past 32 and doesn't double-dip with strafing at the top. On the floor the exit is unchanged (+1). A custom mode's DASH KEEP multiplier scales the share too (no new tunable). The super dash (hyper) keeps its own rule (the speed carried in + 5, at least the floor dash's exit, cap 36), so one started from the air isn't boosted twice. Tests updated: the dash exit test, the chained-dash test.

Release pipeline

  • Compressed packages: an entry with no compressed bytes is refused cleanly (found by fuzz_qpak in 1.19.2's final test): a .qpakz entry claiming 0 compressed bytes for a non-empty file handed stb's zlib decoder a null pointer, which it offset (undefined behaviour; in practice it read nothing and the package was rejected). qpakInflate now returns failure for an empty input up front; the input is kept as tests/data/fuzz/qpak/empty-deflate so every fuzz run replays it.
  • Sanitizer and fuzz builds skip LTO: 1.19.2's first final-test run failed to link the fuzz harnesses (clang-18's ThinLTO objects in libquark_core.a: "Expected at most one ThinLTO module per bitcode file" with GNU ld). Those builds look for bugs, not speed, so QUARK_LTO is ignored when QUARK_SANITIZE or QUARK_FUZZ is on. Release builds keep GCC -O3 + LTO.
  • macos-retry now finds skipped runs that ended as "failure". v1.19.1's tag run had its macos job cancelled by macos-watchdog (no runner in 5 minutes), and because the release job waits on macos, GitHub ended the run as failure, not cancelled. The retry only listed cancelled runs, so it never re-ran it (it was re-run by hand, 2026-10-28 18:0x UTC). It now lists completed runs that ended as cancelled or failure, and still re-runs only a run whose macos job was cancelled before any runner took it, and only when no job really failed (a real failure isn't retried every hour). Scheduled workflows run from the default branch, so this takes effect with the next production release.

Downloads

  • Compressed packages, quark-<platform>.qpakz (owner, 2026-10-28: "is it possible to zip the downloads so they arent so big?"). The launcher installs from quark-<platform>.qpak, which isn't compressed (test-1.19.1.1: Windows 72 MB, Linux 59, macOS 66; the manual zip / tar.gz downloads were already 44 / 40 / 43). tools/qpakz.py turns each finished .qpak (after the launcher's signature went into it) into a .qpakz: format QPAK2 (src/launcher/qpak.h), the same entries with a u64 stored size, each file a zlib stream (level 9) or stored as is when that isn't smaller; it checks the result holds exactly the .qpak's files. The release jobs (production, testing, testing's macOS) name it in the signed manifest and sign and publish it next to the .qpak. A local test (the Linux game, launcher and maps): 24.0 MB -> 14.3 MB. Why zlib: the decoder is stb_image's (already in the tree for the game's PNGs and light bakes), built into the launcher with its symbols static (src/launcher/inflate.c), so no new library; zstd or xz would be ~10-15% smaller but add one. Reading: qpak::read takes either format; a .qpakz entry's unpacked size is bounded (at most 1 GB, at most 1032 x its stored size: deflate's limit) before anything is allocated, and must unpack to exactly that size. releases::parse prefers quark-<platform>.qpakz when the release has it and its .sig (else the .qpak), and the manifest check follows the chosen asset. Old launchers ignore the new files. The launcher that ships is built from the launcher branch, so players get this when that branch takes the same change with a higher VERSION (a separate PR, after 1.19.1 is out so its packages all carry the same launcher). Tests: QPAK2 entries (compressed, stored, empty) and lies (short / long / cut streams, a terabyte claim, stored bigger than the file, truncation, unsafe names) in launcher_tests; the release-page parse picking the .qpakz (and not without its .sig); fuzz_qpak allows QPAK2's bounded expansion. check_secrets.py skips .qpakz like .qpak.
  • macOS builds on their own (.github/workflows/macos.yml) (owner, 2026-10-28: "it should still pass even if no macos build"). GitHub often has no macOS runner free for hours. Inside build-and-release that held the run, and the watchdog's way of skipping it (cancelling the run after 5 minutes) turned the run red although Windows and Linux had passed (runs 399, 402), and production (v1.19.1, run 400) couldn't publish at all until a Mac turned up. Now build-and-release has no macOS jobs: it's green when Windows and Linux are, and PRODUCTION publishes with Windows + Linux like testing always did. The macos workflow runs on the same pushes: prep (Linux) waits for that commit's build-and-release meta and light bakes (a new meta artifact carries the channel, tag and version, so the Mac build is the same <VERSION>.<N>), macos builds on a Mac whenever one is free (it waits in GitHub's queue without holding anything up), and publish (testing and production) waits for the release, then signs and uploads the macOS files with their own manifest quark-release-macos.txt, as testing's macOS files always came. A work branch's newer push cancels the older run, as before. macos-watchdog, release-testing-macos and macos-retry.yml are gone (no run is cancelled any more, so nothing to re-run). Trade-off: a production release can be Latest for a while before its macOS files are there; macOS launchers keep their installed version until then (they need the macOS package), and the release checklist asks to see them arrive.

Modes: surf (MODE_SURF), map Cascade

  • Owner, 2026-10-28: "add a surf gamemode where you cant grapple or dash, only strafe; and make a map for it". MODE_SURF (13, surf, SURF) plays as race does everywhere (isRaceMode: checkpoints, splits, practice warmup, knockback only, a player winner, respawn at your checkpoint 1 s after a fall) with every player spawned run-only (MOVE_RUN_ONLY: no dash, no grapple). Its own tools/modes/surf.qlogic (race's, base surf), mode params, a bot brain objective slot (the default: bots can't surf), Quarkology's mode list. Not added: a matchmaking queue, time trials and leaderboards (race's only).
  • Surfing itself: a slope's wall face (steeper than 45 degrees) in the air is a plain clip, the velocity losing only its part into the face, so a fall onto a ramp turns along it and air strafing into the face (the existing air acceleration, A/D cap 2.53) holds you on it and adds speed. On the floor the old rule stays: running at a steep slope never lifts you.
  • Knife only, nobody hurt (owner, 2026-10-28: "in surf mode you can only hold the knife, and you cant hurt other players"): Server::weaponMask gives surf the knife alone (a custom mode's own weapon list still wins); nativeCanDamage refuses all damage in surf and nativeCanHit every hit on another player, warmup included (race still lets practice warmup hit), and tools/modes/surf.qlogic's canDamage / canHit slots say the same. A fall still sends you back to your checkpoint.
  • Half-strength gravity on a ramp (owner, 2026-10-28: "the slopes are too slippery to make surf work for humans properly; can you make the -y velocity while on a slope like half of what it currently is"): in the air, not dashing, touching a slope's wall face that faces up, half the step's gravity is given back after the clip (kSlopeGravityBack 0.5, pmove.cpp), so the fall along the face, and with it the slide down and off the ramp, builds half as fast. Speed you already have is kept; off the ramp gravity is whole again.
  • Ramp grip (owner, 2026-10-28, after CS surf: servers there raise sv_airaccelerate to ~100-150 from 10 rather than touching gravity): run-only movement (surf) touching a ramp's face (a slope wall face facing up, within 3 cm: onRampFace probes down and to the four sides with the sweep before the air move) gets x2 air acceleration (kRampAirAccel; 8 first, owner: "way too much ... make it like 2x base"). The wish-speed cap (2.3 / 2.53 A/D) and the A/D boost's start are unchanged, so it's grip on the face, not free speed. Two to five extra sweeps a step, surf players only.
  • Cascade (tools/maps/cascade.py, for surf): four stages down a frozen gorge, each ending on a ledge with its checkpoint; the void below (-80) sends a fall back to it. Ridges are two wedges back to back: 52 degrees (6 m over 4.7 m a side; owner: 60 was too steep to hold), the narrow ones 60 (3.5 m). The ramps are clean (owner: no obstacles on a ramp; v2's beams and posts were removed): the challenge is the transfers, a ridge ending and the next starting across a gap, lower and offset. Stage 1 three ridges (the last narrow); stage 2 a 90 degree turn onto ridges running east and a 50 degree kicker up onto the ledge (only if fast); stage 3 a ridge in three pieces, offset west then back east narrow; stage 4 a zig-zag of three short ridges each offset to the other side, then a kicker up onto the finish. It offers surf only (prefabs: a surf map doesn't add free for all: its stages can't be walked between). The map checks don't ask bots or the walk grid to reach its checkpoints (ridden, not walked); quark_tests surf rides the first ridge end to end instead (strafing into the face, from 18 m/s: 268 ticks, never below the ridge's base, speed up to 22 m/s, no dash or grapple).
  • A dash into a slope (owner, same day: "keep your velocity from the dash as if there was no block there") turns along the face at its whole speed, nothing lost to the hit.

Netcode: shooter-side damage

  • Owner, 2026-10-28: "all damage should be client side also, so when you have high ping and shoot a projectile it should not magically not do any damage, this would only happen if you die on the server before the server gets your damage packet, this is high trust society game so dont worry about cheats".
  • The game sends clienthits once per connection (NetClient::setReportHits, ServerPlayer::clientHits). For such a shooter the server no longer applies its own hitscan damage (fireWeapon: glass still breaks, accuracy is counted from the reports), crossbow direct hits (stepProjectile), or rocket splash on anyone but the shooter (explode). Bots, test clients and older games keep the server's hit tests.
  • The game computes each hit against the players as it draws them (framePlayers), mirroring the server's tests: Game::reportShotHits (spread pattern, body/head boxes, knife reach, sniper collateral, falloff, wallbang, the shotgun half-head rule, the laser pull), reportBlast (hitboxClosest, the two cover rays, direct damage, falloff, knock with twice the lift) and reportBolt. Each shot or blast sends one hit <weapon> <alt> <n> [victim damage head direct splash dist knock] command. The laser's 10 ms shots are summed and sent every 50 ms (about 20 commands a second instead of 100).
  • Rockets and bolts report where they land on your screen: the predicted copy's impact, the server copy's extrapolated impact, or, when the server's copy vanished first, where it was last drawn.
  • Server::clientHitReport applies each one through weaponHit, the same path as before (kills, headshot flags, assists, knock), unless the shooter is dead on the server by then. That is the one case q named. It also drops hits on yourself or on dead or missing players, non-finite values, and more than 16 victims. Damage is clamped to 1000 and knock to 100 m/s per axis. There are no other checks (high trust, per q).
  • Kept server-side: your own blast on yourself (rocket jumps keep their exact feel and timing), the shotgun ricochet (its wall bounces aren't predicted), stickies, bubbles, the syringe, the grapple, and any rocket or bolt landing after its shooter died.
  • Test: testClientHits (group materials).

Maps: cut box (owner, 2026-10-29: "cut box (any angle)")

  • Why: a round dome can't be built from wedges, corners and tetras. Its diagonal faces slope across the box's diagonal, and the only piece that slopes that way, the tetra, is highest at its square corner, so it leaves V-shaped gaps between tetras that nothing fills (the 8-sided preset still had 1-2% of its volume missing). A box cut by one plane still can't make an angled ridge exact: near a ridge, a piece has to stay under both faces' planes. So a cut box takes up to kMaxCuts = 6 planes. A dome face is then one piece: its sloped top, the two radial planes at its corners, and the vertical planes under its inner and outer edges.
  • Format (still map format 5, which nothing released reads yet): cut <mat> <n> <a b c d> x n <box>. Each plane is a x + b y + c z <= d over the box's own 0..1 fractions, so it scales with its box like the other slopes and works the same as a group part (part ... cut <n> ...), an edit op (letter x) and maptool JSON ("shape": "cut", "cuts": [[a, b, c, d], ...]). Brush gains ncut and cut[6][4] (96 bytes; dir / up stay 0).
  • Geometry (map.h): cutPlanes (world, unit normals), clipPolygon (Sutherland-Hodgman), cutFaces (each box face clipped by every plane, plus each plane's face clipped to the box and the other planes, as convex polygons) and cutBounds (the bounds of the solid inside a clip box, empty for a sliver thinner than 1e-5).
  • Collision: brushObstacle for a cut is exact, because the solid is convex: it returns the bounds of the solid inside the moving box's slab across the move. brushOverlaps is a non-empty cutBounds, and brushContains tests the planes. slopePlanes now fills up to kMaxCuts planes (callers' arrays grew from 2 to 6), so rays, slopeContact (floors vs walls) and slopePlaneAt work unchanged. Steepness is the steepest upward-facing plane.
  • Drawing: a cut face can have more than 4 corners. The bake splits it into a fan of quads and triangles, with the cuts between them unoutlined (genOff), in a per-brush list. Renderer::slope does the same for the editor view.
  • Editor: rotate / mirror move the planes (cutTurned: each plane through three moved points and a point inside). N on a cut box makes it a box again. Bots get nav cells over it like a tetra's.
  • Tests (group slopes): file round trip, inside / top, ray and normal, player-box overlap at a cut-away corner, faces with outward normals, landing on it, walking into a diagonal cut, turn and mirror (200 points), a group part, the edit op and bots' nav cells.

Maps: tetra pieces, Coat (owner, 2026-10-28)

  • Tetra (BRUSH_TETRA, file keyword tetra, edit-op and group-part letter t, maptool JSON shape tetra; still map format 5, which nothing released reads yet): the owner's "1 0 0 0 / 1 1 1 0", meaning top corners then base corners. In its own frame it is solid where 0 <= h < height * (gu + gv - 1), with gu and gv the fractions toward the apex corner (dir, as a corner's). So it is the box cut by one plane: slopePlanes gives one, and rayBrush / brushContains need nothing more. Its faces are the base triangle, two vertical right triangles and the sloped one (slopePolygons, and the bake's lface). Collision: a tetra's cross-section above a height is a triangle, not a box, so brushObstacle and brushOverlaps cut it to what the moving box's footprint actually meets (the triangle's reach at the footprint's point nearest the apex: exact for an axis-aligned box). A player walking into its open side touches the diagonal face (test: x 10.72 against 9.6 for its box). Bots get cells over the triangle only. Steepness (walkable at 45 degrees or less) is the plane's full gradient, h * sqrt(1/w^2 + 1/d^2). Only 8 tetras are distinct (one per box corner), so its 24 dir/up pairs map 3 to 1. slopeTurned picks the first that matches.
  • In-game editor: N cycles box, wedge x4, corner x4, tetra x4. K is Coat (owner: "select faces of blocks and then it will build a 1/8m surface against the sides you picked"): a 1/8 m box (kCoatThick) in the current material on the side of the selected box you look at, on that side of every selected box, one undo step each. Slopes are skipped, because their sides aren't whole rectangles. Quarkology's map editor has Coat too (quarkology #6) and its Tetra tool (quarkology #8).
  • Tests: the slopes group (file round trip, top heights, the solid, rays and the face normal, 4 triangular faces, a mirror, walking into the diagonal face, bot cells).
  • Fixes (owner, 2026-10-29): "when rocket bounces off of rubber it exists on the server but client side the rocket goes invisible". The server copy's lead extrapolation (X5) treated any wall as the impact: it blew the rocket up on your screen (st.boomed hid it) and, since shooter-side damage, reported a blast there. It now reads the wall's material, and off rubber it draws the rocket along the bounce (kProjRubberBounce, as the server's) with no boom and no report. Also "glass shatter works great, glass break does not, no shards are visible": a shot or a hook pulled back leaves the pane gone with only 10-20 crater shards, 2-5 cm across, flying away from you at 5-10 m/s. Now the rest of the pane also falls as 40-160 visible pieces (by area). These are drawn only, with no damage: only the crater shards hurt, as before.
  • Crumbling (owner, 2026-10-29: "when 80% of a destructable wall gets broken the whole thing should disapear"). Each hole's piece remembers the wall it was cut from (Brush::origin, runtime only). After a punch, Map::punchHole sums the face area its standing pieces still cover. With less than 1 - kCrumble (20%) left, every piece goes, and the wall gets a rectangle "hole" (r < 0: half sizes in cw / ch) that the world shader discards whole. It is deterministic, so the server and every client do the same from the same EV_DESTRUCT. The client throws 40-400 chunks over the wall (by area) with a low thud. A 6 x 3 m wall falls after about 4-5 rockets. Test: the destruction group.
  • Glass shards (owner, 2026-10-29): "glass shatters perfectly, but no breaking; also glass shatter particles should go further depending on source: explosions are 3x current, dash is 2x current, sharding of all sorts should be 1.5x current distance" and "shard does 10 damage per piece, but the pieces need to be bigger and less frequent". glassShards (glass.h) now breaks a shot pane (or one grappled back) into its 10-20 shards across the whole pane, each sized to its share (half-width 0.5 x sqrt(area / n), 5-30 cm, x 0.6-1). Near the crater they fly on along the shot at up to about 8.5 m/s, falling off as e^-distance; further out they mostly drop. A blast, dash or pull makes 12-40 pieces (3 a square metre; it was 120, 100-300). Every piece in flight does 10 (kGlassPieceDamage 1 is gone). Each one's sideways speed is multiplied by glassThrow: 3 for a blast, 2 for a dash, 1.5 for the rest, so the fall is unchanged and they land that much further out. On the server a shard hits a player whose box, grown by the shard's half-width, its path meets (FlyingShard::size). Clients draw each shard as a plate of glass 3 cm thick across the pane's plane, lying flat once it lands. The 40-160 drawn-only pieces a shot added (1.18 testing) are gone, since the shards now are the pane; a puff of 12 tiny glitter bits marks the crater. Trade-off: a blast's pieces each do 10 now, but there are about a tenth as many.
  • Glow (owner, 2026-10-29: "can you make glowing objects brighter"): glow material x1.6 of the palette's glow colour (was x0.9); crystal edges and circuit traces' emission x1.5.