You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

337 lines
8.9 KiB

  1. extends Node
  2. class_name YarnImporter
  3. #
  4. # A YARN Importer for Godot
  5. #
  6. # Credits:
  7. # - Dave Kerr (http://www.naturallyintelligent.com)
  8. #
  9. # Latest: https://github.com/naturally-intelligent/godot-yarn-importer
  10. #
  11. # Yarn: https://github.com/InfiniteAmmoInc/Yarn
  12. # Twine: http://twinery.org
  13. #
  14. # Yarn: a ball of threads (Yarn file)
  15. # Thread: a series of fibres (Yarn node)
  16. # Fibre: a text or choice or logic (Yarn line)
  17. var yarn = {}
  18. # OVERRIDE METHODS
  19. #
  20. # called to request new dialog
  21. func on_new_line(text):
  22. pass
  23. # called to request new choice button
  24. func on_choices(choices_list):
  25. pass
  26. # called to request internal logic handling
  27. func logic(instruction, command):
  28. pass
  29. # called for each line of text
  30. func yarn_text_variables(text):
  31. return text
  32. # called when "settings" node parsed
  33. func story_setting(setting, value):
  34. pass
  35. # called for each node name
  36. func on_node_start(to):
  37. pass
  38. yield(get_tree(), "idle_frame")
  39. # called for each node name (after)
  40. func on_node_end(to):
  41. pass
  42. yield(get_tree(), "idle_frame")
  43. # START SPINNING YOUR YARN
  44. #
  45. func spin_yarn(file, start_thread = false):
  46. yarn = load_yarn(file)
  47. # Find the starting thread...
  48. if not start_thread:
  49. start_thread = yarn['start']
  50. # Load any scene-specific settings
  51. # (Not part of official Yarn standard)
  52. if 'settings' in yarn['threads']:
  53. var settings = yarn['threads']['settings']
  54. for fibre in settings['fibres']:
  55. var line = fibre['text']
  56. var split = line.split('=')
  57. var setting = split[0].strip_edges(true, true)
  58. var value = split[1].strip_edges(true, true)
  59. story_setting(setting, value)
  60. # First thread unravel...
  61. yield(on_dialogue_start(), "completed")
  62. yield(yarn_unravel(start_thread), "completed")
  63. # Internally create a new thread (during loading)
  64. func new_yarn_thread():
  65. var thread = {}
  66. thread['title'] = ''
  67. thread['kind'] = 'branch' # 'branch' for standard dialog, 'code' for gdscript
  68. thread['tags'] = [] # unused
  69. thread['fibres'] = []
  70. return thread
  71. # Internally create a new fibre (during loading)
  72. func new_yarn_fibre(line):
  73. # choice fibre
  74. if line.substr(0,2) == '[[':
  75. if line.find('|') != -1:
  76. var fibre = {}
  77. fibre['kind'] = 'choice'
  78. line = line.replace('[[', '')
  79. line = line.replace(']]', '')
  80. var split = line.split('|')
  81. fibre['text'] = split[0]
  82. fibre['marker'] = split[1]
  83. return fibre
  84. else:
  85. var fibre = {}
  86. fibre['kind'] = 'jump'
  87. line = line.replace('[[', '')
  88. line = line.replace(']]', '')
  89. fibre['marker'] = line
  90. return fibre
  91. # logic instruction (not part of official Yarn standard)
  92. elif line.substr(0,2) == '<<':
  93. if line.find(':') != -1:
  94. var fibre = {}
  95. fibre['kind'] = 'logic'
  96. line = line.replace('<<', '')
  97. line = line.replace('>>', '')
  98. var split = line.split(':')
  99. fibre['instruction'] = split[0]
  100. fibre['command'] = split[1]
  101. #print(line, split[0], split[1])
  102. return fibre
  103. # text fibre
  104. var fibre = {}
  105. fibre['kind'] = 'text'
  106. fibre['text'] = line
  107. return fibre
  108. # Create Yarn data structure from file (must be *.yarn.txt Yarn format)
  109. func load_yarn(path):
  110. var yarn = {}
  111. yarn['threads'] = {}
  112. yarn['start'] = false
  113. yarn['file'] = path
  114. var file = File.new()
  115. file.open(path, file.READ)
  116. if file.is_open():
  117. # yarn reading flags
  118. var start = false
  119. var header = true
  120. var thread = new_yarn_thread()
  121. # loop
  122. while !file.eof_reached():
  123. # read a line
  124. var line = file.get_line()
  125. # header read mode
  126. if header:
  127. if line == '---':
  128. header = false
  129. else:
  130. var split = line.split(': ')
  131. if split[0] == 'title':
  132. var title_split = split[1].split(':')
  133. var thread_title = ''
  134. var thread_kind = 'branch'
  135. if len(title_split) == 1:
  136. thread_title = split[1]
  137. else:
  138. thread_title = title_split[1]
  139. thread_kind = title_split[0]
  140. thread['title'] = thread_title
  141. thread['kind'] = thread_kind
  142. if not yarn['start']:
  143. yarn['start'] = thread_title
  144. # end of thread
  145. elif line == '===':
  146. header = true
  147. yarn['threads'][thread['title']] = thread
  148. thread = new_yarn_thread()
  149. # fibre read mode
  150. else:
  151. var fibre = new_yarn_fibre(line)
  152. if fibre:
  153. thread['fibres'].append(fibre)
  154. else:
  155. print('ERROR: Yarn file missing: ', filename)
  156. return yarn
  157. # Main logic for node handling
  158. #
  159. func yarn_unravel(to, from=false):
  160. if not to in yarn['threads']:
  161. print('WARNING: Missing Yarn thread: ', to, ' in file ',yarn['file'])
  162. return
  163. while to != null and to in yarn['threads']:
  164. yield (on_node_start(to), "completed")
  165. if to in yarn['threads']:
  166. var thread = yarn['threads'][to]
  167. to = null
  168. match thread['kind']:
  169. 'branch':
  170. var i = 0
  171. while i < thread['fibres'].size():
  172. match thread['fibres'][i]['kind']:
  173. 'text':
  174. var fibre = thread['fibres'][i]
  175. var text = yarn_text_variables(fibre['text'])
  176. yield(on_new_line(text), "completed")
  177. 'choice':
  178. var choices = []
  179. while i < thread['fibres'].size() and thread['fibres'][i]['kind'] == 'choice' :
  180. var fibre = thread['fibres'][i]
  181. var text = yarn_text_variables(fibre['text'])
  182. choices.push_back({"text": text, "marker": fibre['marker']})
  183. i += 1
  184. to = yield(on_choices(choices), "completed")
  185. break
  186. 'logic':
  187. var fibre = thread['fibres'][i]
  188. var instruction = fibre['instruction']
  189. var command = fibre['command']
  190. yield(logic(instruction, command), "completed")
  191. 'jump':
  192. var fibre = thread['fibres'][i]
  193. to = fibre['marker']
  194. break
  195. i += 1
  196. 'code':
  197. yarn_code(to)
  198. yield(on_node_end(to), "completed")
  199. yield (on_dialogue_end(), "completed")
  200. func on_dialogue_start():
  201. pass
  202. yield(get_tree(), "idle_frame")
  203. func on_dialogue_end():
  204. pass
  205. yield(get_tree(), "idle_frame")
  206. #
  207. # RUN GDSCRIPT CODE FROM YARN NODE - Special node = code:title
  208. # - Not part of official Yarn standard
  209. #
  210. func yarn_code(title, run=true, parent='parent.', tabs="\t", next_func="yarn_unravel"):
  211. if title in yarn['threads']:
  212. var thread = yarn['threads'][title]
  213. var code = ''
  214. for fibre in thread['fibres']:
  215. match fibre['kind']:
  216. 'text':
  217. var line = yarn_text_variables(fibre['text'])
  218. line = yarn_code_replace(line, parent, next_func)
  219. code += tabs + line + "\n"
  220. 'choice':
  221. var line = parent+next_func+"('"+fibre['marker']+"')"
  222. print(line)
  223. code += tabs + line + "\n"
  224. if run:
  225. run_yarn_code(code)
  226. else:
  227. return code
  228. else:
  229. print('WARNING: Title missing in yarn ball: ', title)
  230. # override to replace convenience variables
  231. func yarn_code_replace(code, parent='parent.', next_func="yarn_unravel"):
  232. if code.find("[[") != -1:
  233. code = code.replace("[[", parent+next_func+"('")
  234. code = code.replace("]]", "')")
  235. code = code.replace("say(", parent+"say(")
  236. code = code.replace("choice(", parent+"choice(")
  237. return code
  238. func run_yarn_code(code):
  239. var front = "extends Node\n"
  240. front += "func dynamic_code():\n"
  241. front += "\tvar parent = get_parent()\n\n"
  242. code = front + code
  243. #print("CODE BLOCK: \n", code)
  244. var script = GDScript.new()
  245. script.set_source_code(code)
  246. script.reload()
  247. #print("Executing code...")
  248. var node = Node.new()
  249. node.set_script(script)
  250. add_child(node)
  251. var result = node.dynamic_code()
  252. remove_child(node)
  253. return result
  254. # EXPORTING TO GDSCRIPT
  255. #
  256. # This code may not be directly usable
  257. # Use if you need an exit from Yarn
  258. func export_to_gdscript():
  259. var script = ''
  260. script += "func start_story():\n\n"
  261. if 'settings' in yarn['threads']:
  262. var settings = yarn['threads']['settings']
  263. for fibre in settings['fibres']:
  264. var line = fibre['text']
  265. var split = line.split('=')
  266. var setting = split[0].strip_edges(true, true)
  267. var value = split[1].strip_edges(true, true)
  268. script += "\t" + 'story_setting("' + setting + '", "' + value + '")' + "\n"
  269. script += "\tstory_logic('" + yarn['start'] + "')\n\n"
  270. # story logic choice/press event
  271. script += "func story_logic(marker):\n\n"
  272. script += "\tmatch marker:\n"
  273. for title in yarn['threads']:
  274. var thread = yarn['threads'][title]
  275. match thread['kind']:
  276. 'branch':
  277. var code = "\n\t\t'" + thread['title'] + "':"
  278. var tabs = "\n\t\t\t"
  279. for fibre in thread['fibres']:
  280. match fibre['kind']:
  281. 'text':
  282. code += tabs + 'say("' + fibre['text'] + '")'
  283. 'choice':
  284. code += tabs + 'choice("' + fibre['text'] + '", "' + fibre['marker'] + '")'
  285. 'logic':
  286. code += tabs + 'logic("' + fibre['instruction'] + '", "' + fibre['command'] + '")'
  287. script += code + "\n"
  288. 'code':
  289. var code = "\n\t\t'" + thread['title'] + "':"
  290. var tabs = "\n\t\t\t"
  291. code += "\n"
  292. code += yarn_code(thread['title'], false, '', "\t\t\t", "story_logic")
  293. script += code + "\n"
  294. # done
  295. return script
  296. func print_gdscript_to_console():
  297. print(export_to_gdscript())
  298. func save_to_gdscript(filename):
  299. var script = export_to_gdscript()
  300. # write to file
  301. var file = File.new()
  302. file.open(filename, file.WRITE)
  303. if not file.is_open():
  304. print('ERROR: Cant open file ', filename)
  305. return false
  306. file.store_string(script)
  307. file.close()