1 /*************************************************************************/
2 /* joystick.cpp */
3 /*************************************************************************/
4 /* This file is part of: */
5 /* GODOT ENGINE */
6 /* https://godotengine.org */
7 /*************************************************************************/
8 /* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur. */
9 /* Copyright (c) 2014-2019 Godot Engine contributors (cf. AUTHORS.md) */
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 //author: Andreas Haas <hondres, liugam3@gmail.com>
31 #include "joystick.h"
32 #include <oleauto.h>
33 #include <wbemidl.h>
34 #include <iostream>
35
36 #ifndef __GNUC__
37 #define __builtin_bswap32 _byteswap_ulong
38 #endif
39
_xinput_get_state(DWORD dwUserIndex,XINPUT_STATE * pState)40 DWORD WINAPI _xinput_get_state(DWORD dwUserIndex, XINPUT_STATE *pState) {
41 return ERROR_DEVICE_NOT_CONNECTED;
42 }
_xinput_set_state(DWORD dwUserIndex,XINPUT_VIBRATION * pVibration)43 DWORD WINAPI _xinput_set_state(DWORD dwUserIndex, XINPUT_VIBRATION *pVibration) {
44 return ERROR_DEVICE_NOT_CONNECTED;
45 }
46
joystick_windows()47 joystick_windows::joystick_windows() {
48 }
49
joystick_windows(InputDefault * _input,HWND * hwnd)50 joystick_windows::joystick_windows(InputDefault *_input, HWND *hwnd) {
51
52 input = _input;
53 hWnd = hwnd;
54 joystick_count = 0;
55 dinput = NULL;
56 xinput_dll = NULL;
57 xinput_get_state = NULL;
58 xinput_set_state = NULL;
59
60 load_xinput();
61
62 for (int i = 0; i < JOYSTICKS_MAX; i++)
63 attached_joysticks[i] = false;
64
65 HRESULT result;
66 result = DirectInput8Create(GetModuleHandle(NULL), DIRECTINPUT_VERSION, IID_IDirectInput8, (void **)&dinput, NULL);
67 if (FAILED(result)) {
68 printf("failed init DINPUT: %ld\n", result);
69 }
70 probe_joysticks();
71 }
72
~joystick_windows()73 joystick_windows::~joystick_windows() {
74
75 close_joystick();
76 dinput->Release();
77 unload_xinput();
78 }
79
have_device(const GUID & p_guid)80 bool joystick_windows::have_device(const GUID &p_guid) {
81
82 for (int i = 0; i < JOYSTICKS_MAX; i++) {
83
84 if (d_joysticks[i].guid == p_guid) {
85
86 d_joysticks[i].confirmed = true;
87 return true;
88 }
89 }
90 return false;
91 }
92
93 // adapted from SDL2, works a lot better than the MSDN version
is_xinput_device(const GUID * p_guid)94 bool joystick_windows::is_xinput_device(const GUID *p_guid) {
95
96 static GUID IID_ValveStreamingGamepad = { MAKELONG(0x28DE, 0x11FF), 0x0000, 0x0000, { 0x00, 0x00, 0x50, 0x49, 0x44, 0x56, 0x49, 0x44 } };
97 static GUID IID_X360WiredGamepad = { MAKELONG(0x045E, 0x02A1), 0x0000, 0x0000, { 0x00, 0x00, 0x50, 0x49, 0x44, 0x56, 0x49, 0x44 } };
98 static GUID IID_X360WirelessGamepad = { MAKELONG(0x045E, 0x028E), 0x0000, 0x0000, { 0x00, 0x00, 0x50, 0x49, 0x44, 0x56, 0x49, 0x44 } };
99
100 if (p_guid == &IID_ValveStreamingGamepad || p_guid == &IID_X360WiredGamepad || p_guid == &IID_X360WirelessGamepad)
101 return true;
102
103 PRAWINPUTDEVICELIST dev_list = NULL;
104 unsigned int dev_list_count = 0;
105
106 if (GetRawInputDeviceList(NULL, &dev_list_count, sizeof(RAWINPUTDEVICELIST)) == -1) {
107 return false;
108 }
109 dev_list = (PRAWINPUTDEVICELIST)malloc(sizeof(RAWINPUTDEVICELIST) * dev_list_count);
110 if (!dev_list) return false;
111
112 if (GetRawInputDeviceList(dev_list, &dev_list_count, sizeof(RAWINPUTDEVICELIST)) == -1) {
113 free(dev_list);
114 return false;
115 }
116 for (int i = 0; i < dev_list_count; i++) {
117
118 RID_DEVICE_INFO rdi;
119 char dev_name[128];
120 UINT rdiSize = sizeof(rdi);
121 UINT nameSize = sizeof(dev_name);
122
123 rdi.cbSize = rdiSize;
124 if ((dev_list[i].dwType == RIM_TYPEHID) &&
125 (GetRawInputDeviceInfoA(dev_list[i].hDevice, RIDI_DEVICEINFO, &rdi, &rdiSize) != (UINT)-1) &&
126 (MAKELONG(rdi.hid.dwVendorId, rdi.hid.dwProductId) == (LONG)p_guid->Data1) &&
127 (GetRawInputDeviceInfoA(dev_list[i].hDevice, RIDI_DEVICENAME, &dev_name, &nameSize) != (UINT)-1) &&
128 (strstr(dev_name, "IG_") != NULL)) {
129
130 free(dev_list);
131 return true;
132 }
133 }
134 free(dev_list);
135 return false;
136 }
137
setup_dinput_joystick(const DIDEVICEINSTANCE * instance)138 bool joystick_windows::setup_dinput_joystick(const DIDEVICEINSTANCE *instance) {
139
140 HRESULT hr;
141 int num = input->get_unused_joy_id();
142
143 if (have_device(instance->guidInstance) || num == -1)
144 return false;
145
146 d_joysticks[joystick_count] = dinput_gamepad();
147 dinput_gamepad *joy = &d_joysticks[joystick_count];
148
149 const DWORD devtype = (instance->dwDevType & 0xFF);
150
151 if ((devtype != DI8DEVTYPE_JOYSTICK) && (devtype != DI8DEVTYPE_GAMEPAD) && (devtype != DI8DEVTYPE_1STPERSON)) {
152 //printf("ignore device %s, type %x\n", instance->tszProductName, devtype);
153 return false;
154 }
155
156 hr = dinput->CreateDevice(instance->guidInstance, &joy->di_joy, NULL);
157
158 if (FAILED(hr)) {
159
160 //std::wcout << "failed to create device: " << instance->tszProductName << std::endl;
161 return false;
162 }
163
164 const GUID &guid = instance->guidProduct;
165 char uid[128];
166 sprintf(uid, "%08lx%04hx%04hx%02hhx%02hhx%02hhx%02hhx%02hhx%02hhx%02hhx%02hhx",
167 __builtin_bswap32(guid.Data1), guid.Data2, guid.Data3,
168 guid.Data4[0], guid.Data4[1], guid.Data4[2], guid.Data4[3],
169 guid.Data4[4], guid.Data4[5], guid.Data4[6], guid.Data4[7]);
170
171 id_to_change = joystick_count;
172
173 joy->di_joy->SetDataFormat(&c_dfDIJoystick2);
174 joy->di_joy->SetCooperativeLevel(*hWnd, DISCL_FOREGROUND);
175 joy->di_joy->EnumObjects(objectsCallback, this, NULL);
176 joy->joy_axis.sort();
177
178 joy->guid = instance->guidInstance;
179 input->joy_connection_changed(num, true, instance->tszProductName, uid);
180 joy->attached = true;
181 joy->id = num;
182 attached_joysticks[num] = true;
183 joy->confirmed = true;
184 joystick_count++;
185 return true;
186 }
187
setup_joystick_object(const DIDEVICEOBJECTINSTANCE * ob,int p_joy_id)188 void joystick_windows::setup_joystick_object(const DIDEVICEOBJECTINSTANCE *ob, int p_joy_id) {
189
190 if (ob->dwType & DIDFT_AXIS) {
191
192 HRESULT res;
193 DIPROPRANGE prop_range;
194 DIPROPDWORD dilong;
195 DWORD ofs;
196 if (ob->guidType == GUID_XAxis)
197 ofs = DIJOFS_X;
198 else if (ob->guidType == GUID_YAxis)
199 ofs = DIJOFS_Y;
200 else if (ob->guidType == GUID_ZAxis)
201 ofs = DIJOFS_Z;
202 else if (ob->guidType == GUID_RxAxis)
203 ofs = DIJOFS_RX;
204 else if (ob->guidType == GUID_RyAxis)
205 ofs = DIJOFS_RY;
206 else if (ob->guidType == GUID_RzAxis)
207 ofs = DIJOFS_RZ;
208 else if (ob->guidType == GUID_Slider)
209 ofs = DIJOFS_SLIDER(0);
210 else
211 return;
212 prop_range.diph.dwSize = sizeof(DIPROPRANGE);
213 prop_range.diph.dwHeaderSize = sizeof(DIPROPHEADER);
214 prop_range.diph.dwObj = ob->dwType;
215 prop_range.diph.dwHow = DIPH_BYID;
216 prop_range.lMin = -MAX_JOY_AXIS;
217 prop_range.lMax = +MAX_JOY_AXIS;
218
219 dinput_gamepad &joy = d_joysticks[p_joy_id];
220
221 res = IDirectInputDevice8_SetProperty(joy.di_joy, DIPROP_RANGE, &prop_range.diph);
222 if (FAILED(res))
223 return;
224
225 dilong.diph.dwSize = sizeof(dilong);
226 dilong.diph.dwHeaderSize = sizeof(dilong.diph);
227 dilong.diph.dwObj = ob->dwType;
228 dilong.diph.dwHow = DIPH_BYID;
229 dilong.dwData = 0;
230
231 res = IDirectInputDevice8_SetProperty(joy.di_joy, DIPROP_DEADZONE, &dilong.diph);
232 if (FAILED(res))
233 return;
234
235 joy.joy_axis.push_back(ofs);
236 }
237 }
238
enumCallback(const DIDEVICEINSTANCE * instance,void * pContext)239 BOOL CALLBACK joystick_windows::enumCallback(const DIDEVICEINSTANCE *instance, void *pContext) {
240
241 joystick_windows *self = (joystick_windows *)pContext;
242 if (self->is_xinput_device(&instance->guidProduct)) {
243 return DIENUM_CONTINUE;
244 }
245 self->setup_dinput_joystick(instance);
246 return DIENUM_CONTINUE;
247 }
248
objectsCallback(const DIDEVICEOBJECTINSTANCE * instance,void * context)249 BOOL CALLBACK joystick_windows::objectsCallback(const DIDEVICEOBJECTINSTANCE *instance, void *context) {
250
251 joystick_windows *self = (joystick_windows *)context;
252 self->setup_joystick_object(instance, self->id_to_change);
253
254 return DIENUM_CONTINUE;
255 }
256
close_joystick(int id)257 void joystick_windows::close_joystick(int id) {
258
259 if (id == -1) {
260
261 for (int i = 0; i < JOYSTICKS_MAX; i++) {
262
263 close_joystick(i);
264 }
265 return;
266 }
267
268 if (!d_joysticks[id].attached) return;
269
270 d_joysticks[id].di_joy->Unacquire();
271 d_joysticks[id].di_joy->Release();
272 d_joysticks[id].attached = false;
273 attached_joysticks[d_joysticks[id].id] = false;
274 d_joysticks[id].guid.Data1 = d_joysticks[id].guid.Data2 = d_joysticks[id].guid.Data3 = 0;
275 input->joy_connection_changed(d_joysticks[id].id, false, "");
276 joystick_count--;
277 }
278
probe_joysticks()279 void joystick_windows::probe_joysticks() {
280
281 DWORD dwResult;
282 for (DWORD i = 0; i < XUSER_MAX_COUNT; i++) {
283
284 ZeroMemory(&x_joysticks[i].state, sizeof(XINPUT_STATE));
285
286 dwResult = xinput_get_state(i, &x_joysticks[i].state);
287 if (dwResult == ERROR_SUCCESS) {
288
289 int id = input->get_unused_joy_id();
290 if (id != -1 && !x_joysticks[i].attached) {
291
292 x_joysticks[i].attached = true;
293 x_joysticks[i].id = id;
294 x_joysticks[i].ff_timestamp = 0;
295 x_joysticks[i].ff_end_timestamp = 0;
296 x_joysticks[i].vibrating = false;
297 attached_joysticks[id] = true;
298 input->joy_connection_changed(id, true, "XInput Gamepad", "__XINPUT_DEVICE__");
299 }
300 } else if (x_joysticks[i].attached) {
301
302 x_joysticks[i].attached = false;
303 attached_joysticks[x_joysticks[i].id] = false;
304 input->joy_connection_changed(x_joysticks[i].id, false, "");
305 }
306 }
307
308 for (int i = 0; i < joystick_count; i++) {
309
310 d_joysticks[i].confirmed = false;
311 }
312
313 dinput->EnumDevices(DI8DEVCLASS_GAMECTRL, enumCallback, this, DIEDFL_ATTACHEDONLY);
314
315 for (int i = 0; i < joystick_count; i++) {
316
317 if (!d_joysticks[i].confirmed) {
318
319 close_joystick(i);
320 }
321 }
322 }
323
process_joysticks(unsigned int p_last_id)324 unsigned int joystick_windows::process_joysticks(unsigned int p_last_id) {
325
326 HRESULT hr;
327
328 for (int i = 0; i < XUSER_MAX_COUNT; i++) {
329
330 xinput_gamepad &joy = x_joysticks[i];
331 if (!joy.attached) {
332 continue;
333 }
334 ZeroMemory(&joy.state, sizeof(XINPUT_STATE));
335
336 xinput_get_state(i, &joy.state);
337 if (joy.state.dwPacketNumber != joy.last_packet) {
338
339 int button_mask = XINPUT_GAMEPAD_DPAD_UP;
340 for (int i = 0; i <= 16; i++) {
341
342 p_last_id = input->joy_button(p_last_id, joy.id, i, joy.state.Gamepad.wButtons & button_mask);
343 button_mask = button_mask * 2;
344 }
345
346 p_last_id = input->joy_axis(p_last_id, joy.id, JOY_AXIS_0, axis_correct(joy.state.Gamepad.sThumbLX, true));
347 p_last_id = input->joy_axis(p_last_id, joy.id, JOY_AXIS_1, axis_correct(joy.state.Gamepad.sThumbLY, true, false, true));
348 p_last_id = input->joy_axis(p_last_id, joy.id, JOY_AXIS_2, axis_correct(joy.state.Gamepad.sThumbRX, true));
349 p_last_id = input->joy_axis(p_last_id, joy.id, JOY_AXIS_3, axis_correct(joy.state.Gamepad.sThumbRY, true, false, true));
350 p_last_id = input->joy_axis(p_last_id, joy.id, JOY_AXIS_4, axis_correct(joy.state.Gamepad.bLeftTrigger, true, true));
351 p_last_id = input->joy_axis(p_last_id, joy.id, JOY_AXIS_5, axis_correct(joy.state.Gamepad.bRightTrigger, true, true));
352 joy.last_packet = joy.state.dwPacketNumber;
353 }
354 uint64_t timestamp = input->get_joy_vibration_timestamp(joy.id);
355 if (timestamp > joy.ff_timestamp) {
356 Vector2 strength = input->get_joy_vibration_strength(joy.id);
357 float duration = input->get_joy_vibration_duration(joy.id);
358 if (strength.x == 0 && strength.y == 0) {
359 joystick_vibration_stop_xinput(i, timestamp);
360 } else {
361 joystick_vibration_start_xinput(i, strength.x, strength.y, duration, timestamp);
362 }
363 } else if (joy.vibrating && joy.ff_end_timestamp != 0) {
364 uint64_t current_time = OS::get_singleton()->get_ticks_usec();
365 if (current_time >= joy.ff_end_timestamp)
366 joystick_vibration_stop_xinput(i, current_time);
367 }
368 }
369
370 for (int i = 0; i < JOYSTICKS_MAX; i++) {
371
372 dinput_gamepad *joy = &d_joysticks[i];
373
374 if (!joy->attached)
375 continue;
376
377 DIJOYSTATE2 js;
378 hr = joy->di_joy->Poll();
379 if (hr == DIERR_INPUTLOST || hr == DIERR_NOTACQUIRED) {
380 IDirectInputDevice8_Acquire(joy->di_joy);
381 joy->di_joy->Poll();
382 }
383 if (FAILED(hr = joy->di_joy->GetDeviceState(sizeof(DIJOYSTATE2), &js))) {
384
385 //printf("failed to read joy #%d\n", i);
386 continue;
387 }
388
389 p_last_id = post_hat(p_last_id, joy->id, js.rgdwPOV[0]);
390
391 for (int j = 0; j < 128; j++) {
392
393 if (js.rgbButtons[j] & 0x80) {
394
395 if (!joy->last_buttons[j]) {
396
397 p_last_id = input->joy_button(p_last_id, joy->id, j, true);
398 joy->last_buttons[j] = true;
399 }
400 } else {
401
402 if (joy->last_buttons[j]) {
403
404 p_last_id = input->joy_button(p_last_id, joy->id, j, false);
405 joy->last_buttons[j] = false;
406 }
407 }
408 }
409
410 // on mingw, these constants are not constants
411 int count = 6;
412 int axes[] = { DIJOFS_X, DIJOFS_Y, DIJOFS_Z, DIJOFS_RX, DIJOFS_RY, DIJOFS_RZ };
413 int values[] = { js.lX, js.lY, js.lZ, js.lRx, js.lRy, js.lRz };
414
415 for (int j = 0; j < joy->joy_axis.size(); j++) {
416
417 for (int k = 0; k < count; k++) {
418 if (joy->joy_axis[j] == axes[k]) {
419 p_last_id = input->joy_axis(p_last_id, joy->id, j, axis_correct(values[k]));
420 break;
421 };
422 };
423 };
424 }
425 return p_last_id;
426 }
427
post_hat(unsigned int p_last_id,int p_device,DWORD p_dpad)428 unsigned int joystick_windows::post_hat(unsigned int p_last_id, int p_device, DWORD p_dpad) {
429
430 int dpad_val = 0;
431
432 if (p_dpad == -1) {
433 dpad_val = InputDefault::HAT_MASK_CENTER;
434 }
435 if (p_dpad == 0) {
436
437 dpad_val = InputDefault::HAT_MASK_UP;
438
439 } else if (p_dpad == 4500) {
440
441 dpad_val = (InputDefault::HAT_MASK_UP | InputDefault::HAT_MASK_RIGHT);
442
443 } else if (p_dpad == 9000) {
444
445 dpad_val = InputDefault::HAT_MASK_RIGHT;
446
447 } else if (p_dpad == 13500) {
448
449 dpad_val = (InputDefault::HAT_MASK_RIGHT | InputDefault::HAT_MASK_DOWN);
450
451 } else if (p_dpad == 18000) {
452
453 dpad_val = InputDefault::HAT_MASK_DOWN;
454
455 } else if (p_dpad == 22500) {
456
457 dpad_val = (InputDefault::HAT_MASK_DOWN | InputDefault::HAT_MASK_LEFT);
458
459 } else if (p_dpad == 27000) {
460
461 dpad_val = InputDefault::HAT_MASK_LEFT;
462
463 } else if (p_dpad == 31500) {
464
465 dpad_val = (InputDefault::HAT_MASK_LEFT | InputDefault::HAT_MASK_UP);
466 }
467 return input->joy_hat(p_last_id, p_device, dpad_val);
468 };
469
axis_correct(int p_val,bool p_xinput,bool p_trigger,bool p_negate) const470 InputDefault::JoyAxis joystick_windows::axis_correct(int p_val, bool p_xinput, bool p_trigger, bool p_negate) const {
471
472 InputDefault::JoyAxis jx;
473 if (Math::abs(p_val) < MIN_JOY_AXIS) {
474 jx.min = p_trigger ? 0 : -1;
475 jx.value = 0.0f;
476 return jx;
477 }
478 if (p_xinput) {
479
480 if (p_trigger) {
481 jx.min = 0;
482 jx.value = (float)p_val / MAX_TRIGGER;
483 return jx;
484 }
485 jx.min = -1;
486 if (p_val < 0) {
487 jx.value = (float)p_val / MAX_JOY_AXIS;
488 } else {
489 jx.value = (float)p_val / (MAX_JOY_AXIS - 1);
490 }
491 if (p_negate) {
492 jx.value = -jx.value;
493 }
494 return jx;
495 }
496 jx.min = -1;
497 jx.value = (float)p_val / MAX_JOY_AXIS;
498 return jx;
499 }
500
joystick_vibration_start_xinput(int p_device,float p_weak_magnitude,float p_strong_magnitude,float p_duration,uint64_t p_timestamp)501 void joystick_windows::joystick_vibration_start_xinput(int p_device, float p_weak_magnitude, float p_strong_magnitude, float p_duration, uint64_t p_timestamp) {
502 xinput_gamepad &joy = x_joysticks[p_device];
503 if (joy.attached) {
504 XINPUT_VIBRATION effect;
505 effect.wLeftMotorSpeed = (65535 * p_strong_magnitude);
506 effect.wRightMotorSpeed = (65535 * p_weak_magnitude);
507 if (xinput_set_state(p_device, &effect) == ERROR_SUCCESS) {
508 joy.ff_timestamp = p_timestamp;
509 joy.ff_end_timestamp = p_duration == 0 ? 0 : p_timestamp + (uint64_t)(p_duration * 1000000.0);
510 joy.vibrating = true;
511 }
512 }
513 }
514
joystick_vibration_stop_xinput(int p_device,uint64_t p_timestamp)515 void joystick_windows::joystick_vibration_stop_xinput(int p_device, uint64_t p_timestamp) {
516 xinput_gamepad &joy = x_joysticks[p_device];
517 if (joy.attached) {
518 XINPUT_VIBRATION effect;
519 effect.wLeftMotorSpeed = 0;
520 effect.wRightMotorSpeed = 0;
521 if (xinput_set_state(p_device, &effect) == ERROR_SUCCESS) {
522 joy.ff_timestamp = p_timestamp;
523 joy.vibrating = false;
524 }
525 }
526 }
527
load_xinput()528 void joystick_windows::load_xinput() {
529
530 xinput_get_state = &_xinput_get_state;
531 xinput_set_state = &_xinput_set_state;
532 xinput_dll = LoadLibrary("XInput1_4.dll");
533 if (!xinput_dll) {
534 xinput_dll = LoadLibrary("XInput1_3.dll");
535 if (!xinput_dll) {
536 xinput_dll = LoadLibrary("XInput9_1_0.dll");
537 }
538 }
539
540 if (!xinput_dll) {
541 if (OS::get_singleton()->is_stdout_verbose()) {
542 print_line("Could not find XInput, using DirectInput only");
543 }
544 return;
545 }
546
547 XInputGetState_t func = (XInputGetState_t)GetProcAddress((HMODULE)xinput_dll, "XInputGetState");
548 XInputSetState_t set_func = (XInputSetState_t)GetProcAddress((HMODULE)xinput_dll, "XInputSetState");
549 if (!func || !set_func) {
550 unload_xinput();
551 return;
552 }
553 xinput_get_state = func;
554 xinput_set_state = set_func;
555 }
556
unload_xinput()557 void joystick_windows::unload_xinput() {
558
559 if (xinput_dll) {
560
561 FreeLibrary((HMODULE)xinput_dll);
562 }
563 }
564