gdharness 1.1.29 → 1.1.30
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.
package/build/cli.js
CHANGED
|
@@ -17841,7 +17841,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17841
17841
|
},
|
|
17842
17842
|
{
|
|
17843
17843
|
name: "runtime_capture",
|
|
17844
|
-
description: "A picture of the running game: the whole screen or one viewport, as an image. The whole screen includes the game's own dialogs and popups where they sit, also when a project opens them as windows of their own (subwindows not embedded). Needs the game running with a window. A capture asked for before the game's first frame is drawn waits for that frame, since the game announces itself before it and the viewport holds nothing it drew until then.",
|
|
17844
|
+
description: "A picture of the running game: the whole screen, one region or node of it at full resolution, or one viewport, as an image. The whole screen includes the game's own dialogs and popups where they sit, also when a project opens them as windows of their own (subwindows not embedded). Needs the game running with a window. A capture asked for before the game's first frame is drawn waits for that frame, since the game announces itself before it and the viewport holds nothing it drew until then.",
|
|
17845
17845
|
parameters: {
|
|
17846
17846
|
projectPath: RUNNING_PROJECT_PATH,
|
|
17847
17847
|
pid: RUNNING_PID,
|
|
@@ -17860,6 +17860,22 @@ var init_tool_definitions = __esm(() => {
|
|
|
17860
17860
|
minimum: 1,
|
|
17861
17861
|
description: "Scale the image to this height. Given alone, the width keeps the picture's proportions."
|
|
17862
17862
|
},
|
|
17863
|
+
region: {
|
|
17864
|
+
type: "object",
|
|
17865
|
+
ops: ["screenshot"],
|
|
17866
|
+
description: "screenshot: capture only this rectangle of the window, at the game's own pixels: x, y, width and height in window pixels, or the window rectangle runtime_inspect rect answers, passed as it came. A region partly outside the window is cut to it and the answer says clipped; one wholly outside is refused. The answer names the region captured in window pixels, and drawnAt when the project draws at its own size and stretches that to the window, since the picture then has the game's pixels rather than the window's."
|
|
17867
|
+
},
|
|
17868
|
+
nodePath: {
|
|
17869
|
+
type: "string",
|
|
17870
|
+
ops: ["screenshot"],
|
|
17871
|
+
description: "screenshot: capture only where this node is drawn, as runtime_inspect rect answers it: a Control's rectangle, what a 3D node's geometry covers, or what a Sprite2D draws. A node with a position and no extent is refused; give region around it instead. Not with region."
|
|
17872
|
+
},
|
|
17873
|
+
zoom: {
|
|
17874
|
+
type: "integer",
|
|
17875
|
+
minimum: 1,
|
|
17876
|
+
maximum: 16,
|
|
17877
|
+
description: "Enlarge the picture this many times, each pixel made a square of pixels rather than blended with its neighbours, so a thin line or a corner can be read. Not with width or height. Default 1."
|
|
17878
|
+
},
|
|
17863
17879
|
outputPath: {
|
|
17864
17880
|
type: "string",
|
|
17865
17881
|
description: "Also save the picture as a PNG at this absolute path, for somebody to open later; the answer says where. Refused unless it ends in .png, its directory exists, it is outside the project, and no file is there already."
|
|
@@ -39645,6 +39661,25 @@ function captureDestinationRefusal(outputPath, projects) {
|
|
|
39645
39661
|
}
|
|
39646
39662
|
return null;
|
|
39647
39663
|
}
|
|
39664
|
+
function captureDetail(payload) {
|
|
39665
|
+
const parts = [];
|
|
39666
|
+
const region = payload["region"];
|
|
39667
|
+
if (region !== null && typeof region === "object") {
|
|
39668
|
+
const named = region;
|
|
39669
|
+
const at = (key) => String(Math.round(Number(named[key]) * 100) / 100);
|
|
39670
|
+
parts.push(`of the window region at ${at("x")},${at("y")}, ${at("width")}x${at("height")} window pixels${payload["clipped"] === true ? ", clipped to the window" : ""}`);
|
|
39671
|
+
}
|
|
39672
|
+
const drawnAt = payload["drawnAt"];
|
|
39673
|
+
if (drawnAt !== null && typeof drawnAt === "object") {
|
|
39674
|
+
const drawn = drawnAt;
|
|
39675
|
+
parts.push(`at the game's own pixels: it draws at ${String(drawn["width"])}x${String(drawn["height"])} and stretches that to its window`);
|
|
39676
|
+
}
|
|
39677
|
+
if (typeof payload["zoom"] === "number") {
|
|
39678
|
+
parts.push(`enlarged ${payload["zoom"]} times`);
|
|
39679
|
+
}
|
|
39680
|
+
const note = typeof payload["note"] === "string" ? `. ${payload["note"]}` : "";
|
|
39681
|
+
return parts.length === 0 ? note : `, ${parts.join(", ")}${note}`;
|
|
39682
|
+
}
|
|
39648
39683
|
function patienceForFrames(frames, atLeast) {
|
|
39649
39684
|
return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
|
|
39650
39685
|
}
|
|
@@ -44398,7 +44433,7 @@ ${staleNote}`);
|
|
|
44398
44433
|
content: [
|
|
44399
44434
|
{
|
|
44400
44435
|
type: "text",
|
|
44401
|
-
text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}${saveTo === null ? "" : `. Saved to ${saveTo}`}${staleNote === undefined ? "" : `
|
|
44436
|
+
text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}${captureDetail(payload)}${saveTo === null ? "" : `. Saved to ${saveTo}`}${staleNote === undefined ? "" : `
|
|
44402
44437
|
|
|
44403
44438
|
${staleNote}`}`
|
|
44404
44439
|
},
|
|
@@ -58,7 +58,7 @@ var _queries: Queries = Queries.new(self, values)
|
|
|
58
58
|
var _changes: Changes = Changes.new(self, values)
|
|
59
59
|
var _input: InputCommands = InputCommands.new(self, values)
|
|
60
60
|
var _typing: Typing = Typing.new(self)
|
|
61
|
-
var _capture: Capture = Capture.new(self)
|
|
61
|
+
var _capture: Capture = Capture.new(self, values)
|
|
62
62
|
var _waits: Waits = Waits.new(self, values)
|
|
63
63
|
|
|
64
64
|
var _server: TCPServer
|
|
@@ -4,12 +4,20 @@ extends RefCounted
|
|
|
4
4
|
|
|
5
5
|
const Read = preload("reading.gd")
|
|
6
6
|
const Values = preload("runtime_values.gd")
|
|
7
|
+
const Queries = preload("runtime_queries.gd")
|
|
8
|
+
const Screen = preload("runtime_screen.gd")
|
|
9
|
+
|
|
10
|
+
## How far zoom enlarges at most. Sixteen makes a 3-pixel ring 48 pixels across, past any need to
|
|
11
|
+
## see it, and a 200-pixel region 3200, past the size anybody reads a picture at.
|
|
12
|
+
const ZOOM_MOST: int = 16
|
|
7
13
|
|
|
8
14
|
var _host: Node
|
|
15
|
+
var _values: Values
|
|
9
16
|
|
|
10
17
|
|
|
11
|
-
func _init(host: Node) -> void:
|
|
18
|
+
func _init(host: Node, values: Values) -> void:
|
|
12
19
|
_host = host
|
|
20
|
+
_values = values
|
|
13
21
|
|
|
14
22
|
|
|
15
23
|
func capture_screenshot(params: Dictionary) -> Dictionary:
|
|
@@ -88,12 +96,155 @@ static func scaled_to(drawn: Vector2i, width: int, height: int) -> Vector2i:
|
|
|
88
96
|
return drawn
|
|
89
97
|
|
|
90
98
|
|
|
99
|
+
## What part of the picture a capture is cut to and how far it is enlarged, from what was asked, or
|
|
100
|
+
## a refusal saying what is wrong with the asking. Read before a frame is waited for, so a call that
|
|
101
|
+
## cannot be answered costs no wait.
|
|
102
|
+
##
|
|
103
|
+
## A region and a node are asked for because width and height scale the whole picture, and a
|
|
104
|
+
## question about a 3-pixel ring in a 1600-pixel window was answered by capturing the window, saving
|
|
105
|
+
## it and cropping it with a script of the caller's own, three calls for every look.
|
|
106
|
+
func _cut_asked(params: Dictionary, in_window: bool) -> Dictionary:
|
|
107
|
+
var region_given: Variant = params.get("region")
|
|
108
|
+
var node_path: String = str(params.get("nodePath", ""))
|
|
109
|
+
var zoom: int = Read.as_int(params.get("zoom", 1), 1)
|
|
110
|
+
var sized: bool = Read.as_int(params.get("width", 0)) > 0 or Read.as_int(params.get("height", 0)) > 0
|
|
111
|
+
if region_given != null and not node_path.is_empty():
|
|
112
|
+
return _refused("region and nodePath each name the part to capture; give one of them")
|
|
113
|
+
if (region_given != null or not node_path.is_empty()) and not in_window:
|
|
114
|
+
return _refused(
|
|
115
|
+
(
|
|
116
|
+
"region and nodePath are in the window's pixels, and this viewport's texture is in its "
|
|
117
|
+
+ "own: capture the screen with them, or this viewport whole"
|
|
118
|
+
)
|
|
119
|
+
)
|
|
120
|
+
if zoom < 1 or zoom > ZOOM_MOST:
|
|
121
|
+
return _refused("zoom is a whole factor from 1 to %d, and %d was asked for" % [ZOOM_MOST, zoom])
|
|
122
|
+
if zoom > 1 and sized:
|
|
123
|
+
return _refused(
|
|
124
|
+
(
|
|
125
|
+
"zoom enlarges by a whole factor and keeps every pixel square, and width and height "
|
|
126
|
+
+ "scale to a size; give one or the other"
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
var asked: Dictionary = {"zoom": zoom}
|
|
130
|
+
if region_given != null:
|
|
131
|
+
var region: Variant = region_of(_values, region_given)
|
|
132
|
+
if region == null:
|
|
133
|
+
var shape: String = (
|
|
134
|
+
"region is a rectangle in window pixels with an area: x, y, width and height, or the "
|
|
135
|
+
+ "window rectangle runtime_inspect rect answers, passed back as it came. %s is neither"
|
|
136
|
+
)
|
|
137
|
+
return _refused(shape % JSON.stringify(region_given))
|
|
138
|
+
asked["region"] = region
|
|
139
|
+
if not node_path.is_empty():
|
|
140
|
+
asked["node_path"] = node_path
|
|
141
|
+
return asked
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
static func _refused(message: String) -> Dictionary:
|
|
145
|
+
return {"type": "error", "message": message}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
## The rectangle [param given] names: the four numbers x, y, width and height, or a rectangle as the
|
|
149
|
+
## runtime answers one, its two corners tagged with their type, passed back as it came. Null when it
|
|
150
|
+
## is neither or has no area.
|
|
151
|
+
static func region_of(values: Values, given: Variant) -> Variant:
|
|
152
|
+
if not given is Dictionary:
|
|
153
|
+
return null
|
|
154
|
+
var fields: Dictionary = given
|
|
155
|
+
fields = fields.duplicate()
|
|
156
|
+
if not fields.has("_type"):
|
|
157
|
+
fields["_type"] = "Rect2"
|
|
158
|
+
var rebuilt: Variant = values.deserialize(fields)
|
|
159
|
+
var region: Rect2
|
|
160
|
+
if rebuilt is Rect2:
|
|
161
|
+
region = rebuilt
|
|
162
|
+
elif rebuilt is Rect2i:
|
|
163
|
+
var whole: Rect2i = rebuilt
|
|
164
|
+
region = Rect2(whole)
|
|
165
|
+
else:
|
|
166
|
+
return null
|
|
167
|
+
if not region.has_area():
|
|
168
|
+
return null
|
|
169
|
+
return region
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
## Where [param node] is drawn, in window pixels, as runtime_inspect rect answers it: a Control's
|
|
173
|
+
## rectangle, what a 3D node's geometry covers, or what a 2D node that knows its own rectangle draws.
|
|
174
|
+
## A refusal's message when it has no extent on screen.
|
|
175
|
+
static func drawn_rect_of(node: Node) -> Variant:
|
|
176
|
+
if node is Control:
|
|
177
|
+
var control: Control = node
|
|
178
|
+
return Screen.rect_in_window(
|
|
179
|
+
control.get_viewport(),
|
|
180
|
+
control.get_global_transform_with_canvas() * Rect2(Vector2.ZERO, control.size)
|
|
181
|
+
)
|
|
182
|
+
if node is Node3D:
|
|
183
|
+
var spatial: Node3D = node
|
|
184
|
+
var found: Dictionary = Queries.in_frame(spatial)
|
|
185
|
+
if not found.has("rect"):
|
|
186
|
+
return "%s is not drawn by any current camera, so it covers nothing on screen" % node.get_path()
|
|
187
|
+
var covered: Rect2 = found["rect"]
|
|
188
|
+
return Screen.rect_in_window(spatial.get_viewport(), covered)
|
|
189
|
+
if node is Node2D and node.has_method("get_rect"):
|
|
190
|
+
var item: Node2D = node
|
|
191
|
+
var own: Rect2 = item.call("get_rect")
|
|
192
|
+
return Screen.rect_in_window(item.get_viewport(), item.get_global_transform_with_canvas() * own)
|
|
193
|
+
var no_extent: String = (
|
|
194
|
+
"%s is a %s, which has a position on screen and no extent; give region around the point "
|
|
195
|
+
+ "runtime_inspect rect answers for it"
|
|
196
|
+
)
|
|
197
|
+
return no_extent % [node.get_path(), node.get_class()]
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
## [param region], in window pixels, as pixels of a picture drawn at [param drawn] in a window
|
|
201
|
+
## [param window] pixels across: the same rectangle when the two match, scaled when the project draws
|
|
202
|
+
## at its own size and stretches that to the window. Widened to whole pixels, so an edge falling
|
|
203
|
+
## inside a pixel keeps that pixel, and cut to the picture. Without area when none of it is in it.
|
|
204
|
+
static func in_picture(region: Rect2, window: Vector2i, drawn: Vector2i) -> Rect2i:
|
|
205
|
+
return widened(region, window, drawn).intersection(Rect2i(Vector2i.ZERO, drawn))
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
## [param region] in the picture's pixels as [method in_picture] takes it, before it is cut to the
|
|
209
|
+
## picture, which is what tells a region that was clipped from one that fitted.
|
|
210
|
+
static func widened(region: Rect2, window: Vector2i, drawn: Vector2i) -> Rect2i:
|
|
211
|
+
var scale: Vector2 = Vector2.ONE
|
|
212
|
+
if window.x > 0 and window.y > 0:
|
|
213
|
+
scale = Vector2(drawn) / Vector2(window)
|
|
214
|
+
var start: Vector2 = (region.position * scale).floor()
|
|
215
|
+
var end: Vector2 = (region.end * scale).ceil()
|
|
216
|
+
return Rect2i(Vector2i(start), Vector2i(end - start))
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
## [param picture], pixels of a picture drawn at [param drawn], as the window pixels it covers in a
|
|
220
|
+
## window [param window] pixels across: what an answer names, since the region was asked for in them.
|
|
221
|
+
static func in_window_pixels(picture: Rect2i, window: Vector2i, drawn: Vector2i) -> Dictionary:
|
|
222
|
+
var scale: Vector2 = Vector2.ONE
|
|
223
|
+
if drawn.x > 0 and drawn.y > 0 and window.x > 0 and window.y > 0:
|
|
224
|
+
scale = Vector2(window) / Vector2(drawn)
|
|
225
|
+
var start: Vector2 = Vector2(picture.position) * scale
|
|
226
|
+
var size: Vector2 = Vector2(picture.size) * scale
|
|
227
|
+
return {"x": start.x, "y": start.y, "width": size.x, "height": size.y}
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
## [param image] cut to [param picture] and enlarged [param zoom] times, each pixel made a square of
|
|
231
|
+
## them rather than blended with its neighbours, so a pixel in the answer is a pixel the game drew.
|
|
232
|
+
static func cut(image: Image, picture: Rect2i, zoom: int) -> Image:
|
|
233
|
+
var part: Image = image.get_region(picture)
|
|
234
|
+
if zoom > 1:
|
|
235
|
+
part.resize(part.get_width() * zoom, part.get_height() * zoom, Image.INTERPOLATE_NEAREST)
|
|
236
|
+
return part
|
|
237
|
+
|
|
238
|
+
|
|
91
239
|
## The server names the file, so a game cannot point it at a path of its own choosing; a call
|
|
92
240
|
## with no path is a call the server did not make.
|
|
93
241
|
func _capture(viewport: Viewport, params: Dictionary, with_windows: bool = false) -> Dictionary:
|
|
94
242
|
var requested_path: String = str(params.get("output_path", ""))
|
|
95
243
|
if requested_path.is_empty():
|
|
96
244
|
return {"type": "error", "message": "output_path required"}
|
|
245
|
+
var asked: Dictionary = _cut_asked(params, with_windows)
|
|
246
|
+
if asked.has("message"):
|
|
247
|
+
return asked
|
|
97
248
|
|
|
98
249
|
# Godot draws nothing to a minimised window and nothing at all without one, and the
|
|
99
250
|
# texture keeps whatever was drawn last. A capture then comes back byte for byte the same
|
|
@@ -127,12 +278,57 @@ func _capture(viewport: Viewport, params: Dictionary, with_windows: bool = false
|
|
|
127
278
|
if with_windows:
|
|
128
279
|
_lay_native_windows(image)
|
|
129
280
|
|
|
130
|
-
var
|
|
131
|
-
var
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
281
|
+
var described: Dictionary = {}
|
|
282
|
+
var region: Variant = asked.get("region")
|
|
283
|
+
if asked.has("node_path"):
|
|
284
|
+
var node_path: String = asked["node_path"]
|
|
285
|
+
var standing: Dictionary = Values.node_at(_host.get_tree().root, node_path)
|
|
286
|
+
if standing.has("message"):
|
|
287
|
+
return standing
|
|
288
|
+
var node: Node = standing["node"]
|
|
289
|
+
var placed: Variant = drawn_rect_of(node)
|
|
290
|
+
if placed is String:
|
|
291
|
+
var why: String = placed
|
|
292
|
+
return _refused(why)
|
|
293
|
+
region = placed
|
|
294
|
+
# A hidden node's rectangle is still a place, and what is drawn there is whatever is behind it.
|
|
295
|
+
if not Queries.shown(node):
|
|
296
|
+
described["note"] = (
|
|
297
|
+
"%s is hidden, so this is what is drawn where it would be rather than the node" % node_path
|
|
298
|
+
)
|
|
299
|
+
if region != null:
|
|
300
|
+
var asked_region: Rect2 = region
|
|
301
|
+
var window: Vector2i = _host.get_tree().root.size
|
|
302
|
+
var picture_size: Vector2i = image.get_size()
|
|
303
|
+
var whole: Rect2i = widened(asked_region, window, picture_size)
|
|
304
|
+
var picture: Rect2i = whole.intersection(Rect2i(Vector2i.ZERO, picture_size))
|
|
305
|
+
if not picture.has_area():
|
|
306
|
+
return _refused(
|
|
307
|
+
(
|
|
308
|
+
"The region at %s, %s pixels across, is outside the window, which is %dx%d pixels"
|
|
309
|
+
% [asked_region.position, asked_region.size, window.x, window.y]
|
|
310
|
+
)
|
|
311
|
+
)
|
|
312
|
+
image = cut(image, picture, 1)
|
|
313
|
+
described["region"] = in_window_pixels(picture, window, picture_size)
|
|
314
|
+
if picture != whole:
|
|
315
|
+
described["clipped"] = true
|
|
316
|
+
# The project draws at its own size and stretches that to the window, so the picture's pixels
|
|
317
|
+
# are not the window's: said, because a caller sizing a region by window pixels gets fewer.
|
|
318
|
+
if picture_size != window:
|
|
319
|
+
described["drawnAt"] = {"width": picture_size.x, "height": picture_size.y}
|
|
320
|
+
|
|
321
|
+
var zoom: int = asked["zoom"]
|
|
322
|
+
if zoom > 1:
|
|
323
|
+
image = cut(image, Rect2i(Vector2i.ZERO, image.get_size()), zoom)
|
|
324
|
+
described["zoom"] = zoom
|
|
325
|
+
else:
|
|
326
|
+
var drawn: Vector2i = Vector2i(image.get_width(), image.get_height())
|
|
327
|
+
var target: Vector2i = scaled_to(
|
|
328
|
+
drawn, Read.as_int(params.get("width", 0)), Read.as_int(params.get("height", 0))
|
|
329
|
+
)
|
|
330
|
+
if target != drawn:
|
|
331
|
+
image.resize(target.x, target.y)
|
|
136
332
|
|
|
137
333
|
var screenshot_path: String = requested_path
|
|
138
334
|
if screenshot_path.begins_with("user://") or screenshot_path.begins_with("res://"):
|
|
@@ -141,10 +337,12 @@ func _capture(viewport: Viewport, params: Dictionary, with_windows: bool = false
|
|
|
141
337
|
if save_error != OK:
|
|
142
338
|
return {"type": "error", "message": "Failed to save screenshot as PNG: " + str(save_error)}
|
|
143
339
|
|
|
144
|
-
|
|
340
|
+
var answer: Dictionary = {
|
|
145
341
|
"type": "screenshot_file",
|
|
146
342
|
"format": "png",
|
|
147
343
|
"width": image.get_width(),
|
|
148
344
|
"height": image.get_height(),
|
|
149
345
|
"path": screenshot_path
|
|
150
346
|
}
|
|
347
|
+
answer.merge(described)
|
|
348
|
+
return answer
|
package/build/index.js
CHANGED
|
@@ -31825,7 +31825,7 @@ var TOOL_SPECS = [
|
|
|
31825
31825
|
},
|
|
31826
31826
|
{
|
|
31827
31827
|
name: "runtime_capture",
|
|
31828
|
-
description: "A picture of the running game: the whole screen or one viewport, as an image. The whole screen includes the game's own dialogs and popups where they sit, also when a project opens them as windows of their own (subwindows not embedded). Needs the game running with a window. A capture asked for before the game's first frame is drawn waits for that frame, since the game announces itself before it and the viewport holds nothing it drew until then.",
|
|
31828
|
+
description: "A picture of the running game: the whole screen, one region or node of it at full resolution, or one viewport, as an image. The whole screen includes the game's own dialogs and popups where they sit, also when a project opens them as windows of their own (subwindows not embedded). Needs the game running with a window. A capture asked for before the game's first frame is drawn waits for that frame, since the game announces itself before it and the viewport holds nothing it drew until then.",
|
|
31829
31829
|
parameters: {
|
|
31830
31830
|
projectPath: RUNNING_PROJECT_PATH,
|
|
31831
31831
|
pid: RUNNING_PID,
|
|
@@ -31844,6 +31844,22 @@ var TOOL_SPECS = [
|
|
|
31844
31844
|
minimum: 1,
|
|
31845
31845
|
description: "Scale the image to this height. Given alone, the width keeps the picture's proportions."
|
|
31846
31846
|
},
|
|
31847
|
+
region: {
|
|
31848
|
+
type: "object",
|
|
31849
|
+
ops: ["screenshot"],
|
|
31850
|
+
description: "screenshot: capture only this rectangle of the window, at the game's own pixels: x, y, width and height in window pixels, or the window rectangle runtime_inspect rect answers, passed as it came. A region partly outside the window is cut to it and the answer says clipped; one wholly outside is refused. The answer names the region captured in window pixels, and drawnAt when the project draws at its own size and stretches that to the window, since the picture then has the game's pixels rather than the window's."
|
|
31851
|
+
},
|
|
31852
|
+
nodePath: {
|
|
31853
|
+
type: "string",
|
|
31854
|
+
ops: ["screenshot"],
|
|
31855
|
+
description: "screenshot: capture only where this node is drawn, as runtime_inspect rect answers it: a Control's rectangle, what a 3D node's geometry covers, or what a Sprite2D draws. A node with a position and no extent is refused; give region around it instead. Not with region."
|
|
31856
|
+
},
|
|
31857
|
+
zoom: {
|
|
31858
|
+
type: "integer",
|
|
31859
|
+
minimum: 1,
|
|
31860
|
+
maximum: 16,
|
|
31861
|
+
description: "Enlarge the picture this many times, each pixel made a square of pixels rather than blended with its neighbours, so a thin line or a corner can be read. Not with width or height. Default 1."
|
|
31862
|
+
},
|
|
31847
31863
|
outputPath: {
|
|
31848
31864
|
type: "string",
|
|
31849
31865
|
description: "Also save the picture as a PNG at this absolute path, for somebody to open later; the answer says where. Refused unless it ends in .png, its directory exists, it is outside the project, and no file is there already."
|
|
@@ -32161,6 +32177,25 @@ function captureDestinationRefusal(outputPath, projects) {
|
|
|
32161
32177
|
}
|
|
32162
32178
|
return null;
|
|
32163
32179
|
}
|
|
32180
|
+
function captureDetail(payload) {
|
|
32181
|
+
const parts = [];
|
|
32182
|
+
const region = payload["region"];
|
|
32183
|
+
if (region !== null && typeof region === "object") {
|
|
32184
|
+
const named = region;
|
|
32185
|
+
const at = (key) => String(Math.round(Number(named[key]) * 100) / 100);
|
|
32186
|
+
parts.push(`of the window region at ${at("x")},${at("y")}, ${at("width")}x${at("height")} window pixels${payload["clipped"] === true ? ", clipped to the window" : ""}`);
|
|
32187
|
+
}
|
|
32188
|
+
const drawnAt = payload["drawnAt"];
|
|
32189
|
+
if (drawnAt !== null && typeof drawnAt === "object") {
|
|
32190
|
+
const drawn = drawnAt;
|
|
32191
|
+
parts.push(`at the game's own pixels: it draws at ${String(drawn["width"])}x${String(drawn["height"])} and stretches that to its window`);
|
|
32192
|
+
}
|
|
32193
|
+
if (typeof payload["zoom"] === "number") {
|
|
32194
|
+
parts.push(`enlarged ${payload["zoom"]} times`);
|
|
32195
|
+
}
|
|
32196
|
+
const note = typeof payload["note"] === "string" ? `. ${payload["note"]}` : "";
|
|
32197
|
+
return parts.length === 0 ? note : `, ${parts.join(", ")}${note}`;
|
|
32198
|
+
}
|
|
32164
32199
|
function patienceForFrames(frames, atLeast) {
|
|
32165
32200
|
return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
|
|
32166
32201
|
}
|
|
@@ -36978,7 +37013,7 @@ ${staleNote}`);
|
|
|
36978
37013
|
content: [
|
|
36979
37014
|
{
|
|
36980
37015
|
type: "text",
|
|
36981
|
-
text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}${saveTo === null ? "" : `. Saved to ${saveTo}`}${staleNote === undefined ? "" : `
|
|
37016
|
+
text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}${captureDetail(payload)}${saveTo === null ? "" : `. Saved to ${saveTo}`}${staleNote === undefined ? "" : `
|
|
36982
37017
|
|
|
36983
37018
|
${staleNote}`}`
|
|
36984
37019
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.30",
|
|
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",
|