script_extension.gd 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. class_name _ModLoaderScriptExtension
  2. extends RefCounted
  3. # This Class provides methods for working with script extensions.
  4. # Currently all of the included methods are internal and should only be used by the mod loader itself.
  5. const LOG_NAME := "ModLoader:ScriptExtension"
  6. # Sort script extensions by inheritance and apply them in order
  7. static func handle_script_extensions() -> void:
  8. var extension_paths := []
  9. for extension_path in ModLoaderStore.script_extensions:
  10. if FileAccess.file_exists(extension_path):
  11. extension_paths.push_back(extension_path)
  12. else:
  13. ModLoaderLog.error(
  14. "The child script path '%s' does not exist" % [extension_path], LOG_NAME
  15. )
  16. # Sort by inheritance
  17. InheritanceSorting.new(extension_paths)
  18. # Load and install all extensions
  19. for extension in extension_paths:
  20. var script: Script = apply_extension(extension)
  21. _reload_vanilla_child_classes_for(script)
  22. # Sorts script paths by their ancestors. Scripts are organized by their common
  23. # ancestors then sorted such that scripts extending script A will be before
  24. # a script extending script B if A is an ancestor of B.
  25. class InheritanceSorting:
  26. var stack_cache := {}
  27. # This dictionary's keys are mod_ids and it stores the corresponding position in the load_order
  28. var load_order := {}
  29. func _init(inheritance_array_to_sort: Array) -> void:
  30. _populate_load_order_table()
  31. inheritance_array_to_sort.sort_custom(check_inheritances)
  32. # Comparator function. return true if a should go before b. This may
  33. # enforce conditions beyond the stated inheritance relationship.
  34. func check_inheritances(extension_a: String, extension_b: String) -> bool:
  35. var a_stack := cached_inheritances_stack(extension_a)
  36. var b_stack := cached_inheritances_stack(extension_b)
  37. var last_index: int
  38. for index in a_stack.size():
  39. if index >= b_stack.size():
  40. return false
  41. if a_stack[index] != b_stack[index]:
  42. return a_stack[index] < b_stack[index]
  43. last_index = index
  44. if last_index < b_stack.size() - 1:
  45. return true
  46. return compare_mods_order(extension_a, extension_b)
  47. # Returns a list of scripts representing all the ancestors of the extension
  48. # script with the most recent ancestor last.
  49. #
  50. # Results are stored in a cache keyed by extension path
  51. func cached_inheritances_stack(extension_path: String) -> Array:
  52. if stack_cache.has(extension_path):
  53. return stack_cache[extension_path]
  54. var stack := []
  55. var parent_script: Script = load(extension_path)
  56. while parent_script:
  57. stack.push_front(parent_script.resource_path)
  58. parent_script = parent_script.get_base_script()
  59. stack.pop_back()
  60. stack_cache[extension_path] = stack
  61. return stack
  62. # Secondary comparator function for resolving scripts extending the same vanilla script
  63. # Will return whether a comes before b in the load order
  64. func compare_mods_order(extension_a: String, extension_b: String) -> bool:
  65. var mod_a_id: String = _ModLoaderPath.get_mod_dir(extension_a)
  66. var mod_b_id: String = _ModLoaderPath.get_mod_dir(extension_b)
  67. return load_order[mod_a_id] < load_order[mod_b_id]
  68. # Populate a load order dictionary for faster access and comparison between mod ids
  69. func _populate_load_order_table() -> void:
  70. var mod_index := 0
  71. for mod in ModLoaderStore.mod_load_order:
  72. load_order[mod.dir_name] = mod_index
  73. mod_index += 1
  74. static func apply_extension(extension_path: String) -> Script:
  75. # Check path to file exists
  76. if not FileAccess.file_exists(extension_path):
  77. ModLoaderLog.error("The child script path '%s' does not exist" % [extension_path], LOG_NAME)
  78. return null
  79. var child_script: Script = load(extension_path)
  80. # Adding metadata that contains the extension script path
  81. # We cannot get that path in any other way
  82. # Passing the child_script as is would return the base script path
  83. # Passing the .duplicate() would return a '' path
  84. child_script.set_meta("extension_script_path", extension_path)
  85. # Force Godot to compile the script now.
  86. # We need to do this here to ensure that the inheritance chain is
  87. # properly set up, and multiple mods can chain-extend the same
  88. # class multiple times.
  89. # This is also needed to make Godot instantiate the extended class
  90. # when creating singletons.
  91. child_script.reload()
  92. var parent_script: Script = child_script.get_base_script()
  93. var parent_script_path: String = parent_script.resource_path
  94. # We want to save scripts for resetting later
  95. # All the scripts are saved in order already
  96. if not ModLoaderStore.saved_scripts.has(parent_script_path):
  97. ModLoaderStore.saved_scripts[parent_script_path] = []
  98. # The first entry in the saved script array that has the path
  99. # used as a key will be the duplicate of the not modified script
  100. ModLoaderStore.saved_scripts[parent_script_path].append(parent_script.duplicate())
  101. ModLoaderStore.saved_scripts[parent_script_path].append(child_script)
  102. ModLoaderLog.info(
  103. "Installing script extension: %s <- %s" % [parent_script_path, extension_path], LOG_NAME
  104. )
  105. child_script.take_over_path(parent_script_path)
  106. return child_script
  107. # Reload all children classes of the vanilla class we just extended
  108. # Calling reload() the children of an extended class seems to allow them to be extended
  109. # e.g if B is a child class of A, reloading B after apply an extender of A allows extenders of B to properly extend B, taking A's extender(s) into account
  110. static func _reload_vanilla_child_classes_for(script: Script) -> void:
  111. if script == null:
  112. return
  113. var current_child_classes := []
  114. var actual_path: String = script.get_base_script().resource_path
  115. var classes: Array = ProjectSettings.get_global_class_list()
  116. for _class in classes:
  117. if _class.path == actual_path:
  118. current_child_classes.push_back(_class)
  119. break
  120. for _class in current_child_classes:
  121. for child_class in classes:
  122. if child_class.base == _class.get_class():
  123. load(child_class.path).reload()