Why I wrote an open quest spec instead of renting plugin chrome
Open Quest Format 0.1.0 is an Apache-licensed, versioned quest spec with a reference parser, validator and runtime, so your quest content outlives whatever engine or plugin you first jacked into, choom.
When you pick a quest plugin, choom, you are also picking the only turf your quest content will ever be able to live on. A Unity asset runs in Unity and an Unreal data table runs in Unreal, and a paid plugin keeps working for as long as its fixer keeps shipping updates, but not one day after he deltas out. The problem is that everything you wrote inside it, every step, branch and reward, is stored in a shape that only that one product knows how to read, so your whole questline ends up sitting behind somebody else’s ICE.
I started Open Quest Format because I wanted a way out of that corpo leash. Version 0.1.0 hit the street on 16 September, and what I am aiming for is a quest format that no fixer owns, including me, so that nobody’s quests depend on a single vendor staying alive and jacked in.
How quest data gets locked behind one tool’s ICE
On paper, quest data should travel light, like a courier with an empty bag. If you strip off the UI chrome, every quest system from Skyrim to Baldur’s Gate 3 is built from the same wetware underneath: steps, the conditions that unlock them, the objectives that complete them, rewards and endings. None of those ideas belong to a particular engine’s chrome, and no megacorp gets to claim them.
What makes them engine-specific is the way they get saved. The plugin stores your quests as its own asset type, its own table schema or its own serialized blob, and nobody hands you the docs for that shape, because it was never meant to be read by anything outside its own rig. In practice that means switching engines turns into rewriting all of the content by hand, scav-style, pulling every step out of the wreck one piece at a time, and moving to another plugin inside the same engine means exactly the same grind. And if the asset you paid eddies for stops getting updates, that is usually the moment you realise your quests were only ever as alive as the one merc maintaining it, and when he flatlines, they go down with him.
Why solo runners and small crews bleed the most
A corpo studio with a tools team can write its own quest system and own the format outright, keeping its ICE in-house, so this is mostly not their problem. The people who buy quest plugins are solo devs and small crews, because building a quest editor is about a month of work at the deck, and nobody on a two-person gig has that month to burn. They are also the chooms most likely to prototype in one engine and ship in another, or to shelve a project for half a year and jack back in to find the asset has been scrubbed from the store.
I run with that crew myself, since I build Cozy Coast solo, on Electron and Phaser. If I had wired a quest system straight into the game’s wetware, it would have been one more private format that only one game could ever read, a shard nobody else’s deck can open. So I decided the format had to come first, before any chrome, and the reference quest in the repo, The Harbormaster’s Ledger, was written as the first gig for Cozy Coast.
Choosing an open spec over plugin chrome
Plugins usually only run inside one engine, and they have been tailored to enhance how that engine works, like custom chrome cut for a single body. This is where the difference is: an open spec is independent of any engine, and it focuses on being translatable by any system, whichever netrunner happens to be doing the translating. What I wanted to protect was the content itself, and content is only safe if its format is written down somewhere any netrunner can implement it without asking a corpo for permission. That is why OQF is a published spec before it is anything else, and why it is released under the Apache 2.0 license, with no fixer taking a cut.
We want to make sure that, as the format evolves, we can always tell which version a file was written for and spot any differences down the line, the way a ripperdoc checks which generation of chrome he is cutting into before he picks up the scalpel. So every compact file carries its version in its first line, OQF1, like a serial number etched into the chrome. That number only changes if a file written today would stop parsing, and preventing that kind of flatline is exactly what the project exists for. For the same reason, columns in the compact format are append only: new columns get bolted on at the end of a record, and existing ones are never reordered or ripped out, so no ripperdoc ever has to rewire a working limb. When a parser runs into a record with more cells than it knows about, it keeps the extras under x-oqf.extra, which means an older tool can pass newer data through its deck without losing any of it.
I also did not want the spec to exist only as a document on some shard, because the easiest way to find the holes in a spec’s ICE is to implement it and run real quests through the code. So the spec ships with a reference implementation in TypeScript, a whole deck of packages: @oqf/core for the model, the parsers and the validator (no I/O, no dependencies), @oqf/runtime for the state machine, @oqf/dialogue for conversations with Ink and Yarn Spinner import and export, @oqf/i18n for translation tables, a CLI and a web editor. The rule across all of that chrome is that everything converts to or from the model in @oqf/core and never around it, so there is only one definition of what a quest is and no back-alley route a scav could use to skip it.
The README says an engine loader should take a few hundred lines, because the format is deliberately kept simple. I wanted to check that claim against the one loader I actually have, since a netrunner who quotes numbers he never ran is a gonk waiting to get zeroed, and packages/core/src/compact/parse.ts comes to 426 lines by wc -l, comments included.
Conditions are usually where quest systems drift into becoming a scripting language, a rogue daemon living inside your content that nobody can audit, and I wanted to stay clear of that, so OQF does not embed one. A condition is written as a line of infix text such as step.find_net, and it is stored as a JSON Logic tree that can only use eleven whitelisted operators, a short list of chrome that is cleared to run and nothing else. I picked JSON Logic because it already has implementations in JavaScript, C#, Python, Go, Rust, Lua and GDScript, which means a Godot loader can take its evaluator from a library that is already on the street today instead of some netrunner cooking up a new one. And since conditions are plain data and nothing in a quest can run arbitrary code, the validator is able to walk the whole step graph like a ripperdoc tracing every wire, and tell you that a step is unreachable before a player runs into that dead end.
For the native file I went with a line oriented, interned layout. Actors, locations and items are declared once and then referenced by index, like a fixer keeping one contact list instead of scribbling names on every job, and the first character of each line tells the parser what kind of line it is, so the parser never has to guess who it is talking to. That is the same trick I pulled for NWF, and it keeps the files lean enough to ride on any shard or deck. On the reference quest in @oqf/examples (nine steps, three endings, one of which is a failure), the .oqf file is 2,017 bytes, while the JSON form of the same quest is 8,551 bytes as it ships, pretty-printed, and 4,189 bytes after JSON.stringify with no indentation. JSON is still a first-class output with a published schema, so if the compact form is not your kind of chrome, choom, you never have to touch it.
A minimal quest gig in the compact format
Here is the smallest quest that does something useful, a two-step gig with one ending, the kind of job a fixer hands a rookie merc on day one. The cells on each line are separated by TABs.
OQF1
L dock|North dock
I net|Fishing net
Q lost_net 1 The Lost Net Maren lost her net on the reef.
R xp_done xp xp 25 returned
S find_net s collect net Find the net
S return_net step.find_net reach dock returned|Net returned 0 Bring it back to Maren
L and I are the location and item dictionaries, and Q opens the quest, the same way a fixer opens a contract. The R line defines a reward of 25 xp that is tied to the returned outcome, so the eddies only land when the job does. The first S line is a step flagged s for start, which gets completed by collect net, the merc’s first move. The second step unlocks on step.find_net, completes on reach dock, and ends the gig with the outcome returned. Any empty cell is a field that this particular gig does not need.
You can jack in the validator straight from npm without installing anything first, so a quick scan costs you zero eddies:
$ npx @oqf/cli validate lost-net.oqf
0 errors, 0 warnings
After that clean scan comes the runtime, the part of the rig that actually runs the job. The idea is that world events go in and quest state and quest events come out, and the runtime does not need to know anything about your engine to do that, so no engine’s corpo fingerprints end up on your questline.
import { readFileSync } from 'node:fs'
import { parseCompact, validateDocument } from '@oqf/core'
import { QuestRuntime } from '@oqf/runtime'
const doc = parseCompact(readFileSync('lost-net.oqf', 'utf8'))
if (!validateDocument(doc).ok) throw new Error('fix the quest first')
const runtime = new QuestRuntime({ documents: [doc] })
runtime.on((event) => console.log(event.name, event.payload))
runtime.offer('lost_net')
runtime.accept('lost_net')
runtime.emit('item.collected', { item: 'net', amount: 1 })
runtime.emit('location.entered', { location: 'dock' })
saveGame.quests = runtime.save()
I ran that code against the 0.1.0 source before writing this post, so what follows is a real run and not a doctored braindance. Every step transition is emitted as its own event, so you can follow each step like a netrunner watching the trace as it goes from locked to available to active to done:
quest.lost_net.offered {}
quest.lost_net.step.find_net.available { from: 'locked' }
quest.lost_net.step.find_net.active { from: 'available' }
quest.lost_net.step.find_net.done { from: 'active' }
quest.lost_net.step.return_net.available { from: 'locked' }
quest.lost_net.step.return_net.active { from: 'available' }
quest.lost_net.step.return_net.done { from: 'active' }
reward.granted {
quest: 'lost_net',
step: 'return_net',
reward: 'xp_done',
kind: 'xp',
ref: 'xp',
amount: 25
}
quest.lost_net.outcome { name: 'returned', failed: false }
On the engine side, crediting the xp only takes one listener on reward.granted, and that listener is where the eddies actually hit the player’s account. If your game already uses a name other than item.collected for pickups, choom, you can give the runtime an event map, so you keep your own street vocabulary instead of renaming your events to match OQF.
runtime.save() returns a small versioned state document containing the step states, the counters, the outcome, the rewards that were already granted, and a log of every transition with its order, a full trace of the run. The granted rewards are recorded so that re-entering a step never pays out twice like a glitched vending machine spitting double eddies, unless the step is marked to repeat. Where you stash that document is up to your game, because OQF is not meant to own your save file, now or later, and no corpo gets a copy of your shard.
What the fixer has lined up next
Incoming, choom. Version 0.1.0 ships the spec, the TypeScript reference runtime, the validator, the CLI and the editor, and the next pieces of chrome are already on the fixer’s board. Engine exporters for Godot, Unity and Unreal are coming soon in v1.5, Cozy Coast is set to be the first shipping game, and timers, factions, radiant quests and tabletop export are planned for v2.
The exporters will hit the street in that order, starting with Godot, then Unity, then Unreal, and each one comes with a loader on the engine side and one sample project, so every engine gets its own jack and a working rig to test it on. Until they are available, the TypeScript runtime is the chrome to run.
Cozy Coast is set to be the first shipping game on OQF, with @oqf/runtime as the loader from day one, and that integration is the next gig on my board. The roadmap is explicit about what counts as done: a player has to finish the reference quest in a Cozy Coast build through every route and every outcome, and the state has to survive a save and a load without getting zeroed. That is the bar I set before I say “it runs in a real game”, because street cred talk does not replace a real run, and it is what I am building toward now.
Today a step can only have one objective, one job per gig. The reference quest actually needs two on its trade step (collect five silver koi and talk to the otter), so for now the talking half rides on a dialogue binding and a flag, which any merc would recognise as tape over a cracked casing. Supporting a list of objectives is a candidate for v0.2, but I would rather wait until the runtime has lived inside Cozy Coast long enough to show that it is really needed, and not just a netrunner’s hunch.
The packages are still 0.x and share a single version number, and the TypeScript API may still shift between minor versions while the rest of the chrome lands, so I would recommend pinning a minor range, choom. The file format is the part that stays frozen solid as ICE, because the v2 features are designed so that they can land without breaking a single OQF1 file.
How to jack in and try it
Like the validator, the editor runs without an install. Running npx @oqf/editor serves it locally on port 4600, and the whole rig then runs in your browser: you draw steps on a graph, edit every field with live validation, and play the quest through @oqf/runtime in the Play tab, a braindance of your own questline before any player jacks in. You will need Node 24 or newer for it. The easiest way to start is from one of the nine templates, and linear, for example, is a longer cut of the lost net quest, with Maren pacing the north dock.
The spec, the reference quest and a folder of deliberately busted fixtures, gonk quests on purpose, all live in the repo on GitHub. If you wire a loader for your engine’s rig, or you find a quest that the validator should have caught and didn’t, please open an issue, because at this stage that kind of intel is the most preem thing any choom can hand the project.