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()