Skip to content
Zizeeria
Menu

News

Development log

Written for people who want to know how it works. Pre-production entries are drawn from the commit history rather than published as they happened.

Posts

Milestone

Two clients in one day

Release 0.1.21 in the small hours with the framed HUD, the F8 card and the item tiers; release 0.1.22 at night with Savhamn lit at dawn under a cloud deck, four people to talk to and a journal, the bag on I, chat on Enter, two abilities on 1 and 2, a body in the character creator and a Controls page in words. And what is not in it, which starts with copper.

Two clients went out today, and the rule here is that a client is an event and not a habit: improvements gather on the branch and a build is cut when the person paying for the game says so, because each one costs an hour of wall clock, a third of a gigabyte in a bucket with a ceiling, and a network version the zones have to be re-cooked against. Release 0.1.21 went out in the small hours, at half past one by the clock the servers keep, with what the branch held then: the HUD on its painted frame, a card on F8 for telling us what went wrong, a tier and a colour for every item, and the first deer standing in the marsh. By the evening the branch held a day's work by many hands again, most of it the kind where the server and the client have to change together, and the word came to run all of it, so 0.1.22 was published at twenty past nine the same night. It was cooked twice. The first client build was played before it was published, and on its Settings card the Controls page had grown from nine rows of key bindings to twenty-one and spilled past the bottom of the card, over the buttons and out onto the HUD's key strip, with several rows reading as the code's own names for things, MoveAction four times over, rather than as words. That was fixed, the fix was checked and photographed, and both halves were cooked again before anything moved; the second client cook took four minutes and ten seconds. The client archive grew from 315.5 MB to 346.8 MB; the four bodies' new texture maps are the likely reason, and nobody has read the build's own manifest to say so.

The first thing a player sees is the world, and it is lit differently. Savhamn now sits at dawn under a cloud deck: in the arrival photograph the sky is a deep blue-grey, the lantern on the tower's mast burns warm against it, the boardwalk's planks catch a low sun and throw long shadows, the pale pyramid roofs of the huts stand beyond the deck, and thin green lines of the Weave's light run across the ground on either side. It is still a place made of blocks, the huts, the tower and the deer alike, over a smooth ground with no surface on it yet; but the peoples' bodies and the one prop placed in the map so far, a round shield, no longer shine like wet plastic, because the material they wear was rebuilt and a probe now goes red on any import that skipped it. The rest of what you look at the world through is 0.1.21's, so here it is once. The HUD stands on a frame that was painted first and cut into parts afterwards, so the client stretches the middles and keeps the corners. At the upper left is the character's plate: a stand-in portrait, the name, the level, and the health as two figures over a bar. Along the bottom run eight dark wells on a rail with their key figures, 1 to 8, engraved in a strip beneath. At the lower right are nine small bronze squares with a glyph in each, for bag, crafting, the Weave, journal, character, map, guild, settings and menu; five of them open something in 0.1.22 and the other four say so by name, in the band above the wells, when pressed. At the upper right, in a bronze ring with a star at its top, is the minimap: Savhamn captured from above, north up, 140 metres of world across the disc, the player's chevron at the centre turning with the camera, and dots for the creatures and players the client knows of. The ring's picture was retaken under the new light for 0.1.22; in it the water reads pale blue, the land dark, two creeks glow green across the disc, and the settlement at the upper left still burns out to white, because the capture is exposed for the marsh in a ridge's shadow and the settlement pays for it.

Then the people. Four doorways have been cut into the huts of Savhamn, each with a stoop of four treads stepping down to the ground, and behind three of them somebody keeps: Sanna Fenrow the herb bench, Orm Bogiron the forge, Vessa of the Long Road the northernmost hut. The fourth, Halla Tideward, keeps the harbour from the deck at the tower's foot. Stand within three metres of one of them and press E, and the journal opens on what they have to say (Halla's first line is that the tide brings what it likes, and this morning it brought you); J opens the journal on its own. Two quests are in the marsh: What the Tide Left, from Halla at level one, which asks for five reed fibre cut on the way back from the flats, and Too Many Deer, from Vessa at level eight, which she will not give until the first is done. Accepting, marking and turning in go through the zone to the character service under one key each, so a lost connection cannot accept a quest twice; a turn-in takes the fibre, gives the Reedwoven drawing and the experience the row promises, and writes down the copper it owes without paying it, because copper is not in this release. What this release cannot do is put the fibre in your bag: there is no reed to cut and nothing that drops one yet, so the first quest can be accepted and its second step stands at nought of five, and the second quest waits behind it. In the photograph of the journal the card reads JOURNAL, then Halla Tideward, her offer on two lines, the quest's name beneath, and CLOSE, DECLINE and ACCEPT; it was taken in the client's preview with a stubbed person. The doorway photograph, from the editor, shows a figure standing in the smith's doorway at the top of the treads with her arms held out, the forge's window lit behind her, which is the pose a body takes when no idle plays. Whether she stands at ease in the live zone is one of the things this release will teach us: the two players the release gate sent in had their journals read and found empty, and that is as far as the people have been watched.

Three keys do something new in the zone. I opens the bag: a card that says how many of forty slots are taken and lists each stack with its name in its tier's colour, a thin frame at the row's start where the icon will go, a count, and the word bound where a thing is bound; in the photograph it holds one reed bow. Enter opens the chat box in the framed corner at the lower left. A line typed there goes to the zone, which judges it (ten refusals of its own, from an empty line to a whisper aimed at nobody) and hands it to the social service, which keeps it and names who should hear it; the zone then delivers it to everyone seated in the zone. /w and a name whispers to somebody in the same zone, /g would speak to a guild, and nothing in the game lets you join one yet, arrivals and departures are the zone's own words in the ash ink, and the last twenty lines are shown a step dimmer when you join. Every refusal is a sentence on the band: Nobody called Ylva is here, You cannot whisper yourself, Slow down, You are not in a guild. Keys 1 and 2 cast. The two abilities a Calling starts with sit in the first two wells as their initials, because no icon is drawn yet; for a Warden they are Calm Beast and Stand Fast. Press 1 and the band says Casting Calm Beast (1.2 s) while a teal well rises in the tile; when it lands on a marsh deer within twenty metres the deer stops and stands, and twelve seconds later it is gone from Savhamn and counted into Askvidderna, the region next door, by the service that keeps the populations. It does not walk there. Stand Fast is instant, on a twelve-second cooldown that counts down in its well, and for eight seconds it makes the character take seven tenths of any damage; nothing in the marsh hits back yet, so that is an ability waiting for its first enemy. Both cost stamina. A level-one Warden has four points of it, Stand Fast takes one and Calm Beast two, and it refills at a point a second. There is no bar for it on the screen: the pool is a number the band tells you only when it refuses you.

Two things that went out in 0.1.21 belong here because that release was never written up. Press F8, or REPORT A BUG in the pause menu, and a card opens: six words to pick from with OTHER already chosen, a box for up to two thousand characters, and two switches that ship on, the screenshot, taken before the card is drawn so the card is not in it, and the last 64 KB of what the game wrote this session, with anything shaped like a token, a ticket, an e-mail address or a user name in a profile path replaced before it is kept. The report lands as one row under the account you signed in with; the same report sent twice is one row; a report that cannot be sent waits in an outbox of up to twenty; and one account gets five an hour and twenty a day. This morning the card's sentence about the team reading these on Discord was ahead of the truth. Tonight the forum channel exists, the service reports that posting is on, and the first report filed will open its own thread there with the picture inside it; none has been filed since it was turned on, so no thread exists yet. And every item is in one of five tiers with a colour each, its name drawn in that colour on the crafting rows and in the bag, a card on a quarter-second hover naming the tier, the slot, the damage or the armour, the level and the sell price, and the numbers on that card derived from a budget rather than typed: a base by level, times the tier's multiplier, times a weight for the slot, with each tier's main number at least the tier below's plus one. Applying the rule moved twenty-six of the thirty-one pieces of equipment and corrected the two the catalogue had out of order. The catalogue is fifty items, seventeen common, eighteen uncommon, thirteen rare, two epic and no legendary, and nothing rolls; two reed bows are the same reed bow.

Before the zone, the character creator has a body in it. Choose a people on the Weavers' side and a figure stands in a panel to the left of the choices, lit by two lights of its own and nothing else, and turns under the mouse at half a degree a pixel or in fifteen-degree steps on the two TURN buttons; in the photograph it is the Reedfolk's body, a woman in a fur-collared coat with a satchel on her belt and long fair hair, standing. That body is a generated rig, one per people, and it says so by what it cannot do: the face is the one face, the coat is the body, the Wanderers stand as the Reedfolk because their own body is the Reedfolk's under another name, and a Tearer people shows a portrait and a sentence saying its body is not made yet and in the zone it wears the Reedfolk's until it is. The faces to come are another road. In Settings, the Controls page reads as words now, Chat, Report a bug, Open the journal, Talk to a person, Strafe right, in a scroll box, and the four movement rows are named from what the key does and not from the key, so a player who moves forward to K still reads Move forward beside K; photographing that fix found a second fault nobody had seen, that rebinding a key moved its row to the end of the list, and that is fixed in the same release. And the interface scale reads as a percentage of the intended look rather than as a raw number, so the size every screen was designed at reads 100 and not 70.

What is not in it, plainly. No copper. The harbour trade exists in the data with three things on its shelf, the crafting attempt has a price written for it, and the half of the character service that would charge it is deliberately not running, because at a balance of nothing it would refuse every craft and there is no way yet to earn a coin; the counter you would walk up to and the quest's payout are being built now, and until they land crafting costs nothing and a turned-in quest's copper is a number the zone writes down. No way to gather: the reed beds are a mesh you walk through, the deer drop nothing when they fall, so the arrival quest cannot be finished and the tracker's cannot be taken. No equipment panel: the character glyph refuses by name, and the paper doll is designed and not built. The wells past the first two are empty, and the two that are full carry initials because no icon is drawn. The creator's body is a generated rig and not a face anyone chose. The cloud deck's cost was measured on one desk's card in a viewport a quarter the size of a 1080p frame and scaled up by arithmetic, so on a small card it is a guess until somebody measures it, and the deck stays on at every preset until they do. The ground is a plane, the huts and the deer are blocks, the settlement in the minimap is blown to white, the word under a glyph still sits in the engine's own white box, a whisper reaches only somebody in the same zone, and nothing on the screen shows you your stamina until you run out of it.

Milestone

The first thing you can make

Release 0.1.20 carries the first craft: a panel on P, five professions, thirty-six recipes read from the world's own data, and a server that keeps the clock. Also the first thing a server compiler refused that a desk compiler had let through for four days.

Until this release a character could walk, jump, look at reeds and be counted. Now it can make a thing. Press P and a panel opens: a rail of five professions, a list of what each one knows, and a column that says what a recipe needs and what your bag holds against it -- four of four cut reed, two of two hide cord, none of one bog iron, in a warning colour where you are short. One button. It sends the recipe's name to the server and nothing else.

That last sentence is the whole design. The server holds the recipe, the timer and the bag; the client holds a picture of them. When you press CRAFT the zone checks that you are admitted, that you are not already crafting, that the recipe exists, and then runs the recipe's own time on its own clock, watching four times a second for the things that should stop it: a death, a lost body, or a step further from where you started than the world allows. The distance is a number in the database, not in the code, so it can be tuned without a build. When the clock runs out the zone asks the character service for the craft under one key, exactly once, and reads the answer back into your bag. If the network drops between the two, the same key is retried and the service answers the same way, which is what keeps a flaky connection from making two swords out of one set of parts.

Thirty-six recipes across five professions -- smith, herbalist, cook, builder, cartographer -- all of them rows in the same tables the site's content editor already publishes, so a recipe is a thing a person types and a version number moves, not a thing a programmer ships. The stations the recipes name (a forge, an herb bench, a firepit) are not in the world yet; the zone counts the check it would have made and lets the craft through, and says so in its log, which was chosen over pretending the huts had anything in them.

What it cost was the release itself, because a client and a server that share a new message have to go out together, and the server is built on a different machine with a different compiler. The Windows compiler had accepted, for four days, a route pattern written inside a comment -- the kind of thing that reads as documentation and is, to clang, a nested comment and an error. The server build died on it at the first attempt, both files were reworded in words instead of glyphs, and both targets built clean on the second. The whole thing, from the word to the download page, took an hour; the gate that plays two clients against the new server before anyone else can see it was run before the channel moved this time, so a red gate would have cost nothing public.

What is not in it: no icon on any recipe (the panel is words until the icon sheets are cut, and that work started the same week), no station in any hut, and no bag you can open on its own -- the panel shows what the bag holds, and the bag panel is the next thing the same column already draws half of. And the HUD you would expect around all this is three lines of text and three buttons in this release, which the person paying for the game looked at and, correctly, did not recognise as a HUD. That one is being redrawn.

Milestone

The first bow

The first thing ever made in Zizeeria is a reed bow, crafted this morning from four reed fibre and a marsh hide. Thirty-six recipes, five professions, a chance to fail and a drawing that teaches seven at once -- all on the server, and none of it yet reachable from inside the game.

The first thing ever made in Zizeeria is a reed bow. It was made this morning, a little after six, from four reed fibre and a marsh hide, for a character nobody was playing -- because the act of crafting lives on the server, and nothing in the game can yet ask for it. That sentence has two halves and the second is the honest one. Both are worth explaining.

Crafting here is five professions -- Smith, Herbalist, Builder, Cook and Cartographer -- and one rule from the plan: the best equipment in the game is player-made, and what drops from a raid is materials and drawings, never a finished weapon. Until last week that rule was a sentence. There was not one recipe in the world, four drawings that taught nothing, and a finished reed spear dropping from a marsh predator. There are thirty-six recipes now -- thirty-one for the smith, three for the herbalist, two for the cook; the builder and the cartographer have nothing to make yet, because nothing in the catalogue is theirs, which is a gap in the catalogue rather than in the professions -- each taking its ingredients from ten materials, and nothing a profession makes drops from anything any more.

Every profession's first recipe is within reach at level one, on purpose, so that the profession is never what stops you: a reed bow is reed and a hide grip, a reed spear is a bog-iron head on a reed shaft. Each thing you make earns the profession experience, and the ladder climbs with the catalogue -- the smith's ends at a Binder's rod, thirty-three rungs up -- because a profession is meant to take as long to master as a Calling. The numbers on that ladder are guesses, and they are written down as guesses; nothing has been measured against a player yet.

A craft can fail. One attempt in ten does, today, and on a failure half of what went in comes back -- rounded down, so a single hide lost is a hide lost. The roll is drawn on the server and recorded with the attempt, and the same attempt sent twice, by a client that lost its connection and asked again, is answered with the first answer rather than rolled again. The first bow's draw was 998,600 out of a million, against a failure line at 100,000, which is to say it was never close.

Ten of the thirty-six are learned from a drawing rather than with the level. A drawing is an object in the bag; reading it burns the paper and teaches everything on it at once. The Reedwoven drawing teaches all seven pieces of the set, and it drops from the common creatures of Reedhaven one time in ten, or comes as the reward of the first quest there. The first one was read this morning, and the character who read it now knows seven recipes and can make none of them yet: the set sits one rung up the ladder, and that character is a smith of level one with ten points of experience, all of them from the bow. A drawing that teaches nothing you do not already know is refused and stays in the bag, rather than burning for nothing.

None of this can be reached from inside the game today, and it is worth being exact about why. The act runs on the server, and the client people download has no panel that asks for it. The panel is designed and being built, and it goes out with the next client -- together with the server build it needs, because the two have to change at once. Until then the only hands that can put a material in a bag are an operator's, which is how the first bow's fibre and hide got there: placed by hand, through the one route that exists for it, so the act could be watched on the real service with the real data rather than argued about from a test. And nothing in the world drops anything yet, because nothing in the world can yet die. The creatures' side of this loop is its own piece of work, and it is the next one.

The far end of the loop moved too. When a world boss is brought down for the first time, the Chronicle will say who did it -- the guild, when everybody who fought belongs to one, and otherwise every name -- and a guild that did it alone earns the naming of the region. The service that keeps the Chronicle can write that now; the zone's half of the conversation rides the next server build. Getting a zone able to speak to it at all meant a second credential on the zone host, and that was worth watching: installed last night, it was refused for seven hours because nobody had yet granted it the right to ask, and the refusal was an alert with the reason in its name rather than a file that quietly never appeared. The grant went in this morning, and the next attempt, two minutes later, cleared it.

So the plain summary is this. The crafting loop is closed on the server: a material goes into a bag, a recipe consumes it, a bow comes out, and the profession remembers. The part a player would touch -- pressing the key, waiting the three seconds a common recipe takes, seeing the bow appear in the bag -- is the next client. The part that gives the loop its real entry, a deer in the marsh that drops the fibre, is the one after that.

Milestone
A marsh-born figure of the other side, drowned-pale hide with silt and weed in its folds, reed cord unpicked through the shoulders, standing in black water among dead reeds.

Four more, on the other side

The Tearers were a side with Callings and nobody to be born to it. Now they have four peoples, the same stock as the Weavers' four after the tearing, and the other side is whole in the data.

The morning's post said the Tearers were six Callings and not yet a side, because a side is more than the roles it offers: it is the peoples who belong to it, the rule that a character's side follows from its people, and a character screen that offers one side's Callings and not the other's. By the afternoon two of those three existed, and the third is a matter of drawing two buttons.

The peoples first, because that was the open question. The Weavers have four -- the Reedfolk, the Ashborn, the Deepbound and the Wanderers -- and the answer was to mirror them rather than invent beside them: the same four stocks, after the tearing. The Rotfolk were born to the marsh after the reeds died. The Ashgnawed are children of the burn who never rebuilt, and eat what the fire left. The Deeploosed were raised under stone and cut loose from it. The Strayed belonged to the road between places, and then the road forgot them. Each is drawn, in the same hand as the Tearer Callings, and each is a draft until a person paints over it.

Then the rule. Every people and every Calling now names its side in the database, and the service that creates a character refuses a Calling that is not of the people's side -- by name, so a client can say which of the two choices was the wrong one. The creation screen narrows what it offers to the chosen people's side, but the narrowing is a courtesy; the refusal is the rule, because a client is a client and the server answers for the world.

With a people behind the side, the six Tearer Callings went into the data too, and their abilities mirror the Weavers' number for number: the same cost, the same resource, the same cooldown, cast and range, read off the Weaver ability rather than typed a second time. A mirror, not a handicap, is a claim about balance, and the honest way to make it is to make it true by construction. What differs is what each does to the world: the Warden's calm is the Goader's stampede, the Binder's servant is the Unbinder's feral.

What is not done is the part a player would touch. The screen that creates a character has four tiles for peoples, which is one side's worth, and the button that chooses a side is drawn in code and not yet in the interface. And the starting region has to learn where four new peoples arrive, which is a change to the map itself. Both are content rather than code, and both are next.

Milestone
A hulking horned beast, low and broad, frayed cord and rusted bronze grown into its shoulders and a drover's goad grown from one forelimb, a herd blurring away behind it.

Six things that came apart

The game has had six Callings and one side. It now has two: six Tearers, one for each Calling, drawn and on the site, and not yet choosable.

Zizeeria has never had a villain, and that was not an oversight. The world is held together by the Weave; the Weave tore; the players are the Weavers who can still feel the threads. There is nothing in that to fight, which is a fine premise for a world and a poor one for a Tuesday evening. So the question was put plainly: six good ones exist, what are the six bad ones called, and what do they look like?

The first answer was wrong and worth recording. Asked for dark mirrors of the six Callings, the first sheet drew six people in armour, which is what the phrase suggests and not what the world needs. It was rejected on sight. The other side of this game is not an army of people in different colours. It is what came apart when the Weave tore: bodies that were once bound by it, frayed cord and unpicked thread run through hide and bone and rusted bronze that has grown into them. Rivarna, the Tearers. A side you can choose and play, the way the other games have two, with one Calling for each of the Weavers' six and the same hold on the world turned to harm.

The Warden calms a herd; the Goader drives it into stampede and takes its blows from behind the panic. The Pathfinder reads the land; the Misleader writes lies into it, and a migration led astray is a population that starves in the wrong place. The Reedsinger draws light up out of the water; the Rotsinger draws up rot. The Ashbearer burns the region it fights in and pays for it; the Ashsower burns it and pays nothing. The Binder ties a creature into service; the Unbinder cuts the cord and looses it feral on whatever is nearest. The Chronicler writes the world's history; the Unwriter eats the page.

They are on the Callings page now, each beside the thing it mirrors, with the same role and resource. The portraits are drafts, made the same way the Callings' were and recorded the same way: generated from a written brief, no human pass yet, and the ledger says so on every one of them. The one accent colour is the palette's ember, the red this site already uses to say something is wrong right now, because a torn thread is exactly that.

What they are not, yet, is choosable. A side is more than six Callings: it is the peoples who belong to it, the rule that a character's side follows from its people, and a character screen that offers one side's Callings and not the other's. None of that exists today, so the six rows are written and deliberately not inserted -- six extra Callings on the current screen would be six choices with no side behind them. The peoples of the Tearers are the next thing to draw, and the rule is the next thing to build.

Fix

Three screens nobody could photograph

Every picture this project had ever taken of its own client was of the login screen, because the rest are behind a sign-in. Three faults had been shipping in the dark.

The report came with a screenshot, which is the useful kind. Past the sign-in, on the screen that offers to send a character into the world, the painted background stopped two thirds of the way across and everything to the right of that line was washed black. The card sat in the wash. It looked like something had failed to load.

Nothing had. Four of the screens deliberately darken the painting behind them, because they are made of words and a lit stone arch behind a name field is a lit stone arch somebody has to read through. Each of those screens drew that wash for itself — and each of them lives inside the right-hand column of the shell, beside the news panel. So the wash covered the column and stopped at its edge.

The screenshot proved it without anybody needing to run anything. The card centres itself in that column, and in the picture its centre sits at 1357 pixels across a 1920-pixel window, which puts the column’s left edge at 794 — give or take the few pixels of a border, exactly where the line was. Two other explanations were ruled out the same way. The interface scale was not involved, because the capture that looks correct was taken at seventy per cent and its own settings file says so. The darkness value was not involved either, because the settings screen uses the same one and is intact — for the unrelated reason that it hides the news panel and therefore gets the whole width.

The wash is one thing in the shell now, over the painting and under the interface, and the screens ask for it rather than drawing it. It is switched by visibility rather than by opacity, which sounds like a detail and is not: a transparent panel in this engine draws nothing at all, so a wash authored invisible could never have been faded up. The obvious control is the one that cannot work.

The same screen carried a second fault, and it was older. The button that starts the game pointed at the local machine — not at a server, at whatever happened to be running on the player’s own computer, which is nothing. That default is correct in the code and deliberate: a client with no configuration should aim at a zone somebody is knowingly running rather than at a live one. What was missing was the line of configuration that every shipped build should have carried and none ever did.

And a third, on every screen at once. The ornamented plate behind the primary button on each page was cut from the approved artwork with twenty-two pixels of the page behind it baked into each end — opaque, and darker than the card it sits on. That kind of frame draws its ends at a fixed size whatever the button’s width, so those pixels became a black box at each end with the bronze flourish stranded inside. On the widest button it was a fifth of the width. On the settings screen, where the buttons are narrow, it was fifty-nine per cent — more of the button was margin than button.

What connects all three is duller than any of them and matters more. Those screens are behind a sign-in, and the machine this client is built on is not allowed to handle a password. So every picture ever taken of it, in months of work, has been of the login screen. Three faults sat there in plain sight of anybody who had signed in, and nobody who could sign in was looking at the layout.

The fix for that is one switch on the command line that forces a screen. It was written before the wash was repaired, so the repair could be photographed rather than argued about — and the same picture is what confirmed the plate and the server address in one frame. It is not compiled out of the shipping build, which is a decision: a switch that only exists in a build nobody ships is a switch that cannot photograph the build people actually have.

Fix

A setting that could not be put back

The client has a look now, and getting there meant finding out that both of its scale settings were quietly destroying themselves.

The report was four words long and entirely accurate: with scaling, the whole interface goes strange. What made it tractable is that a client keeps its settings in a file, and that file said exactly what was being looked at — interface at seventy-four per cent, text at a hundred and sixty-three. A description of a fault is an argument. A saved state is a reproduction, and it can be photographed.

So it was, six times, at six combinations of the two sliders, all at the same window size and all through the same capture. That is more pictures than the problem seemed to warrant, and it is the reason four separate defects came out of it rather than one.

The first is the one that earns the title. The settings screen is built once and kept, so opening it a second time re-runs its setup over the same labels — and that setup read each label's original size straight off the label, which by then was carrying the size the screen itself had written on the previous visit. At a hundred and fifty per cent, the first visit showed text half again as large, the second showed it twice as large again, and the third was over three times the size it started at. Dragging the slider back to a hundred could not undo any of it, because the number it multiplied had moved with the text. Below a hundred it runs the other way and the words shrink toward nothing: sixteen points becomes eleven, then eight, then five.

Underneath that was a second one that only exists in the client people actually download. Two ways of asking a label its size disagree unless the display is at a particular resolution — which the editor is and a packaged build is not — so every write shrank the text by a further quarter, in the one configuration nobody here had ever measured. Both are gone: the original size is remembered once, per label, and read and written in the same units at both ends.

The other slider had the opposite fault. Above a hundred per cent it did nothing at all — not a little, nothing. The composition sits inside a box that shrinks it to fit the window, and that box divides out precisely what the setting multiplies in, so the two cancel and the arithmetic is exact. The pictures agree: a hundred per cent and two hundred came back within one byte of each other. Below a hundred it did work, and left the interface floating in the middle of the screen with the footer stranded above the bottom edge and the window buttons drifted in from their corner — which is what the original report was actually describing.

The box follows the window now. It is sized to exactly the space available, so its edges are the screen's edges at every scale, and anything anchored to one is anchored to the other. The footer sits on the bottom. The buttons sit in the corner. The interface gets smaller and the frame stays where it is, which is what making a UI smaller ought to mean. Seventy per cent is what a fresh install gets, because that is the one somebody looked at and liked, and it is worth being the default only now that the layout can reflow into it.

The part worth writing down is the part that went wrong while proving any of this. The compounding is a claim about what happens on the second and third visit to a screen, so the obvious check is to drive the real client, open the screen three times and photograph each. That harness cannot answer the question. The buttons move as the text grows, so scripted clicks miss them; and the step that waits for the screen looks for a line in a log it does not clear, so the second and third waits return immediately and the clicks race. Two runs produced a photograph of the login screen carrying an error message, and nothing in either run's output said the picture was of the wrong screen. Only opening it did.

So the decision moved out of the widget instead — the same move this project made a week earlier for the menu's screen choice, and for the same reason. A check that needs a running client is a check nobody runs, and a check whose apparatus can fail silently is worse than none. It is four small functions now, and a test that walks four scales against four visit counts with no world, no game instance and no interface at all. Put the old line back and it fails nine assertions, at exactly the values the arithmetic predicts. At a hundred per cent it stays green in both, which is the whole explanation for why nobody who never touched the slider could have seen this.

There was one more picture worth mentioning, taken while checking that none of the above had broken anything else. It showed the sign-in card as a black rectangle, the news panel empty and scattered black squares where letters belong — a total collapse, convincingly. It is a frame captured before the fonts had finished loading. Retaken twice it was normal both times. Nothing in the tooling can tell the difference; the only tell is that the file was fifty-six kilobytes away from the picture it should have matched, which is why it got opened rather than believed.

Along the way an adversarial pass over every button on every screen traced seventy-one controls to their handlers and found nine that promised something they did not do. Rebinding a movement key onto another movement key was accepted, and left two contradictory bindings on one key and one direction bound to nothing, while the screen said the rebind had worked. Closing the settings neither saved nor discarded, so a restart resurrected whatever had been changed. The character creator kept the previous name in the box, so pressing create again replayed the first request and reported a success that made no character. Create account told everybody to open a link that only one of two possible deployments sends. And the drawn envelope and padlock inside the text fields were taking the clicks aimed at the fields behind them.

Two releases went out, each verified before its installer reached the site rather than after: the published archive unpacked to an empty directory, the launcher pointed at the live channel, fourteen files fetched and checked, and the game started and drew. The second of them is the first version number this project has had with two digits after the last dot, which is where naive version ordering breaks — as text, 0.1.10 sorts before 0.1.9. The download page compares the three numbers separately and has since it was written, with a comment naming that exact trap. Today was the first time it was ever tested, and it was tested by looking at what the page offered.

Fix

The gate that caught itself

Pre-production is closed, on evidence this time. The check that closed it very nearly passed with the keyboard unplugged.

The last post said no character in this game had ever taken a step, and that the milestone recorded as met four days earlier had been certified by a script whose only possible non-zero exit was a missing executable. Both are now false, which is the cheerful way of saying the work got done.

A cube walks. Two clients connect to the same zone, each spawns its own character, each sees the other's arrive over the wire, and each watches the other move — one of them travelled a little over a thousand centimetres while the machine watching wrote down where it was four times a second. The keys are bound in code rather than in an asset, which is what this project's own rule asks for on anything in the gameplay path: a binding somebody can read in a diff beats one buried in a binary file.

The part worth writing down is not that it works. It is what happened when the check was pointed at a build with the input deliberately switched off, which is the only way to learn whether a check can tell the difference. It passed. It reported eighty-four centimetres of travel and called that movement.

Both players were spawning on the same point. Two character capsules cannot occupy one position, so the physics pushed them apart — by exactly two capsule radii, every time, which is where the eighty-four came from. The threshold for 'it moved' had been set at fifty. A collision artefact had been quietly clearing the bar since the bar was drawn, and it would have gone on doing so on every future run, in a check written specifically to stop this project believing something it had not seen.

The threshold is two hundred and fifty now. Collision tops out at eighty-four and walking covers seven to ten times that, so the gap between the two is no longer a matter of opinion. The spawn selection was fixed as well — sixteen spawns across eight runs, all on distinct points, where every recorded run before it had put both players on the same one.

And then the fix broke the check again, in a way worth admitting. With players finally starting apart, the movement assertion reported twelve hundred centimetres travelled with the input still off. Both clients name their own character with the same identifier, because the number comes from that connection's own numbering rather than from the world. The check pooled both logs and grouped by name, so two motionless characters standing on opposite sides of the map became one character crossing it. That fault had been present from the beginning and could not possibly have fired while both players stood on the same spot, because the distance between them was zero.

Three false greens in one afternoon, in three different places, all of them in the instrument rather than the thing being measured. That is not a run of bad luck. It is what happens the first time anybody insists a check demonstrate that it can fail before being allowed to say something passed.

Then a person opened the client and looked at it, which nothing automated had done, and found two things in about ten seconds. The mouse was inverted — the pitch direction had been reasoned carefully from the engine's own source, both cited facts were true, and the conclusion came out backwards. And a decorative shield placed in the harbour turned out to be a hundred metres tall on a map forty metres across, at default scale because nobody had ever said what size it should be. It also explained a number that had been sitting there unexplained: characters walked measurably less far in that one region than in the two empty ones. They were walking into it.

Both are fixed and the mouse is a setting now rather than a constant, since half of everybody wants the other one. But the lesson is the shield. Every check in this project was watching numbers, and the numbers were fine — the region was simply wrong to look at, and looking is the one thing none of them do.

Pre-production is closed. What comes next is a vertical slice: one region built properly, two of the six Callings, and combat — which was designed this week and turns on a constraint that was already in the plan without anybody noticing. The ecosystem simulation and a treadmill of respawning creatures cannot both be true. Kill five hundred of something that comes back in ninety seconds and the simulation is decoration. So killing is one resolution among three, alongside moving a creature somewhere else and leaving it alone, and the region remembers which one you chose.

Fix

The cube has never moved

Pre-production was declared closed on a criterion with the word move in it. Two clients did see each other. Neither of them could walk.

Four days ago this log published a post titled "A cube can move". It is wrong, and which half is wrong is the interesting part. Two Windows clients did connect to the same zone server, the server did count one and then two with a pawn for each, and both windows did render the other player's character. All of that happened and all of it stands. What did not happen is the verb the criterion turns on. Nothing moved. No character in Zizeeria has ever taken a step, because no key on either keyboard was connected to anything.

The movement itself is written, and it is right. The character has a fully configured movement component — walk speed, braking, air control, rotation that follows the direction of travel, network smoothing tuned for the latency the design targets — and it replicates. The movement function takes a two-axis input, flattens it against the camera's yaw so that a player pushing forward while looking down walks along the ground rather than into it, and hands the result to the engine call that proposes a move to the server. That is not a sketch or a stub. It is the finished thing, and it has never once been called.

What is missing is the binding. Three properties say which key means forward: a mapping context and two input actions. All three were marked as set outside of code, under a comment giving the reason — input bindings are content rather than source, and content belongs in an asset a designer can edit. Two things are wrong with that. The asset does not exist: there is no pawn Blueprint, and no Blueprint of any kind anywhere in the project, the entire content tree being three maps and five files belonging to one wooden shield. And the rule at the top of that directory had already ruled the asset out — Blueprints are for the interface and for throwaway prototypes and nowhere else, because a Blueprint on the gameplay critical path is one nobody can diff, review or merge, and moving a pawn is as critical as that path gets. So the comment was reaching for a file the project had decided not to have. The game mode hands out the bare C++ class as the default pawn, nothing ever supplied the three, and they were null from the moment the client started with the movement code sitting behind them.

It fails without saying a word, which is how it survived four days of being described as working. The call that installs the mapping context is inside a test for whether the mapping context exists; each of the two calls that bind a key is inside a test for its own action. A null pointer does not take a branch nobody wrote — it skips the block, and the frame goes on. There is an error log a few lines away, but it guards a different question: whether the input component is an enhanced one, which it always is. So every client log this project has ever produced is silent on the subject, and a silent log reads exactly like a working one.

The script that was meant to certify the milestone states three checks in its own header, and states them well: each client must log a successful join; the server must log two of them, because one client joining twice is not two clients; and each client's log must name a pawn it did not spawn itself, which is the other player arriving over the wire. The third is the one that carries the claim, and it is not implemented at all. The second is asked of the clients instead of the server, which throws away the reason the header gives for asking it — the clients are exactly the witnesses that cannot tell two of them apart from one of them twice — and it prints whatever it finds without judging it. The first does reach a verdict and then does nothing with it. The only exit code the script can return is for a missing client executable, so once it has found something to launch, a run that satisfies none of the three ends exactly as successfully as a run that satisfies all three. That is the run that was recorded as a pass.

So the plain consequence: Phase 0's exit criterion is not met. The plan's sentence is that a cube can move in a networked zone and that two clients can see each other move, and this project's own rule is never to enter the next phase before the current one's criterion is met. Applied to itself, that reopens pre-production until somebody has pressed a key and watched a cube move on another person's screen.

The small part is now done, and it was small. Three objects and the keys that go in them — a mapping context, two input actions, the familiar four letters and the mouse — built in C++ where the rest of this path lives, so they arrive in a diff somebody can read rather than inside a binary file nobody can. A cube walks. Measured rather than watched: pressing each key for a second and a half moves the character along its own axis and no other, with nothing leaking sideways, which is the specific failure this kind of binding produces when the input's shape is declared wrong. Then the binding was deliberately broken again, to confirm that the two error lines added alongside it actually appear. Four days of silence is what let this hide.

That does not close the criterion, and it is worth being exact about why. The sentence is that a cube can move in a networked zone and that two clients can see each other move. What has been demonstrated is one character walking, in a headless run, with the keys injected below the layer a real keyboard goes through. Nobody has yet pressed W on one machine and watched a shape move on another. The gate script has been rewritten to ask that — it parses the server for two joins rather than asking the clients, requires each client to name a pawn it did not spawn, and asserts a position actually changed — and it was pointed at the old recorded run first, where it comes out red and names what is missing. It fails against today's build too, on the movement check, because the fix has not been cooked and shipped to the zone hosts. Pre-production stays open until it goes green on its own terms. A check nobody has watched fail is not a check — it is a script with an opinion.

Both earlier posts keep their addresses. "A cube can move" is now titled "A cube can be seen", which is what its own body described all along — two windows side by side, each showing the other player's character standing on the ground — and it now opens with a note pointing here and no longer ends by handing the project to Phase 1. Its slug is untouched, and that is deliberate: the slug is the identifier a feed reader and the announcer in Discord deduplicate on, so renaming it would send a four-day-old post to everybody a second time, while correcting its words sends nothing. That is the right way round. The text is allowed to be corrected; the identity is not. The post before it, "Two now connected", is left exactly as it stands. It reported what the server logged and nothing more, and it said in its own words that nobody had yet looked at a screen — which is the half of that evening that turned out to be worth the most.

Fix

Seventeen alarms nobody had heard

One of them could not have fired in any state the world could be in. Finding that out took asking a question the green dashboard could not answer.

Seventeen alerting rules watch this project: backups that have stopped succeeding, a zone falling below a playable tick rate, a log shipper that quietly stopped, a disk filling. They had been evaluating for days and every one of them was quiet, which is what you want. It is also what a rule that cannot fire looks like.

Two of them were eventually forced to fire by hand, by planting a stale file on the live host and watching the alert go pending, then firing, then clear. That is worth doing once. It is not worth doing seventeen times: it takes a person, it touches production, and it proves the rule as it was that afternoon rather than as it is after the next edit.

The second one is the reason this post exists. It compares each zone's content version against the highest any zone is serving, and it had been written the obvious way: take the maximum, subtract, alert if the difference is positive. That expression matches nothing. Not when the zones disagree, not when they agree - nothing, in every possible state of the world, because taking a maximum discards the labels that say which zone a number came from, and there is then nothing left to match the two sides on.

So it returned no alerts. And no alerts was the correct answer that day, because the zones were in fact all on the same version. A panel fed by the same expression read zero for a day and was right to. There is no observation you could have made of that rule, short of breaking something on purpose, that would have told you it was broken.

The fix is not cleverer monitoring. It is Prometheus' own test framework, which feeds invented measurements to the real rule evaluator and asserts which alerts come out. All seventeen rules are under test now, and every one is asserted twice: once with data that must make it fire, and once with data that must leave it silent. One direction alone is worthless. A test that only checks firing cannot tell a working rule from one that fires always; a test that only checks silence cannot tell one that fires never, which was exactly the bug.

Then the suite was pointed at itself. The old broken expression went back in, on purpose, to see whether the tests would notice - and the case went red naming the zone it should have caught. A test suite nobody has seen fail is decoration, and this project has been bitten by decoration often enough to stop trusting green on sight.

The same shape turned up twice more the same day, which is why it is worth writing down rather than filing as one bug. A test in the authentication service spent ten seconds failing on a developer machine for the wrong reason: it needed a database, was not marked as needing one, and so neither skipped cleanly nor failed honestly. And a piece of infrastructure was configured to narrow something and, because of how the network underneath it behaved, narrowed nothing at all - while reading, to anybody who looked, exactly like a working one.

All three are the same defect wearing different clothes: a check that cannot distinguish the healthy state from the broken one. It is a more dangerous class of bug than the ordinary kind, because the ordinary kind eventually shows itself. This kind produces exactly the reassuring output it would produce if everything were fine, and it goes on producing it for as long as you leave it alone.

The rule now, written down where the next person will find it: before trusting any check, make it fail on purpose. If you cannot make it fail, it is not a check.

Milestone

A cube can be seen

Two clients, one zone, and each of them can see the other standing there. Corrected on 2026-08-15: neither of them could move, so this was not the end of pre-production.

Corrected on 2026-08-15. This post was published as "A cube can move", and that title was wrong. Everything below about two clients seeing each other happened and stands. Moving did not: the character's input actions were never bound to a key, so nothing on either keyboard ever reached the pawn, and no character in this game had yet taken a step. Phase 0 is therefore still open. The full account is in "The cube has never moved".

The plan states the exit criterion for pre-production in one line: a cube can move in a networked zone, and two clients can see each other move. Tonight two windows were open side by side, connected to the same dedicated server, and each showed the other player's character standing on the ground under a red sky.

The chain behind that: the engine built from source on a machine bought for it, a Linux dedicated server cross-compiled and cooked with three maps in it, three server processes running one zone each on real hardware, a Windows client from the same source, and two instances of it connected at once.

The last missing piece was the smallest and the best hidden. Both players were connected, the server counted them, each had a character, the metrics said two — and the screen showed the floor and the sky. Unreal gives every C++ character a mesh COMPONENT and never an asset to put in it, so both characters existed, collided and replicated, and neither had anything to draw. Every signal we had said success. None of them could say whether there was anything to see.

That is the same lesson this project keeps paying for, arriving from a new direction each time: a check that cannot tell the healthy state from the broken one. A panel that always reads zero. A test that defends a false sentence. Two log lines that mean the same thing whether one client joined twice or two joined once. And now a world with two players in it and nothing rendered.

The character is a cube for now, and that is deliberate rather than sheepish: the criterion says cube, so the cube is the deliverable. It is scaled to the collision capsule rather than to a round number, because a visible shape that disagrees with what actually collides means the eye and the server disagree about where a player is.

Phase 1 is the vertical slice, and its exit criterion is a harder sentence: ten players play for an hour and want to come back. This paragraph originally opened it. It is not open — never enter the next phase before the current one's criterion is met, because the largest risk on this project's register is not technical, and a rule is only worth writing down if it also applies to the person who wrote it.

Milestone

Two now connected

Two clients joined the same zone at the same time and each was given a character in the world. The line that says so did not exist this morning.

The milestone that ends pre-production is stated plainly in the plan: a cube that can move in a networked zone, and specifically two clients seeing each other move. Today the server said this, twice, eight seconds apart:

zone Savhamn: player joined, 1 now connected, pawn ZizeeriaCharacter_2147482352 — zone Savhamn: player joined, 2 now connected, pawn ZizeeriaCharacter_2147482333

The count going from one to two is the entire point, and it is worth explaining why. Before this, the server logged a join per client and a timeout per client — and a run where one client joined, dropped, and another joined after it produced exactly the same two lines as a run where both were present together. Two clients and one client twice were indistinguishable in the evidence. We read those two lines as success once, before checking the timestamps and finding they were not.

So the server now logs the number rather than the event, using the engine's own tally rather than a counter we keep, and it warns when a player arrives without a character — which is the failure that looks most like success: connected, welcomed, and with nothing in the world for anybody else to see.

What is honestly still missing is a person watching it. Two characters exist in one world and the replication that puts each on the other's screen is the engine's default, but nobody has yet looked at a screen and seen it. That needs a display, and a build host does not have one. Until somebody has looked, this is the server half of the gate and not the gate.

Getting here today: the engine built from source in seventy-six minutes, a Linux dedicated server cooked in thirty-seven, a Windows client in forty-two, 2.2 GB moved twice through a transport that had to be repaired twice, and a five-minute path for recompiling the server without cooking it again — which is what made the last iteration cheap enough to do properly.

Milestone

Three zones, three processes

The Linux server is built from the engine source and running on its own machine — one dedicated process per zone, each with its own map and its own port.

Zizeeria's architecture says one server process per zone rather than one big world server, and until today that was a sentence in a document. It is now three processes on one machine, holding three UDP ports, each having loaded a different map and brought its world up for play: a marsh harbour, a burnt plain and a cave system.

Getting there took the engine built from source on a dedicated Windows host, a cross-compiler producing Linux binaries, a cook that packs the maps into a container, and 2.2 GB moved across the network. The build itself took seventy-six minutes and the cook another thirty-seven.

The interesting failures were in the last hundred metres. Moving the build with the obvious command — pack it on one machine and unpack it on the other through a single pipe — does not work, and it does not fail either: it hangs, completely, with the connection established, the receiving process running and zero bytes ever crossing. It sat like that for eleven minutes before anybody measured the far side instead of watching the near one. The cause turned out to have nothing to do with the size or the network, and the way to find it was to shrink the problem until one small file could be watched failing.

What this is NOT yet is the milestone that ends pre-production. That one is stated as a cube that can move in a networked zone, and specifically as two clients seeing each other move — which needs a client, and the client is the next build. The server half is done; the half a player would notice is not.

There is a rule about this in the project's own plan: never enter the next phase before the exit criterion is met. It exists because the largest risk on the register is not a technical one — it is deciding a thing is finished because most of it is.

Milestone

A character exists

The fifth service runs. A character can be created, its stats computed by the server and read back — over the same route a zone server will use.

Character state is the part of an MMORPG that cannot be trusted to the client, so it was built as its own service with its own database role: it may write the tables that hold who you are, and read the catalogue it needs to work out what you are wearing. It cannot write the catalogue, and it cannot write the rules about which Callings may equip what. Those belong to content, and a rule a request can change is not a rule.

Deploying it took ten steps and three of them fail quietly if done wrong, which is the interesting half. The migration tool reported success while doing nothing, because the container image it ran from predated the change it was asked to apply — from inside that image, "nothing to apply" and "I have never heard of this" are the same sentence. A missing flag on one command handed the authentication service a placeholder password and took logins down for about ninety seconds; the command printed that it had started the service and exited successfully.

So the test that says this works does not ask whether the process is running. It mints a real token, creates a character over HTTPS, reads it back, and checks one number: a Warden's health must arrive as 15, being a base of 12 multiplied by their 1.25. A service whose stat model failed to load answers 12 — with a 201, and nothing in the logs. The status code cannot tell those apart. That number can.

Everything the test creates, it erases, and then it counts the rows to make sure. The first version reported that it had cleaned up while leaving a character behind.

Note

A review that argued back

Six independent reviewers were pointed at one evening's code and found fifty-seven things. Twelve of them were real, and two had been introduced by earlier fixes.

The service above was written in one session by one author who was also its only reader. A hundred and ten tests passed, the type checker was clean, the linter was clean. None of that is a claim about whether a single statement matches the database schema, because every one of those tests had been written to need no database — which made them fast and made them structurally unable to see the two defects that stopped the service starting and stopped it creating anything.

So the code was reviewed adversarially: several readers, each given one lens, each told to report only what they could point at with a file and a line, and each finding then handed to a skeptic instructed to refute it and to default to refuted when unsure. Of fifty-seven claims, twenty-seven were already fixed or were wrong, ten were genuine forks where the fix means choosing something, and twelve were real defects with a traced path from an input to a wrong answer.

Two of the twelve had been created by previous corrections. A fix for a full bag refusing a swap made every swap fail. A fix meant to make a monitoring gauge honest wrapped the two lines that cannot fail and left uncovered the one that can. That is the shape of most of what was found here, and it has a name worth saying out loud: a check that cannot tell the healthy state from the broken one. A panel that always reads zero. A test that defends a sentence that is false. A counter that freezes on good news.

Every fix was verified the same way. Remove it, watch the test fail, put it back, watch it pass. A test that has never failed has proved nothing.

Infrastructure

The engine has a machine

Unreal Engine 5.8 is built from source on a dedicated Windows host, with a cross-compiler the engine itself agrees can produce Linux servers.

Zizeeria's dedicated server runs on Linux and the engine is built on Windows, which means a cross-compiler and a matching toolchain pinned to the exact version the engine expects. That combination was verified before anything was built on top of it: a program compiled on the Windows host, sent to a Linux machine, and run there. Discovering a broken cross-compiler after a two-hour build is the expensive order to discover it in.

The engine now lives on its own host rather than on a workstation — a hundred and nineteen gigabytes of it, before anything is cooked. Three of the four things that went wrong getting there were the same shape: a step that reported success and did nothing. An installer that returned zero having installed nothing, because an earlier flag had quietly removed the file it needed to know what to install. A build launched over a remote session that vanished when the session closed, leaving a log that stops mid-sentence and looks exactly like a build still running.

The fourth was better: the setup script hangs for ever on a machine with nobody at the keyboard, because its last step opens a dialog box asking whether to register the installation. Nothing in the log says so. The way to find it is to count the processes rather than read the log, which is now written down where the next person will look.

Milestone

The simulation runs

The ecosystem service ticks: populations, predation, food coupling and migration between regions, on a fifteen-minute cycle.

The service that makes this game different from the others is the first one we built past the schema. It holds a population per species per region, applies logistic growth against a carrying capacity, resolves predation with a saturating response, and moves animals across region borders when a region stops being able to feed them.

The saturating response is the part that matters. A naive predation term scales without limit and the model runs away inside a few ticks; with a handling time, predation saturates and the populations settle instead of oscillating into the safety cap. Every dial in the model is a row in the database, not a constant in the code, so tuning the world does not require a deployment.

The world data — species, the food chain, region borders, starting populations — lives in one file that both the service and the database seed read. It used to live in two, they drifted, and the copy in the seed ended up with predation rates a thousand times too large. One copy now.

Infrastructure

The database schema

Accounts, characters, content, the ecosystem and the Chronicle, with migrations, seed data and a verification pass.

Items, creatures, loot tables, spawn tables, quests, abilities and balance values are rows. That is not a preference; it is the mechanism by which most changes ship without a client download. A hard-coded balance number anywhere in this project is a bug.

The Chronicle got its own table early, before there was anything to write into it, because its columns constrain the rest. It stores actor names as they were at the time rather than as foreign keys — a guild that renames itself does not get to rewrite what it did.

The seed brings up three regions, six Callings, twenty creatures and fifty items on a fresh database, and it is idempotent, because it runs after every schema rollback as well as on every new developer machine.

Infrastructure

Repository and infrastructure

Monorepo layout, the Git/Perforce boundary, local infrastructure and the first meta-service.

Code is in Git and binary game content is in Perforce, and the boundary is the file type rather than the directory. Text a human wrote is versioned in Git; binary a tool produced needs exclusive locking and gigabyte-scale storage, which is the thing Perforce does and Git does not.

Continuous integration refuses files over a mebibyte, anything under the engine project tracked in Git, and anything credential-shaped. The jobs skip cleanly while a workspace is still empty, so a green build means what it says rather than meaning nothing ran.