| 1 | /**************************************************************************/ |
| 2 | /* project_settings.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 "project_settings.h" |
| 32 | |
| 33 | #include "core/core_bind.h" // For Compression enum. |
| 34 | #include "core/core_string_names.h" |
| 35 | #include "core/input/input_map.h" |
| 36 | #include "core/io/config_file.h" |
| 37 | #include "core/io/dir_access.h" |
| 38 | #include "core/io/file_access.h" |
| 39 | #include "core/io/file_access_pack.h" |
| 40 | #include "core/io/marshalls.h" |
| 41 | #include "core/os/keyboard.h" |
| 42 | #include "core/variant/typed_array.h" |
| 43 | #include "core/variant/variant_parser.h" |
| 44 | #include "core/version.h" |
| 45 | |
| 46 | #ifdef TOOLS_ENABLED |
| 47 | #include "modules/modules_enabled.gen.h" // For mono. |
| 48 | #endif // TOOLS_ENABLED |
| 49 | |
| 50 | const String ProjectSettings::PROJECT_DATA_DIR_NAME_SUFFIX = "godot" ; |
| 51 | |
| 52 | ProjectSettings *ProjectSettings::singleton = nullptr; |
| 53 | |
| 54 | ProjectSettings *ProjectSettings::get_singleton() { |
| 55 | return singleton; |
| 56 | } |
| 57 | |
| 58 | String ProjectSettings::get_project_data_dir_name() const { |
| 59 | return project_data_dir_name; |
| 60 | } |
| 61 | |
| 62 | String ProjectSettings::get_project_data_path() const { |
| 63 | return "res://" + get_project_data_dir_name(); |
| 64 | } |
| 65 | |
| 66 | String ProjectSettings::get_resource_path() const { |
| 67 | return resource_path; |
| 68 | } |
| 69 | |
| 70 | String ProjectSettings::get_imported_files_path() const { |
| 71 | return get_project_data_path().path_join("imported" ); |
| 72 | } |
| 73 | |
| 74 | #ifdef TOOLS_ENABLED |
| 75 | // Returns the features that a project must have when opened with this build of Godot. |
| 76 | // This is used by the project manager to provide the initial_settings for config/features. |
| 77 | const PackedStringArray ProjectSettings::get_required_features() { |
| 78 | PackedStringArray features; |
| 79 | features.append(VERSION_BRANCH); |
| 80 | #ifdef REAL_T_IS_DOUBLE |
| 81 | features.append("Double Precision" ); |
| 82 | #endif |
| 83 | return features; |
| 84 | } |
| 85 | |
| 86 | // Returns the features supported by this build of Godot. Includes all required features. |
| 87 | const PackedStringArray ProjectSettings::_get_supported_features() { |
| 88 | PackedStringArray features = get_required_features(); |
| 89 | #ifdef MODULE_MONO_ENABLED |
| 90 | features.append("C#" ); |
| 91 | #endif |
| 92 | // Allow pinning to a specific patch number or build type by marking |
| 93 | // them as supported. They're only used if the user adds them manually. |
| 94 | features.append(VERSION_BRANCH "." _MKSTR(VERSION_PATCH)); |
| 95 | features.append(VERSION_FULL_CONFIG); |
| 96 | features.append(VERSION_FULL_BUILD); |
| 97 | |
| 98 | #ifdef VULKAN_ENABLED |
| 99 | features.append("Forward Plus" ); |
| 100 | features.append("Mobile" ); |
| 101 | #endif |
| 102 | |
| 103 | #ifdef GLES3_ENABLED |
| 104 | features.append("GL Compatibility" ); |
| 105 | #endif |
| 106 | return features; |
| 107 | } |
| 108 | |
| 109 | // Returns the features that this project needs but this build of Godot lacks. |
| 110 | const PackedStringArray ProjectSettings::get_unsupported_features(const PackedStringArray &p_project_features) { |
| 111 | PackedStringArray unsupported_features; |
| 112 | PackedStringArray supported_features = singleton->_get_supported_features(); |
| 113 | for (int i = 0; i < p_project_features.size(); i++) { |
| 114 | if (!supported_features.has(p_project_features[i])) { |
| 115 | // Temporary compatibility code to ease upgrade to 4.0 beta 2+. |
| 116 | if (p_project_features[i].begins_with("Vulkan" )) { |
| 117 | continue; |
| 118 | } |
| 119 | unsupported_features.append(p_project_features[i]); |
| 120 | } |
| 121 | } |
| 122 | unsupported_features.sort(); |
| 123 | return unsupported_features; |
| 124 | } |
| 125 | |
| 126 | // Returns the features that both this project has and this build of Godot has, ensuring required features exist. |
| 127 | const PackedStringArray ProjectSettings::_trim_to_supported_features(const PackedStringArray &p_project_features) { |
| 128 | // Remove unsupported features if present. |
| 129 | PackedStringArray features = PackedStringArray(p_project_features); |
| 130 | PackedStringArray supported_features = _get_supported_features(); |
| 131 | for (int i = p_project_features.size() - 1; i > -1; i--) { |
| 132 | if (!supported_features.has(p_project_features[i])) { |
| 133 | features.remove_at(i); |
| 134 | } |
| 135 | } |
| 136 | // Add required features if not present. |
| 137 | PackedStringArray required_features = get_required_features(); |
| 138 | for (int i = 0; i < required_features.size(); i++) { |
| 139 | if (!features.has(required_features[i])) { |
| 140 | features.append(required_features[i]); |
| 141 | } |
| 142 | } |
| 143 | features.sort(); |
| 144 | return features; |
| 145 | } |
| 146 | #endif // TOOLS_ENABLED |
| 147 | |
| 148 | String ProjectSettings::localize_path(const String &p_path) const { |
| 149 | String path = p_path.simplify_path(); |
| 150 | |
| 151 | if (resource_path.is_empty() || (path.is_absolute_path() && !path.begins_with(resource_path))) { |
| 152 | return path; |
| 153 | } |
| 154 | |
| 155 | // Check if we have a special path (like res://) or a protocol identifier. |
| 156 | int p = path.find("://" ); |
| 157 | bool found = false; |
| 158 | if (p > 0) { |
| 159 | found = true; |
| 160 | for (int i = 0; i < p; i++) { |
| 161 | if (!is_ascii_alphanumeric_char(path[i])) { |
| 162 | found = false; |
| 163 | break; |
| 164 | } |
| 165 | } |
| 166 | } |
| 167 | if (found) { |
| 168 | return path; |
| 169 | } |
| 170 | |
| 171 | Ref<DirAccess> dir = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); |
| 172 | |
| 173 | if (dir->change_dir(path) == OK) { |
| 174 | String cwd = dir->get_current_dir(); |
| 175 | cwd = cwd.replace("\\" , "/" ); |
| 176 | |
| 177 | // Ensure that we end with a '/'. |
| 178 | // This is important to ensure that we do not wrongly localize the resource path |
| 179 | // in an absolute path that just happens to contain this string but points to a |
| 180 | // different folder (e.g. "/my/project" as resource_path would be contained in |
| 181 | // "/my/project_data", even though the latter is not part of res://. |
| 182 | // `path_join("")` is an easy way to ensure we have a trailing '/'. |
| 183 | const String res_path = resource_path.path_join("" ); |
| 184 | |
| 185 | // DirAccess::get_current_dir() is not guaranteed to return a path that with a trailing '/', |
| 186 | // so we must make sure we have it as well in order to compare with 'res_path'. |
| 187 | cwd = cwd.path_join("" ); |
| 188 | |
| 189 | if (!cwd.begins_with(res_path)) { |
| 190 | return path; |
| 191 | } |
| 192 | |
| 193 | return cwd.replace_first(res_path, "res://" ); |
| 194 | } else { |
| 195 | int sep = path.rfind("/" ); |
| 196 | if (sep == -1) { |
| 197 | return "res://" + path; |
| 198 | } |
| 199 | |
| 200 | String parent = path.substr(0, sep); |
| 201 | |
| 202 | String plocal = localize_path(parent); |
| 203 | if (plocal.is_empty()) { |
| 204 | return "" ; |
| 205 | } |
| 206 | // Only strip the starting '/' from 'path' if its parent ('plocal') ends with '/' |
| 207 | if (plocal[plocal.length() - 1] == '/') { |
| 208 | sep += 1; |
| 209 | } |
| 210 | return plocal + path.substr(sep, path.size() - sep); |
| 211 | } |
| 212 | } |
| 213 | |
| 214 | void ProjectSettings::set_initial_value(const String &p_name, const Variant &p_value) { |
| 215 | ERR_FAIL_COND_MSG(!props.has(p_name), "Request for nonexistent project setting: " + p_name + "." ); |
| 216 | |
| 217 | // Duplicate so that if value is array or dictionary, changing the setting will not change the stored initial value. |
| 218 | props[p_name].initial = p_value.duplicate(); |
| 219 | } |
| 220 | |
| 221 | void ProjectSettings::set_restart_if_changed(const String &p_name, bool p_restart) { |
| 222 | ERR_FAIL_COND_MSG(!props.has(p_name), "Request for nonexistent project setting: " + p_name + "." ); |
| 223 | props[p_name].restart_if_changed = p_restart; |
| 224 | } |
| 225 | |
| 226 | void ProjectSettings::set_as_basic(const String &p_name, bool p_basic) { |
| 227 | ERR_FAIL_COND_MSG(!props.has(p_name), "Request for nonexistent project setting: " + p_name + "." ); |
| 228 | props[p_name].basic = p_basic; |
| 229 | } |
| 230 | |
| 231 | void ProjectSettings::set_as_internal(const String &p_name, bool p_internal) { |
| 232 | ERR_FAIL_COND_MSG(!props.has(p_name), "Request for nonexistent project setting: " + p_name + "." ); |
| 233 | props[p_name].internal = p_internal; |
| 234 | } |
| 235 | |
| 236 | void ProjectSettings::set_ignore_value_in_docs(const String &p_name, bool p_ignore) { |
| 237 | ERR_FAIL_COND_MSG(!props.has(p_name), "Request for nonexistent project setting: " + p_name + "." ); |
| 238 | #ifdef DEBUG_METHODS_ENABLED |
| 239 | props[p_name].ignore_value_in_docs = p_ignore; |
| 240 | #endif |
| 241 | } |
| 242 | |
| 243 | bool ProjectSettings::get_ignore_value_in_docs(const String &p_name) const { |
| 244 | ERR_FAIL_COND_V_MSG(!props.has(p_name), false, "Request for nonexistent project setting: " + p_name + "." ); |
| 245 | #ifdef DEBUG_METHODS_ENABLED |
| 246 | return props[p_name].ignore_value_in_docs; |
| 247 | #else |
| 248 | return false; |
| 249 | #endif |
| 250 | } |
| 251 | |
| 252 | void ProjectSettings::add_hidden_prefix(const String &p_prefix) { |
| 253 | ERR_FAIL_COND_MSG(hidden_prefixes.find(p_prefix) > -1, vformat("Hidden prefix '%s' already exists." , p_prefix)); |
| 254 | hidden_prefixes.push_back(p_prefix); |
| 255 | } |
| 256 | |
| 257 | String ProjectSettings::globalize_path(const String &p_path) const { |
| 258 | if (p_path.begins_with("res://" )) { |
| 259 | if (!resource_path.is_empty()) { |
| 260 | return p_path.replace("res:/" , resource_path); |
| 261 | } |
| 262 | return p_path.replace("res://" , "" ); |
| 263 | } else if (p_path.begins_with("user://" )) { |
| 264 | String data_dir = OS::get_singleton()->get_user_data_dir(); |
| 265 | if (!data_dir.is_empty()) { |
| 266 | return p_path.replace("user:/" , data_dir); |
| 267 | } |
| 268 | return p_path.replace("user://" , "" ); |
| 269 | } |
| 270 | |
| 271 | return p_path; |
| 272 | } |
| 273 | |
| 274 | bool ProjectSettings::_set(const StringName &p_name, const Variant &p_value) { |
| 275 | _THREAD_SAFE_METHOD_ |
| 276 | |
| 277 | if (p_value.get_type() == Variant::NIL) { |
| 278 | props.erase(p_name); |
| 279 | if (p_name.operator String().begins_with("autoload/" )) { |
| 280 | String node_name = p_name.operator String().split("/" )[1]; |
| 281 | if (autoloads.has(node_name)) { |
| 282 | remove_autoload(node_name); |
| 283 | } |
| 284 | } |
| 285 | } else { |
| 286 | if (p_name == CoreStringNames::get_singleton()->_custom_features) { |
| 287 | Vector<String> custom_feature_array = String(p_value).split("," ); |
| 288 | for (int i = 0; i < custom_feature_array.size(); i++) { |
| 289 | custom_features.insert(custom_feature_array[i]); |
| 290 | } |
| 291 | _queue_changed(); |
| 292 | return true; |
| 293 | } |
| 294 | |
| 295 | { // Feature overrides. |
| 296 | int dot = p_name.operator String().find("." ); |
| 297 | if (dot != -1) { |
| 298 | Vector<String> s = p_name.operator String().split("." ); |
| 299 | |
| 300 | for (int i = 1; i < s.size(); i++) { |
| 301 | String feature = s[i].strip_edges(); |
| 302 | Pair<StringName, StringName> feature_override(feature, p_name); |
| 303 | |
| 304 | if (!feature_overrides.has(s[0])) { |
| 305 | feature_overrides[s[0]] = LocalVector<Pair<StringName, StringName>>(); |
| 306 | } |
| 307 | |
| 308 | feature_overrides[s[0]].push_back(feature_override); |
| 309 | } |
| 310 | } |
| 311 | } |
| 312 | |
| 313 | if (props.has(p_name)) { |
| 314 | props[p_name].variant = p_value; |
| 315 | } else { |
| 316 | props[p_name] = VariantContainer(p_value, last_order++); |
| 317 | } |
| 318 | if (p_name.operator String().begins_with("autoload/" )) { |
| 319 | String node_name = p_name.operator String().split("/" )[1]; |
| 320 | AutoloadInfo autoload; |
| 321 | autoload.name = node_name; |
| 322 | String path = p_value; |
| 323 | if (path.begins_with("*" )) { |
| 324 | autoload.is_singleton = true; |
| 325 | autoload.path = path.substr(1); |
| 326 | } else { |
| 327 | autoload.path = path; |
| 328 | } |
| 329 | add_autoload(autoload); |
| 330 | } |
| 331 | } |
| 332 | |
| 333 | _queue_changed(); |
| 334 | return true; |
| 335 | } |
| 336 | |
| 337 | bool ProjectSettings::_get(const StringName &p_name, Variant &r_ret) const { |
| 338 | _THREAD_SAFE_METHOD_ |
| 339 | |
| 340 | if (!props.has(p_name)) { |
| 341 | WARN_PRINT("Property not found: " + String(p_name)); |
| 342 | return false; |
| 343 | } |
| 344 | r_ret = props[p_name].variant; |
| 345 | return true; |
| 346 | } |
| 347 | |
| 348 | Variant ProjectSettings::get_setting_with_override(const StringName &p_name) const { |
| 349 | _THREAD_SAFE_METHOD_ |
| 350 | |
| 351 | StringName name = p_name; |
| 352 | if (feature_overrides.has(name)) { |
| 353 | const LocalVector<Pair<StringName, StringName>> &overrides = feature_overrides[name]; |
| 354 | for (uint32_t i = 0; i < overrides.size(); i++) { |
| 355 | if (OS::get_singleton()->has_feature(overrides[i].first)) { // Custom features are checked in OS.has_feature() already. No need to check twice. |
| 356 | if (props.has(overrides[i].second)) { |
| 357 | name = overrides[i].second; |
| 358 | break; |
| 359 | } |
| 360 | } |
| 361 | } |
| 362 | } |
| 363 | |
| 364 | if (!props.has(name)) { |
| 365 | WARN_PRINT("Property not found: " + String(name)); |
| 366 | return Variant(); |
| 367 | } |
| 368 | return props[name].variant; |
| 369 | } |
| 370 | |
| 371 | struct _VCSort { |
| 372 | String name; |
| 373 | Variant::Type type = Variant::VARIANT_MAX; |
| 374 | int order = 0; |
| 375 | uint32_t flags = 0; |
| 376 | |
| 377 | bool operator<(const _VCSort &p_vcs) const { return order == p_vcs.order ? name < p_vcs.name : order < p_vcs.order; } |
| 378 | }; |
| 379 | |
| 380 | void ProjectSettings::_get_property_list(List<PropertyInfo> *p_list) const { |
| 381 | _THREAD_SAFE_METHOD_ |
| 382 | |
| 383 | RBSet<_VCSort> vclist; |
| 384 | |
| 385 | for (const KeyValue<StringName, VariantContainer> &E : props) { |
| 386 | const VariantContainer *v = &E.value; |
| 387 | |
| 388 | if (v->hide_from_editor) { |
| 389 | continue; |
| 390 | } |
| 391 | |
| 392 | _VCSort vc; |
| 393 | vc.name = E.key; |
| 394 | vc.order = v->order; |
| 395 | vc.type = v->variant.get_type(); |
| 396 | |
| 397 | bool internal = v->internal; |
| 398 | if (!internal) { |
| 399 | for (const String &F : hidden_prefixes) { |
| 400 | if (vc.name.begins_with(F)) { |
| 401 | internal = true; |
| 402 | break; |
| 403 | } |
| 404 | } |
| 405 | } |
| 406 | |
| 407 | if (internal) { |
| 408 | vc.flags = PROPERTY_USAGE_STORAGE; |
| 409 | } else { |
| 410 | vc.flags = PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_STORAGE; |
| 411 | } |
| 412 | |
| 413 | if (v->internal) { |
| 414 | vc.flags |= PROPERTY_USAGE_INTERNAL; |
| 415 | } |
| 416 | |
| 417 | if (v->basic) { |
| 418 | vc.flags |= PROPERTY_USAGE_EDITOR_BASIC_SETTING; |
| 419 | } |
| 420 | |
| 421 | if (v->restart_if_changed) { |
| 422 | vc.flags |= PROPERTY_USAGE_RESTART_IF_CHANGED; |
| 423 | } |
| 424 | vclist.insert(vc); |
| 425 | } |
| 426 | |
| 427 | for (const _VCSort &E : vclist) { |
| 428 | String prop_info_name = E.name; |
| 429 | int dot = prop_info_name.find("." ); |
| 430 | if (dot != -1 && !custom_prop_info.has(prop_info_name)) { |
| 431 | prop_info_name = prop_info_name.substr(0, dot); |
| 432 | } |
| 433 | |
| 434 | if (custom_prop_info.has(prop_info_name)) { |
| 435 | PropertyInfo pi = custom_prop_info[prop_info_name]; |
| 436 | pi.name = E.name; |
| 437 | pi.usage = E.flags; |
| 438 | p_list->push_back(pi); |
| 439 | } else { |
| 440 | p_list->push_back(PropertyInfo(E.type, E.name, PROPERTY_HINT_NONE, "" , E.flags)); |
| 441 | } |
| 442 | } |
| 443 | } |
| 444 | |
| 445 | void ProjectSettings::_queue_changed() { |
| 446 | if (is_changed || !MessageQueue::get_singleton() || MessageQueue::get_singleton()->get_max_buffer_usage() == 0) { |
| 447 | return; |
| 448 | } |
| 449 | is_changed = true; |
| 450 | callable_mp(this, &ProjectSettings::_emit_changed).call_deferred(); |
| 451 | } |
| 452 | |
| 453 | void ProjectSettings::_emit_changed() { |
| 454 | if (!is_changed) { |
| 455 | return; |
| 456 | } |
| 457 | is_changed = false; |
| 458 | emit_signal("settings_changed" ); |
| 459 | } |
| 460 | |
| 461 | bool ProjectSettings::_load_resource_pack(const String &p_pack, bool p_replace_files, int p_offset) { |
| 462 | if (PackedData::get_singleton()->is_disabled()) { |
| 463 | return false; |
| 464 | } |
| 465 | |
| 466 | bool ok = PackedData::get_singleton()->add_pack(p_pack, p_replace_files, p_offset) == OK; |
| 467 | |
| 468 | if (!ok) { |
| 469 | return false; |
| 470 | } |
| 471 | |
| 472 | //if data.pck is found, all directory access will be from here |
| 473 | DirAccess::make_default<DirAccessPack>(DirAccess::ACCESS_RESOURCES); |
| 474 | using_datapack = true; |
| 475 | |
| 476 | return true; |
| 477 | } |
| 478 | |
| 479 | void ProjectSettings::_convert_to_last_version(int p_from_version) { |
| 480 | if (p_from_version <= 3) { |
| 481 | // Converts the actions from array to dictionary (array of events to dictionary with deadzone + events) |
| 482 | for (KeyValue<StringName, ProjectSettings::VariantContainer> &E : props) { |
| 483 | Variant value = E.value.variant; |
| 484 | if (String(E.key).begins_with("input/" ) && value.get_type() == Variant::ARRAY) { |
| 485 | Array array = value; |
| 486 | Dictionary action; |
| 487 | action["deadzone" ] = Variant(0.5f); |
| 488 | action["events" ] = array; |
| 489 | E.value.variant = action; |
| 490 | } |
| 491 | } |
| 492 | } |
| 493 | } |
| 494 | |
| 495 | /* |
| 496 | * This method is responsible for loading a project.godot file and/or data file |
| 497 | * using the following merit order: |
| 498 | * - If using NetworkClient, try to lookup project file or fail. |
| 499 | * - If --main-pack was passed by the user (`p_main_pack`), load it or fail. |
| 500 | * - Search for project PCKs automatically. For each step we try loading a potential |
| 501 | * PCK, and if it doesn't work, we proceed to the next step. If any step succeeds, |
| 502 | * we try loading the project settings, and abort if it fails. Steps: |
| 503 | * o Bundled PCK in the executable. |
| 504 | * o [macOS only] PCK with same basename as the binary in the .app resource dir. |
| 505 | * o PCK with same basename as the binary in the binary's directory. We handle both |
| 506 | * changing the extension to '.pck' (e.g. 'win_game.exe' -> 'win_game.pck') and |
| 507 | * appending '.pck' to the binary name (e.g. 'linux_game' -> 'linux_game.pck'). |
| 508 | * o PCK with the same basename as the binary in the current working directory. |
| 509 | * Same as above for the two possible PCK file names. |
| 510 | * - On relevant platforms (Android/iOS), lookup project file in OS resource path. |
| 511 | * If found, load it or fail. |
| 512 | * - Lookup project file in passed `p_path` (--path passed by the user), i.e. we |
| 513 | * are running from source code. |
| 514 | * If not found and `p_upwards` is true (--upwards passed by the user), look for |
| 515 | * project files in parent folders up to the system root (used to run a game |
| 516 | * from command line while in a subfolder). |
| 517 | * If a project file is found, load it or fail. |
| 518 | * If nothing was found, error out. |
| 519 | */ |
| 520 | Error ProjectSettings::_setup(const String &p_path, const String &p_main_pack, bool p_upwards, bool p_ignore_override) { |
| 521 | if (!OS::get_singleton()->get_resource_dir().is_empty()) { |
| 522 | // OS will call ProjectSettings->get_resource_path which will be empty if not overridden! |
| 523 | // If the OS would rather use a specific location, then it will not be empty. |
| 524 | resource_path = OS::get_singleton()->get_resource_dir().replace("\\" , "/" ); |
| 525 | if (!resource_path.is_empty() && resource_path[resource_path.length() - 1] == '/') { |
| 526 | resource_path = resource_path.substr(0, resource_path.length() - 1); // Chop end. |
| 527 | } |
| 528 | } |
| 529 | |
| 530 | // Attempt with a user-defined main pack first |
| 531 | |
| 532 | if (!p_main_pack.is_empty()) { |
| 533 | bool ok = _load_resource_pack(p_main_pack); |
| 534 | ERR_FAIL_COND_V_MSG(!ok, ERR_CANT_OPEN, "Cannot open resource pack '" + p_main_pack + "'." ); |
| 535 | |
| 536 | Error err = _load_settings_text_or_binary("res://project.godot" , "res://project.binary" ); |
| 537 | if (err == OK && !p_ignore_override) { |
| 538 | // Load override from location of the main pack |
| 539 | // Optional, we don't mind if it fails |
| 540 | _load_settings_text(p_main_pack.get_base_dir().path_join("override.cfg" )); |
| 541 | } |
| 542 | return err; |
| 543 | } |
| 544 | |
| 545 | String exec_path = OS::get_singleton()->get_executable_path(); |
| 546 | |
| 547 | if (!exec_path.is_empty()) { |
| 548 | // We do several tests sequentially until one succeeds to find a PCK, |
| 549 | // and if so, we attempt loading it at the end. |
| 550 | |
| 551 | // Attempt with PCK bundled into executable. |
| 552 | bool found = _load_resource_pack(exec_path); |
| 553 | |
| 554 | // Attempt with exec_name.pck. |
| 555 | // (This is the usual case when distributing a Godot game.) |
| 556 | String exec_dir = exec_path.get_base_dir(); |
| 557 | String exec_filename = exec_path.get_file(); |
| 558 | String exec_basename = exec_filename.get_basename(); |
| 559 | |
| 560 | // Based on the OS, it can be the exec path + '.pck' (Linux w/o extension, macOS in .app bundle) |
| 561 | // or the exec path's basename + '.pck' (Windows). |
| 562 | // We need to test both possibilities as extensions for Linux binaries are optional |
| 563 | // (so both 'mygame.bin' and 'mygame' should be able to find 'mygame.pck'). |
| 564 | |
| 565 | #ifdef MACOS_ENABLED |
| 566 | if (!found) { |
| 567 | // Attempt to load PCK from macOS .app bundle resources. |
| 568 | found = _load_resource_pack(OS::get_singleton()->get_bundle_resource_dir().path_join(exec_basename + ".pck" )) || _load_resource_pack(OS::get_singleton()->get_bundle_resource_dir().path_join(exec_filename + ".pck" )); |
| 569 | } |
| 570 | #endif |
| 571 | |
| 572 | if (!found) { |
| 573 | // Try to load data pack at the location of the executable. |
| 574 | // As mentioned above, we have two potential names to attempt. |
| 575 | found = _load_resource_pack(exec_dir.path_join(exec_basename + ".pck" )) || _load_resource_pack(exec_dir.path_join(exec_filename + ".pck" )); |
| 576 | } |
| 577 | |
| 578 | if (!found) { |
| 579 | // If we couldn't find them next to the executable, we attempt |
| 580 | // the current working directory. Same story, two tests. |
| 581 | found = _load_resource_pack(exec_basename + ".pck" ) || _load_resource_pack(exec_filename + ".pck" ); |
| 582 | } |
| 583 | |
| 584 | // If we opened our package, try and load our project. |
| 585 | if (found) { |
| 586 | Error err = _load_settings_text_or_binary("res://project.godot" , "res://project.binary" ); |
| 587 | if (err == OK && !p_ignore_override) { |
| 588 | // Load overrides from the PCK and the executable location. |
| 589 | // Optional, we don't mind if either fails. |
| 590 | _load_settings_text("res://override.cfg" ); |
| 591 | _load_settings_text(exec_path.get_base_dir().path_join("override.cfg" )); |
| 592 | } |
| 593 | return err; |
| 594 | } |
| 595 | } |
| 596 | |
| 597 | // Try to use the filesystem for files, according to OS. |
| 598 | // (Only Android -when reading from pck- and iOS use this.) |
| 599 | |
| 600 | if (!OS::get_singleton()->get_resource_dir().is_empty()) { |
| 601 | Error err = _load_settings_text_or_binary("res://project.godot" , "res://project.binary" ); |
| 602 | if (err == OK && !p_ignore_override) { |
| 603 | // Optional, we don't mind if it fails. |
| 604 | _load_settings_text("res://override.cfg" ); |
| 605 | } |
| 606 | return err; |
| 607 | } |
| 608 | |
| 609 | // Nothing was found, try to find a project file in provided path (`p_path`) |
| 610 | // or, if requested (`p_upwards`) in parent directories. |
| 611 | |
| 612 | Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); |
| 613 | ERR_FAIL_COND_V_MSG(d.is_null(), ERR_CANT_CREATE, "Cannot create DirAccess for path '" + p_path + "'." ); |
| 614 | d->change_dir(p_path); |
| 615 | |
| 616 | String current_dir = d->get_current_dir(); |
| 617 | bool found = false; |
| 618 | Error err; |
| 619 | |
| 620 | while (true) { |
| 621 | // Set the resource path early so things can be resolved when loading. |
| 622 | resource_path = current_dir; |
| 623 | resource_path = resource_path.replace("\\" , "/" ); // Windows path to Unix path just in case. |
| 624 | err = _load_settings_text_or_binary(current_dir.path_join("project.godot" ), current_dir.path_join("project.binary" )); |
| 625 | if (err == OK && !p_ignore_override) { |
| 626 | // Optional, we don't mind if it fails. |
| 627 | _load_settings_text(current_dir.path_join("override.cfg" )); |
| 628 | found = true; |
| 629 | break; |
| 630 | } |
| 631 | |
| 632 | if (p_upwards) { |
| 633 | // Try to load settings ascending through parent directories |
| 634 | d->change_dir(".." ); |
| 635 | if (d->get_current_dir() == current_dir) { |
| 636 | break; // not doing anything useful |
| 637 | } |
| 638 | current_dir = d->get_current_dir(); |
| 639 | } else { |
| 640 | break; |
| 641 | } |
| 642 | } |
| 643 | |
| 644 | if (!found) { |
| 645 | return err; |
| 646 | } |
| 647 | |
| 648 | if (resource_path.length() && resource_path[resource_path.length() - 1] == '/') { |
| 649 | resource_path = resource_path.substr(0, resource_path.length() - 1); // Chop end. |
| 650 | } |
| 651 | |
| 652 | return OK; |
| 653 | } |
| 654 | |
| 655 | Error ProjectSettings::setup(const String &p_path, const String &p_main_pack, bool p_upwards, bool p_ignore_override) { |
| 656 | Error err = _setup(p_path, p_main_pack, p_upwards, p_ignore_override); |
| 657 | if (err == OK && !p_ignore_override) { |
| 658 | String custom_settings = GLOBAL_GET("application/config/project_settings_override" ); |
| 659 | if (!custom_settings.is_empty()) { |
| 660 | _load_settings_text(custom_settings); |
| 661 | } |
| 662 | } |
| 663 | |
| 664 | // Updating the default value after the project settings have loaded. |
| 665 | bool use_hidden_directory = GLOBAL_GET("application/config/use_hidden_project_data_directory" ); |
| 666 | project_data_dir_name = (use_hidden_directory ? "." : "" ) + PROJECT_DATA_DIR_NAME_SUFFIX; |
| 667 | |
| 668 | // Using GLOBAL_GET on every block for compressing can be slow, so assigning here. |
| 669 | Compression::zstd_long_distance_matching = GLOBAL_GET("compression/formats/zstd/long_distance_matching" ); |
| 670 | Compression::zstd_level = GLOBAL_GET("compression/formats/zstd/compression_level" ); |
| 671 | Compression::zstd_window_log_size = GLOBAL_GET("compression/formats/zstd/window_log_size" ); |
| 672 | |
| 673 | Compression::zlib_level = GLOBAL_GET("compression/formats/zlib/compression_level" ); |
| 674 | |
| 675 | Compression::gzip_level = GLOBAL_GET("compression/formats/gzip/compression_level" ); |
| 676 | |
| 677 | project_loaded = err == OK; |
| 678 | return err; |
| 679 | } |
| 680 | |
| 681 | bool ProjectSettings::has_setting(String p_var) const { |
| 682 | _THREAD_SAFE_METHOD_ |
| 683 | |
| 684 | return props.has(p_var); |
| 685 | } |
| 686 | |
| 687 | Error ProjectSettings::_load_settings_binary(const String &p_path) { |
| 688 | Error err; |
| 689 | Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ, &err); |
| 690 | if (err != OK) { |
| 691 | return err; |
| 692 | } |
| 693 | |
| 694 | uint8_t hdr[4]; |
| 695 | f->get_buffer(hdr, 4); |
| 696 | ERR_FAIL_COND_V_MSG((hdr[0] != 'E' || hdr[1] != 'C' || hdr[2] != 'F' || hdr[3] != 'G'), ERR_FILE_CORRUPT, "Corrupted header in binary project.binary (not ECFG)." ); |
| 697 | |
| 698 | uint32_t count = f->get_32(); |
| 699 | |
| 700 | for (uint32_t i = 0; i < count; i++) { |
| 701 | uint32_t slen = f->get_32(); |
| 702 | CharString cs; |
| 703 | cs.resize(slen + 1); |
| 704 | cs[slen] = 0; |
| 705 | f->get_buffer((uint8_t *)cs.ptr(), slen); |
| 706 | String key; |
| 707 | key.parse_utf8(cs.ptr()); |
| 708 | |
| 709 | uint32_t vlen = f->get_32(); |
| 710 | Vector<uint8_t> d; |
| 711 | d.resize(vlen); |
| 712 | f->get_buffer(d.ptrw(), vlen); |
| 713 | Variant value; |
| 714 | err = decode_variant(value, d.ptr(), d.size(), nullptr, true); |
| 715 | ERR_CONTINUE_MSG(err != OK, "Error decoding property: " + key + "." ); |
| 716 | set(key, value); |
| 717 | } |
| 718 | |
| 719 | return OK; |
| 720 | } |
| 721 | |
| 722 | Error ProjectSettings::_load_settings_text(const String &p_path) { |
| 723 | Error err; |
| 724 | Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ, &err); |
| 725 | |
| 726 | if (f.is_null()) { |
| 727 | // FIXME: Above 'err' error code is ERR_FILE_CANT_OPEN if the file is missing |
| 728 | // This needs to be streamlined if we want decent error reporting |
| 729 | return ERR_FILE_NOT_FOUND; |
| 730 | } |
| 731 | |
| 732 | VariantParser::StreamFile stream; |
| 733 | stream.f = f; |
| 734 | |
| 735 | String assign; |
| 736 | Variant value; |
| 737 | VariantParser::Tag next_tag; |
| 738 | |
| 739 | int lines = 0; |
| 740 | String error_text; |
| 741 | String section; |
| 742 | int config_version = 0; |
| 743 | |
| 744 | while (true) { |
| 745 | assign = Variant(); |
| 746 | next_tag.fields.clear(); |
| 747 | next_tag.name = String(); |
| 748 | |
| 749 | err = VariantParser::parse_tag_assign_eof(&stream, lines, error_text, next_tag, assign, value, nullptr, true); |
| 750 | if (err == ERR_FILE_EOF) { |
| 751 | // If we're loading a project.godot from source code, we can operate some |
| 752 | // ProjectSettings conversions if need be. |
| 753 | _convert_to_last_version(config_version); |
| 754 | last_save_time = FileAccess::get_modified_time(get_resource_path().path_join("project.godot" )); |
| 755 | return OK; |
| 756 | } |
| 757 | ERR_FAIL_COND_V_MSG(err != OK, err, "Error parsing " + p_path + " at line " + itos(lines) + ": " + error_text + " File might be corrupted." ); |
| 758 | |
| 759 | if (!assign.is_empty()) { |
| 760 | if (section.is_empty() && assign == "config_version" ) { |
| 761 | config_version = value; |
| 762 | ERR_FAIL_COND_V_MSG(config_version > CONFIG_VERSION, ERR_FILE_CANT_OPEN, vformat("Can't open project at '%s', its `config_version` (%d) is from a more recent and incompatible version of the engine. Expected config version: %d." , p_path, config_version, CONFIG_VERSION)); |
| 763 | } else { |
| 764 | if (section.is_empty()) { |
| 765 | set(assign, value); |
| 766 | } else { |
| 767 | set(section + "/" + assign, value); |
| 768 | } |
| 769 | } |
| 770 | } else if (!next_tag.name.is_empty()) { |
| 771 | section = next_tag.name; |
| 772 | } |
| 773 | } |
| 774 | } |
| 775 | |
| 776 | Error ProjectSettings::_load_settings_text_or_binary(const String &p_text_path, const String &p_bin_path) { |
| 777 | // Attempt first to load the binary project.godot file. |
| 778 | Error err = _load_settings_binary(p_bin_path); |
| 779 | if (err == OK) { |
| 780 | return OK; |
| 781 | } else if (err != ERR_FILE_NOT_FOUND) { |
| 782 | // If the file exists but can't be loaded, we want to know it. |
| 783 | ERR_PRINT("Couldn't load file '" + p_bin_path + "', error code " + itos(err) + "." ); |
| 784 | } |
| 785 | |
| 786 | // Fallback to text-based project.godot file if binary was not found. |
| 787 | err = _load_settings_text(p_text_path); |
| 788 | if (err == OK) { |
| 789 | return OK; |
| 790 | } else if (err != ERR_FILE_NOT_FOUND) { |
| 791 | ERR_PRINT("Couldn't load file '" + p_text_path + "', error code " + itos(err) + "." ); |
| 792 | } |
| 793 | |
| 794 | return err; |
| 795 | } |
| 796 | |
| 797 | Error ProjectSettings::load_custom(const String &p_path) { |
| 798 | if (p_path.ends_with(".binary" )) { |
| 799 | return _load_settings_binary(p_path); |
| 800 | } |
| 801 | return _load_settings_text(p_path); |
| 802 | } |
| 803 | |
| 804 | int ProjectSettings::get_order(const String &p_name) const { |
| 805 | ERR_FAIL_COND_V_MSG(!props.has(p_name), -1, "Request for nonexistent project setting: " + p_name + "." ); |
| 806 | return props[p_name].order; |
| 807 | } |
| 808 | |
| 809 | void ProjectSettings::set_order(const String &p_name, int p_order) { |
| 810 | ERR_FAIL_COND_MSG(!props.has(p_name), "Request for nonexistent project setting: " + p_name + "." ); |
| 811 | props[p_name].order = p_order; |
| 812 | } |
| 813 | |
| 814 | void ProjectSettings::set_builtin_order(const String &p_name) { |
| 815 | ERR_FAIL_COND_MSG(!props.has(p_name), "Request for nonexistent project setting: " + p_name + "." ); |
| 816 | if (props[p_name].order >= NO_BUILTIN_ORDER_BASE) { |
| 817 | props[p_name].order = last_builtin_order++; |
| 818 | } |
| 819 | } |
| 820 | |
| 821 | bool ProjectSettings::is_builtin_setting(const String &p_name) const { |
| 822 | // Return true because a false negative is worse than a false positive. |
| 823 | ERR_FAIL_COND_V_MSG(!props.has(p_name), true, "Request for nonexistent project setting: " + p_name + "." ); |
| 824 | return props[p_name].order < NO_BUILTIN_ORDER_BASE; |
| 825 | } |
| 826 | |
| 827 | void ProjectSettings::clear(const String &p_name) { |
| 828 | ERR_FAIL_COND_MSG(!props.has(p_name), "Request for nonexistent project setting: " + p_name + "." ); |
| 829 | props.erase(p_name); |
| 830 | } |
| 831 | |
| 832 | Error ProjectSettings::save() { |
| 833 | Error error = save_custom(get_resource_path().path_join("project.godot" )); |
| 834 | if (error == OK) { |
| 835 | last_save_time = FileAccess::get_modified_time(get_resource_path().path_join("project.godot" )); |
| 836 | } |
| 837 | return error; |
| 838 | } |
| 839 | |
| 840 | Error ProjectSettings::_save_settings_binary(const String &p_file, const RBMap<String, List<String>> &p_props, const CustomMap &p_custom, const String &p_custom_features) { |
| 841 | Error err; |
| 842 | Ref<FileAccess> file = FileAccess::open(p_file, FileAccess::WRITE, &err); |
| 843 | ERR_FAIL_COND_V_MSG(err != OK, err, "Couldn't save project.binary at " + p_file + "." ); |
| 844 | |
| 845 | uint8_t hdr[4] = { 'E', 'C', 'F', 'G' }; |
| 846 | file->store_buffer(hdr, 4); |
| 847 | |
| 848 | int count = 0; |
| 849 | |
| 850 | for (const KeyValue<String, List<String>> &E : p_props) { |
| 851 | count += E.value.size(); |
| 852 | } |
| 853 | |
| 854 | if (!p_custom_features.is_empty()) { |
| 855 | file->store_32(count + 1); |
| 856 | //store how many properties are saved, add one for custom featuers, which must always go first |
| 857 | String key = CoreStringNames::get_singleton()->_custom_features; |
| 858 | file->store_pascal_string(key); |
| 859 | |
| 860 | int len; |
| 861 | err = encode_variant(p_custom_features, nullptr, len, false); |
| 862 | ERR_FAIL_COND_V(err != OK, err); |
| 863 | |
| 864 | Vector<uint8_t> buff; |
| 865 | buff.resize(len); |
| 866 | |
| 867 | err = encode_variant(p_custom_features, buff.ptrw(), len, false); |
| 868 | ERR_FAIL_COND_V(err != OK, err); |
| 869 | file->store_32(len); |
| 870 | file->store_buffer(buff.ptr(), buff.size()); |
| 871 | |
| 872 | } else { |
| 873 | file->store_32(count); //store how many properties are saved |
| 874 | } |
| 875 | |
| 876 | for (const KeyValue<String, List<String>> &E : p_props) { |
| 877 | for (const String &key : E.value) { |
| 878 | String k = key; |
| 879 | if (!E.key.is_empty()) { |
| 880 | k = E.key + "/" + k; |
| 881 | } |
| 882 | Variant value; |
| 883 | if (p_custom.has(k)) { |
| 884 | value = p_custom[k]; |
| 885 | } else { |
| 886 | value = get(k); |
| 887 | } |
| 888 | |
| 889 | file->store_pascal_string(k); |
| 890 | |
| 891 | int len; |
| 892 | err = encode_variant(value, nullptr, len, true); |
| 893 | ERR_FAIL_COND_V_MSG(err != OK, ERR_INVALID_DATA, "Error when trying to encode Variant." ); |
| 894 | |
| 895 | Vector<uint8_t> buff; |
| 896 | buff.resize(len); |
| 897 | |
| 898 | err = encode_variant(value, buff.ptrw(), len, true); |
| 899 | ERR_FAIL_COND_V_MSG(err != OK, ERR_INVALID_DATA, "Error when trying to encode Variant." ); |
| 900 | file->store_32(len); |
| 901 | file->store_buffer(buff.ptr(), buff.size()); |
| 902 | } |
| 903 | } |
| 904 | |
| 905 | return OK; |
| 906 | } |
| 907 | |
| 908 | Error ProjectSettings::_save_settings_text(const String &p_file, const RBMap<String, List<String>> &p_props, const CustomMap &p_custom, const String &p_custom_features) { |
| 909 | Error err; |
| 910 | Ref<FileAccess> file = FileAccess::open(p_file, FileAccess::WRITE, &err); |
| 911 | |
| 912 | ERR_FAIL_COND_V_MSG(err != OK, err, "Couldn't save project.godot - " + p_file + "." ); |
| 913 | |
| 914 | file->store_line("; Engine configuration file." ); |
| 915 | file->store_line("; It's best edited using the editor UI and not directly," ); |
| 916 | file->store_line("; since the parameters that go here are not all obvious." ); |
| 917 | file->store_line(";" ); |
| 918 | file->store_line("; Format:" ); |
| 919 | file->store_line("; [section] ; section goes between []" ); |
| 920 | file->store_line("; param=value ; assign values to parameters" ); |
| 921 | file->store_line("" ); |
| 922 | |
| 923 | file->store_string("config_version=" + itos(CONFIG_VERSION) + "\n" ); |
| 924 | if (!p_custom_features.is_empty()) { |
| 925 | file->store_string("custom_features=\"" + p_custom_features + "\"\n" ); |
| 926 | } |
| 927 | file->store_string("\n" ); |
| 928 | |
| 929 | for (const KeyValue<String, List<String>> &E : p_props) { |
| 930 | if (E.key != p_props.begin()->key) { |
| 931 | file->store_string("\n" ); |
| 932 | } |
| 933 | |
| 934 | if (!E.key.is_empty()) { |
| 935 | file->store_string("[" + E.key + "]\n\n" ); |
| 936 | } |
| 937 | for (const String &F : E.value) { |
| 938 | String key = F; |
| 939 | if (!E.key.is_empty()) { |
| 940 | key = E.key + "/" + key; |
| 941 | } |
| 942 | Variant value; |
| 943 | if (p_custom.has(key)) { |
| 944 | value = p_custom[key]; |
| 945 | } else { |
| 946 | value = get(key); |
| 947 | } |
| 948 | |
| 949 | String vstr; |
| 950 | VariantWriter::write_to_string(value, vstr); |
| 951 | file->store_string(F.property_name_encode() + "=" + vstr + "\n" ); |
| 952 | } |
| 953 | } |
| 954 | |
| 955 | return OK; |
| 956 | } |
| 957 | |
| 958 | Error ProjectSettings::_save_custom_bnd(const String &p_file) { // add other params as dictionary and array? |
| 959 | return save_custom(p_file); |
| 960 | } |
| 961 | |
| 962 | #ifdef TOOLS_ENABLED |
| 963 | bool _csproj_exists(String p_root_dir) { |
| 964 | Ref<DirAccess> dir = DirAccess::open(p_root_dir); |
| 965 | |
| 966 | dir->list_dir_begin(); |
| 967 | String file_name = dir->_get_next(); |
| 968 | while (file_name != "" ) { |
| 969 | if (!dir->current_is_dir() && file_name.get_extension() == "csproj" ) { |
| 970 | return true; |
| 971 | } |
| 972 | file_name = dir->_get_next(); |
| 973 | } |
| 974 | |
| 975 | return false; |
| 976 | } |
| 977 | #endif // TOOLS_ENABLED |
| 978 | |
| 979 | Error ProjectSettings::save_custom(const String &p_path, const CustomMap &p_custom, const Vector<String> &p_custom_features, bool p_merge_with_current) { |
| 980 | ERR_FAIL_COND_V_MSG(p_path.is_empty(), ERR_INVALID_PARAMETER, "Project settings save path cannot be empty." ); |
| 981 | |
| 982 | #ifdef TOOLS_ENABLED |
| 983 | PackedStringArray project_features = get_setting("application/config/features" ); |
| 984 | // If there is no feature list currently present, force one to generate. |
| 985 | if (project_features.is_empty()) { |
| 986 | project_features = ProjectSettings::get_required_features(); |
| 987 | } |
| 988 | // Check the rendering API. |
| 989 | const String rendering_api = has_setting("rendering/renderer/rendering_method" ) ? (String)get_setting("rendering/renderer/rendering_method" ) : String(); |
| 990 | if (!rendering_api.is_empty()) { |
| 991 | // Add the rendering API as a project feature if it doesn't already exist. |
| 992 | if (!project_features.has(rendering_api)) { |
| 993 | project_features.append(rendering_api); |
| 994 | } |
| 995 | } |
| 996 | // Check for the existence of a csproj file. |
| 997 | if (_csproj_exists(get_resource_path())) { |
| 998 | // If there is a csproj file, add the C# feature if it doesn't already exist. |
| 999 | if (!project_features.has("C#" )) { |
| 1000 | project_features.append("C#" ); |
| 1001 | } |
| 1002 | } else { |
| 1003 | // If there isn't a csproj file, remove the C# feature if it exists. |
| 1004 | if (project_features.has("C#" )) { |
| 1005 | project_features.remove_at(project_features.find("C#" )); |
| 1006 | } |
| 1007 | } |
| 1008 | project_features = _trim_to_supported_features(project_features); |
| 1009 | set_setting("application/config/features" , project_features); |
| 1010 | #endif // TOOLS_ENABLED |
| 1011 | |
| 1012 | RBSet<_VCSort> vclist; |
| 1013 | |
| 1014 | if (p_merge_with_current) { |
| 1015 | for (const KeyValue<StringName, VariantContainer> &G : props) { |
| 1016 | const VariantContainer *v = &G.value; |
| 1017 | |
| 1018 | if (v->hide_from_editor) { |
| 1019 | continue; |
| 1020 | } |
| 1021 | |
| 1022 | if (p_custom.has(G.key)) { |
| 1023 | continue; |
| 1024 | } |
| 1025 | |
| 1026 | _VCSort vc; |
| 1027 | vc.name = G.key; //*k; |
| 1028 | vc.order = v->order; |
| 1029 | vc.type = v->variant.get_type(); |
| 1030 | vc.flags = PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_STORAGE; |
| 1031 | if (v->variant == v->initial) { |
| 1032 | continue; |
| 1033 | } |
| 1034 | |
| 1035 | vclist.insert(vc); |
| 1036 | } |
| 1037 | } |
| 1038 | |
| 1039 | for (const KeyValue<String, Variant> &E : p_custom) { |
| 1040 | // Lookup global prop to store in the same order |
| 1041 | RBMap<StringName, VariantContainer>::Iterator global_prop = props.find(E.key); |
| 1042 | |
| 1043 | _VCSort vc; |
| 1044 | vc.name = E.key; |
| 1045 | vc.order = global_prop ? global_prop->value.order : 0xFFFFFFF; |
| 1046 | vc.type = E.value.get_type(); |
| 1047 | vc.flags = PROPERTY_USAGE_STORAGE; |
| 1048 | vclist.insert(vc); |
| 1049 | } |
| 1050 | |
| 1051 | RBMap<String, List<String>> save_props; |
| 1052 | |
| 1053 | for (const _VCSort &E : vclist) { |
| 1054 | String category = E.name; |
| 1055 | String name = E.name; |
| 1056 | |
| 1057 | int div = category.find("/" ); |
| 1058 | |
| 1059 | if (div < 0) { |
| 1060 | category = "" ; |
| 1061 | } else { |
| 1062 | category = category.substr(0, div); |
| 1063 | name = name.substr(div + 1, name.size()); |
| 1064 | } |
| 1065 | save_props[category].push_back(name); |
| 1066 | } |
| 1067 | |
| 1068 | String save_features; |
| 1069 | |
| 1070 | for (int i = 0; i < p_custom_features.size(); i++) { |
| 1071 | if (i > 0) { |
| 1072 | save_features += "," ; |
| 1073 | } |
| 1074 | |
| 1075 | String f = p_custom_features[i].strip_edges().replace("\"" , "" ); |
| 1076 | save_features += f; |
| 1077 | } |
| 1078 | |
| 1079 | if (p_path.ends_with(".godot" ) || p_path.ends_with("override.cfg" )) { |
| 1080 | return _save_settings_text(p_path, save_props, p_custom, save_features); |
| 1081 | } else if (p_path.ends_with(".binary" )) { |
| 1082 | return _save_settings_binary(p_path, save_props, p_custom, save_features); |
| 1083 | } else { |
| 1084 | ERR_FAIL_V_MSG(ERR_FILE_UNRECOGNIZED, "Unknown config file format: " + p_path + "." ); |
| 1085 | } |
| 1086 | } |
| 1087 | |
| 1088 | Variant _GLOBAL_DEF(const String &p_var, const Variant &p_default, bool p_restart_if_changed, bool p_ignore_value_in_docs, bool p_basic, bool p_internal) { |
| 1089 | Variant ret; |
| 1090 | if (!ProjectSettings::get_singleton()->has_setting(p_var)) { |
| 1091 | ProjectSettings::get_singleton()->set(p_var, p_default); |
| 1092 | } |
| 1093 | ret = GLOBAL_GET(p_var); |
| 1094 | |
| 1095 | ProjectSettings::get_singleton()->set_initial_value(p_var, p_default); |
| 1096 | ProjectSettings::get_singleton()->set_builtin_order(p_var); |
| 1097 | ProjectSettings::get_singleton()->set_as_basic(p_var, p_basic); |
| 1098 | ProjectSettings::get_singleton()->set_restart_if_changed(p_var, p_restart_if_changed); |
| 1099 | ProjectSettings::get_singleton()->set_ignore_value_in_docs(p_var, p_ignore_value_in_docs); |
| 1100 | ProjectSettings::get_singleton()->set_as_internal(p_var, p_internal); |
| 1101 | return ret; |
| 1102 | } |
| 1103 | |
| 1104 | Variant _GLOBAL_DEF(const PropertyInfo &p_info, const Variant &p_default, bool p_restart_if_changed, bool p_ignore_value_in_docs, bool p_basic, bool p_internal) { |
| 1105 | Variant ret = _GLOBAL_DEF(p_info.name, p_default, p_restart_if_changed, p_ignore_value_in_docs, p_basic, p_internal); |
| 1106 | ProjectSettings::get_singleton()->set_custom_property_info(p_info); |
| 1107 | return ret; |
| 1108 | } |
| 1109 | |
| 1110 | void ProjectSettings::_add_property_info_bind(const Dictionary &p_info) { |
| 1111 | ERR_FAIL_COND(!p_info.has("name" )); |
| 1112 | ERR_FAIL_COND(!p_info.has("type" )); |
| 1113 | |
| 1114 | PropertyInfo pinfo; |
| 1115 | pinfo.name = p_info["name" ]; |
| 1116 | ERR_FAIL_COND(!props.has(pinfo.name)); |
| 1117 | pinfo.type = Variant::Type(p_info["type" ].operator int()); |
| 1118 | ERR_FAIL_INDEX(pinfo.type, Variant::VARIANT_MAX); |
| 1119 | |
| 1120 | if (p_info.has("hint" )) { |
| 1121 | pinfo.hint = PropertyHint(p_info["hint" ].operator int()); |
| 1122 | } |
| 1123 | if (p_info.has("hint_string" )) { |
| 1124 | pinfo.hint_string = p_info["hint_string" ]; |
| 1125 | } |
| 1126 | |
| 1127 | set_custom_property_info(pinfo); |
| 1128 | } |
| 1129 | |
| 1130 | void ProjectSettings::set_custom_property_info(const PropertyInfo &p_info) { |
| 1131 | const String &prop_name = p_info.name; |
| 1132 | ERR_FAIL_COND(!props.has(prop_name)); |
| 1133 | custom_prop_info[prop_name] = p_info; |
| 1134 | } |
| 1135 | |
| 1136 | const HashMap<StringName, PropertyInfo> &ProjectSettings::get_custom_property_info() const { |
| 1137 | return custom_prop_info; |
| 1138 | } |
| 1139 | |
| 1140 | bool ProjectSettings::is_using_datapack() const { |
| 1141 | return using_datapack; |
| 1142 | } |
| 1143 | |
| 1144 | bool ProjectSettings::is_project_loaded() const { |
| 1145 | return project_loaded; |
| 1146 | } |
| 1147 | |
| 1148 | bool ProjectSettings::_property_can_revert(const StringName &p_name) const { |
| 1149 | if (!props.has(p_name)) { |
| 1150 | return false; |
| 1151 | } |
| 1152 | |
| 1153 | return props[p_name].initial != props[p_name].variant; |
| 1154 | } |
| 1155 | |
| 1156 | bool ProjectSettings::_property_get_revert(const StringName &p_name, Variant &r_property) const { |
| 1157 | if (!props.has(p_name)) { |
| 1158 | return false; |
| 1159 | } |
| 1160 | |
| 1161 | // Duplicate so that if value is array or dictionary, changing the setting will not change the stored initial value. |
| 1162 | r_property = props[p_name].initial.duplicate(); |
| 1163 | |
| 1164 | return true; |
| 1165 | } |
| 1166 | |
| 1167 | void ProjectSettings::set_setting(const String &p_setting, const Variant &p_value) { |
| 1168 | set(p_setting, p_value); |
| 1169 | } |
| 1170 | |
| 1171 | Variant ProjectSettings::get_setting(const String &p_setting, const Variant &p_default_value) const { |
| 1172 | if (has_setting(p_setting)) { |
| 1173 | return get(p_setting); |
| 1174 | } else { |
| 1175 | return p_default_value; |
| 1176 | } |
| 1177 | } |
| 1178 | |
| 1179 | TypedArray<Dictionary> ProjectSettings::get_global_class_list() { |
| 1180 | if (is_global_class_list_loaded) { |
| 1181 | return global_class_list; |
| 1182 | } |
| 1183 | |
| 1184 | Ref<ConfigFile> cf; |
| 1185 | cf.instantiate(); |
| 1186 | if (cf->load(get_global_class_list_path()) == OK) { |
| 1187 | global_class_list = cf->get_value("" , "list" , Array()); |
| 1188 | } else { |
| 1189 | #ifndef TOOLS_ENABLED |
| 1190 | // Script classes can't be recreated in exported project, so print an error. |
| 1191 | ERR_PRINT("Could not load global script cache." ); |
| 1192 | #endif |
| 1193 | } |
| 1194 | |
| 1195 | // File read succeeded or failed. If it failed, assume everything is still okay. |
| 1196 | // We will later receive updated class data in store_global_class_list(). |
| 1197 | is_global_class_list_loaded = true; |
| 1198 | |
| 1199 | return global_class_list; |
| 1200 | } |
| 1201 | |
| 1202 | String ProjectSettings::get_global_class_list_path() const { |
| 1203 | return get_project_data_path().path_join("global_script_class_cache.cfg" ); |
| 1204 | } |
| 1205 | |
| 1206 | void ProjectSettings::store_global_class_list(const Array &p_classes) { |
| 1207 | Ref<ConfigFile> cf; |
| 1208 | cf.instantiate(); |
| 1209 | cf->set_value("" , "list" , p_classes); |
| 1210 | cf->save(get_global_class_list_path()); |
| 1211 | |
| 1212 | global_class_list = p_classes; |
| 1213 | } |
| 1214 | |
| 1215 | bool ProjectSettings::has_custom_feature(const String &p_feature) const { |
| 1216 | return custom_features.has(p_feature); |
| 1217 | } |
| 1218 | |
| 1219 | const HashMap<StringName, ProjectSettings::AutoloadInfo> &ProjectSettings::get_autoload_list() const { |
| 1220 | return autoloads; |
| 1221 | } |
| 1222 | |
| 1223 | void ProjectSettings::add_autoload(const AutoloadInfo &p_autoload) { |
| 1224 | ERR_FAIL_COND_MSG(p_autoload.name == StringName(), "Trying to add autoload with no name." ); |
| 1225 | autoloads[p_autoload.name] = p_autoload; |
| 1226 | } |
| 1227 | |
| 1228 | void ProjectSettings::remove_autoload(const StringName &p_autoload) { |
| 1229 | ERR_FAIL_COND_MSG(!autoloads.has(p_autoload), "Trying to remove non-existent autoload." ); |
| 1230 | autoloads.erase(p_autoload); |
| 1231 | } |
| 1232 | |
| 1233 | bool ProjectSettings::has_autoload(const StringName &p_autoload) const { |
| 1234 | return autoloads.has(p_autoload); |
| 1235 | } |
| 1236 | |
| 1237 | ProjectSettings::AutoloadInfo ProjectSettings::get_autoload(const StringName &p_name) const { |
| 1238 | ERR_FAIL_COND_V_MSG(!autoloads.has(p_name), AutoloadInfo(), "Trying to get non-existent autoload." ); |
| 1239 | return autoloads[p_name]; |
| 1240 | } |
| 1241 | |
| 1242 | void ProjectSettings::_bind_methods() { |
| 1243 | ClassDB::bind_method(D_METHOD("has_setting" , "name" ), &ProjectSettings::has_setting); |
| 1244 | ClassDB::bind_method(D_METHOD("set_setting" , "name" , "value" ), &ProjectSettings::set_setting); |
| 1245 | ClassDB::bind_method(D_METHOD("get_setting" , "name" , "default_value" ), &ProjectSettings::get_setting, DEFVAL(Variant())); |
| 1246 | ClassDB::bind_method(D_METHOD("get_setting_with_override" , "name" ), &ProjectSettings::get_setting_with_override); |
| 1247 | ClassDB::bind_method(D_METHOD("get_global_class_list" ), &ProjectSettings::get_global_class_list); |
| 1248 | ClassDB::bind_method(D_METHOD("set_order" , "name" , "position" ), &ProjectSettings::set_order); |
| 1249 | ClassDB::bind_method(D_METHOD("get_order" , "name" ), &ProjectSettings::get_order); |
| 1250 | ClassDB::bind_method(D_METHOD("set_initial_value" , "name" , "value" ), &ProjectSettings::set_initial_value); |
| 1251 | ClassDB::bind_method(D_METHOD("set_as_basic" , "name" , "basic" ), &ProjectSettings::set_as_basic); |
| 1252 | ClassDB::bind_method(D_METHOD("set_as_internal" , "name" , "internal" ), &ProjectSettings::set_as_internal); |
| 1253 | ClassDB::bind_method(D_METHOD("add_property_info" , "hint" ), &ProjectSettings::_add_property_info_bind); |
| 1254 | ClassDB::bind_method(D_METHOD("set_restart_if_changed" , "name" , "restart" ), &ProjectSettings::set_restart_if_changed); |
| 1255 | ClassDB::bind_method(D_METHOD("clear" , "name" ), &ProjectSettings::clear); |
| 1256 | ClassDB::bind_method(D_METHOD("localize_path" , "path" ), &ProjectSettings::localize_path); |
| 1257 | ClassDB::bind_method(D_METHOD("globalize_path" , "path" ), &ProjectSettings::globalize_path); |
| 1258 | ClassDB::bind_method(D_METHOD("save" ), &ProjectSettings::save); |
| 1259 | ClassDB::bind_method(D_METHOD("load_resource_pack" , "pack" , "replace_files" , "offset" ), &ProjectSettings::_load_resource_pack, DEFVAL(true), DEFVAL(0)); |
| 1260 | |
| 1261 | ClassDB::bind_method(D_METHOD("save_custom" , "file" ), &ProjectSettings::_save_custom_bnd); |
| 1262 | |
| 1263 | ADD_SIGNAL(MethodInfo("settings_changed" )); |
| 1264 | } |
| 1265 | |
| 1266 | void ProjectSettings::_add_builtin_input_map() { |
| 1267 | if (InputMap::get_singleton()) { |
| 1268 | HashMap<String, List<Ref<InputEvent>>> builtins = InputMap::get_singleton()->get_builtins(); |
| 1269 | |
| 1270 | for (KeyValue<String, List<Ref<InputEvent>>> &E : builtins) { |
| 1271 | Array events; |
| 1272 | |
| 1273 | // Convert list of input events into array |
| 1274 | for (List<Ref<InputEvent>>::Element *I = E.value.front(); I; I = I->next()) { |
| 1275 | events.push_back(I->get()); |
| 1276 | } |
| 1277 | |
| 1278 | Dictionary action; |
| 1279 | action["deadzone" ] = Variant(0.5f); |
| 1280 | action["events" ] = events; |
| 1281 | |
| 1282 | String action_name = "input/" + E.key; |
| 1283 | GLOBAL_DEF(action_name, action); |
| 1284 | input_presets.push_back(action_name); |
| 1285 | } |
| 1286 | } |
| 1287 | } |
| 1288 | |
| 1289 | ProjectSettings::ProjectSettings() { |
| 1290 | // Initialization of engine variables should be done in the setup() method, |
| 1291 | // so that the values can be overridden from project.godot or project.binary. |
| 1292 | |
| 1293 | CRASH_COND_MSG(singleton != nullptr, "Instantiating a new ProjectSettings singleton is not supported." ); |
| 1294 | singleton = this; |
| 1295 | |
| 1296 | GLOBAL_DEF_BASIC("application/config/name" , "" ); |
| 1297 | GLOBAL_DEF_BASIC(PropertyInfo(Variant::DICTIONARY, "application/config/name_localized" , PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary()); |
| 1298 | GLOBAL_DEF_BASIC(PropertyInfo(Variant::STRING, "application/config/description" , PROPERTY_HINT_MULTILINE_TEXT), "" ); |
| 1299 | GLOBAL_DEF_BASIC("application/config/version" , "" ); |
| 1300 | GLOBAL_DEF_INTERNAL(PropertyInfo(Variant::STRING, "application/config/tags" ), PackedStringArray()); |
| 1301 | GLOBAL_DEF_BASIC(PropertyInfo(Variant::STRING, "application/run/main_scene" , PROPERTY_HINT_FILE, "*.tscn,*.scn,*.res" ), "" ); |
| 1302 | GLOBAL_DEF("application/run/disable_stdout" , false); |
| 1303 | GLOBAL_DEF("application/run/disable_stderr" , false); |
| 1304 | GLOBAL_DEF_RST("application/config/use_hidden_project_data_directory" , true); |
| 1305 | GLOBAL_DEF("application/config/use_custom_user_dir" , false); |
| 1306 | GLOBAL_DEF("application/config/custom_user_dir_name" , "" ); |
| 1307 | GLOBAL_DEF("application/config/project_settings_override" , "" ); |
| 1308 | |
| 1309 | GLOBAL_DEF("application/run/main_loop_type" , "SceneTree" ); |
| 1310 | GLOBAL_DEF("application/config/auto_accept_quit" , true); |
| 1311 | GLOBAL_DEF("application/config/quit_on_go_back" , true); |
| 1312 | |
| 1313 | // The default window size is tuned to: |
| 1314 | // - Have a 16:9 aspect ratio, |
| 1315 | // - Have both dimensions divisible by 8 to better play along with video recording, |
| 1316 | // - Be displayable correctly in windowed mode on a 1366×768 display (tested on Windows 10 with default settings). |
| 1317 | GLOBAL_DEF_BASIC(PropertyInfo(Variant::INT, "display/window/size/viewport_width" , PROPERTY_HINT_RANGE, "0,7680,1,or_greater" ), 1152); // 8K resolution |
| 1318 | GLOBAL_DEF_BASIC(PropertyInfo(Variant::INT, "display/window/size/viewport_height" , PROPERTY_HINT_RANGE, "0,4320,1,or_greater" ), 648); // 8K resolution |
| 1319 | |
| 1320 | GLOBAL_DEF_BASIC(PropertyInfo(Variant::INT, "display/window/size/mode" , PROPERTY_HINT_ENUM, "Windowed,Minimized,Maximized,Fullscreen,Exclusive Fullscreen" ), 0); |
| 1321 | |
| 1322 | // Keep the enum values in sync with the `DisplayServer::SCREEN_` enum. |
| 1323 | GLOBAL_DEF_BASIC(PropertyInfo(Variant::INT, "display/window/size/initial_position_type" , PROPERTY_HINT_ENUM, "Absolute,Center of Primary Screen,Center of Other Screen,Center of Screen With Mouse Pointer,Center of Screen With Keyboard Focus" ), 1); |
| 1324 | GLOBAL_DEF_BASIC(PropertyInfo(Variant::VECTOR2I, "display/window/size/initial_position" ), Vector2i()); |
| 1325 | GLOBAL_DEF_BASIC(PropertyInfo(Variant::INT, "display/window/size/initial_screen" , PROPERTY_HINT_RANGE, "0,64,1,or_greater" ), 0); |
| 1326 | |
| 1327 | GLOBAL_DEF_BASIC("display/window/size/resizable" , true); |
| 1328 | GLOBAL_DEF_BASIC("display/window/size/borderless" , false); |
| 1329 | GLOBAL_DEF("display/window/size/always_on_top" , false); |
| 1330 | GLOBAL_DEF("display/window/size/transparent" , false); |
| 1331 | GLOBAL_DEF("display/window/size/extend_to_title" , false); |
| 1332 | GLOBAL_DEF("display/window/size/no_focus" , false); |
| 1333 | |
| 1334 | GLOBAL_DEF(PropertyInfo(Variant::INT, "display/window/size/window_width_override" , PROPERTY_HINT_RANGE, "0,7680,1,or_greater" ), 0); // 8K resolution |
| 1335 | GLOBAL_DEF(PropertyInfo(Variant::INT, "display/window/size/window_height_override" , PROPERTY_HINT_RANGE, "0,4320,1,or_greater" ), 0); // 8K resolution |
| 1336 | |
| 1337 | GLOBAL_DEF("display/window/energy_saving/keep_screen_on" , true); |
| 1338 | GLOBAL_DEF("display/window/energy_saving/keep_screen_on.editor" , false); |
| 1339 | |
| 1340 | GLOBAL_DEF_BASIC(PropertyInfo(Variant::STRING, "audio/buses/default_bus_layout" , PROPERTY_HINT_FILE, "*.tres" ), "res://default_bus_layout.tres" ); |
| 1341 | GLOBAL_DEF_RST("audio/general/text_to_speech" , false); |
| 1342 | GLOBAL_DEF_RST(PropertyInfo(Variant::FLOAT, "audio/general/2d_panning_strength" , PROPERTY_HINT_RANGE, "0,2,0.01" ), 0.5f); |
| 1343 | GLOBAL_DEF_RST(PropertyInfo(Variant::FLOAT, "audio/general/3d_panning_strength" , PROPERTY_HINT_RANGE, "0,2,0.01" ), 0.5f); |
| 1344 | |
| 1345 | PackedStringArray extensions; |
| 1346 | extensions.push_back("gd" ); |
| 1347 | if (Engine::get_singleton()->has_singleton("GodotSharp" )) { |
| 1348 | extensions.push_back("cs" ); |
| 1349 | } |
| 1350 | extensions.push_back("gdshader" ); |
| 1351 | |
| 1352 | GLOBAL_DEF(PropertyInfo(Variant::PACKED_STRING_ARRAY, "editor/script/search_in_file_extensions" ), extensions); |
| 1353 | |
| 1354 | _add_builtin_input_map(); |
| 1355 | |
| 1356 | // Keep the enum values in sync with the `DisplayServer::ScreenOrientation` enum. |
| 1357 | custom_prop_info["display/window/handheld/orientation" ] = PropertyInfo(Variant::INT, "display/window/handheld/orientation" , PROPERTY_HINT_ENUM, "Landscape,Portrait,Reverse Landscape,Reverse Portrait,Sensor Landscape,Sensor Portrait,Sensor" ); |
| 1358 | GLOBAL_DEF("display/window/subwindows/embed_subwindows" , true); |
| 1359 | // Keep the enum values in sync with the `DisplayServer::VSyncMode` enum. |
| 1360 | custom_prop_info["display/window/vsync/vsync_mode" ] = PropertyInfo(Variant::INT, "display/window/vsync/vsync_mode" , PROPERTY_HINT_ENUM, "Disabled,Enabled,Adaptive,Mailbox" ); |
| 1361 | custom_prop_info["rendering/driver/threads/thread_model" ] = PropertyInfo(Variant::INT, "rendering/driver/threads/thread_model" , PROPERTY_HINT_ENUM, "Single-Unsafe,Single-Safe,Multi-Threaded" ); |
| 1362 | GLOBAL_DEF("physics/2d/run_on_separate_thread" , false); |
| 1363 | GLOBAL_DEF("physics/3d/run_on_separate_thread" , false); |
| 1364 | |
| 1365 | GLOBAL_DEF_BASIC(PropertyInfo(Variant::STRING, "display/window/stretch/mode" , PROPERTY_HINT_ENUM, "disabled,canvas_items,viewport" ), "disabled" ); |
| 1366 | GLOBAL_DEF_BASIC(PropertyInfo(Variant::STRING, "display/window/stretch/aspect" , PROPERTY_HINT_ENUM, "ignore,keep,keep_width,keep_height,expand" ), "keep" ); |
| 1367 | GLOBAL_DEF_BASIC(PropertyInfo(Variant::FLOAT, "display/window/stretch/scale" , PROPERTY_HINT_RANGE, "0.5,8.0,0.01" ), 1.0); |
| 1368 | GLOBAL_DEF_BASIC(PropertyInfo(Variant::STRING, "display/window/stretch/scale_mode" , PROPERTY_HINT_ENUM, "fractional,integer" ), "fractional" ); |
| 1369 | |
| 1370 | GLOBAL_DEF(PropertyInfo(Variant::INT, "debug/settings/profiler/max_functions" , PROPERTY_HINT_RANGE, "128,65535,1" ), 16384); |
| 1371 | |
| 1372 | GLOBAL_DEF(PropertyInfo(Variant::BOOL, "compression/formats/zstd/long_distance_matching" ), Compression::zstd_long_distance_matching); |
| 1373 | GLOBAL_DEF(PropertyInfo(Variant::INT, "compression/formats/zstd/compression_level" , PROPERTY_HINT_RANGE, "1,22,1" ), Compression::zstd_level); |
| 1374 | GLOBAL_DEF(PropertyInfo(Variant::INT, "compression/formats/zstd/window_log_size" , PROPERTY_HINT_RANGE, "10,30,1" ), Compression::zstd_window_log_size); |
| 1375 | GLOBAL_DEF(PropertyInfo(Variant::INT, "compression/formats/zlib/compression_level" , PROPERTY_HINT_RANGE, "-1,9,1" ), Compression::zlib_level); |
| 1376 | GLOBAL_DEF(PropertyInfo(Variant::INT, "compression/formats/gzip/compression_level" , PROPERTY_HINT_RANGE, "-1,9,1" ), Compression::gzip_level); |
| 1377 | |
| 1378 | GLOBAL_DEF("debug/settings/crash_handler/message" , |
| 1379 | String("Please include this when reporting the bug to the project developer." )); |
| 1380 | GLOBAL_DEF("debug/settings/crash_handler/message.editor" , |
| 1381 | String("Please include this when reporting the bug on: https://github.com/godotengine/godot/issues" )); |
| 1382 | GLOBAL_DEF_RST(PropertyInfo(Variant::INT, "rendering/occlusion_culling/bvh_build_quality" , PROPERTY_HINT_ENUM, "Low,Medium,High" ), 2); |
| 1383 | GLOBAL_DEF(PropertyInfo(Variant::INT, "memory/limits/multithreaded_server/rid_pool_prealloc" , PROPERTY_HINT_RANGE, "0,500,1" ), 60); // No negative and limit to 500 due to crashes. |
| 1384 | GLOBAL_DEF_RST("internationalization/rendering/force_right_to_left_layout_direction" , false); |
| 1385 | GLOBAL_DEF_BASIC(PropertyInfo(Variant::INT, "internationalization/rendering/root_node_layout_direction" , PROPERTY_HINT_ENUM, "Based on Locale,Left-to-Right,Right-to-Left" ), 0); |
| 1386 | |
| 1387 | GLOBAL_DEF(PropertyInfo(Variant::INT, "gui/timers/incremental_search_max_interval_msec" , PROPERTY_HINT_RANGE, "0,10000,1,or_greater" ), 2000); |
| 1388 | |
| 1389 | GLOBAL_DEF_BASIC("gui/common/snap_controls_to_pixels" , true); |
| 1390 | GLOBAL_DEF_BASIC("gui/fonts/dynamic_fonts/use_oversampling" , true); |
| 1391 | |
| 1392 | GLOBAL_DEF("rendering/rendering_device/staging_buffer/block_size_kb" , 256); |
| 1393 | GLOBAL_DEF("rendering/rendering_device/staging_buffer/max_size_mb" , 128); |
| 1394 | GLOBAL_DEF("rendering/rendering_device/staging_buffer/texture_upload_region_size_px" , 64); |
| 1395 | GLOBAL_DEF("rendering/rendering_device/pipeline_cache/save_chunk_size_mb" , 3.0); |
| 1396 | GLOBAL_DEF("rendering/rendering_device/vulkan/max_descriptors_per_pool" , 64); |
| 1397 | |
| 1398 | GLOBAL_DEF_BASIC(PropertyInfo(Variant::INT, "rendering/textures/canvas_textures/default_texture_filter" , PROPERTY_HINT_ENUM, "Nearest,Linear,Linear Mipmap,Nearest Mipmap" ), 1); |
| 1399 | GLOBAL_DEF_BASIC(PropertyInfo(Variant::INT, "rendering/textures/canvas_textures/default_texture_repeat" , PROPERTY_HINT_ENUM, "Disable,Enable,Mirror" ), 0); |
| 1400 | |
| 1401 | GLOBAL_DEF("collada/use_ambient" , false); |
| 1402 | |
| 1403 | // These properties will not show up in the dialog. If you want to exclude whole groups, use add_hidden_prefix(). |
| 1404 | GLOBAL_DEF_INTERNAL("application/config/features" , PackedStringArray()); |
| 1405 | GLOBAL_DEF_INTERNAL("internationalization/locale/translation_remaps" , PackedStringArray()); |
| 1406 | GLOBAL_DEF_INTERNAL("internationalization/locale/translations" , PackedStringArray()); |
| 1407 | GLOBAL_DEF_INTERNAL("internationalization/locale/translations_pot_files" , PackedStringArray()); |
| 1408 | |
| 1409 | ProjectSettings::get_singleton()->add_hidden_prefix("input/" ); |
| 1410 | } |
| 1411 | |
| 1412 | ProjectSettings::~ProjectSettings() { |
| 1413 | singleton = nullptr; |
| 1414 | } |
| 1415 | |