gdharness 1.3.27 → 1.3.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,10 +6,27 @@ extends EditorPlugin
6
6
  ## that keeps showing the old file is how a change reads as one that never happened. The
7
7
  ## files are polled once a second; the editor's own watcher only fires on window focus.
8
8
 
9
+
10
+ ## What was last seen of a watched file.
11
+ class GdhSeen:
12
+ ## Its modification time, in whole seconds.
13
+ var time: int
14
+ ## The MD5 of its contents.
15
+ var md5: String
16
+
17
+ func _init(seen_time: int, seen_md5: String) -> void:
18
+ time = seen_time
19
+ md5 = seen_md5
20
+
21
+
9
22
  const CHECK_INTERVAL_SECONDS: float = 1.0
23
+ ## How long after a file's modification time its contents are compared as well. The time is in
24
+ ## whole seconds, so a second write inside the second of the first leaves it as it was and changes
25
+ ## only the contents; past that second any write moves the time.
26
+ const SAME_SECOND_SECONDS: int = 5
10
27
 
11
28
  var _timer: Timer
12
- ## The last modification time seen, by path.
29
+ ## What was last seen of each watched file, by path.
13
30
  var _watched_files: Dictionary = {}
14
31
 
15
32
 
@@ -22,6 +39,11 @@ func _enter_tree() -> void:
22
39
  var watching: int = _timer.timeout.connect(_check_for_changes)
23
40
  if watching != OK:
24
41
  push_error("gdharness: could not start watching for changes on disk")
42
+ # A scene's files are watched from the moment it opens: found at the next poll instead, a file
43
+ # written in between was first seen already changed, and its change was never reloaded.
44
+ var opening: int = scene_changed.connect(_on_scene_changed)
45
+ if opening != OK:
46
+ push_error("gdharness: could not start watching the scenes the editor opens")
25
47
  add_child(_timer)
26
48
  _timer.start()
27
49
  _update_watched_files()
@@ -33,12 +55,16 @@ func _exit_tree() -> void:
33
55
  _timer.queue_free()
34
56
 
35
57
 
58
+ func _on_scene_changed(_scene_root: Node) -> void:
59
+ _update_watched_files()
60
+
61
+
36
62
  func _update_watched_files() -> void:
37
63
  var edited_scene: Node = EditorInterface.get_edited_scene_root()
38
64
  if edited_scene and edited_scene.scene_file_path:
39
65
  var path: String = edited_scene.scene_file_path
40
66
  if not _watched_files.has(path):
41
- _watched_files[path] = _get_modified_time(path)
67
+ _watched_files[path] = _seen(path)
42
68
  _watch_node_scripts(edited_scene)
43
69
 
44
70
 
@@ -48,11 +74,15 @@ func _watch_node_scripts(node: Node) -> void:
48
74
  var attached: Script = script
49
75
  var path: String = attached.resource_path
50
76
  if not path.is_empty() and not _watched_files.has(path):
51
- _watched_files[path] = _get_modified_time(path)
77
+ _watched_files[path] = _seen(path)
52
78
  for child: Node in node.get_children():
53
79
  _watch_node_scripts(child)
54
80
 
55
81
 
82
+ func _seen(path: String) -> GdhSeen:
83
+ return GdhSeen.new(_get_modified_time(path), _md5_of(path))
84
+
85
+
56
86
  func _get_modified_time(path: String) -> int:
57
87
  var global_path: String = ProjectSettings.globalize_path(path)
58
88
  if FileAccess.file_exists(global_path):
@@ -60,20 +90,32 @@ func _get_modified_time(path: String) -> int:
60
90
  return 0
61
91
 
62
92
 
93
+ func _md5_of(path: String) -> String:
94
+ var global_path: String = ProjectSettings.globalize_path(path)
95
+ if FileAccess.file_exists(global_path):
96
+ return FileAccess.get_md5(global_path)
97
+ return ""
98
+
99
+
63
100
  func _check_for_changes() -> void:
64
101
  _update_watched_files()
65
102
 
103
+ var now: int = int(Time.get_unix_time_from_system())
66
104
  var scenes_to_reload: Array[String] = []
67
105
  var scripts_to_reload: Array[String] = []
68
106
  for path: String in _watched_files.keys():
107
+ var last: GdhSeen = _watched_files[path]
69
108
  var current_time: int = _get_modified_time(path)
70
- var last_time: int = _watched_files[path]
71
- if current_time > last_time:
72
- if path.ends_with(".gd"):
73
- scripts_to_reload.append(path)
74
- else:
75
- scenes_to_reload.append(path)
76
- _watched_files[path] = current_time
109
+ if current_time == last.time and now - current_time >= SAME_SECOND_SECONDS:
110
+ continue
111
+ var current_md5: String = _md5_of(path)
112
+ _watched_files[path] = GdhSeen.new(current_time, current_md5)
113
+ if current_md5 == last.md5:
114
+ continue
115
+ if path.ends_with(".gd"):
116
+ scripts_to_reload.append(path)
117
+ else:
118
+ scenes_to_reload.append(path)
77
119
 
78
120
  # Scripts first, so a scene reloaded after them instantiates the new code.
79
121
  for path: String in scripts_to_reload:
@@ -85,16 +85,19 @@ static func button_event(
85
85
  ## [param answer], saying so when one of [param points] lies outside [param viewport]: the GUI picks
86
86
  ## what is under the pointer only inside it, so such a press or motion reaches no control, and its
87
87
  ## answer read the same as one that landed.
88
+ ##
89
+ ## The points are window pixels and the visible rect is canvas units, which differ by the window's
90
+ ## content scale and any letterbox offset, so the rect is taken into window pixels first.
88
91
  static func outside_noted(answer: Dictionary, viewport: Viewport, points: Array[Vector2]) -> Dictionary:
89
- var shown: Rect2 = viewport.get_visible_rect()
92
+ var shown: Rect2 = viewport.get_final_transform() * viewport.get_visible_rect()
90
93
  for point: Vector2 in points:
91
94
  if not shown.has_point(point):
92
95
  answer["outside_viewport"] = (
93
96
  (
94
- "%s is outside the window's viewport, %s, so no control there receives it; the game's"
97
+ "%s is outside the window's viewport, %s in window pixels, so no control there receives"
95
98
  % [point, shown]
96
99
  )
97
- + " _input and _unhandled_input still do."
100
+ + " it; the game's _input and _unhandled_input still do."
98
101
  )
99
102
  break
100
103
  return answer
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.3.27",
3
+ "version": "1.3.28",
4
4
  "mcpName": "io.github.Aureliolo/gdharness",
5
5
  "description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
6
6
  "type": "module",