|
- extends Node
- class_name YarnImporter
-
- #
- # A YARN Importer for Godot
- #
- # Credits:
- # - Dave Kerr (http://www.naturallyintelligent.com)
- #
- # Latest: https://github.com/naturally-intelligent/godot-yarn-importer
- #
- # Yarn: https://github.com/InfiniteAmmoInc/Yarn
- # Twine: http://twinery.org
- #
- # Yarn: a ball of threads (Yarn file)
- # Thread: a series of fibres (Yarn node)
- # Fibre: a text or choice or logic (Yarn line)
-
- var yarn = {}
-
- # OVERRIDE METHODS
- #
- # called to request new dialog
- func on_new_line(text):
- pass
-
- # called to request new choice button
- func on_choices(choices_list):
- pass
-
- # called to request internal logic handling
- func logic(instruction, command):
- pass
-
- # called for each line of text
- func yarn_text_variables(text):
- return text
-
- # called when "settings" node parsed
- func story_setting(setting, value):
- pass
-
- # called for each node name
- func on_node_start(to):
- pass
- yield(get_tree(), "idle_frame")
-
- # called for each node name (after)
- func on_node_end(to):
- pass
- yield(get_tree(), "idle_frame")
-
- # START SPINNING YOUR YARN
- #
- func spin_yarn(file, start_thread = false):
- yarn = load_yarn(file)
- # Find the starting thread...
- if not start_thread:
- start_thread = yarn['start']
- # Load any scene-specific settings
- # (Not part of official Yarn standard)
- if 'settings' in yarn['threads']:
- var settings = yarn['threads']['settings']
- for fibre in settings['fibres']:
- var line = fibre['text']
- var split = line.split('=')
- var setting = split[0].strip_edges(true, true)
- var value = split[1].strip_edges(true, true)
- story_setting(setting, value)
- # First thread unravel...
- yield(on_dialogue_start(), "completed")
- yield(yarn_unravel(start_thread), "completed")
-
- # Internally create a new thread (during loading)
- func new_yarn_thread():
- var thread = {}
- thread['title'] = ''
- thread['kind'] = 'branch' # 'branch' for standard dialog, 'code' for gdscript
- thread['tags'] = [] # unused
- thread['fibres'] = []
- return thread
-
- func exec(stri):
- var expr := Expression.new()
- print("input : " + stri)
- var err := expr.parse(stri)
- if err == OK:
- var res = expr.execute()
- print("res : " + str(res))
- else:
- print("err : " + str(err))
-
- func jajoujaj():
- print("jaaaja")
-
- # Internally create a new fibre (during loading)
- func new_yarn_fibre(line:String):
- # choice fibre
- if line.substr(0,2) == '[[':
- if line.find('|') != -1:
- var fibre = {}
- fibre['kind'] = 'choice'
- line = line.replace('[[', '')
- line = line.replace(']]', '')
- var split = line.split('|')
- fibre['text'] = split[0]
- fibre['marker'] = split[1]
- return fibre
- else:
- var fibre = {}
- fibre['kind'] = 'jump'
- line = line.replace('[[', '')
- line = line.replace(']]', '')
- fibre['marker'] = line
- return fibre
- # logic instruction (not part of official Yarn standard)
- elif line.substr(0,2) == '<<':
- line = line.replace('<<', '')
- line = line.replace('>>', '')
- var line_split = line.split(' ')
- print (line_split)
- var command = line.substr(line.find(' '))
- exec(command)
-
- # if line.find(':') != -1:
- # fibre['kind'] = 'logic'
- # line = line.replace('<<', '')
- # line = line.replace('>>', '')
- # var split = line.split(':')
- # fibre['instruction'] = split[0]
- # fibre['command'] = split[1]
- # #print(line, split[0], split[1])
- # return fibre
- # text fibre
- var fibre = {}
- fibre['kind'] = 'text'
- fibre['text'] = line
- return fibre
-
- # Create Yarn data structure from file (must be *.yarn.txt Yarn format)
- func load_yarn(path):
- var yarn = {}
- yarn['threads'] = {}
- yarn['start'] = false
- yarn['file'] = path
- var file = File.new()
- file.open(path, file.READ)
- if file.is_open():
- # yarn reading flags
- var start = false
- var header = true
- var thread = new_yarn_thread()
- # loop
- while !file.eof_reached():
- # read a line
- var line = file.get_line()
- # header read mode
- if header:
- if line == '---':
- header = false
- else:
- var split = line.split(': ')
- if split[0] == 'title':
- var title_split = split[1].split(':')
- var thread_title = ''
- var thread_kind = 'branch'
- if len(title_split) == 1:
- thread_title = split[1]
- else:
- thread_title = title_split[1]
- thread_kind = title_split[0]
- thread['title'] = thread_title
- thread['kind'] = thread_kind
- if not yarn['start']:
- yarn['start'] = thread_title
- # end of thread
- elif line == '===':
- header = true
- yarn['threads'][thread['title']] = thread
- thread = new_yarn_thread()
- # fibre read mode
- else:
- var fibre = new_yarn_fibre(line)
- if fibre:
- thread['fibres'].append(fibre)
- else:
- print('ERROR: Yarn file missing: ', filename)
- return yarn
-
- # Main logic for node handling
- #
- func yarn_unravel(to, from=false):
- if not to in yarn['threads']:
- print('WARNING: Missing Yarn thread: ', to, ' in file ',yarn['file'])
- return
-
- while to != null and to in yarn['threads']:
- yield (on_node_start(to), "completed")
- if to in yarn['threads']:
- var thread = yarn['threads'][to]
- to = null
- match thread['kind']:
- 'branch':
- var i = 0
- while i < thread['fibres'].size():
- match thread['fibres'][i]['kind']:
- 'text':
- var fibre = thread['fibres'][i]
- var text = yarn_text_variables(fibre['text'])
- yield(on_new_line(text), "completed")
- 'choice':
- var choices = []
- while i < thread['fibres'].size() and thread['fibres'][i]['kind'] == 'choice' :
- var fibre = thread['fibres'][i]
- var text = yarn_text_variables(fibre['text'])
- choices.push_back({"text": text, "marker": fibre['marker']})
- i += 1
- to = yield(on_choices(choices), "completed")
- break
- 'logic':
- var fibre = thread['fibres'][i]
- var instruction = fibre['instruction']
- var command = fibre['command']
- yield(logic(instruction, command), "completed")
- 'jump':
- var fibre = thread['fibres'][i]
- to = fibre['marker']
- break
- i += 1
- 'code':
- yarn_code(to)
- yield(on_node_end(to), "completed")
-
- yield (on_dialogue_end(), "completed")
-
- func on_dialogue_start():
- pass
- yield(get_tree(), "idle_frame")
-
- func on_dialogue_end():
- pass
- yield(get_tree(), "idle_frame")
-
- #
- # RUN GDSCRIPT CODE FROM YARN NODE - Special node = code:title
- # - Not part of official Yarn standard
- #
- func yarn_code(title, run=true, parent='parent.', tabs="\t", next_func="yarn_unravel"):
- if title in yarn['threads']:
- var thread = yarn['threads'][title]
- var code = ''
- for fibre in thread['fibres']:
- match fibre['kind']:
- 'text':
- var line = yarn_text_variables(fibre['text'])
- line = yarn_code_replace(line, parent, next_func)
- code += tabs + line + "\n"
- 'choice':
- var line = parent+next_func+"('"+fibre['marker']+"')"
- print(line)
- code += tabs + line + "\n"
- if run:
- run_yarn_code(code)
- else:
- return code
- else:
- print('WARNING: Title missing in yarn ball: ', title)
-
- # override to replace convenience variables
- func yarn_code_replace(code, parent='parent.', next_func="yarn_unravel"):
- if code.find("[[") != -1:
- code = code.replace("[[", parent+next_func+"('")
- code = code.replace("]]", "')")
- code = code.replace("say(", parent+"say(")
- code = code.replace("choice(", parent+"choice(")
- return code
-
- func run_yarn_code(code):
- var front = "extends Node\n"
- front += "func dynamic_code():\n"
- front += "\tvar parent = get_parent()\n\n"
- code = front + code
- #print("CODE BLOCK: \n", code)
-
- var script = GDScript.new()
- script.set_source_code(code)
- script.reload()
-
- #print("Executing code...")
- var node = Node.new()
- node.set_script(script)
- add_child(node)
- var result = node.dynamic_code()
- remove_child(node)
-
- return result
-
- # EXPORTING TO GDSCRIPT
- #
- # This code may not be directly usable
- # Use if you need an exit from Yarn
-
- func export_to_gdscript():
- var script = ''
- script += "func start_story():\n\n"
- if 'settings' in yarn['threads']:
- var settings = yarn['threads']['settings']
- for fibre in settings['fibres']:
- var line = fibre['text']
- var split = line.split('=')
- var setting = split[0].strip_edges(true, true)
- var value = split[1].strip_edges(true, true)
- script += "\t" + 'story_setting("' + setting + '", "' + value + '")' + "\n"
- script += "\tstory_logic('" + yarn['start'] + "')\n\n"
- # story logic choice/press event
- script += "func story_logic(marker):\n\n"
- script += "\tmatch marker:\n"
- for title in yarn['threads']:
- var thread = yarn['threads'][title]
- match thread['kind']:
- 'branch':
- var code = "\n\t\t'" + thread['title'] + "':"
- var tabs = "\n\t\t\t"
- for fibre in thread['fibres']:
- match fibre['kind']:
- 'text':
- code += tabs + 'say("' + fibre['text'] + '")'
- 'choice':
- code += tabs + 'choice("' + fibre['text'] + '", "' + fibre['marker'] + '")'
- 'logic':
- code += tabs + 'logic("' + fibre['instruction'] + '", "' + fibre['command'] + '")'
- script += code + "\n"
- 'code':
- var code = "\n\t\t'" + thread['title'] + "':"
- var tabs = "\n\t\t\t"
- code += "\n"
- code += yarn_code(thread['title'], false, '', "\t\t\t", "story_logic")
- script += code + "\n"
- # done
- return script
-
- func print_gdscript_to_console():
- print(export_to_gdscript())
-
- func save_to_gdscript(filename):
- var script = export_to_gdscript()
- # write to file
- var file = File.new()
- file.open(filename, file.WRITE)
- if not file.is_open():
- print('ERROR: Cant open file ', filename)
- return false
- file.store_string(script)
- file.close()
|