Yarn
The yarn package implements the YarnSpinner virtual machine in Go.
Install / Use
/learn @DrJosh9000/YarnREADME
yarn
A Go implementation of parts of Yarn Spinner 2.3.
The yarn package is a Go implementation of the
Yarn Spinner 2.0 dialogue
system. Given a compiled .yarn file (into the VM bytecode and string table)
and DialogueHandler implementation, the VirtualMachine can execute the
program as the original Yarn Spinner VM would, delivering lines, options, and
commands to the handler.
Supported features
- ✅ All Yarn Spinner 2.0 machine opcodes, instruction forms, and standard functions.
- ✅ Custom functions, similar to the
text/templatepackage. - ✅ Yarn Spinner CSV string tables.
- ✅ String substitutions (
Hello, {0} - you're looking well!). - ✅
selectformat function (Hey [select value={0} m="bro" f="sis" nb="doc"]). - ✅
pluralformat function (That'll be [plural value={0} one="% dollar" other="% dollars"]). - ✅
ordinalformat function (You are currently [ordinal value={0} one="%st" two="%nd" few="%rd" other="%th"] in the queue).- ✅ ...including using Unicode CLDR for cardinal/ordinal form selection
(
en-AUnot assumed!)
- ✅ ...including using Unicode CLDR for cardinal/ordinal form selection
(
- ✅ Custom markup tags are also parsed, and rendered to an
AttributedString. - ✅
visitedandvisit_count - ✅ Built-in functions like
dice,round, andfloorthat are mentioned in the Yarn Spinner documentation.
Basic Usage
-
Compile your
.yarnfile. You can probably get the compiled output from a Unity project, or you can compile without using Unity with a tool like the Yarn Spinner Console:ysc compile Example.yarnThis produces two files: the VM bytecode
.yarnc, and a string table.csv. -
Implement a
DialogueHandler, which receives events from the VM. Here's an example that plays the dialogue on the terminal:type MyHandler struct{ stringTable *yarn.StringTable // ... and your own fields ... } func (m *MyHandler) Line(line yarn.Line) error { // StringTable's Render turns the Line into a string, applying all the // substitutions and format functions that might be present. text, _ := m.stringTable.Render(line) fmt.Println(text) // You can block in here to give the player time to read the text. fmt.Println("\n\nPress ENTER to continue") fmt.Scanln() return nil } func (m *MyHandler) Options(opts []yarn.Option) (int, error) { fmt.Println("Choose:") for _, opt := range opts { text, _ := m.stringTable.Render(opt.Line) fmt.Printf("%d: %s\n", opt.ID, text) } fmt.Print("Enter the number of your choice: ") var choice int fmt.Scanln(&choice) return choice, nil } // ... and also the other methods. // Alternatively you can embed yarn.FakeDialogueHandler in your handler. -
Load the two files, your
DialogueHandler, aVariableStorage, and any custom functions, into aVirtualMachine, and then pass the name of the first node toRun:package main import "drjosh.dev/yarn" func main() { // Load the files (error handling omitted for brevity): program, stringTable, _ := yarn.LoadFiles("Example.yarn.yarnc", "en-AU") // Set up your DialogueHandler and the VirtualMachine: myHandler := &MyHandler{ stringTable: stringTable, } vm := &yarn.VirtualMachine{ Program: program, Handler: myHandler, Vars: yarn.NewMapVariableStorage(), // or your own VariableStorage implementation FuncMap: yarn.FuncMap{ // this is optional "last_value": func(x ...any) any { return x[len(x)-1] }, // or your own custom functions! } } // Run the VirtualMachine starting with the Start node! vm.Run("Start") }
See cmd/yarnrunner.go for a complete example.
Async usage
To avoid the VM delivering the lines, options, and commands all at once,
your DialogueHandler implementation is allowed to block execution of the VM
goroutine - for example, using a channel operation.
However, in a typical game, each line or option would be associated with two distinct operations: showing the line/option to the player, and hiding it later on in response to user input.
To make this easier, AsyncAdapter can handle blocking the VM for you.
sequenceDiagram
yarn.VirtualMachine->>+yarn.AsyncAdapter: Line
yarn.AsyncAdapter->>+myHandler: Line
myHandler->>-gameEngine: showDialogue
Note right of myHandler: (time passes)
gameEngine->>+myHandler: Update
myHandler->>gameEngine: hideDialogue
myHandler->>-yarn.AsyncAdapter: Go
yarn.AsyncAdapter-->>-yarn.VirtualMachine: (return)
Use
AsyncAdapter as the VirtualMachine.Handler, and create the AsyncAdapter
with an AsyncDialogueHandler:
// MyHandler should now implement yarn.AsyncDialogueHandler.
type MyHandler struct {
stringTable *yarn.StringTable
dialogueDisplay Component
// Maintain a reference to the AsyncAdapter in order to call Go on it
// in response to user input.
// (It doesn't have to be stored in the handler, there are probably better
// places in a real project. This is just an example.)
asyncAdapter *yarn.AsyncAdapter
}
// Line is called by AsyncAdapter from the goroutine running VirtualMachine.Run.
// The AsyncAdapter pauses the VM.
func (m *MyHandler) Line(line yarn.Line) {
text, _ := m.stringTable.Render(line)
m.dialogueDisplay.Show(text)
}
// Update is called on every tick by the game engine, which is a separate
// goroutine to the one the Yarn virtual machine is running in.
func (m *MyHandler) Update() error {
//...
if m.dialogueDisplay.Visible() && inpututil.IsKeyJustPressed(ebiten.KeyEnter) {
// Hide the dialogue display.
m.dialogueDisplay.Hide()
// Calling AsyncAdapter.Go un-pauses the VM.
m.asyncAdapter.Go()
}
//...
}
// --- Setup ---
myHandler := &MyHandler{}
myHandler.asyncAdapter = yarn.NewAsyncAdapter(myHandler)
vm := &yarn.VirtualMachine{
Program: program,
Handler: myHandler.asyncAdapter,
...
}
Usage notes
Note that using an earlier Yarn Spinner compiler will result in some unusual
behaviour when compiling Yarn files with newer features. For example, with v1.0
<<jump ...>> may be compiled as a command. Your implementation of Command
may implement jump by calling the SetNode VM method.
If you need the tags for a node, you can read these from the Node protobuf
message directly. Source text of a rawText node can be looked up manually:
prog, st, _ := yarn.LoadFiles("testdata/Example.yarn.yarnc", "en")
node := prog.Nodes["LearnMore"]
// Tags for the LearnMore node:
fmt.Println(node.Tags)
// Source text string ID:
fmt.Println(node.SourceTextStringID)
// Source text is in the string table:
fmt.Println(st.Table[node.SourceTextStringID].Text)
Licence
This project is available under the Apache 2.0 license. See the LICENSE file
for more information.
The bytecode and testdata directories contains files or derivative works
from Yarn Spinner. See bytecode/README.md and testdata/README.md for more
information.
Related Skills
node-connect
351.2kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
110.6kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
351.2kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
351.2kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
