file.gd 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. class_name _ModLoaderFile
  2. extends RefCounted
  3. # This Class provides util functions for working with files.
  4. # Currently all of the included functions are internal and should only be used by the mod loader itself.
  5. const LOG_NAME := "ModLoader:File"
  6. # Get Data
  7. # =============================================================================
  8. # Parses JSON from a given file path and returns a [Dictionary].
  9. # Returns an empty [Dictionary] if no file exists (check with size() < 1)
  10. static func get_json_as_dict(path: String) -> Dictionary:
  11. if not file_exists(path):
  12. return {}
  13. var file := FileAccess.open(path, FileAccess.READ)
  14. var error = file.get_open_error()
  15. if file == null:
  16. ModLoaderLog.error("Error opening file. Code: %s" % error, LOG_NAME)
  17. var content := file.get_as_text()
  18. return _get_json_string_as_dict(content)
  19. # Parses JSON from a given [String] and returns a [Dictionary].
  20. # Returns an empty [Dictionary] on error (check with size() < 1)
  21. static func _get_json_string_as_dict(string: String) -> Dictionary:
  22. if string == "":
  23. return {}
  24. var test_json_conv = JSON.new()
  25. var error = test_json_conv.parse(string)
  26. if not error == OK:
  27. ModLoaderLog.error("Error parsing JSON", LOG_NAME)
  28. return {}
  29. if not test_json_conv.data is Dictionary:
  30. ModLoaderLog.error("JSON is not a dictionary", LOG_NAME)
  31. return {}
  32. return test_json_conv.data
  33. # Opens the path and reports all the errors that can happen
  34. static func open_dir(folder_path: String) -> DirAccess:
  35. var mod_dir := DirAccess.open(folder_path)
  36. if mod_dir == null:
  37. ModLoaderLog.error("Can't open mod folder %s" % [folder_path], LOG_NAME)
  38. return null
  39. var mod_dir_open_error := mod_dir.get_open_error()
  40. if not mod_dir_open_error == OK:
  41. ModLoaderLog.info(
  42. "Can't open mod folder %s (Error: %s, %s)" %
  43. [folder_path, mod_dir_open_error, error_string(mod_dir_open_error)],
  44. LOG_NAME
  45. )
  46. return null
  47. var mod_dir_listdir_error := mod_dir.list_dir_begin() # TODOGODOT4 fill missing arguments https://github.com/godotengine/godot/pull/40547
  48. if not mod_dir_listdir_error == OK:
  49. ModLoaderLog.error(
  50. "Can't read mod folder %s (Error: %s, %s)" %
  51. [folder_path, mod_dir_listdir_error, error_string(mod_dir_listdir_error)],
  52. LOG_NAME
  53. )
  54. return null
  55. return mod_dir
  56. static func get_json_as_dict_from_zip(zip_path: String, file_path: String, is_full_path := false) -> Dictionary:
  57. if not file_exists(zip_path):
  58. ModLoaderLog.error("Zip was not found at %s" % [zip_path], LOG_NAME)
  59. return {}
  60. var reader := ZIPReader.new()
  61. var zip_open_error := reader.open(zip_path)
  62. if not zip_open_error == OK:
  63. ModLoaderLog.error(
  64. "Error opening zip. (Error: %s, %s)" %
  65. [zip_open_error, error_string(zip_open_error)],
  66. LOG_NAME
  67. )
  68. var full_path := ""
  69. if is_full_path:
  70. full_path = file_path
  71. if not reader.file_exists(full_path):
  72. ModLoaderLog.error("File was not found in zip at path %s" % [file_path], LOG_NAME)
  73. return {}
  74. else:
  75. # Go through all files and find the file
  76. # Since we don't know which mod folder will be in the zip to get the exact full path
  77. # (zip naming is not required to be the name as folder name)
  78. for path in reader.get_files():
  79. if Array(path.rsplit("/", false, 1)).back() == file_path:
  80. full_path = path
  81. if not full_path:
  82. ModLoaderLog.error("File was not found in zip at path %s" % [file_path], LOG_NAME)
  83. return {}
  84. var content := reader.read_file(full_path).get_string_from_utf8()
  85. return _get_json_string_as_dict(content)
  86. # Save Data
  87. # =============================================================================
  88. # Saves a dictionary to a file, as a JSON string
  89. static func _save_string_to_file(save_string: String, filepath: String) -> bool:
  90. # Create directory if it doesn't exist yet
  91. var file_directory := filepath.get_base_dir()
  92. var dir := DirAccess.open(file_directory)
  93. _code_note(str(
  94. "View error codes here:",
  95. "https://docs.godotengine.org/en/stable/classes/class_%40globalscope.html#enum-globalscope-error"
  96. ))
  97. if not dir:
  98. var makedir_error := DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(file_directory))
  99. if not makedir_error == OK:
  100. ModLoaderLog.fatal("Encountered an error (%s) when attempting to create a directory, with the path: %s" % [makedir_error, file_directory], LOG_NAME)
  101. return false
  102. # Save data to the file
  103. var file := FileAccess.open(filepath, FileAccess.WRITE)
  104. if not file:
  105. ModLoaderLog.fatal("Encountered an error (%s) when attempting to write to a file, with the path: %s" % [FileAccess.get_open_error(), filepath], LOG_NAME)
  106. return false
  107. file.store_string(save_string)
  108. file.close()
  109. return true
  110. # Saves a dictionary to a file, as a JSON string
  111. static func save_dictionary_to_json_file(data: Dictionary, filepath: String) -> bool:
  112. var json_string := JSON.stringify(data, "\t")
  113. return _save_string_to_file(json_string, filepath)
  114. # Remove Data
  115. # =============================================================================
  116. # Removes a file from the given path
  117. static func remove_file(file_path: String) -> bool:
  118. var dir := DirAccess.open(file_path)
  119. if not dir.file_exists(file_path):
  120. ModLoaderLog.error("No file found at \"%s\"" % file_path, LOG_NAME)
  121. return false
  122. var error := dir.remove(file_path)
  123. if error:
  124. ModLoaderLog.error(
  125. "Encountered an error (%s) when attempting to remove the file, with the path: %s"
  126. % [error, file_path],
  127. LOG_NAME
  128. )
  129. return false
  130. return true
  131. # Checks
  132. # =============================================================================
  133. static func file_exists(path: String, zip_path: String = "") -> bool:
  134. if not zip_path.is_empty():
  135. return file_exists_in_zip(zip_path, path)
  136. var exists := FileAccess.file_exists(path)
  137. # If the file is not found, check if it has been remapped because it is a Resource.
  138. if not exists:
  139. exists = ResourceLoader.exists(path)
  140. return exists
  141. static func dir_exists(path: String) -> bool:
  142. return DirAccess.dir_exists_absolute(path)
  143. static func file_exists_in_zip(zip_path: String, path: String) -> bool:
  144. var reader := zip_reader_open(zip_path)
  145. if not reader:
  146. return false
  147. if _ModLoaderGodot.is_version_below(_ModLoaderGodot.ENGINE_VERSION_HEX_4_2_0):
  148. return reader.get_files().has(path.trim_prefix("res://"))
  149. else:
  150. return reader.file_exists(path.trim_prefix("res://"))
  151. static func get_mod_dir_name_in_zip(zip_path: String) -> String:
  152. var reader := _ModLoaderFile.zip_reader_open(zip_path)
  153. if not reader:
  154. return ""
  155. var file_paths := reader.get_files()
  156. for file_path in file_paths:
  157. # We asume tat the mod_main.gd is at the root of the mod dir
  158. if file_path.ends_with("mod_main.gd") and file_path.split("/").size() == 3:
  159. return file_path.split("/")[-2]
  160. return ""
  161. static func zip_reader_open(zip_path) -> ZIPReader:
  162. var reader := ZIPReader.new()
  163. var err := reader.open(zip_path)
  164. if err != OK:
  165. ModLoaderLog.error("Could not open zip with error: %s" % error_string(err), LOG_NAME)
  166. return
  167. return reader
  168. static func load_manifest_file(path: String) -> Dictionary:
  169. ModLoaderLog.debug("Loading mod_manifest from -> %s" % path, LOG_NAME)
  170. if _ModLoaderPath.is_zip(path):
  171. return get_json_as_dict_from_zip(path, ModData.MANIFEST)
  172. return get_json_as_dict(path.path_join(ModData.MANIFEST))
  173. # This is a dummy func. It is exclusively used to show notes in the code that
  174. # stay visible after decompiling a PCK, as is primarily intended to assist new
  175. # modders in understanding and troubleshooting issues.
  176. static func _code_note(_msg:String):
  177. pass