1/**************************************************************************/
2/* editor_run.cpp */
3/**************************************************************************/
4/* This file is part of: */
5/* GODOT ENGINE */
6/* https://godotengine.org */
7/**************************************************************************/
8/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
9/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
10/* */
11/* Permission is hereby granted, free of charge, to any person obtaining */
12/* a copy of this software and associated documentation files (the */
13/* "Software"), to deal in the Software without restriction, including */
14/* without limitation the rights to use, copy, modify, merge, publish, */
15/* distribute, sublicense, and/or sell copies of the Software, and to */
16/* permit persons to whom the Software is furnished to do so, subject to */
17/* the following conditions: */
18/* */
19/* The above copyright notice and this permission notice shall be */
20/* included in all copies or substantial portions of the Software. */
21/* */
22/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
23/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
24/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
25/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
26/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
27/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
28/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
29/**************************************************************************/
30
31#include "editor_run.h"
32
33#include "core/config/project_settings.h"
34#include "editor/debugger/editor_debugger_node.h"
35#include "editor/editor_node.h"
36#include "editor/editor_settings.h"
37#include "main/main.h"
38#include "servers/display_server.h"
39
40/**
41 * Separates command line arguments without splitting up quoted strings.
42 */
43Vector<String> EditorRun::_split_cmdline_args(const String &arg_string) {
44 Vector<String> split_args;
45 int arg_start = 0;
46 bool is_quoted = false;
47 char32_t quote_char = '-';
48 char32_t arg_char;
49 int arg_length;
50 for (int i = 0; i < arg_string.length(); i++) {
51 arg_char = arg_string[i];
52 if (arg_char == '\"' || arg_char == '\'') {
53 if (i == 0 || arg_string[i - 1] != '\\') {
54 if (is_quoted) {
55 if (arg_char == quote_char) {
56 is_quoted = false;
57 quote_char = '-';
58 }
59 } else {
60 is_quoted = true;
61 quote_char = arg_char;
62 }
63 }
64 } else if (!is_quoted && arg_char == ' ') {
65 arg_length = i - arg_start;
66 if (arg_length > 0) {
67 split_args.push_back(arg_string.substr(arg_start, arg_length));
68 }
69 arg_start = i + 1;
70 }
71 }
72 arg_length = arg_string.length() - arg_start;
73 if (arg_length > 0) {
74 split_args.push_back(arg_string.substr(arg_start, arg_length));
75 }
76 return split_args;
77}
78
79EditorRun::Status EditorRun::get_status() const {
80 return status;
81}
82
83String EditorRun::get_running_scene() const {
84 return running_scene;
85}
86
87Error EditorRun::run(const String &p_scene, const String &p_write_movie) {
88 List<String> args;
89
90 for (const String &a : Main::get_forwardable_cli_arguments(Main::CLI_SCOPE_PROJECT)) {
91 args.push_back(a);
92 }
93
94 String resource_path = ProjectSettings::get_singleton()->get_resource_path();
95 if (!resource_path.is_empty()) {
96 args.push_back("--path");
97 args.push_back(resource_path.replace(" ", "%20"));
98 }
99
100 const String debug_uri = EditorDebuggerNode::get_singleton()->get_server_uri();
101 if (debug_uri.size()) {
102 args.push_back("--remote-debug");
103 args.push_back(debug_uri);
104 }
105
106 args.push_back("--editor-pid");
107 args.push_back(itos(OS::get_singleton()->get_process_id()));
108
109 bool debug_collisions = EditorSettings::get_singleton()->get_project_metadata("debug_options", "run_debug_collisions", false);
110 bool debug_paths = EditorSettings::get_singleton()->get_project_metadata("debug_options", "run_debug_paths", false);
111 bool debug_navigation = EditorSettings::get_singleton()->get_project_metadata("debug_options", "run_debug_navigation", false);
112 bool debug_avoidance = EditorSettings::get_singleton()->get_project_metadata("debug_options", "run_debug_avoidance", false);
113 if (debug_collisions) {
114 args.push_back("--debug-collisions");
115 }
116
117 if (debug_paths) {
118 args.push_back("--debug-paths");
119 }
120
121 if (debug_navigation) {
122 args.push_back("--debug-navigation");
123 }
124
125 if (debug_avoidance) {
126 args.push_back("--debug-avoidance");
127 }
128
129 if (p_write_movie != "") {
130 args.push_back("--write-movie");
131 args.push_back(p_write_movie);
132 args.push_back("--fixed-fps");
133 args.push_back(itos(GLOBAL_GET("editor/movie_writer/fps")));
134 if (bool(GLOBAL_GET("editor/movie_writer/disable_vsync"))) {
135 args.push_back("--disable-vsync");
136 }
137 }
138
139 int screen = EDITOR_GET("run/window_placement/screen");
140 if (screen == -5) {
141 // Same as editor
142 screen = DisplayServer::get_singleton()->window_get_current_screen();
143 } else if (screen == -4) {
144 // Previous monitor (wrap to the other end if needed)
145 screen = Math::wrapi(
146 DisplayServer::get_singleton()->window_get_current_screen() - 1,
147 0,
148 DisplayServer::get_singleton()->get_screen_count());
149 } else if (screen == -3) {
150 // Next monitor (wrap to the other end if needed)
151 screen = Math::wrapi(
152 DisplayServer::get_singleton()->window_get_current_screen() + 1,
153 0,
154 DisplayServer::get_singleton()->get_screen_count());
155 }
156
157 Rect2 screen_rect = DisplayServer::get_singleton()->screen_get_usable_rect(screen);
158
159 int window_placement = EDITOR_GET("run/window_placement/rect");
160 if (screen_rect != Rect2()) {
161 Size2 window_size;
162 window_size.x = GLOBAL_GET("display/window/size/viewport_width");
163 window_size.y = GLOBAL_GET("display/window/size/viewport_height");
164
165 Size2 desired_size;
166 desired_size.x = GLOBAL_GET("display/window/size/window_width_override");
167 desired_size.y = GLOBAL_GET("display/window/size/window_height_override");
168 if (desired_size.x > 0 && desired_size.y > 0) {
169 window_size = desired_size;
170 }
171
172 if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_HIDPI)) {
173 bool hidpi_proj = GLOBAL_GET("display/window/dpi/allow_hidpi");
174 int display_scale = 1;
175
176 if (OS::get_singleton()->is_hidpi_allowed()) {
177 if (hidpi_proj) {
178 display_scale = 1; // Both editor and project runs in hiDPI mode, do not scale.
179 } else {
180 display_scale = DisplayServer::get_singleton()->screen_get_max_scale(); // Editor is in hiDPI mode, project is not, scale down.
181 }
182 } else {
183 if (hidpi_proj) {
184 display_scale = (1.f / DisplayServer::get_singleton()->screen_get_max_scale()); // Editor is not in hiDPI mode, project is, scale up.
185 } else {
186 display_scale = 1; // Both editor and project runs in lowDPI mode, do not scale.
187 }
188 }
189 screen_rect.position /= display_scale;
190 screen_rect.size /= display_scale;
191 }
192
193 switch (window_placement) {
194 case 0: { // top left
195 args.push_back("--position");
196 args.push_back(itos(screen_rect.position.x) + "," + itos(screen_rect.position.y));
197 } break;
198 case 1: { // centered
199 Vector2 pos = (screen_rect.position) + ((screen_rect.size - window_size) / 2).floor();
200 args.push_back("--position");
201 args.push_back(itos(pos.x) + "," + itos(pos.y));
202 } break;
203 case 2: { // custom pos
204 Vector2 pos = EDITOR_GET("run/window_placement/rect_custom_position");
205 pos += screen_rect.position;
206 args.push_back("--position");
207 args.push_back(itos(pos.x) + "," + itos(pos.y));
208 } break;
209 case 3: { // force maximized
210 Vector2 pos = screen_rect.position + screen_rect.size / 2;
211 args.push_back("--position");
212 args.push_back(itos(pos.x) + "," + itos(pos.y));
213 args.push_back("--maximized");
214 } break;
215 case 4: { // force fullscreen
216 Vector2 pos = screen_rect.position + screen_rect.size / 2;
217 args.push_back("--position");
218 args.push_back(itos(pos.x) + "," + itos(pos.y));
219 args.push_back("--fullscreen");
220 } break;
221 }
222 } else {
223 // Unable to get screen info, skip setting position.
224 switch (window_placement) {
225 case 3: { // force maximized
226 args.push_back("--maximized");
227 } break;
228 case 4: { // force fullscreen
229 args.push_back("--fullscreen");
230 } break;
231 }
232 }
233
234 List<String> breakpoints;
235 EditorNode::get_editor_data().get_editor_breakpoints(&breakpoints);
236
237 if (!breakpoints.is_empty()) {
238 args.push_back("--breakpoints");
239 String bpoints;
240 for (const List<String>::Element *E = breakpoints.front(); E; E = E->next()) {
241 bpoints += E->get().replace(" ", "%20");
242 if (E->next()) {
243 bpoints += ",";
244 }
245 }
246
247 args.push_back(bpoints);
248 }
249
250 if (EditorDebuggerNode::get_singleton()->is_skip_breakpoints()) {
251 args.push_back("--skip-breakpoints");
252 }
253
254 if (!p_scene.is_empty()) {
255 args.push_back(p_scene);
256 }
257
258 String exec = OS::get_singleton()->get_executable_path();
259
260 const String raw_custom_args = GLOBAL_GET("editor/run/main_run_args");
261 if (!raw_custom_args.is_empty()) {
262 // Allow the user to specify a command to run, similar to Steam's launch options.
263 // In this case, Godot will no longer be run directly; it's up to the underlying command
264 // to run it. For instance, this can be used on Linux to force a running project
265 // to use Optimus using `prime-run` or similar.
266 // Example: `prime-run %command% --time-scale 0.5`
267 const int placeholder_pos = raw_custom_args.find("%command%");
268
269 Vector<String> custom_args;
270
271 if (placeholder_pos != -1) {
272 // Prepend executable-specific custom arguments.
273 // If nothing is placed before `%command%`, behave as if no placeholder was specified.
274 Vector<String> exec_args = _split_cmdline_args(raw_custom_args.substr(0, placeholder_pos));
275 if (exec_args.size() >= 1) {
276 exec = exec_args[0];
277 exec_args.remove_at(0);
278
279 // Append the Godot executable name before we append executable arguments
280 // (since the order is reversed when using `push_front()`).
281 args.push_front(OS::get_singleton()->get_executable_path());
282 }
283
284 for (int i = exec_args.size() - 1; i >= 0; i--) {
285 // Iterate backwards as we're pushing items in the reverse order.
286 args.push_front(exec_args[i].replace(" ", "%20"));
287 }
288
289 // Append Godot-specific custom arguments.
290 custom_args = _split_cmdline_args(raw_custom_args.substr(placeholder_pos + String("%command%").size()));
291 for (int i = 0; i < custom_args.size(); i++) {
292 args.push_back(custom_args[i].replace(" ", "%20"));
293 }
294 } else {
295 // Append Godot-specific custom arguments.
296 custom_args = _split_cmdline_args(raw_custom_args);
297 for (int i = 0; i < custom_args.size(); i++) {
298 args.push_back(custom_args[i].replace(" ", "%20"));
299 }
300 }
301 }
302
303 // Pass the debugger stop shortcut to the running instance(s).
304 String shortcut;
305 VariantWriter::write_to_string(ED_GET_SHORTCUT("editor/stop_running_project"), shortcut);
306 OS::get_singleton()->set_environment("__GODOT_EDITOR_STOP_SHORTCUT__", shortcut);
307
308 if (OS::get_singleton()->is_stdout_verbose()) {
309 print_line(vformat("Running: %s", exec));
310 for (const String &E : args) {
311 print_line(vformat(" %s", E));
312 }
313 }
314
315 int instances = EditorSettings::get_singleton()->get_project_metadata("debug_options", "run_debug_instances", 1);
316 for (int i = 0; i < instances; i++) {
317 OS::ProcessID pid = 0;
318 Error err = OS::get_singleton()->create_instance(args, &pid);
319 ERR_FAIL_COND_V(err, err);
320 if (pid != 0) {
321 pids.push_back(pid);
322 }
323 }
324
325 status = STATUS_PLAY;
326 if (!p_scene.is_empty()) {
327 running_scene = p_scene;
328 }
329
330 return OK;
331}
332
333bool EditorRun::has_child_process(OS::ProcessID p_pid) const {
334 for (const OS::ProcessID &E : pids) {
335 if (E == p_pid) {
336 return true;
337 }
338 }
339 return false;
340}
341
342void EditorRun::stop_child_process(OS::ProcessID p_pid) {
343 if (has_child_process(p_pid)) {
344 OS::get_singleton()->kill(p_pid);
345 pids.erase(p_pid);
346 }
347}
348
349void EditorRun::stop() {
350 if (status != STATUS_STOP && pids.size() > 0) {
351 for (const OS::ProcessID &E : pids) {
352 OS::get_singleton()->kill(E);
353 }
354 pids.clear();
355 }
356
357 status = STATUS_STOP;
358 running_scene = "";
359}
360
361EditorRun::EditorRun() {
362 status = STATUS_STOP;
363 running_scene = "";
364}
365