yarn

package module
v0.5.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 3, 2023 License: Apache-2.0 Imports: 21 Imported by: 0

README

yarn

A Go implementation of parts of Yarn Spinner 2.2.

Derived from github.com/DrJosh9000/yarn, but updated to use most the recent Protobuf files and include more tests from upstream.

Build status Go Reference Go Report Card License

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/template package.
  • ✅ Yarn Spinner CSV string tables.
  • ✅ String substitutions (Hello, {0} - you're looking well!).
  • select format function (Hey [select value={0} m="bro" f="sis" nb="doc"]).
  • plural format function (That'll be [plural value={0} one="% dollar" other="% dollars"]).
  • ordinal format 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-AU not assumed!)
  • ✅ Custom markup tags are also parsed, and rendered to an AttributedString.
  • visited and visit_count
  • ✅ Built-in functions like dice, round, and floor that are mentioned in the Yarn Spinner documentation.

Usage

  1. Compile your .yarn file. 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.yarn
    

    This produces two files: the VM bytecode .yarnc, and a string table .csv.

  2. 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.
    
  3. Load the two files, your DialogueHandler, a VariableStorage, and any custom functions, into a VirtualMachine, and then pass the name of the first node to Run:

    package main
    
    import "github.com/DrJosh9000/yarn"
    
    func main() {
        // Load the files (error handling omitted for brevity):
        program, stringTable, _ := yarn.LoadFiles("Example.yarn.yarnc", "Example.yarn.csv", "en-AU")
    
        // Set up your DialogueHandler and the VirtualMachine:
        myHandler := &MyHandler{
            stringTable: stringTable,
        }
        vm := &yarn.VirtualMachine{
            Program: program,
            Handler: myHandler,
            Vars: make(yarn.MapVariableStorage), // or your own VariableStorage implementation
            FuncMap: yarn.FuncMap{ // this is optional
                "last_value": func(x ...interface{}) interface{} {
                    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.

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", "testdata/Example.yarn.csv", "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)

In a typical game, vm.Run would happen in a separate goroutine. To avoid the VM delivering all the lines, options, and commands at once, your DialogueHandler implementation is allowed to block execution of the VM goroutine - for example, using a channel operation:

type MyHandler struct {
    stringTable *yarn.StringTable

    dialogueDisplay Component

    // next is used to block Line from returning until the player is ready for
    // more tasty, tasty content.
    next chan struct{}

    // waiting tracks whether the game is waiting for player input.
    // It is guarded by a mutex since it is changed by two different
    // goroutines.
    waitingMu sync.Mutex
    waiting   bool
}

func (m *MyHandler) setWaiting(w bool) {
    m.waitingMu.Lock()
    m.waiting = w
    m.waitingMu.Unlock()
}

// Line is called from the goroutine running VirtualMachine.Run.
func (m *MyHandler) Line(line yarn.Line) error {
    text, _ := m.stringTable.Render(line)
    m.dialogueDisplay.Show(text)
    
    // Go into waiting-for-player-input state
    m.setWaiting(true)

    // Recieve on m.next, which blocks until another goroutine sends on it.
    <-m.next
    return nil
}

// Update is called on every tick by the game engine, which is a separate
// goroutine to the one the virtual machine is running in.
func (m *MyHandler) Update() error {
    //...
    if m.waiting && inpututil.IsKeyJustPressed(ebiten.KeyEnter) {
        // Hide the dialogue display.
        m.dialogueDisplay.Hide()
        // No longer waiting for player input.
        m.setWaiting(false)
        // Send on m.next, which unblocks the call to Line.
        // Do this after setting m.waiting to false.
       m.next <- struct{}{}
    }
    //...
}

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.

Documentation

Overview

Package yarn implements the Yarn Spinner virtual machine and dialogue system. For the original implementation, see https://yarnspinner.dev and https://github.com/YarnSpinnerTool/YarnSpinner.

Index

Constants

View Source
const (
	// ErrNilDialogueHandler indicates that Handler hasn't been set.
	ErrNilDialogueHandler = virtualMachineError("nil dialogue handler")

	// ErrNilVariableStorage indicates that Vars hasn't been set.
	ErrNilVariableStorage = virtualMachineError("nil variable storage")

	// ErrMissingProgram indicates that Program hasn't been set.
	ErrMissingProgram = virtualMachineError("missing or empty program")

	// ErrNoOptions indicates the program is invalid - it tried to show options
	// but none had been added.
	ErrNoOptions = virtualMachineError("no options were added")

	// ErrStackUnderflow indicates the program tried to pop or peek when the
	// stack was empty.
	ErrStackUnderflow = virtualMachineError("stack underflow")

	// ErrWrongType indicates the program needed a stack value, operand, or
	// function of one type, but got something else instead.
	ErrWrongType = virtualMachineError("wrong type")

	// ErrNotConvertible indicates the program tried to convert a stack value
	// or operand to a different type, but it was not convertible to that type.
	ErrNotConvertible = virtualMachineError("not convertible")

	// ErrNodeNotFound is returned where Run or SetNode is passed the name of a
	// node that is not in the program.
	ErrNodeNotFound = virtualMachineError("node not found")

	// ErrLabelNotFound indicates the program tries to jump to a label that
	// isn't in the label table for the current node.
	ErrLabelNotFound = virtualMachineError("label not found")

	// ErrNilOperand indicates the a malformed program containing an instruction
	// that requires a usable operand but the operand was nil.
	ErrNilOperand = virtualMachineError("nil operand")

	// ErrFunctionNotFound indicates the program tried to call a function but
	// that function is not in the FuncMap.
	ErrFunctionNotFound = virtualMachineError("function not found")

	// ErrFunctionArgMismatch indicates the program tried to call a function but
	// had the wrong number or types of args to pass to it.
	ErrFunctionArgMismatch = virtualMachineError("arg mismatch")
)

Various sentinel errors returned by the virtual machine.

View Source
const Stop = virtualMachineError("stop")

Stop stops the virtual machine without error. It is used by the STOP instruction, but can also be returned by your handler to stop the VM in the same way. However a stop happens, NodeComplete and DialogueComplete are still called.

Variables

This section is empty.

Functions

func ConvertToBool

func ConvertToBool(x interface{}) (bool, error)

ConvertToBool attempts conversion of the standard Yarn Spinner VM types (bool, number, string, null) to bool.

func ConvertToFloat32

func ConvertToFloat32(x interface{}) (float32, error)

ConvertToFloat32 attempts conversion of the standard Yarn Spinner VM types (bool, number, string, null) to a float32.

func ConvertToFloat64

func ConvertToFloat64(x interface{}) (float64, error)

ConvertToFloat64 attempts conversion of the standard Yarn Spinner VM types (bool, number, string, null) to a float64.

func ConvertToInt

func ConvertToInt(x interface{}) (int, error)

ConvertToInt attempts conversion of the standard Yarn Spinner VM types to (bool, number, string, null) to int.

func ConvertToString

func ConvertToString(x interface{}) string

ConvertToString converts a value to a string, in a way that matches what Yarn Spinner does. nil becomes "null", and booleans are title-cased.

func FormatInstruction

func FormatInstruction(inst *yarnpb.Instruction) string

FormatInstruction prints an instruction in a format convenient for debugging. The output is intended for human consumption only and may change between incremental versions of this package.

func FormatProgram

func FormatProgram(w io.Writer, prog *yarnpb.Program) error

FormatProgram prints a program in a format convenient for debugging to the io.Writer. The output is intended for human consumption only and may change between incremental versions of this package.

func FormatProgramString

func FormatProgramString(prog *yarnpb.Program) string

FormatProgramString prints the whole program into a string.

func LoadProgramFile

func LoadProgramFile(programPath string) (*yarnpb.Program, error)

LoadProgramFile is a convenient function for loading a compiled Yarn Spinner program given a file path.

Types

type Attribute

type Attribute struct {
	Start, End int
	Name       string
	Props      map[string]string
}

Attribute describes a range within a string with additional information provided by markup tags. Start and End specify the range in bytes. Name is the tag name, and Props contains any additional key="value" tag properties.

type AttributedString

type AttributedString struct {
	// contains filtered or unexported fields
}

AttributedString is a string with additional attributes, such as presentation or styling information, that apply to the whole string or substrings.

func (*AttributedString) ScanAttribEvents

func (s *AttributedString) ScanAttribEvents(visit func(pos int, atts []*Attribute))

ScanAttribEvents calls visit with each change in attribute state. pos is the byte position in the string where the change occurs. atts will contain the attributes that either start or end at pos, in the same order they were read from the original markup. Self-closing tags, or an open and close pair that apply to the same position (i.e. marking up nothing) will only be present in atts once (in the order of the start tag). For example, for the original string:

`[a]Hello A[/a] [b]Hello B[/b] [c][d][/c]No C, [e/]only D[/d]`

which is processed into the unattributed string:

`Hello A Hello B No C, only D`

ScanAttribEvents will visit: * (0, [a]) -- open of a * (7, [a]) -- close of a * (8, [b]) -- open of b * (15, [b]) -- close of b * (16, [c,d]) -- close of c applies to same position, so it appears once * (22, [e]) -- e is self-closing, so it appears once * (28, [d]) -- close of d

func (*AttributedString) String

func (s *AttributedString) String() string

type DialogueHandler

type DialogueHandler interface {
	// NodeStart is called when a node has begun executing. It is passed the
	// name of the node.
	NodeStart(nodeName string) error

	// PrepareForLines is called when the dialogue system anticipates that it
	// will deliver some lines. Note that not every line prepared may end up
	// being run.
	PrepareForLines(lineIDs []string) error

	// Line is called when the dialogue system runs a line of dialogue.
	Line(line Line) error

	// Options is called to deliver a set of options to the game. The player
	// should choose one of the options, and Options should return the ID of the
	// chosen option.
	Options(options []Option) (int, error)

	// Command is called when the dialogue system runs a command.
	Command(command string) error

	// NodeComplete is called when a node has completed execution. It is passed
	// the name of the node.
	NodeComplete(nodeName string) error

	// DialogueComplete is called when the dialogue as a whole is complete.
	DialogueComplete() error
}

DialogueHandler receives events from the virtual machine.

type FakeDialogueHandler

type FakeDialogueHandler struct{}

FakeDialogueHandler implements DialogueHandler with minimal, do-nothing methods. This is useful both for testing, and for satisfying the interface via embedding, e.g.:

   type MyHandler struct {
	      FakeDialogueHandler
   }
   // MyHandler is only interested in Line and Options.
   func (m MyHandler) Line(line Line) error { ... }
   func (m MyHandler) Options(options []Option) (int, error) { ... }
   // All the other DialogueHandler methods provided by FakeDialogueHandler.

func (FakeDialogueHandler) Command

func (FakeDialogueHandler) Command(string) error

Command returns nil.

func (FakeDialogueHandler) DialogueComplete

func (FakeDialogueHandler) DialogueComplete() error

DialogueComplete returns nil.

func (FakeDialogueHandler) Line

Line returns nil.

func (FakeDialogueHandler) NodeComplete

func (FakeDialogueHandler) NodeComplete(string) error

NodeComplete returns nil.

func (FakeDialogueHandler) NodeStart

func (FakeDialogueHandler) NodeStart(string) error

NodeStart returns nil.

func (FakeDialogueHandler) Options

func (FakeDialogueHandler) Options(options []Option) (int, error)

Options returns the first option ID, or an error if there are no options.

func (FakeDialogueHandler) PrepareForLines

func (FakeDialogueHandler) PrepareForLines([]string) error

PrepareForLines returns nil.

type FuncMap

type FuncMap map[string]interface{}

FuncMap maps function names to implementations. It is similar to the text/template FuncMap.

Each function must return either 0, 1, or 2 values, and if 2 are returned, the latter must be type `error`.

If the arguments being passed by the program are not assignable to an argument, and the argument has type bool, int, float32, float64, or string, then a conversion is attempted by the VM. For example, if the stack has the values ("3", true, 2) on top, CALL_FUNC with "Number.Add" (see below) would cause Number.Add's implementation to be called with (3.0, 1.0) (the 2 is the argument count).

type Line

type Line struct {
	// The string ID for the line.
	ID string
	// Values that should be interpolated into the user-facing text.
	Substitutions []string
}

Line represents a line of dialogue.

type MapVariableStorage

type MapVariableStorage map[string]interface{}

MapVariableStorage implements VariableStorage, in memory, using a map.

func (MapVariableStorage) Clear

func (m MapVariableStorage) Clear()

Clear empties the storage of all values.

func (MapVariableStorage) GetValue

func (m MapVariableStorage) GetValue(name string) (value interface{}, found bool)

GetValue fetches a value from the map, returning (nil, false) if not present.

func (MapVariableStorage) SetValue

func (m MapVariableStorage) SetValue(name string, value interface{})

SetValue sets a value in the map.

type Option

type Option struct {
	// A number identifying this option. If this option is selected, pass
	// this number back to the dialogue system.
	ID int

	// The line that should be presented for this option.
	Line Line

	// Name of the node that will run next, if this option is selected.
	DestinationNode string

	// Indicates whether the player should be permitted to select the option.
	// This is false for options that the player _could_ have taken if they had
	// satisfied some prerequisite earlier in the game.
	IsAvailable bool
}

Option represents one option (among others) that the player could choose.

type StringTable

type StringTable struct {
	Language language.Tag
	Table    map[string]*StringTableRow
}

StringTable contains all the information from a string table, keyed by string ID. This can be constructed either by using ReadStringTable, or manually (e.g. if you are not using Yarn Spinner CSV string tables but still want to use substitutions, format functions, and markup tags).

func LoadFiles

func LoadFiles(programPath, langCode string) (*yarnpb.Program, *StringTable, error)

LoadFiles is a convenient way of loading a compiled Yarn Spinner program and string table from files in one function call. When passing a programPath named foo/bar/file.yarnc, LoadFiles expects that files named foo/bar/file-Lines.csv and foo/bar/file-Metadata.csv are also available. langCode should be a valid BCP 47 language tag.

func LoadFilesFS added in v0.5.1

func LoadFilesFS(fsys fs.FS, programPath, langCode string) (*yarnpb.Program, *StringTable, error)

LoadFilesFS loads compiled Yarn Spinner files from the provided fs.FS. See LoadFiles for more information.

func LoadStringTableFile

func LoadStringTableFile(stringTablePath, langCode string) (*StringTable, error)

LoadStringTableFile is a convenient function for loading a CSV string table given a file path. If stringTablePath is foo/bar/file-Lines.csv then it expects a corresponding Metadata file at foo/bar/file-Metadata.csv. It assumes the first row of both files are a header. langCode must be a valid BCP 47 language tag.

func LoadStringTableFileFS added in v0.5.1

func LoadStringTableFileFS(fsys fs.FS, stringTablePath, langCode string) (*StringTable, error)

LoadStringTableFileFS loads compiled Yarn Spinner files from the provided fs.FS. See LoadStringTableFile for details.

func ReadStringTable

func ReadStringTable(r io.Reader, langCode string) (*StringTable, error)

ReadStringTable reads a CSV string table from the reader. It assumes the first row is a header. langCode must be a valid BCP 47 language tag. In addition to checking the CSV structure as it is parsed, each lineNumber is parsed as an int, and each text is also parsed. Any malformed substitution tokens or markup tags will cause an error.

func (*StringTable) Render

func (t *StringTable) Render(line Line) (*AttributedString, error)

Render looks up the row corresponding to line.ID, interpolates substitutions (from line.Substitutions), applies format functions, and processes style tags into attributes.

type StringTableRow

type StringTableRow struct {
	ID, Text, File, Node string
	LineNumber           int

	Tags []string // Tags are set in the metadata table.
	// contains filtered or unexported fields
}

StringTableRow contains all the information from one row in a string table.

func (*StringTableRow) Render

func (r *StringTableRow) Render(substs []string, lang language.Tag) (*AttributedString, error)

Render interpolates substitutions, applies format functions, and processes style tags into attributes.

type TestPlan

type TestPlan struct {
	StringTable *StringTable
	Steps       []TestStep
	Step        int

	FakeDialogueHandler // implements remaining methods
	// contains filtered or unexported fields
}

TestPlan implements test plans. A test plan is a dialogue handler that expects specific lines and options from the dialogue system.

func LoadTestPlanFile

func LoadTestPlanFile(testPlanPath string) (*TestPlan, error)

LoadTestPlanFile is a convenient function for loading a test plan given a file path.

func ReadTestPlan

func ReadTestPlan(r io.Reader) (*TestPlan, error)

ReadTestPlan reads a testplan from an io.Reader into a TestPlan.

func (*TestPlan) Command

func (p *TestPlan) Command(command string) error

Command handles the command... somehow.

func (*TestPlan) Complete

func (p *TestPlan) Complete() error

Complete checks if the test plan was completed.

func (*TestPlan) DialogueComplete

func (p *TestPlan) DialogueComplete() error

DialogueComplete records the event in p.DialogueCompleted.

func (*TestPlan) Line

func (p *TestPlan) Line(line Line) error

Line checks that the line matches the one expected by the plan.

func (*TestPlan) Options

func (p *TestPlan) Options(opts []Option) (int, error)

Options checks that the options match those expected by the plan, then selects the option specified in the plan.

type TestStep

type TestStep struct {
	Type     string
	Contents string
}

TestStep is a step in a test plan.

func (TestStep) String

func (s TestStep) String() string

type VariableStorage

type VariableStorage interface {
	Clear()
	GetValue(name string) (value interface{}, ok bool)
	SetValue(name string, value interface{})
}

VariableStorage stores values of any kind.

type VirtualMachine

type VirtualMachine struct {
	// Program is the program to execute.
	Program *yarnpb.Program

	// Handler receives content (lines, options, etc) and other events.
	Handler DialogueHandler

	// Vars stores variables used and provided by the dialogue.
	Vars VariableStorage

	// FuncMap is used to provide user-defined functions.
	FuncMap FuncMap

	// TraceLogf, if not nil, is called before each instruction to log the
	// current stack, options, and the instruction about to be executed.
	TraceLogf func(string, ...interface{})
	// contains filtered or unexported fields
}

VirtualMachine implements the Yarn Spinner virtual machine.

func (*VirtualMachine) Run

func (vm *VirtualMachine) Run(startNode string) error

Run executes the program, starting at a particular node.

func (*VirtualMachine) SetNode

func (vm *VirtualMachine) SetNode(name string) error

SetNode sets the VM to begin a node. If a node is already selected, NodeComplete will be called for that node. Then NodeStart and PrepareForLines will be called (for the newly selected node). Passing the current node is one way to reset to the start of the node.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL