-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathplugin.gd
252 lines (194 loc) · 8.29 KB
/
plugin.gd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
@tool
extends EditorPlugin
var godot_project: GodotProject
var config: PatchworkConfig
var file_system: FileSystem
var sidebar
var last_synced_heads: PackedStringArray
# Array of [<path>, <content>]
var file_content_to_reload: Array = []
var files_to_reload_mutex: Mutex = Mutex.new()
var current_pw_to_godot_sync_task_id: int = -1
var deferred_pw_to_godot_sync: bool = false
var timer: SceneTreeTimer = null
var prev_checked_out_branch_id
func add_new_uid(path: String, uid: String):
var id = ResourceUID.text_to_id(uid)
if id == ResourceUID.INVALID_ID:
return
if not ResourceUID.has_id(id):
ResourceUID.add_id(id, path)
elif not ResourceUID.get_id_path(id) == path:
ResourceUID.set_id(id, path)
func _process(_delta: float) -> void:
if godot_project:
godot_project.process()
if current_pw_to_godot_sync_task_id != -1:
# currently running sync task
_try_wait_for_pw_to_godot_sync_task()
else:
# reload after sync
var new_files_to_reload: Array = []
var should_rerun = false
files_to_reload_mutex.lock()
# append array here, because otherwise new_files_to_reload just gets a reference to file_content_to_reload
new_files_to_reload.append_array(file_content_to_reload)
file_content_to_reload.clear()
should_rerun = deferred_pw_to_godot_sync
deferred_pw_to_godot_sync = false
files_to_reload_mutex.unlock()
if len(new_files_to_reload) > 0:
print("reloading %d files: " % new_files_to_reload.size())
for token in new_files_to_reload:
var path = token[0]
file_system.save_file(path, token[1])
if path.get_extension() == "uid":
var new_path = path.get_basename()
var uid = token[1].strip_edges()
add_new_uid(new_path, uid)
if path.get_extension() == "import":
var new_path = path.get_basename()
var uid = ""
for line in token[1].split("\n"):
if line.begins_with("uid="):
uid = line.split("=")[1].strip_edges()
add_new_uid(new_path, uid)
if path.get_extension() == "tscn":
# reload scene files to update references
get_editor_interface().reload_scene_from_path(path)
if should_rerun:
timer = get_tree().create_timer(5, true)
timer.timeout.connect(self.sync_patchwork_to_godot)
func _enter_tree() -> void:
print("start patchwork!!!");
config = PatchworkConfig.new();
file_system = FileSystem.new(self)
print("_enter_tree() -> init_godot_project()")
await init_godot_project()
print("end _enter_tree() -> init_godot_project()")
# listen for file changes once we have initialized the godot project
file_system.connect("file_changed", _on_local_file_changed)
# setup patchwork sidebar
sidebar = preload("res://addons/patchwork/sidebar.tscn").instantiate()
sidebar.init(self, godot_project)
add_control_to_dock(DOCK_SLOT_RIGHT_UL, sidebar)
func init_godot_project():
print("init_godot_project()")
var project_doc_id = config.get_value("project_doc_id", "")
godot_project = GodotProject.create(project_doc_id)
if godot_project == null:
print("Failed to create GodotProject instance.")
return
await godot_project.initialized
print("branches: ", godot_project.get_branches())
print("*** Patchwork Godot Project initialized! ***")
if !project_doc_id:
config.set_value("project_doc_id", godot_project.get_doc_id())
sync_godot_to_patchwork()
else:
sync_patchwork_to_godot()
godot_project.connect("files_changed", sync_patchwork_to_godot)
godot_project.checked_out_branch.connect(_on_checked_out_branch)
print("end init_godot_project()")
func do_sync_godot_to_patchwork():
var files_in_godot = get_relevant_godot_files()
print("sync godot -> patchwork (", files_in_godot.size(), ")")
for path in files_in_godot:
print(" save file: ", path)
godot_project.save_file(path, file_system.get_file(path))
func sync_godot_to_patchwork():
# TODO: this is synchronous for right now because GodotProject doesn't seem to be thread safe currently, getting deadlocks
# We need to wait for any pw_to_godot sync to finish before we start syncing in the other direction
_try_wait_for_pw_to_godot_sync_task(true)
do_sync_godot_to_patchwork()
last_synced_heads = godot_project.get_heads()
func _do_pw_to_godot_sync_element(i: int, files_in_patchwork: PackedStringArray):
if i >= files_in_patchwork.size():
return
var path = files_in_patchwork[i]
var gp_content = godot_project.get_file(path)
var fs_content = file_system.get_file(path)
if typeof(gp_content) == TYPE_NIL:
printerr("patchwork missing file content even though path exists: ", path)
files_to_reload_mutex.lock()
deferred_pw_to_godot_sync = true
files_to_reload_mutex.unlock()
return
elif fs_content != null and typeof(fs_content) != typeof(gp_content):
# log if current content is not the same type as content
printerr("different types at ", path, ": ", typeof(fs_content), " vs ", typeof(gp_content))
return
if gp_content != fs_content:
print(" reload file: ", path)
# The reason why we're not simply reloading here is that loading resources gets kinda dicey on anything other than the main thread
files_to_reload_mutex.lock()
file_content_to_reload.append([path, gp_content])
files_to_reload_mutex.unlock()
func do_pw_to_godot_sync_task():
print("performing patchwork to godot sync in parallel...")
var files_in_godot = get_relevant_godot_files()
var files_in_patchwork = godot_project.list_all_files()
print("sync patchwork -> godot (", files_in_patchwork.size(), ")")
var group_id = WorkerThreadPool.add_group_task(self._do_pw_to_godot_sync_element.bind(files_in_patchwork), files_in_patchwork.size())
WorkerThreadPool.wait_for_group_task_completion(group_id)
# todo: this is still buggy
# delete gd and tscn files that are not in checked out patchwork files
# for path in files_in_godot:
# if !files_in_patchwork.has(path) and (path.ends_with(".gd") or path.ends_with(".tscn")):
# print(" delete file: ", path)
# file_system.delete_file(path)
print("end patchwork to godot sync")
func _try_wait_for_pw_to_godot_sync_task(force: bool = false):
# We have to wait for a task to complete before the program exits so we don't have zombie threads,
# but we don't want to block waiting for the task to complete;
# so right now, we just check if it's completed and if not, we return immediately;
# _process calls this, so it will keep getting called each frame until we actually finish.
if current_pw_to_godot_sync_task_id != -1:
if force or WorkerThreadPool.is_task_completed(current_pw_to_godot_sync_task_id):
WorkerThreadPool.wait_for_task_completion(current_pw_to_godot_sync_task_id)
current_pw_to_godot_sync_task_id = -1
last_synced_heads = godot_project.get_heads()
func sync_patchwork_to_godot():
# only sync once the user has saved all files
# todo: add unsaved files check back
if PatchworkEditor.unsaved_files_open():
print("unsaved files open, not syncing")
files_to_reload_mutex.lock()
deferred_pw_to_godot_sync = true
files_to_reload_mutex.unlock()
return
current_pw_to_godot_sync_task_id = WorkerThreadPool.add_task(self.do_pw_to_godot_sync_task, false, "sync_patchwork_to_godot")
call_deferred("_try_wait_for_pw_to_godot_sync_task")
return
const BANNED_FILES = [".DS_Store", "thumbs.db", "desktop.ini"] # system files that should be ignored
func _is_relevant_file(path: String) -> bool:
if path.trim_prefix("res://").begins_with("target"):
return false
if path.begins_with("res://addons/") or path.begins_with("res://target/") or path.begins_with("res://."):
return false
var file = path.get_file()
if BANNED_FILES.has(file):
return false
return true
func get_relevant_godot_files() -> Array[String]:
# right now we only sync script and scene files, also we ignore the addons folder
var ret = file_system.list_all_files().filter(_is_relevant_file)
# print(ret)
return ret
func _on_checked_out_branch(checked_out_branch: String):
print("checked out branch ", checked_out_branch, " (", godot_project.list_all_files().size(), " files)")
sidebar.update_ui()
sync_patchwork_to_godot()
func _on_local_file_changed(path: String, content: Variant):
print("file changed", path)
if _is_relevant_file(path):
godot_project.save_file_at(path, last_synced_heads, content)
last_synced_heads = godot_project.get_heads()
func _exit_tree() -> void:
_try_wait_for_pw_to_godot_sync_task(true)
if sidebar:
remove_control_from_docks(sidebar)
if godot_project:
godot_project.stop();
if file_system:
file_system.stop()