@beremaran/godot-agent-loop 1.0.0
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/LICENSE +23 -0
- package/README.md +925 -0
- package/addons/godot_agent_loop/LICENSE +23 -0
- package/addons/godot_agent_loop/README.md +31 -0
- package/addons/godot_agent_loop/plugin.cfg +8 -0
- package/addons/godot_agent_loop/plugin.gd +417 -0
- package/agent-plugin/.claude-plugin/plugin.json +19 -0
- package/agent-plugin/.codex-plugin/plugin.json +37 -0
- package/agent-plugin/.mcp.json +14 -0
- package/agent-plugin/adapter-manifest.json +45 -0
- package/agent-plugin/pi/extension.ts +136 -0
- package/agent-plugin/skills/build-godot-game/SKILL.md +41 -0
- package/agent-plugin/skills/debug-godot-game/SKILL.md +37 -0
- package/agent-plugin/skills/ship-godot-game/SKILL.md +46 -0
- package/agent-plugin/skills/ship-godot-game/agents/openai.yaml +4 -0
- package/agent-plugin/skills/verify-godot-change/SKILL.md +35 -0
- package/build/authoring-session-manager.js +237 -0
- package/build/domain-tool-registries.js +207 -0
- package/build/editor-bridge-protocol.js +1 -0
- package/build/editor-connection.js +112 -0
- package/build/editor-mutation-guard.js +30 -0
- package/build/editor-plugin-installer.js +220 -0
- package/build/engine-api/extension_api-4.7.stable.official.5b4e0cb0f.json +356830 -0
- package/build/game-command-service.js +49 -0
- package/build/game-connection.js +337 -0
- package/build/godot-executable.js +156 -0
- package/build/godot-process-manager.js +112 -0
- package/build/godot-subprocess.js +23 -0
- package/build/headless-operation-runner.js +68 -0
- package/build/headless-operation-service.js +65 -0
- package/build/index.js +478 -0
- package/build/interaction-server-installer.js +234 -0
- package/build/opencode-setup.js +199 -0
- package/build/project-support.js +219 -0
- package/build/runtime-protocol.js +202 -0
- package/build/scripts/godot_operations.gd +1534 -0
- package/build/scripts/mcp_editor_plugin.gd +417 -0
- package/build/scripts/mcp_interaction_server.gd +1255 -0
- package/build/scripts/mcp_runtime/audio_animation_domain.gd +568 -0
- package/build/scripts/mcp_runtime/command_params.gd +339 -0
- package/build/scripts/mcp_runtime/core_domain.gd +591 -0
- package/build/scripts/mcp_runtime/input_domain.gd +695 -0
- package/build/scripts/mcp_runtime/networking_domain.gd +356 -0
- package/build/scripts/mcp_runtime/physics_domain.gd +653 -0
- package/build/scripts/mcp_runtime/privileged_command_policy.gd +81 -0
- package/build/scripts/mcp_runtime/rendering_domain.gd +807 -0
- package/build/scripts/mcp_runtime/runtime_domain.gd +108 -0
- package/build/scripts/mcp_runtime/scene_2d_domain.gd +527 -0
- package/build/scripts/mcp_runtime/scene_3d_domain.gd +573 -0
- package/build/scripts/mcp_runtime/system_domain.gd +277 -0
- package/build/scripts/mcp_runtime/ui_domain.gd +619 -0
- package/build/scripts/mcp_runtime/variant_codec.gd +335 -0
- package/build/scripts/validate_script.gd +28 -0
- package/build/server-instructions.js +11 -0
- package/build/session-timing.js +20 -0
- package/build/tool-argument-validation.js +120 -0
- package/build/tool-definitions.js +2727 -0
- package/build/tool-handlers/game-tool-handlers.js +1345 -0
- package/build/tool-handlers/lifecycle-tool-handlers.js +346 -0
- package/build/tool-handlers/project-handler-services.js +1458 -0
- package/build/tool-handlers/project-tool-handlers.js +1506 -0
- package/build/tool-handlers/visual-regression-service.js +139 -0
- package/build/tool-manifest.js +1193 -0
- package/build/tool-mutation-policy.js +122 -0
- package/build/tool-registry.js +34 -0
- package/build/tool-surface.js +70 -0
- package/build/utils.js +273 -0
- package/package.json +110 -0
- package/product.json +52 -0
- package/scripts/prepare-package.js +42 -0
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
extends "res://mcp_runtime/runtime_domain.gd"
|
|
2
|
+
|
|
3
|
+
# Networking and multiplayer runtime commands. This domain owns the persistent
|
|
4
|
+
# WebSocket peer so its lifetime follows the command domain rather than the
|
|
5
|
+
# transport composition root.
|
|
6
|
+
|
|
7
|
+
var _websocket: WebSocketPeer = null
|
|
8
|
+
const MAX_WEBSOCKET_MESSAGE_BYTES := 1024 * 1024
|
|
9
|
+
const MAX_HTTP_BODY_BYTES := 1024 * 1024
|
|
10
|
+
const MAX_HTTP_RESPONSE_BYTES := 4 * 1024 * 1024
|
|
11
|
+
const MAX_HTTP_HEADER_BYTES := 32 * 1024
|
|
12
|
+
const MAX_HTTP_HEADERS := 64
|
|
13
|
+
const MAX_HTTP_URL_BYTES := 8192
|
|
14
|
+
const MAX_RPC_ARGS := 64
|
|
15
|
+
|
|
16
|
+
var _active_http: HTTPRequest = null
|
|
17
|
+
var _http_result: Array = []
|
|
18
|
+
var _rpc_call_local: Dictionary = {}
|
|
19
|
+
var _rpc_authority_only: Dictionary = {}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
func register_commands() -> void:
|
|
23
|
+
register_command("http_request", _cmd_http_request)
|
|
24
|
+
register_command("websocket", _cmd_websocket)
|
|
25
|
+
register_command("multiplayer", _cmd_multiplayer)
|
|
26
|
+
register_command("rpc", _cmd_rpc)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
func _cmd_http_request(params: Dictionary) -> void:
|
|
30
|
+
var reader := CommandParams.new(params)
|
|
31
|
+
var url: String = reader.required_string("url")
|
|
32
|
+
var method_str: String = reader.optional_enum("method", "GET", ["GET", "POST", "PUT", "DELETE"])
|
|
33
|
+
var timeout: float = reader.optional_number("timeout", 30.0, 0.01, 30.0)
|
|
34
|
+
var header_values: Dictionary = reader.optional_dictionary("headers")
|
|
35
|
+
var body: String = reader.optional_string("body")
|
|
36
|
+
if params_invalid(reader):
|
|
37
|
+
return
|
|
38
|
+
if url.is_empty():
|
|
39
|
+
reader.fail("url must be non-empty", {"param": "url", "reason": "invalid_value"})
|
|
40
|
+
send_params_error(reader)
|
|
41
|
+
return
|
|
42
|
+
if not (url.begins_with("http://") or url.begins_with("https://")):
|
|
43
|
+
reader.fail("url must use http:// or https://", {"param": "url", "reason": "invalid_value"})
|
|
44
|
+
if url.to_utf8_buffer().size() > MAX_HTTP_URL_BYTES:
|
|
45
|
+
reader.fail("HTTP URL exceeds the configured limit", {"param": "url", "reason": "limit_exceeded", "max_bytes": MAX_HTTP_URL_BYTES})
|
|
46
|
+
if body.to_utf8_buffer().size() > MAX_HTTP_BODY_BYTES:
|
|
47
|
+
reader.fail("HTTP body exceeds the configured limit", {"param": "body", "reason": "payload_too_large", "max_bytes": MAX_HTTP_BODY_BYTES})
|
|
48
|
+
if header_values.size() > MAX_HTTP_HEADERS:
|
|
49
|
+
reader.fail("HTTP headers exceed the configured count limit", {"param": "headers", "reason": "limit_exceeded", "max_items": MAX_HTTP_HEADERS})
|
|
50
|
+
var header_bytes: int = 0
|
|
51
|
+
for key: Variant in header_values:
|
|
52
|
+
if not key is String or not header_values[key] is String:
|
|
53
|
+
reader.fail("HTTP header names and values must be strings", {"param": "headers", "reason": "invalid_type"})
|
|
54
|
+
break
|
|
55
|
+
header_bytes += (str(key) + ": " + str(header_values[key])).to_utf8_buffer().size()
|
|
56
|
+
if header_bytes > MAX_HTTP_HEADER_BYTES:
|
|
57
|
+
reader.fail("HTTP headers exceed the configured byte limit", {"param": "headers", "reason": "limit_exceeded", "max_bytes": MAX_HTTP_HEADER_BYTES})
|
|
58
|
+
if params_invalid(reader):
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
_cleanup_http()
|
|
62
|
+
var http := HTTPRequest.new()
|
|
63
|
+
_active_http = http
|
|
64
|
+
_http_result = []
|
|
65
|
+
http.timeout = timeout
|
|
66
|
+
add_child(http)
|
|
67
|
+
@warning_ignore("return_value_discarded")
|
|
68
|
+
http.request_completed.connect(_capture_http_result, CONNECT_ONE_SHOT)
|
|
69
|
+
var headers := PackedStringArray()
|
|
70
|
+
for key: Variant in header_values:
|
|
71
|
+
@warning_ignore("return_value_discarded")
|
|
72
|
+
headers.append("%s: %s" % [key, str(header_values[key])])
|
|
73
|
+
var method_enum: int = {
|
|
74
|
+
"GET": HTTPClient.METHOD_GET,
|
|
75
|
+
"POST": HTTPClient.METHOD_POST,
|
|
76
|
+
"PUT": HTTPClient.METHOD_PUT,
|
|
77
|
+
"DELETE": HTTPClient.METHOD_DELETE,
|
|
78
|
+
}[method_str]
|
|
79
|
+
var err: int = http.request(url, headers, method_enum, body)
|
|
80
|
+
if err != OK:
|
|
81
|
+
_cleanup_http()
|
|
82
|
+
reader.fail("HTTP request failed to start", godot_error_data(err))
|
|
83
|
+
send_params_error(reader)
|
|
84
|
+
return
|
|
85
|
+
while _http_result.is_empty():
|
|
86
|
+
if cancellation_requested():
|
|
87
|
+
_cleanup_http()
|
|
88
|
+
respond({})
|
|
89
|
+
return
|
|
90
|
+
await get_tree().process_frame
|
|
91
|
+
var result: Array = _http_result.duplicate()
|
|
92
|
+
_cleanup_http()
|
|
93
|
+
var request_result: int = result[0]
|
|
94
|
+
var status_code: int = result[1]
|
|
95
|
+
var response_headers: PackedStringArray = result[2]
|
|
96
|
+
var body_bytes: PackedByteArray = result[3]
|
|
97
|
+
if request_result != HTTPRequest.RESULT_SUCCESS:
|
|
98
|
+
respond({"error": "HTTP request did not complete successfully", "error_data": {"reason": "http_request_failed", "result": request_result, "status_code": status_code}})
|
|
99
|
+
return
|
|
100
|
+
if body_bytes.size() > MAX_HTTP_RESPONSE_BYTES:
|
|
101
|
+
respond({"error": "HTTP response exceeds the configured payload limit", "error_data": {"reason": "payload_too_large", "bytes": body_bytes.size(), "max_bytes": MAX_HTTP_RESPONSE_BYTES, "status_code": status_code}})
|
|
102
|
+
return
|
|
103
|
+
respond({
|
|
104
|
+
"success": true,
|
|
105
|
+
"result": request_result,
|
|
106
|
+
"status_code": status_code,
|
|
107
|
+
"headers": Array(response_headers),
|
|
108
|
+
"body": body_bytes.get_string_from_utf8(),
|
|
109
|
+
"bytes": body_bytes.size(),
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
func _capture_http_result(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray) -> void:
|
|
114
|
+
_http_result = [result, response_code, headers, body]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
func _cleanup_http() -> void:
|
|
118
|
+
if _active_http != null:
|
|
119
|
+
_active_http.cancel_request()
|
|
120
|
+
_active_http.queue_free()
|
|
121
|
+
_active_http = null
|
|
122
|
+
_http_result = []
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
func _cmd_websocket(params: Dictionary) -> void:
|
|
126
|
+
var reader := CommandParams.new(params)
|
|
127
|
+
var action: String = reader.required_enum("action", ["connect", "disconnect", "send", "receive", "status"])
|
|
128
|
+
var url: String = reader.optional_string("url")
|
|
129
|
+
var message: String = reader.optional_string("message")
|
|
130
|
+
var timeout: float = reader.optional_number("timeout", 5.0, 0.0, 10.0)
|
|
131
|
+
if params_invalid(reader):
|
|
132
|
+
return
|
|
133
|
+
if action == "connect" and url.is_empty():
|
|
134
|
+
reader.fail("url is required for connect", {"param": "url", "reason": "missing"})
|
|
135
|
+
send_params_error(reader)
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
match action:
|
|
139
|
+
"connect":
|
|
140
|
+
_close_websocket()
|
|
141
|
+
_websocket = WebSocketPeer.new()
|
|
142
|
+
var err: int = _websocket.connect_to_url(url)
|
|
143
|
+
if err != OK:
|
|
144
|
+
_websocket = null
|
|
145
|
+
reader.fail("WebSocket connection failed", godot_error_data(err))
|
|
146
|
+
send_params_error(reader)
|
|
147
|
+
return
|
|
148
|
+
var deadline: int = Time.get_ticks_msec() + ceili(timeout * 1000.0)
|
|
149
|
+
while _websocket != null and _websocket.get_ready_state() == WebSocketPeer.STATE_CONNECTING and Time.get_ticks_msec() <= deadline:
|
|
150
|
+
_websocket.poll()
|
|
151
|
+
await get_tree().process_frame
|
|
152
|
+
if _websocket == null or _websocket.get_ready_state() != WebSocketPeer.STATE_OPEN:
|
|
153
|
+
var state: String = _websocket_state_name(_websocket)
|
|
154
|
+
_close_websocket()
|
|
155
|
+
reader.fail("WebSocket connection did not open", {"param": "url", "reason": "connection_failed", "state": state})
|
|
156
|
+
send_params_error(reader)
|
|
157
|
+
return
|
|
158
|
+
respond({"success": true, "action": action, "url": url, "status": "open"})
|
|
159
|
+
"disconnect":
|
|
160
|
+
_close_websocket()
|
|
161
|
+
respond({"success": true, "action": action})
|
|
162
|
+
"send":
|
|
163
|
+
if not reader.has_param("message"):
|
|
164
|
+
reader.fail("message is required for send", {"param": "message", "reason": "missing"})
|
|
165
|
+
if message.to_utf8_buffer().size() > MAX_WEBSOCKET_MESSAGE_BYTES:
|
|
166
|
+
reader.fail("WebSocket message exceeds the payload limit", {"param": "message", "reason": "payload_too_large", "max_bytes": MAX_WEBSOCKET_MESSAGE_BYTES})
|
|
167
|
+
if _websocket == null or _websocket.get_ready_state() != WebSocketPeer.STATE_OPEN:
|
|
168
|
+
reader.fail("No open WebSocket connection", {"reason": "invalid_state", "state": _websocket_state_name(_websocket)})
|
|
169
|
+
if params_invalid(reader):
|
|
170
|
+
return
|
|
171
|
+
_websocket.poll()
|
|
172
|
+
var err: int = _websocket.send_text(message)
|
|
173
|
+
if err != OK:
|
|
174
|
+
reader.fail("WebSocket send failed", godot_error_data(err))
|
|
175
|
+
send_params_error(reader)
|
|
176
|
+
return
|
|
177
|
+
respond({"success": true, "action": action})
|
|
178
|
+
"receive":
|
|
179
|
+
if _websocket == null or _websocket.get_ready_state() != WebSocketPeer.STATE_OPEN:
|
|
180
|
+
reader.fail("No open WebSocket connection", {"reason": "invalid_state", "state": _websocket_state_name(_websocket)})
|
|
181
|
+
if params_invalid(reader):
|
|
182
|
+
return
|
|
183
|
+
var deadline: int = Time.get_ticks_msec() + ceili(timeout * 1000.0)
|
|
184
|
+
while _websocket.get_available_packet_count() == 0 and Time.get_ticks_msec() <= deadline:
|
|
185
|
+
_websocket.poll()
|
|
186
|
+
if _websocket.get_ready_state() != WebSocketPeer.STATE_OPEN:
|
|
187
|
+
reader.fail("WebSocket closed while waiting for a message", {"reason": "connection_closed", "state": _websocket_state_name(_websocket)})
|
|
188
|
+
break
|
|
189
|
+
await get_tree().process_frame
|
|
190
|
+
if reader.failed():
|
|
191
|
+
send_params_error(reader)
|
|
192
|
+
return
|
|
193
|
+
if _websocket.get_available_packet_count() == 0:
|
|
194
|
+
reader.fail("WebSocket receive timed out", {"reason": "timeout", "timeout": timeout})
|
|
195
|
+
send_params_error(reader)
|
|
196
|
+
return
|
|
197
|
+
var packet: PackedByteArray = _websocket.get_packet()
|
|
198
|
+
if packet.size() > MAX_WEBSOCKET_MESSAGE_BYTES:
|
|
199
|
+
reader.fail("Received WebSocket message exceeds the payload limit", {"reason": "payload_too_large", "max_bytes": MAX_WEBSOCKET_MESSAGE_BYTES, "bytes": packet.size()})
|
|
200
|
+
send_params_error(reader)
|
|
201
|
+
return
|
|
202
|
+
respond({"success": true, "action": action, "message": packet.get_string_from_utf8(), "bytes": packet.size()})
|
|
203
|
+
"status":
|
|
204
|
+
if _websocket != null:
|
|
205
|
+
_websocket.poll()
|
|
206
|
+
respond({"success": true, "status": _websocket_state_name(_websocket)})
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
func _cmd_multiplayer(params: Dictionary) -> void:
|
|
210
|
+
var reader := CommandParams.new(params)
|
|
211
|
+
var action: String = reader.required_enum("action", ["create_server", "create_client", "disconnect", "status"])
|
|
212
|
+
var port: int = reader.optional_int("port", 7000, 1, 65535)
|
|
213
|
+
var max_clients: int = reader.optional_int("max_clients", 32, 1)
|
|
214
|
+
var address: String = reader.optional_string("address", "127.0.0.1")
|
|
215
|
+
if params_invalid(reader):
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
match action:
|
|
219
|
+
"create_server":
|
|
220
|
+
_close_multiplayer()
|
|
221
|
+
var peer := ENetMultiplayerPeer.new()
|
|
222
|
+
var err: int = peer.create_server(port, max_clients)
|
|
223
|
+
if err != OK:
|
|
224
|
+
reader.fail("Failed to create multiplayer server", godot_error_data(err))
|
|
225
|
+
send_params_error(reader)
|
|
226
|
+
return
|
|
227
|
+
multiplayer.multiplayer_peer = peer
|
|
228
|
+
respond({"success": true, "action": action, "port": port})
|
|
229
|
+
"create_client":
|
|
230
|
+
_close_multiplayer()
|
|
231
|
+
var peer := ENetMultiplayerPeer.new()
|
|
232
|
+
var err: int = peer.create_client(address, port)
|
|
233
|
+
if err != OK:
|
|
234
|
+
reader.fail("Failed to create multiplayer client", godot_error_data(err))
|
|
235
|
+
send_params_error(reader)
|
|
236
|
+
return
|
|
237
|
+
multiplayer.multiplayer_peer = peer
|
|
238
|
+
respond({"success": true, "action": action, "address": address, "port": port})
|
|
239
|
+
"disconnect":
|
|
240
|
+
_close_multiplayer()
|
|
241
|
+
respond({"success": true, "action": action})
|
|
242
|
+
"status":
|
|
243
|
+
var peer: MultiplayerPeer = multiplayer.multiplayer_peer
|
|
244
|
+
if peer == null:
|
|
245
|
+
respond({"success": true, "connected": false, "status": "disconnected", "peer_count": 0})
|
|
246
|
+
return
|
|
247
|
+
var connection_status: MultiplayerPeer.ConnectionStatus = peer.get_connection_status()
|
|
248
|
+
var status_name: String = {MultiplayerPeer.CONNECTION_DISCONNECTED: "disconnected", MultiplayerPeer.CONNECTION_CONNECTING: "connecting", MultiplayerPeer.CONNECTION_CONNECTED: "connected"}.get(connection_status, "unknown")
|
|
249
|
+
respond({"success": true, "connected": connection_status == MultiplayerPeer.CONNECTION_CONNECTED, "status": status_name, "unique_id": multiplayer.get_unique_id(), "is_server": multiplayer.is_server(), "peer_count": multiplayer.get_peers().size()})
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
func _cmd_rpc(params: Dictionary) -> void:
|
|
253
|
+
var reader := CommandParams.new(params)
|
|
254
|
+
var node: Node = require_node(reader)
|
|
255
|
+
var action: String = reader.optional_enum("action", "call", ["call", "configure"])
|
|
256
|
+
var method: String = reader.required_string("method")
|
|
257
|
+
var args: Array = reader.optional_array("args")
|
|
258
|
+
var peer_id: int = reader.optional_int("peer_id", 0, 0)
|
|
259
|
+
if params_invalid(reader):
|
|
260
|
+
return
|
|
261
|
+
if method.is_empty():
|
|
262
|
+
reader.fail("method must be non-empty", {"param": "method", "reason": "invalid_value"})
|
|
263
|
+
send_params_error(reader)
|
|
264
|
+
return
|
|
265
|
+
if args.size() > MAX_RPC_ARGS:
|
|
266
|
+
reader.fail("RPC arguments exceed the configured limit", {"param": "args", "reason": "limit_exceeded", "max_items": MAX_RPC_ARGS})
|
|
267
|
+
if not node.has_method(method):
|
|
268
|
+
reader.fail("RPC method not found", {"param": "method", "reason": "method_not_found", "value": method})
|
|
269
|
+
if params_invalid(reader):
|
|
270
|
+
return
|
|
271
|
+
|
|
272
|
+
if action == "call":
|
|
273
|
+
var call_config_key: String = "%s:%s" % [node.get_instance_id(), method]
|
|
274
|
+
if _rpc_authority_only.get(call_config_key, false) and not node.is_multiplayer_authority():
|
|
275
|
+
reader.fail("Only the multiplayer authority may call this RPC method", {"param": "method", "reason": "not_multiplayer_authority", "value": method})
|
|
276
|
+
send_params_error(reader)
|
|
277
|
+
return
|
|
278
|
+
if _rpc_call_local.get(call_config_key, false):
|
|
279
|
+
Callable(node, method).callv(args)
|
|
280
|
+
var call_args: Array = [method]
|
|
281
|
+
call_args.append_array(args)
|
|
282
|
+
var call_result: Variant
|
|
283
|
+
if peer_id == 0:
|
|
284
|
+
call_result = Callable(node, "rpc").callv(call_args)
|
|
285
|
+
else:
|
|
286
|
+
call_args.push_front(peer_id)
|
|
287
|
+
call_result = Callable(node, "rpc_id").callv(call_args)
|
|
288
|
+
var rpc_error: int = CommandParams.to_int(call_result, FAILED)
|
|
289
|
+
if rpc_error != OK:
|
|
290
|
+
reader.fail("RPC call failed", godot_error_data(rpc_error))
|
|
291
|
+
send_params_error(reader)
|
|
292
|
+
return
|
|
293
|
+
respond({"success": true, "action": action, "method": method, "peer_id": peer_id, "argument_count": args.size()})
|
|
294
|
+
return
|
|
295
|
+
|
|
296
|
+
var config: Dictionary = {}
|
|
297
|
+
if reader.has_param("mode"):
|
|
298
|
+
var mode: Variant = reader.raw("mode")
|
|
299
|
+
if mode is String:
|
|
300
|
+
var mode_text: String = mode
|
|
301
|
+
var mode_name: String = mode_text.to_lower()
|
|
302
|
+
if not ["any_peer", "authority"].has(mode_name):
|
|
303
|
+
reader.fail("mode must be one of: any_peer, authority", {"param": "mode", "reason": "invalid_value", "allowed": ["any_peer", "authority"], "value": mode})
|
|
304
|
+
else:
|
|
305
|
+
config["rpc_mode"] = MultiplayerAPI.RPC_MODE_ANY_PEER if mode_name == "any_peer" else MultiplayerAPI.RPC_MODE_AUTHORITY
|
|
306
|
+
elif mode is int or mode is float:
|
|
307
|
+
config["rpc_mode"] = CommandParams.to_int(mode)
|
|
308
|
+
else:
|
|
309
|
+
reader.fail("mode must be a string or integer", {"param": "mode", "reason": "invalid_type"})
|
|
310
|
+
if reader.has_param("sync"):
|
|
311
|
+
var sync: String = reader.optional_enum("sync", "call_remote", ["call_local", "call_remote"])
|
|
312
|
+
config["call_local"] = sync == "call_local"
|
|
313
|
+
if reader.has_param("transfer_mode"):
|
|
314
|
+
var transfer_mode: String = reader.optional_enum("transfer_mode", "unreliable", ["unreliable", "unreliable_ordered", "reliable"])
|
|
315
|
+
config["transfer_mode"] = {
|
|
316
|
+
"unreliable": MultiplayerPeer.TRANSFER_MODE_UNRELIABLE,
|
|
317
|
+
"unreliable_ordered": MultiplayerPeer.TRANSFER_MODE_UNRELIABLE_ORDERED,
|
|
318
|
+
"reliable": MultiplayerPeer.TRANSFER_MODE_RELIABLE,
|
|
319
|
+
}[transfer_mode]
|
|
320
|
+
if reader.has_param("channel"):
|
|
321
|
+
config["channel"] = reader.optional_int("channel", 0, 0)
|
|
322
|
+
if params_invalid(reader):
|
|
323
|
+
return
|
|
324
|
+
var requested_config: Dictionary = config.duplicate()
|
|
325
|
+
var config_key: String = "%s:%s" % [node.get_instance_id(), method]
|
|
326
|
+
_rpc_call_local[config_key] = config.get("call_local", false)
|
|
327
|
+
_rpc_authority_only[config_key] = config.get("rpc_mode", MultiplayerAPI.RPC_MODE_AUTHORITY) == MultiplayerAPI.RPC_MODE_AUTHORITY
|
|
328
|
+
# Invoke locally ourselves so broadcast and rpc_id have identical semantics.
|
|
329
|
+
config["call_local"] = false
|
|
330
|
+
node.rpc_config(method, config)
|
|
331
|
+
respond({"success": true, "action": action, "method": method, "config": requested_config})
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
func _close_websocket() -> void:
|
|
335
|
+
if _websocket != null:
|
|
336
|
+
_websocket.close()
|
|
337
|
+
_websocket = null
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
func _websocket_state_name(peer: WebSocketPeer) -> String:
|
|
341
|
+
if peer == null:
|
|
342
|
+
return "disconnected"
|
|
343
|
+
return {WebSocketPeer.STATE_CONNECTING: "connecting", WebSocketPeer.STATE_OPEN: "open", WebSocketPeer.STATE_CLOSING: "closing", WebSocketPeer.STATE_CLOSED: "closed"}.get(peer.get_ready_state(), "unknown")
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
func _close_multiplayer() -> void:
|
|
347
|
+
var peer: MultiplayerPeer = multiplayer.multiplayer_peer
|
|
348
|
+
if peer != null:
|
|
349
|
+
peer.close()
|
|
350
|
+
multiplayer.multiplayer_peer = null
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
func _exit_tree() -> void:
|
|
354
|
+
_cleanup_http()
|
|
355
|
+
_close_websocket()
|
|
356
|
+
_close_multiplayer()
|