Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
godotengine
GitHub Repository: godotengine/godot
Path: blob/master/platform/ios/export/export_plugin.cpp
11353 views
1
/**************************************************************************/
2
/* export_plugin.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 "export_plugin.h"
32
33
#include "logo_svg.gen.h"
34
#include "run_icon_svg.gen.h"
35
36
#include "editor/editor_node.h"
37
38
Vector<String> EditorExportPlatformIOS::device_types({ "iPhone", "iPad" });
39
40
void EditorExportPlatformIOS::initialize() {
41
if (EditorNode::get_singleton()) {
42
EditorExportPlatformAppleEmbedded::_initialize(_ios_logo_svg, _ios_run_icon_svg);
43
#ifdef MACOS_ENABLED
44
_start_remote_device_poller_thread();
45
#endif
46
}
47
}
48
49
EditorExportPlatformIOS::~EditorExportPlatformIOS() {
50
}
51
52
void EditorExportPlatformIOS::get_export_options(List<ExportOption> *r_options) const {
53
EditorExportPlatformAppleEmbedded::get_export_options(r_options);
54
55
r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "application/targeted_device_family", PROPERTY_HINT_ENUM, "iPhone,iPad,iPhone & iPad"), 2));
56
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/min_ios_version"), get_minimum_deployment_target()));
57
58
r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "storyboard/image_scale_mode", PROPERTY_HINT_ENUM, "Same as Logo,Center,Scale to Fit,Scale to Fill,Scale"), 0));
59
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "storyboard/custom_image@2x", PROPERTY_HINT_FILE_PATH, "*.png,*.jpg,*.jpeg"), ""));
60
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "storyboard/custom_image@3x", PROPERTY_HINT_FILE_PATH, "*.png,*.jpg,*.jpeg"), ""));
61
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "storyboard/use_custom_bg_color"), false));
62
r_options->push_back(ExportOption(PropertyInfo(Variant::COLOR, "storyboard/custom_bg_color"), Color()));
63
}
64
65
bool EditorExportPlatformIOS::has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug) const {
66
bool valid = EditorExportPlatformAppleEmbedded::has_valid_export_configuration(p_preset, r_error, r_missing_templates, p_debug);
67
68
String err;
69
String rendering_method = get_project_setting(p_preset, "rendering/renderer/rendering_method.mobile");
70
String rendering_driver = get_project_setting(p_preset, "rendering/rendering_device/driver." + get_platform_name());
71
if ((rendering_method == "forward_plus" || rendering_method == "mobile") && rendering_driver == "metal") {
72
float version = p_preset->get("application/min_ios_version").operator String().to_float();
73
if (version < 14.0) {
74
err += TTR("Metal renderer require iOS 14+.") + "\n";
75
}
76
}
77
78
if (!err.is_empty()) {
79
if (!r_error.is_empty()) {
80
r_error += err;
81
} else {
82
r_error = err;
83
}
84
}
85
86
return valid;
87
}
88
89
HashMap<String, Variant> EditorExportPlatformIOS::get_custom_project_settings(const Ref<EditorExportPreset> &p_preset) const {
90
HashMap<String, Variant> settings;
91
92
int image_scale_mode = p_preset->get("storyboard/image_scale_mode");
93
String value;
94
95
switch (image_scale_mode) {
96
case 0: {
97
String logo_path = get_project_setting(p_preset, "application/boot_splash/image");
98
bool is_on = get_project_setting(p_preset, "application/boot_splash/fullsize");
99
// If custom logo is not specified, Godot does not scale default one, so we should do the same.
100
value = (is_on && logo_path.length() > 0) ? "scaleAspectFit" : "center";
101
} break;
102
default: {
103
value = storyboard_image_scale_mode[image_scale_mode - 1];
104
}
105
}
106
settings["ios/launch_screen_image_mode"] = value;
107
return settings;
108
}
109
110
Error EditorExportPlatformIOS::_export_loading_screen_file(const Ref<EditorExportPreset> &p_preset, const String &p_dest_dir) {
111
const String custom_launch_image_2x = p_preset->get("storyboard/custom_image@2x");
112
const String custom_launch_image_3x = p_preset->get("storyboard/custom_image@3x");
113
114
if (custom_launch_image_2x.length() > 0 && custom_launch_image_3x.length() > 0) {
115
String image_path = p_dest_dir.path_join("[email protected]");
116
Error err = OK;
117
Ref<Image> image = _load_icon_or_splash_image(custom_launch_image_2x, &err);
118
119
if (err != OK || image.is_null() || image->is_empty()) {
120
return err;
121
}
122
123
if (image->save_png(image_path) != OK) {
124
return ERR_FILE_CANT_WRITE;
125
}
126
127
image_path = p_dest_dir.path_join("[email protected]");
128
image = _load_icon_or_splash_image(custom_launch_image_3x, &err);
129
130
if (err != OK || image.is_null() || image->is_empty()) {
131
return err;
132
}
133
134
if (image->save_png(image_path) != OK) {
135
return ERR_FILE_CANT_WRITE;
136
}
137
} else {
138
Error err = OK;
139
Ref<Image> splash;
140
141
const String splash_path = get_project_setting(p_preset, "application/boot_splash/image");
142
143
if (!splash_path.is_empty()) {
144
splash = _load_icon_or_splash_image(splash_path, &err);
145
}
146
147
if (err != OK || splash.is_null() || splash->is_empty()) {
148
splash.instantiate(boot_splash_png);
149
}
150
151
// Using same image for both @2x and @3x
152
// because Godot's own boot logo uses single image for all resolutions.
153
// Also not using @1x image, because devices using this image variant
154
// are not supported by iOS 9, which is minimal target.
155
const String splash_png_path_2x = p_dest_dir.path_join("[email protected]");
156
const String splash_png_path_3x = p_dest_dir.path_join("[email protected]");
157
158
if (splash->save_png(splash_png_path_2x) != OK) {
159
return ERR_FILE_CANT_WRITE;
160
}
161
162
if (splash->save_png(splash_png_path_3x) != OK) {
163
return ERR_FILE_CANT_WRITE;
164
}
165
}
166
167
return OK;
168
}
169
170
Vector<EditorExportPlatformAppleEmbedded::IconInfo> EditorExportPlatformIOS::get_icon_infos() const {
171
Vector<EditorExportPlatformAppleEmbedded::IconInfo> icon_infos;
172
return {
173
// Settings on iPhone, iPad Pro, iPad, iPad mini
174
{ PNAME("icons/settings_58x58"), "universal", "Icon-58", "58", "2x", "29x29", false },
175
{ PNAME("icons/settings_87x87"), "universal", "Icon-87", "87", "3x", "29x29", false },
176
177
// Notifications on iPhone, iPad Pro, iPad, iPad mini
178
{ PNAME("icons/notification_40x40"), "universal", "Icon-40", "40", "2x", "20x20", false },
179
{ PNAME("icons/notification_60x60"), "universal", "Icon-60", "60", "3x", "20x20", false },
180
{ PNAME("icons/notification_76x76"), "universal", "Icon-76", "76", "2x", "38x38", false },
181
{ PNAME("icons/notification_114x114"), "universal", "Icon-114", "114", "3x", "38x38", false },
182
183
// Spotlight on iPhone, iPad Pro, iPad, iPad mini
184
{ PNAME("icons/spotlight_80x80"), "universal", "Icon-80", "80", "2x", "40x40", false },
185
{ PNAME("icons/spotlight_120x120"), "universal", "Icon-120", "120", "3x", "40x40", false },
186
187
// Home Screen on iPhone
188
{ PNAME("icons/iphone_120x120"), "universal", "Icon-120-1", "120", "2x", "60x60", false },
189
{ PNAME("icons/iphone_180x180"), "universal", "Icon-180", "180", "3x", "60x60", false },
190
191
// Home Screen on iPad Pro
192
{ PNAME("icons/ipad_167x167"), "universal", "Icon-167", "167", "2x", "83.5x83.5", false },
193
194
// Home Screen on iPad, iPad mini
195
{ PNAME("icons/ipad_152x152"), "universal", "Icon-152", "152", "2x", "76x76", false },
196
197
{ PNAME("icons/ios_128x128"), "universal", "Icon-128", "128", "2x", "64x64", false },
198
{ PNAME("icons/ios_192x192"), "universal", "Icon-192", "192", "3x", "64x64", false },
199
200
{ PNAME("icons/ios_136x136"), "universal", "Icon-136", "136", "2x", "68x68", false },
201
202
// App Store
203
{ PNAME("icons/app_store_1024x1024"), "universal", "Icon-1024", "1024", "1x", "1024x1024", true },
204
};
205
}
206
207
Error EditorExportPlatformIOS::_export_icons(const Ref<EditorExportPreset> &p_preset, const String &p_iconset_dir) {
208
String json_description = "{\"images\":[";
209
String sizes;
210
211
Ref<DirAccess> da = DirAccess::open(p_iconset_dir);
212
if (da.is_null()) {
213
add_message(EXPORT_MESSAGE_ERROR, TTR("Export Icons"), vformat(TTR("Could not open a directory at path \"%s\"."), p_iconset_dir));
214
return ERR_CANT_OPEN;
215
}
216
217
Color boot_bg_color = get_project_setting(p_preset, "application/boot_splash/bg_color");
218
219
enum IconColorMode {
220
ICON_NORMAL,
221
ICON_DARK,
222
ICON_TINTED,
223
ICON_MAX,
224
};
225
226
Vector<IconInfo> icon_infos = get_icon_infos();
227
bool first_icon = true;
228
for (int i = 0; i < icon_infos.size(); ++i) {
229
for (int color_mode = ICON_NORMAL; color_mode < ICON_MAX; color_mode++) {
230
IconInfo info = icon_infos[i];
231
int side_size = String(info.actual_size_side).to_int();
232
String key = info.preset_key;
233
String exp_name = info.export_name;
234
if (color_mode == ICON_DARK) {
235
key += "_dark";
236
exp_name += "_dark";
237
} else if (color_mode == ICON_TINTED) {
238
key += "_tinted";
239
exp_name += "_tinted";
240
}
241
exp_name += ".png";
242
String icon_path = p_preset->get(key);
243
bool resize_waning = true;
244
if (icon_path.is_empty()) {
245
// Load and resize base icon.
246
key = "icons/icon_1024x1024";
247
if (color_mode == ICON_DARK) {
248
key += "_dark";
249
} else if (color_mode == ICON_TINTED) {
250
key += "_tinted";
251
}
252
icon_path = p_preset->get(key);
253
resize_waning = false;
254
}
255
if (icon_path.is_empty()) {
256
if (color_mode != ICON_NORMAL) {
257
continue;
258
}
259
// Resize main app icon.
260
icon_path = get_project_setting(p_preset, "application/config/icon");
261
Error err = OK;
262
Ref<Image> img = _load_icon_or_splash_image(icon_path, &err);
263
if (err != OK || img.is_null() || img->is_empty()) {
264
add_message(EXPORT_MESSAGE_ERROR, TTR("Export Icons"), vformat("Invalid icon (%s): '%s'.", info.preset_key, icon_path));
265
return ERR_UNCONFIGURED;
266
} else if (info.force_opaque && img->detect_alpha() != Image::ALPHA_NONE) {
267
img->resize(side_size, side_size, (Image::Interpolation)(p_preset->get("application/icon_interpolation").operator int()));
268
Ref<Image> new_img = Image::create_empty(side_size, side_size, false, Image::FORMAT_RGBA8);
269
new_img->fill(boot_bg_color);
270
_blend_and_rotate(new_img, img, false);
271
err = new_img->save_png(p_iconset_dir + exp_name);
272
} else {
273
img->resize(side_size, side_size, (Image::Interpolation)(p_preset->get("application/icon_interpolation").operator int()));
274
err = img->save_png(p_iconset_dir + exp_name);
275
}
276
if (err) {
277
add_message(EXPORT_MESSAGE_ERROR, TTR("Export Icons"), vformat("Failed to export icon (%s): '%s'.", info.preset_key, icon_path));
278
return err;
279
}
280
} else {
281
// Load custom icon and resize if required.
282
Error err = OK;
283
Ref<Image> img = _load_icon_or_splash_image(icon_path, &err);
284
if (err != OK || img.is_null() || img->is_empty()) {
285
add_message(EXPORT_MESSAGE_ERROR, TTR("Export Icons"), vformat("Invalid icon (%s): '%s'.", info.preset_key, icon_path));
286
return ERR_UNCONFIGURED;
287
} else if (info.force_opaque && img->detect_alpha() != Image::ALPHA_NONE) {
288
if (resize_waning) {
289
add_message(EXPORT_MESSAGE_WARNING, TTR("Export Icons"), vformat("Icon (%s) must be opaque.", info.preset_key));
290
}
291
img->resize(side_size, side_size, (Image::Interpolation)(p_preset->get("application/icon_interpolation").operator int()));
292
Ref<Image> new_img = Image::create_empty(side_size, side_size, false, Image::FORMAT_RGBA8);
293
new_img->fill(boot_bg_color);
294
_blend_and_rotate(new_img, img, false);
295
err = new_img->save_png(p_iconset_dir + exp_name);
296
} else if (img->get_width() != side_size || img->get_height() != side_size) {
297
if (resize_waning) {
298
add_message(EXPORT_MESSAGE_WARNING, TTR("Export Icons"), vformat("Icon (%s): '%s' has incorrect size %s and was automatically resized to %s.", info.preset_key, icon_path, img->get_size(), Vector2i(side_size, side_size)));
299
}
300
img->resize(side_size, side_size, (Image::Interpolation)(p_preset->get("application/icon_interpolation").operator int()));
301
err = img->save_png(p_iconset_dir + exp_name);
302
} else if (!icon_path.ends_with(".png")) {
303
err = img->save_png(p_iconset_dir + exp_name);
304
} else {
305
err = da->copy(icon_path, p_iconset_dir + exp_name);
306
}
307
308
if (err) {
309
add_message(EXPORT_MESSAGE_ERROR, TTR("Export Icons"), vformat("Failed to export icon (%s): '%s'.", info.preset_key, icon_path));
310
return err;
311
}
312
}
313
sizes += String(info.actual_size_side) + "\n";
314
if (first_icon) {
315
first_icon = false;
316
} else {
317
json_description += ",";
318
}
319
json_description += String("{");
320
if (color_mode != ICON_NORMAL) {
321
json_description += String("\"appearances\":[{");
322
json_description += String("\"appearance\":\"luminosity\",");
323
if (color_mode == ICON_DARK) {
324
json_description += String("\"value\":\"dark\"");
325
} else if (color_mode == ICON_TINTED) {
326
json_description += String("\"value\":\"tinted\"");
327
}
328
json_description += String("}],");
329
}
330
json_description += String("\"idiom\":") + "\"" + info.idiom + "\",";
331
json_description += String("\"platform\":\"" + get_platform_name() + "\",");
332
json_description += String("\"size\":") + "\"" + info.unscaled_size + "\",";
333
if (String(info.scale) != "1x") {
334
json_description += String("\"scale\":") + "\"" + info.scale + "\",";
335
}
336
json_description += String("\"filename\":") + "\"" + exp_name + "\"";
337
json_description += String("}");
338
}
339
}
340
json_description += "],\"info\":{\"author\":\"xcode\",\"version\":1}}";
341
342
Ref<FileAccess> json_file = FileAccess::open(p_iconset_dir + "Contents.json", FileAccess::WRITE);
343
if (json_file.is_null()) {
344
add_message(EXPORT_MESSAGE_ERROR, TTR("Export Icons"), vformat(TTR("Could not write to a file at path \"%s\"."), p_iconset_dir + "Contents.json"));
345
return ERR_CANT_CREATE;
346
}
347
348
CharString json_utf8 = json_description.utf8();
349
json_file->store_buffer((const uint8_t *)json_utf8.get_data(), json_utf8.length());
350
351
Ref<FileAccess> sizes_file = FileAccess::open(p_iconset_dir + "sizes", FileAccess::WRITE);
352
if (sizes_file.is_null()) {
353
add_message(EXPORT_MESSAGE_ERROR, TTR("Export Icons"), vformat(TTR("Could not write to a file at path \"%s\"."), p_iconset_dir + "sizes"));
354
return ERR_CANT_CREATE;
355
}
356
357
CharString sizes_utf8 = sizes.utf8();
358
sizes_file->store_buffer((const uint8_t *)sizes_utf8.get_data(), sizes_utf8.length());
359
360
return OK;
361
}
362
363
String EditorExportPlatformIOS::_process_config_file_line(const Ref<EditorExportPreset> &p_preset, const String &p_line, const AppleEmbeddedConfigData &p_config, bool p_debug, const CodeSigningDetails &p_code_signing) {
364
// Do iOS specific processing first, and call super implementation if there are no matches
365
366
String strnew;
367
368
// Supported Destinations
369
if (p_line.contains("$targeted_device_family")) {
370
String xcode_value;
371
switch ((int)p_preset->get("application/targeted_device_family")) {
372
case 0: // iPhone
373
xcode_value = "1";
374
break;
375
case 1: // iPad
376
xcode_value = "2";
377
break;
378
case 2: // iPhone & iPad
379
xcode_value = "1,2";
380
break;
381
}
382
strnew += p_line.replace("$targeted_device_family", xcode_value) + "\n";
383
384
// MoltenVK Framework
385
} else if (p_line.contains("$moltenvk_buildfile")) {
386
String value = "9039D3BE24C093AC0020482C /* MoltenVK.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9039D3BD24C093AC0020482C /* MoltenVK.xcframework */; };";
387
strnew += p_line.replace("$moltenvk_buildfile", value) + "\n";
388
} else if (p_line.contains("$moltenvk_fileref")) {
389
String value = "9039D3BD24C093AC0020482C /* MoltenVK.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = MoltenVK; path = MoltenVK.xcframework; sourceTree = \"<group>\"; };";
390
strnew += p_line.replace("$moltenvk_fileref", value) + "\n";
391
} else if (p_line.contains("$moltenvk_buildphase")) {
392
String value = "9039D3BE24C093AC0020482C /* MoltenVK.xcframework in Frameworks */,";
393
strnew += p_line.replace("$moltenvk_buildphase", value) + "\n";
394
} else if (p_line.contains("$moltenvk_buildgrp")) {
395
String value = "9039D3BD24C093AC0020482C /* MoltenVK.xcframework */,";
396
strnew += p_line.replace("$moltenvk_buildgrp", value) + "\n";
397
398
// Launch Storyboard
399
} else if (p_line.contains("$plist_launch_screen_name")) {
400
String value = "<key>UILaunchStoryboardName</key>\n<string>Launch Screen</string>";
401
strnew += p_line.replace("$plist_launch_screen_name", value) + "\n";
402
} else if (p_line.contains("$pbx_launch_screen_file_reference")) {
403
String value = "90DD2D9D24B36E8000717FE1 = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = \"Launch Screen.storyboard\"; sourceTree = \"<group>\"; };";
404
strnew += p_line.replace("$pbx_launch_screen_file_reference", value) + "\n";
405
} else if (p_line.contains("$pbx_launch_screen_copy_files")) {
406
String value = "90DD2D9D24B36E8000717FE1 /* Launch Screen.storyboard */,";
407
strnew += p_line.replace("$pbx_launch_screen_copy_files", value) + "\n";
408
} else if (p_line.contains("$pbx_launch_screen_build_phase")) {
409
String value = "90DD2D9E24B36E8000717FE1 /* Launch Screen.storyboard in Resources */,";
410
strnew += p_line.replace("$pbx_launch_screen_build_phase", value) + "\n";
411
} else if (p_line.contains("$pbx_launch_screen_build_reference")) {
412
String value = "90DD2D9E24B36E8000717FE1 /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 90DD2D9D24B36E8000717FE1 /* Launch Screen.storyboard */; };";
413
strnew += p_line.replace("$pbx_launch_screen_build_reference", value) + "\n";
414
415
// Launch Storyboard customization
416
} else if (p_line.contains("$launch_screen_image_mode")) {
417
int image_scale_mode = p_preset->get("storyboard/image_scale_mode");
418
String value;
419
420
switch (image_scale_mode) {
421
case 0: {
422
String logo_path = get_project_setting(p_preset, "application/boot_splash/image");
423
bool is_on = get_project_setting(p_preset, "application/boot_splash/fullsize");
424
// If custom logo is not specified, Godot does not scale default one, so we should do the same.
425
value = (is_on && logo_path.length() > 0) ? "scaleAspectFit" : "center";
426
} break;
427
default: {
428
value = storyboard_image_scale_mode[image_scale_mode - 1];
429
}
430
}
431
432
strnew += p_line.replace("$launch_screen_image_mode", value) + "\n";
433
} else if (p_line.contains("$launch_screen_background_color")) {
434
bool use_custom = p_preset->get("storyboard/use_custom_bg_color");
435
Color color = use_custom ? p_preset->get("storyboard/custom_bg_color") : get_project_setting(p_preset, "application/boot_splash/bg_color");
436
const String value_format = "red=\"$red\" green=\"$green\" blue=\"$blue\" alpha=\"$alpha\"";
437
438
Dictionary value_dictionary;
439
value_dictionary["red"] = color.r;
440
value_dictionary["green"] = color.g;
441
value_dictionary["blue"] = color.b;
442
value_dictionary["alpha"] = color.a;
443
String value = value_format.format(value_dictionary, "$_");
444
445
strnew += p_line.replace("$launch_screen_background_color", value) + "\n";
446
447
// OS Deployment Target
448
} else if (p_line.contains("$os_deployment_target")) {
449
String min_version = p_preset->get("application/min_" + get_platform_name() + "_version");
450
String value = "IPHONEOS_DEPLOYMENT_TARGET = " + min_version + ";";
451
strnew += p_line.replace("$os_deployment_target", value) + "\n";
452
453
// Valid Archs
454
} else if (p_line.contains("$valid_archs")) {
455
strnew += p_line.replace("$valid_archs", "arm64 x86_64") + "\n";
456
457
// Apple Embedded common
458
} else {
459
strnew += EditorExportPlatformAppleEmbedded::_process_config_file_line(p_preset, p_line, p_config, p_debug, p_code_signing);
460
}
461
462
return strnew;
463
}
464
465