1 | /* |
2 | * fg_joystick.c |
3 | * |
4 | * Joystick handling code |
5 | * |
6 | * Copyright (c) 1999-2000 Pawel W. Olszta. All Rights Reserved. |
7 | * Written by Steve Baker, <sjbaker1@airmail.net> |
8 | * |
9 | * Permission is hereby granted, free of charge, to any person obtaining a |
10 | * copy of this software and associated documentation files (the "Software"), |
11 | * to deal in the Software without restriction, including without limitation |
12 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, |
13 | * and/or sell copies of the Software, and to permit persons to whom the |
14 | * Software is furnished to do so, subject to the following conditions: |
15 | * |
16 | * The above copyright notice and this permission notice shall be included |
17 | * in all copies or substantial portions of the Software. |
18 | * |
19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS |
20 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL |
22 | * PAWEL W. OLSZTA BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER |
23 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
24 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
25 | */ |
26 | |
27 | /* |
28 | * FreeBSD port by Stephen Montgomery-Smith <stephen@math.missouri.edu> |
29 | * |
30 | * Redone by John Fay 2/4/04 with another look from the PLIB "js" library. |
31 | * Many thanks for Steve Baker for permission to pull from that library. |
32 | */ |
33 | |
34 | #include <GL/freeglut.h> |
35 | #include "fg_internal.h" |
36 | #ifdef HAVE_SYS_PARAM_H |
37 | # include <sys/param.h> |
38 | #endif |
39 | |
40 | #define JS_TRUE 1 |
41 | #define JS_FALSE 0 |
42 | |
43 | /* BSD defines from "jsBSD.cxx" around lines 42-270 */ |
44 | |
45 | #if defined(__NetBSD__) || defined(__FreeBSD__) || defined(__FreeBSD_kernel__) |
46 | |
47 | # ifdef HAVE_USB_JS |
48 | # if defined(__NetBSD__) |
49 | /* XXX The below hack is done until freeglut's autoconf is updated. */ |
50 | # define HAVE_USBHID_H 1 |
51 | # ifdef HAVE_USBHID_H |
52 | # include <usbhid.h> |
53 | # else |
54 | # include <usb.h> |
55 | # endif |
56 | # elif defined(__FreeBSD__) || defined(__FreeBSD_kernel__) |
57 | # ifdef HAVE_USBHID_H |
58 | # include <usbhid.h> |
59 | # else |
60 | # include <libusbhid.h> |
61 | # endif |
62 | # endif |
63 | # include <legacy/dev/usb/usb.h> |
64 | # include <dev/usb/usbhid.h> |
65 | |
66 | /* Compatibility with older usb.h revisions */ |
67 | # if !defined(USB_MAX_DEVNAMES) && defined(MAXDEVNAMES) |
68 | # define USB_MAX_DEVNAMES MAXDEVNAMES |
69 | # endif |
70 | # endif |
71 | |
72 | static int hatmap_x[9] = { 0, 0, 1, 1, 1, 0, -1, -1, -1 }; |
73 | static int hatmap_y[9] = { 0, 1, 1, 0, -1, -1, -1, 0, 1 }; |
74 | struct os_specific_s { |
75 | char fname [128 ]; |
76 | int fd; |
77 | int is_analog; |
78 | /* The following structure members are specific to analog joysticks */ |
79 | struct joystick ajs; |
80 | # ifdef HAVE_USB_JS |
81 | /* The following structure members are specific to USB joysticks */ |
82 | struct hid_item *hids; |
83 | int hid_dlen; |
84 | int hid_offset; |
85 | char *hid_data_buf; |
86 | int axes_usage [ _JS_MAX_AXES ]; |
87 | # endif |
88 | /* We keep button and axes state ourselves, as they might not be updated |
89 | * on every read of a USB device |
90 | */ |
91 | int cache_buttons; |
92 | float cache_axes [ _JS_MAX_AXES ]; |
93 | }; |
94 | |
95 | /* Idents lower than USB_IDENT_OFFSET are for analog joysticks. */ |
96 | # define USB_IDENT_OFFSET 2 |
97 | |
98 | # define USBDEV "/dev/usb" |
99 | # define UHIDDEV "/dev/uhid" |
100 | # define AJSDEV "/dev/joy" |
101 | |
102 | # ifdef HAVE_USB_JS |
103 | /* |
104 | * fghJoystickFindUSBdev (and its helper, fghJoystickWalkUSBdev) try to locate |
105 | * the full name of a USB device. If /dev/usbN isn't readable, we punt and |
106 | * return the uhidN device name. We warn the user of this situation once. |
107 | */ |
108 | static char *fghJoystickWalkUSBdev(int f, char *dev, char *out, int outlen) |
109 | { |
110 | struct usb_device_info di; |
111 | int i, a; |
112 | char *cp; |
113 | |
114 | for (a = 1; a < USB_MAX_DEVICES; a++) { |
115 | di.udi_addr = a; |
116 | if (ioctl(f, USB_DEVICEINFO, &di) != 0) |
117 | return NULL; |
118 | for (i = 0; i < USB_MAX_DEVNAMES; i++) |
119 | if (di.udi_devnames[i][0] && |
120 | strcmp(di.udi_devnames[i], dev) == 0) { |
121 | cp = calloc( 1, strlen(di.udi_vendor) + strlen(di.udi_product) + 2); |
122 | strcpy(cp, di.udi_vendor); |
123 | strcat(cp, " " ); |
124 | strcat(cp, di.udi_product); |
125 | strncpy(out, cp, outlen - 1); |
126 | out[outlen - 1] = 0; |
127 | free( cp ); |
128 | return out; |
129 | } |
130 | } |
131 | return NULL; |
132 | } |
133 | |
134 | static int fghJoystickFindUSBdev(char *name, char *out, int outlen) |
135 | { |
136 | int i, f; |
137 | char buf[50]; |
138 | char *cp; |
139 | static int protection_warned = 0; |
140 | |
141 | for (i = 0; i < 16; i++) { |
142 | snprintf(buf, sizeof(buf), "%s%d" , USBDEV, i); |
143 | f = open(buf, O_RDONLY); |
144 | if (f >= 0) { |
145 | cp = fghJoystickWalkUSBdev(f, name, out, outlen); |
146 | close(f); |
147 | if (cp) |
148 | return 1; |
149 | } |
150 | else if (errno == EACCES) { |
151 | if (!protection_warned) { |
152 | fgWarning ( "Can't open %s for read!" , buf ); |
153 | protection_warned = 1; |
154 | } |
155 | } |
156 | } |
157 | return 0; |
158 | } |
159 | |
160 | static int fghJoystickInitializeHID(struct os_specific_s *os, |
161 | int *num_axes, int *num_buttons) |
162 | { |
163 | int size, is_joystick; |
164 | # ifdef HAVE_USBHID_H |
165 | int report_id = 0; |
166 | # endif |
167 | struct hid_data *d; |
168 | struct hid_item h; |
169 | report_desc_t rd; |
170 | |
171 | if ( ( rd = hid_get_report_desc( os->fd ) ) == 0 ) |
172 | { |
173 | fgWarning ( "error: %s: %s" , os->fname, strerror( errno ) ); |
174 | return FALSE; |
175 | } |
176 | |
177 | os->hids = NULL; |
178 | |
179 | # ifdef HAVE_USBHID_H |
180 | if( ioctl( os->fd, USB_GET_REPORT_ID, &report_id ) < 0) |
181 | { |
182 | /*** XXX {report_id} may not be the right variable? ***/ |
183 | fgWarning ( "error: %s%d: %s" , UHIDDEV, report_id, strerror( errno ) ); |
184 | return FALSE; |
185 | } |
186 | |
187 | size = hid_report_size( rd, hid_input, report_id ); |
188 | # else |
189 | size = hid_report_size( rd, 0, hid_input ); |
190 | # endif |
191 | os->hid_data_buf = calloc( 1, size ); |
192 | os->hid_dlen = size; |
193 | |
194 | is_joystick = 0; |
195 | # ifdef HAVE_USBHID_H |
196 | d = hid_start_parse( rd, 1 << hid_input, report_id ); |
197 | # else |
198 | d = hid_start_parse( rd, 1 << hid_input ); |
199 | # endif |
200 | while( hid_get_item( d, &h ) ) |
201 | { |
202 | int usage, page, interesting_hid; |
203 | |
204 | page = HID_PAGE( h.usage ); |
205 | usage = HID_USAGE( h.usage ); |
206 | |
207 | /* This test is somewhat too simplistic, but this is how MicroSoft |
208 | * does, so I guess it works for all joysticks/game pads. */ |
209 | is_joystick = is_joystick || |
210 | ( h.kind == hid_collection && |
211 | page == HUP_GENERIC_DESKTOP && |
212 | ( usage == HUG_JOYSTICK || usage == HUG_GAME_PAD ) ); |
213 | |
214 | if( h.kind != hid_input ) |
215 | continue; |
216 | |
217 | if( !is_joystick ) |
218 | continue; |
219 | |
220 | interesting_hid = TRUE; |
221 | if( page == HUP_GENERIC_DESKTOP ) |
222 | { |
223 | switch( usage ) |
224 | { |
225 | case HUG_X: |
226 | case HUG_RX: |
227 | case HUG_Y: |
228 | case HUG_RY: |
229 | case HUG_Z: |
230 | case HUG_RZ: |
231 | case HUG_SLIDER: |
232 | if( *num_axes < _JS_MAX_AXES ) |
233 | { |
234 | os->axes_usage[ *num_axes ] = usage; |
235 | ( *num_axes )++; |
236 | } |
237 | break; |
238 | case HUG_HAT_SWITCH: |
239 | /* Allocate two axes for a hat */ |
240 | if( *num_axes + 1 < _JS_MAX_AXES ) |
241 | { |
242 | os->axes_usage[ *num_axes ] = usage; |
243 | (*num_axes)++; |
244 | os->axes_usage[ *num_axes ] = usage; |
245 | (*num_axes)++; |
246 | } |
247 | break; |
248 | default: |
249 | interesting_hid = FALSE; |
250 | break; |
251 | } |
252 | } |
253 | else if( page == HUP_BUTTON ) |
254 | { |
255 | interesting_hid = ( usage > 0 ) && |
256 | ( usage <= _JS_MAX_BUTTONS ); |
257 | |
258 | if( interesting_hid && usage - 1 > *num_buttons ) |
259 | *num_buttons = usage - 1; |
260 | } |
261 | |
262 | if( interesting_hid ) |
263 | { |
264 | h.next = os->hids; |
265 | os->hids = calloc( 1, sizeof ( struct hid_item ) ); |
266 | *os->hids = h; |
267 | } |
268 | } |
269 | hid_end_parse( d ); |
270 | |
271 | return os->hids != NULL; |
272 | } |
273 | # endif |
274 | #endif |
275 | |
276 | /* |
277 | * Functions associated with the "jsJoystick" class in PLIB |
278 | */ |
279 | #if TARGET_HOST_MAC_OSX |
280 | #define K_NUM_DEVICES 32 |
281 | int numDevices; |
282 | io_object_t ioDevices[K_NUM_DEVICES]; |
283 | |
284 | static void fghJoystickFindDevices ( SFG_Joystick* joy, mach_port_t ); |
285 | static CFDictionaryRef fghJoystickGetCFProperties ( SFG_Joystick* joy, io_object_t ); |
286 | |
287 | static void fghJoystickEnumerateElements ( SFG_Joystick* joy, CFTypeRef element ); |
288 | /* callback for CFArrayApply */ |
289 | static void fghJoystickElementEnumerator ( SFG_Joystick* joy, void *element, void* vjs ); |
290 | |
291 | static void fghJoystickAddAxisElement ( SFG_Joystick* joy, CFDictionaryRef axis ); |
292 | static void fghJoystickAddButtonElement ( SFG_Joystick* joy, CFDictionaryRef button ); |
293 | static void fghJoystickAddHatElement ( SFG_Joystick* joy, CFDictionaryRef hat ); |
294 | #endif |
295 | |
296 | |
297 | /* External function declarations (mostly platform-specific) */ |
298 | extern void fgPlatformJoystickRawRead( SFG_Joystick* joy, int* buttons, float* axes ); |
299 | extern void fgPlatformJoystickOpen( SFG_Joystick* joy ); |
300 | extern void fgPlatformJoystickInit( SFG_Joystick *fgJoystick[], int ident ); |
301 | extern void fgPlatformJoystickClose ( int ident ); |
302 | |
303 | /* |
304 | * The static joystick structure pointer |
305 | */ |
306 | #define MAX_NUM_JOYSTICKS 2 |
307 | SFG_Joystick *fgJoystick [ MAX_NUM_JOYSTICKS ]; |
308 | |
309 | /* |
310 | * Read the raw joystick data |
311 | */ |
312 | static void fghJoystickRawRead( SFG_Joystick* joy, int* buttons, float* axes ) |
313 | { |
314 | int i; |
315 | |
316 | /* Defaults */ |
317 | if( buttons ) |
318 | *buttons = 0; |
319 | |
320 | if( axes ) |
321 | for( i = 0; i < joy->num_axes; i++ ) |
322 | axes[ i ] = 1500.0f; |
323 | |
324 | if( joy->error ) |
325 | return; |
326 | |
327 | fgPlatformJoystickRawRead ( joy, buttons, axes ); |
328 | } |
329 | |
330 | /* |
331 | * Correct the joystick axis data |
332 | */ |
333 | static float fghJoystickFudgeAxis( SFG_Joystick* joy, float value, int axis ) |
334 | { |
335 | if( value < joy->center[ axis ] ) |
336 | { |
337 | float xx = ( value - joy->center[ axis ] ) / ( joy->center[ axis ] - |
338 | joy->min[ axis ] ); |
339 | |
340 | if( xx < -joy->saturate[ axis ] ) |
341 | return -1.0f; |
342 | |
343 | if( xx > -joy->dead_band [ axis ] ) |
344 | return 0.0f; |
345 | |
346 | xx = ( xx + joy->dead_band[ axis ] ) / ( joy->saturate[ axis ] - |
347 | joy->dead_band[ axis ] ); |
348 | |
349 | return ( xx < -1.0f ) ? -1.0f : xx; |
350 | } |
351 | else |
352 | { |
353 | float xx = ( value - joy->center [ axis ] ) / ( joy->max[ axis ] - |
354 | joy->center[ axis ] ); |
355 | |
356 | if( xx > joy->saturate[ axis ] ) |
357 | return 1.0f; |
358 | |
359 | if( xx < joy->dead_band[ axis ] ) |
360 | return 0.0f; |
361 | |
362 | xx = ( xx - joy->dead_band[ axis ] ) / ( joy->saturate[ axis ] - |
363 | joy->dead_band[ axis ] ); |
364 | |
365 | return ( xx > 1.0f ) ? 1.0f : xx; |
366 | } |
367 | } |
368 | |
369 | /* |
370 | * Read the corrected joystick data |
371 | */ |
372 | static void fghJoystickRead( SFG_Joystick* joy, int* buttons, float* axes ) |
373 | { |
374 | float raw_axes[ _JS_MAX_AXES ]; |
375 | int i; |
376 | |
377 | if( joy->error ) |
378 | { |
379 | if( buttons ) |
380 | *buttons = 0; |
381 | |
382 | if( axes ) |
383 | for ( i=0; i<joy->num_axes; i++ ) |
384 | axes[ i ] = 0.0f; |
385 | } |
386 | |
387 | fghJoystickRawRead( joy, buttons, raw_axes ); |
388 | |
389 | if( axes ) |
390 | for( i=0; i<joy->num_axes; i++ ) |
391 | axes[ i ] = fghJoystickFudgeAxis( joy, raw_axes[ i ], i ); |
392 | } |
393 | |
394 | /* |
395 | * Happy happy happy joy joy joy (happy new year toudi :D) |
396 | */ |
397 | |
398 | |
399 | #if TARGET_HOST_MAC_OSX |
400 | /** open the IOKit connection, enumerate all the HID devices, add their |
401 | interface references to the static array. We then use the array index |
402 | as the device number when we come to open() the joystick. */ |
403 | static int fghJoystickFindDevices ( SFG_Joystick *joy, mach_port_t masterPort ) |
404 | { |
405 | CFMutableDictionaryRef hidMatch = NULL; |
406 | IOReturn rv = kIOReturnSuccess; |
407 | |
408 | io_iterator_t hidIterator; |
409 | io_object_t ioDev; |
410 | |
411 | /* build a dictionary matching HID devices */ |
412 | hidMatch = IOServiceMatching(kIOHIDDeviceKey); |
413 | |
414 | rv = IOServiceGetMatchingServices(masterPort, hidMatch, &hidIterator); |
415 | if (rv != kIOReturnSuccess || !hidIterator) { |
416 | fgWarning( "no joystick (HID) devices found" ); |
417 | return; |
418 | } |
419 | |
420 | /* iterate */ |
421 | while ((ioDev = IOIteratorNext(hidIterator))) { |
422 | /* filter out keyboard and mouse devices */ |
423 | CFDictionaryRef properties = getCFProperties(ioDev); |
424 | long usage, page; |
425 | |
426 | CFTypeRef refPage = CFDictionaryGetValue (properties, CFSTR(kIOHIDPrimaryUsagePageKey)); |
427 | CFTypeRef refUsage = CFDictionaryGetValue (properties, CFSTR(kIOHIDPrimaryUsageKey)); |
428 | CFNumberGetValue((CFNumberRef) refUsage, kCFNumberLongType, &usage); |
429 | CFNumberGetValue((CFNumberRef) refPage, kCFNumberLongType, &page); |
430 | |
431 | /* keep only joystick devices */ |
432 | if ( ( page == kHIDPage_GenericDesktop ) && ( |
433 | (usage == kHIDUsage_GD_Joystick) |
434 | || (usage == kHIDUsage_GD_GamePad) |
435 | || (usage == kHIDUsage_GD_MultiAxisController) |
436 | || (usage == kHIDUsage_GD_Hatswitch) /* last two necessary ? */ |
437 | /* add it to the array */ |
438 | ioDevices[numDevices++] = ioDev; |
439 | } |
440 | |
441 | IOObjectRelease(hidIterator); |
442 | } |
443 | |
444 | static CFDictionaryRef fghJoystickGetCFProperties ( SFG_Joystick *joy, io_object_t ioDev ) |
445 | { |
446 | IOReturn rv; |
447 | CFMutableDictionaryRef cfProperties; |
448 | |
449 | #if 0 |
450 | /* comment copied from darwin/SDL_sysjoystick.c */ |
451 | /* Mac OS X currently is not mirroring all USB properties to HID page so need to look at USB device page also |
452 | * get dictionary for usb properties: step up two levels and get CF dictionary for USB properties |
453 | */ |
454 | |
455 | io_registry_entry_t parent1, parent2; |
456 | |
457 | rv = IORegistryEntryGetParentEntry (ioDev, kIOServicePlane, &parent1); |
458 | if (rv != kIOReturnSuccess) { |
459 | fgWarning ( "error getting device entry parent" ); |
460 | return NULL; |
461 | } |
462 | |
463 | rv = IORegistryEntryGetParentEntry (parent1, kIOServicePlane, &parent2); |
464 | if (rv != kIOReturnSuccess) { |
465 | fgWarning ( "error getting device entry parent 2" ); |
466 | return NULL; |
467 | } |
468 | #endif |
469 | |
470 | rv = IORegistryEntryCreateCFProperties( ioDev /*parent2*/, |
471 | &cfProperties, kCFAllocatorDefault, kNilOptions); |
472 | if (rv != kIOReturnSuccess || !cfProperties) { |
473 | fgWarning ( "error getting device properties" ); |
474 | return NULL; |
475 | } |
476 | |
477 | return cfProperties; |
478 | } |
479 | |
480 | static void fghJoystickElementEnumerator ( SFG_Joystick *joy, void *element, void* vjs ) |
481 | { |
482 | if (CFGetTypeID((CFTypeRef) element) != CFDictionaryGetTypeID()) { |
483 | fgError ( "%s" , "element enumerator passed non-dictionary value" ); |
484 | return; |
485 | } |
486 | |
487 | static_cast<jsJoystick*>(vjs)->parseElement ( (CFDictionaryRef) element ); |
488 | } |
489 | |
490 | /** element enumerator function : pass NULL for top-level*/ |
491 | static void fghJoystickEnumerateElements ( SFG_Joystick *joy, CFTypeRef element ) |
492 | { |
493 | FREEGLUT_INTERNAL_ERROR_EXIT( (CFGetTypeID(element) == CFArrayGetTypeID(), |
494 | "Joystick element type mismatch" , |
495 | "fghJoystickEnumerateElements" ); |
496 | |
497 | CFRange range = {0, CFArrayGetCount ((CFArrayRef)element)}; |
498 | CFArrayApplyFunction((CFArrayRef) element, range, |
499 | &fghJoystickElementEnumerator, joy ); |
500 | } |
501 | |
502 | static void fghJoystickAddAxisElement ( SFG_Joystick *joy, CFDictionaryRef axis ) |
503 | { |
504 | long cookie, lmin, lmax; |
505 | int index = joy->num_axes++; |
506 | |
507 | CFNumberGetValue ((CFNumberRef) |
508 | CFDictionaryGetValue ( axis, CFSTR(kIOHIDElementCookieKey) ), |
509 | kCFNumberLongType, &cookie); |
510 | |
511 | joy->pJoystick.axisCookies[index] = (IOHIDElementCookie) cookie; |
512 | |
513 | CFNumberGetValue ((CFNumberRef) |
514 | CFDictionaryGetValue ( axis, CFSTR(kIOHIDElementMinKey) ), |
515 | kCFNumberLongType, &lmin); |
516 | |
517 | CFNumberGetValue ((CFNumberRef) |
518 | CFDictionaryGetValue ( axis, CFSTR(kIOHIDElementMaxKey) ), |
519 | kCFNumberLongType, &lmax); |
520 | |
521 | joy->min[index] = lmin; |
522 | joy->max[index] = lmax; |
523 | joy->dead_band[index] = 0.0; |
524 | joy->saturate[index] = 1.0; |
525 | joy->center[index] = (lmax + lmin) * 0.5; |
526 | } |
527 | |
528 | static void fghJoystickAddButtonElement ( SFG_Joystick *joy, CFDictionaryRef button ) |
529 | { |
530 | long cookie; |
531 | CFNumberGetValue ((CFNumberRef) |
532 | CFDictionaryGetValue ( button, CFSTR(kIOHIDElementCookieKey) ), |
533 | kCFNumberLongType, &cookie); |
534 | |
535 | joy->pJoystick.buttonCookies[num_buttons++] = (IOHIDElementCookie) cookie; |
536 | /* anything else for buttons? */ |
537 | } |
538 | |
539 | static void fghJoystickAddHatElement ( SFG_Joystick *joy, CFDictionaryRef button ) |
540 | { |
541 | /* hatCookies[num_hats++] = (IOHIDElementCookie) cookie; */ |
542 | /* do we map hats to axes or buttons? */ |
543 | } |
544 | #endif |
545 | |
546 | /* |
547 | * Platform-Specific Code |
548 | */ |
549 | |
550 | #if TARGET_HOST_MACINTOSH |
551 | void fgPlatformJoystickRawRead( SFG_Joystick* joy, int* buttons, float* axes ) |
552 | { |
553 | int i; |
554 | |
555 | if ( buttons ) |
556 | { |
557 | *buttons = 0; |
558 | |
559 | for ( i = 0; i < joy->num_buttons; i++ ) |
560 | { |
561 | UInt32 state; |
562 | int err = ISpElement_GetSimpleState ( joy->pJoystick.isp_elem [ i + ISP_NUM_AXIS ], &state); |
563 | ISP_CHECK_ERR(err) |
564 | |
565 | *buttons |= state << i; |
566 | } |
567 | } |
568 | |
569 | if ( axes ) |
570 | { |
571 | for ( i = 0; i < joy->num_axes; i++ ) |
572 | { |
573 | UInt32 state; |
574 | int err = ISpElement_GetSimpleState ( joy->pJoystick.isp_elem [ i ], &state ); |
575 | ISP_CHECK_ERR(err) |
576 | |
577 | axes [i] = (float) state; |
578 | } |
579 | } |
580 | } |
581 | |
582 | |
583 | void fgPlatformJoystickOpen( SFG_Joystick* joy ) |
584 | { |
585 | int i = 0; |
586 | OSStatus err; |
587 | |
588 | /* XXX FIXME: get joystick name in Mac */ |
589 | |
590 | err = ISpStartup( ); |
591 | |
592 | if( err == noErr ) |
593 | { |
594 | #define ISP_CHECK_ERR(x) if( x != noErr ) { joy->error = GL_TRUE; return; } |
595 | |
596 | joy->error = GL_TRUE; |
597 | |
598 | /* initialize the needs structure */ |
599 | ISpNeed temp_isp_needs[ ISP_NUM_NEEDS ] = |
600 | { |
601 | { "\pX-Axis" , 128, 0, 0, kISpElementKind_Axis, kISpElementLabel_None, 0, 0, 0, 0 }, |
602 | { "\pY-Axis" , 128, 0, 0, kISpElementKind_Axis, kISpElementLabel_None, 0, 0, 0, 0 }, |
603 | { "\pZ-Axis" , 128, 0, 0, kISpElementKind_Axis, kISpElementLabel_None, 0, 0, 0, 0 }, |
604 | { "\pR-Axis" , 128, 0, 0, kISpElementKind_Axis, kISpElementLabel_None, 0, 0, 0, 0 }, |
605 | { "\pAxis 4" , 128, 0, 0, kISpElementKind_Axis, kISpElementLabel_None, 0, 0, 0, 0 }, |
606 | { "\pAxis 5" , 128, 0, 0, kISpElementKind_Axis, kISpElementLabel_None, 0, 0, 0, 0 }, |
607 | { "\pAxis 6" , 128, 0, 0, kISpElementKind_Axis, kISpElementLabel_None, 0, 0, 0, 0 }, |
608 | { "\pAxis 7" , 128, 0, 0, kISpElementKind_Axis, kISpElementLabel_None, 0, 0, 0, 0 }, |
609 | { "\pAxis 8" , 128, 0, 0, kISpElementKind_Axis, kISpElementLabel_None, 0, 0, 0, 0 }, |
610 | |
611 | { "\pButton 0" , 128, 0, 0, kISpElementKind_Button, kISpElementLabel_Btn_Select, 0, 0, 0, 0 }, |
612 | { "\pButton 1" , 128, 0, 0, kISpElementKind_Button, kISpElementLabel_Btn_Select, 0, 0, 0, 0 }, |
613 | { "\pButton 2" , 128, 0, 0, kISpElementKind_Button, kISpElementLabel_Btn_Select, 0, 0, 0, 0 }, |
614 | { "\pButton 3" , 128, 0, 0, kISpElementKind_Button, kISpElementLabel_Btn_Select, 0, 0, 0, 0 }, |
615 | { "\pButton 4" , 128, 0, 0, kISpElementKind_Button, kISpElementLabel_Btn_Select, 0, 0, 0, 0 }, |
616 | { "\pButton 5" , 128, 0, 0, kISpElementKind_Button, kISpElementLabel_Btn_Select, 0, 0, 0, 0 }, |
617 | { "\pButton 6" , 128, 0, 0, kISpElementKind_Button, kISpElementLabel_Btn_Select, 0, 0, 0, 0 }, |
618 | { "\pButton 7" , 128, 0, 0, kISpElementKind_Button, kISpElementLabel_Btn_Select, 0, 0, 0, 0 }, |
619 | { "\pButton 8" , 128, 0, 0, kISpElementKind_Button, kISpElementLabel_Btn_Select, 0, 0, 0, 0 }, |
620 | { "\pButton 9" , 128, 0, 0, kISpElementKind_Button, kISpElementLabel_Btn_Select, 0, 0, 0, 0 }, |
621 | { "\pButton 10" , 128, 0, 0, kISpElementKind_Button, kISpElementLabel_Btn_Select, 0, 0, 0, 0 }, |
622 | { "\pButton 11" , 128, 0, 0, kISpElementKind_Button, kISpElementLabel_Btn_Select, 0, 0, 0, 0 }, |
623 | { "\pButton 12" , 128, 0, 0, kISpElementKind_Button, kISpElementLabel_Btn_Select, 0, 0, 0, 0 }, |
624 | { "\pButton 13" , 128, 0, 0, kISpElementKind_Button, kISpElementLabel_Btn_Select, 0, 0, 0, 0 }, |
625 | { "\pButton 14" , 128, 0, 0, kISpElementKind_Button, kISpElementLabel_Btn_Select, 0, 0, 0, 0 }, |
626 | { "\pButton 15" , 128, 0, 0, kISpElementKind_Button, kISpElementLabel_Btn_Select, 0, 0, 0, 0 }, |
627 | { "\pButton 16" , 128, 0, 0, kISpElementKind_Button, kISpElementLabel_Btn_Select, 0, 0, 0, 0 }, |
628 | { "\pButton 17" , 128, 0, 0, kISpElementKind_Button, kISpElementLabel_Btn_Select, 0, 0, 0, 0 }, |
629 | { "\pButton 18" , 128, 0, 0, kISpElementKind_Button, kISpElementLabel_Btn_Select, 0, 0, 0, 0 }, |
630 | { "\pButton 19" , 128, 0, 0, kISpElementKind_Button, kISpElementLabel_Btn_Select, 0, 0, 0, 0 }, |
631 | { "\pButton 20" , 128, 0, 0, kISpElementKind_Button, kISpElementLabel_Btn_Select, 0, 0, 0, 0 }, |
632 | { "\pButton 21" , 128, 0, 0, kISpElementKind_Button, kISpElementLabel_Btn_Select, 0, 0, 0, 0 }, |
633 | { "\pButton 22" , 128, 0, 0, kISpElementKind_Button, kISpElementLabel_Btn_Select, 0, 0, 0, 0 }, |
634 | { "\pButton 23" , 128, 0, 0, kISpElementKind_Button, kISpElementLabel_Btn_Select, 0, 0, 0, 0 }, |
635 | { "\pButton 24" , 128, 0, 0, kISpElementKind_Button, kISpElementLabel_Btn_Select, 0, 0, 0, 0 }, |
636 | { "\pButton 25" , 128, 0, 0, kISpElementKind_Button, kISpElementLabel_Btn_Select, 0, 0, 0, 0 }, |
637 | { "\pButton 26" , 128, 0, 0, kISpElementKind_Button, kISpElementLabel_Btn_Select, 0, 0, 0, 0 }, |
638 | { "\pButton 27" , 128, 0, 0, kISpElementKind_Button, kISpElementLabel_Btn_Select, 0, 0, 0, 0 }, |
639 | { "\pButton 28" , 128, 0, 0, kISpElementKind_Button, kISpElementLabel_Btn_Select, 0, 0, 0, 0 }, |
640 | { "\pButton 29" , 128, 0, 0, kISpElementKind_Button, kISpElementLabel_Btn_Select, 0, 0, 0, 0 }, |
641 | { "\pButton 30" , 128, 0, 0, kISpElementKind_Button, kISpElementLabel_Btn_Select, 0, 0, 0, 0 }, |
642 | { "\pButton 31" , 128, 0, 0, kISpElementKind_Button, kISpElementLabel_Btn_Select, 0, 0, 0, 0 }, |
643 | }; |
644 | |
645 | memcpy( joy->pJoystick.isp_needs, temp_isp_needs, sizeof (temp_isp_needs ) ); |
646 | |
647 | |
648 | /* next two calls allow keyboard and mouse to emulate other input |
649 | * devices (gamepads, joysticks, etc) |
650 | */ |
651 | /* |
652 | err = ISpDevices_ActivateClass ( kISpDeviceClass_Keyboard ); |
653 | ISP_CHECK_ERR(err) |
654 | |
655 | |
656 | err = ISpDevices_ActivateClass ( kISpDeviceClass_Mouse ); |
657 | ISP_CHECK_ERR(err) |
658 | */ |
659 | |
660 | err = ISpElement_NewVirtualFromNeeds( ISP_NUM_NEEDS, |
661 | joy->pJoystick.isp_needs, joy->pJoystick.isp_elem, |
662 | 0 ); |
663 | ISP_CHECK_ERR( err ) |
664 | |
665 | err = ISpInit( ISP_NUM_NEEDS, joy->pJoystick.isp_needs, joy->pJoystick.isp_elem, |
666 | 'freeglut', nil, 0, 128, 0 ); |
667 | ISP_CHECK_ERR( err ) |
668 | |
669 | joy->num_buttons = ISP_NUM_NEEDS - ISP_NUM_AXIS; |
670 | joy->num_axes = ISP_NUM_AXIS; |
671 | |
672 | for( i = 0; i < joy->num_axes; i++ ) |
673 | { |
674 | joy->dead_band[ i ] = 0; |
675 | joy->saturate [ i ] = 1; |
676 | joy->center [ i ] = kISpAxisMiddle; |
677 | joy->max [ i ] = kISpAxisMaximum; |
678 | joy->min [ i ] = kISpAxisMinimum; |
679 | } |
680 | |
681 | joy->error = GL_FALSE; |
682 | } |
683 | else |
684 | joy->num_buttons = joy->num_axes = 0; |
685 | } |
686 | |
687 | |
688 | void fgPlatformJoystickInit( SFG_Joystick *fgJoystick[], int ident ) |
689 | { |
690 | fgJoystick[ ident ]->id = ident; |
691 | snprintf( fgJoystick[ ident ]->pJoystick.fname, sizeof(fgJoystick[ ident ]->pJoystick.fname), "/dev/js%d" , ident ); /* FIXME */ |
692 | fgJoystick[ ident ]->error = GL_FALSE; |
693 | } |
694 | |
695 | |
696 | void fgPlatformJoystickClose ( int ident ) |
697 | { |
698 | ISpSuspend( ); |
699 | ISpStop( ); |
700 | ISpShutdown( ); |
701 | } |
702 | #endif |
703 | |
704 | #if TARGET_HOST_MAC_OSX |
705 | void fgPlatformJoystickRawRead( SFG_Joystick* joy, int* buttons, float* axes ) |
706 | { |
707 | int i; |
708 | |
709 | if ( buttons != NULL ) |
710 | { |
711 | *buttons = 0; |
712 | |
713 | for ( i = 0; i < joy->num_buttons; i++ ) |
714 | { |
715 | IOHIDEventStruct hidEvent; |
716 | (*(joy->pJoystick.hidDev))->getElementValue ( joy->pJoystick.hidDev, joy->pJoystick.buttonCookies[i], &hidEvent ); |
717 | if ( hidEvent.value ) |
718 | *buttons |= 1 << i; |
719 | } |
720 | } |
721 | |
722 | if ( axes != NULL ) |
723 | { |
724 | for ( i = 0; i < joy->num_axes; i++ ) |
725 | { |
726 | IOHIDEventStruct hidEvent; |
727 | (*(joy->pJoystick.hidDev))->getElementValue ( joy->pJoystick.hidDev, joy->pJoystick.axisCookies[i], &hidEvent ); |
728 | axes[i] = hidEvent.value; |
729 | } |
730 | } |
731 | } |
732 | |
733 | |
734 | void fgPlatformJoystickOpen( SFG_Joystick* joy ) |
735 | { |
736 | IOReturn rv; |
737 | SInt32 score; |
738 | IOCFPlugInInterface **plugin; |
739 | |
740 | HRESULT pluginResult; |
741 | |
742 | CFDictionaryRef props; |
743 | CFTypeRef topLevelElement; |
744 | |
745 | if( joy->id >= numDevices ) |
746 | { |
747 | fgWarning( "device index out of range in fgJoystickOpen()" ); |
748 | return; |
749 | } |
750 | |
751 | /* create device interface */ |
752 | rv = IOCreatePlugInInterfaceForService( ioDevices[ joy->id ], |
753 | kIOHIDDeviceUserClientTypeID, |
754 | kIOCFPlugInInterfaceID, |
755 | &plugin, &score ); |
756 | |
757 | if( rv != kIOReturnSuccess ) |
758 | { |
759 | fgWarning( "error creating plugin for io device" ); |
760 | return; |
761 | } |
762 | |
763 | pluginResult = ( *plugin )->QueryInterface( |
764 | plugin, |
765 | CFUUIDGetUUIDBytes(kIOHIDDeviceInterfaceID), |
766 | &( LPVOID )joy->pJoystick.hidDev |
767 | ); |
768 | |
769 | if( pluginResult != S_OK ) |
770 | fgWarning ( "QI-ing IO plugin to HID Device interface failed" ); |
771 | |
772 | ( *plugin )->Release( plugin ); /* don't leak a ref */ |
773 | if( joy->pJoystick.hidDev == NULL ) |
774 | return; |
775 | |
776 | /* store the interface in this instance */ |
777 | rv = ( *( joy->pJoystick.hidDev ) )->open( joy->pJoystick.hidDev, 0 ); |
778 | if( rv != kIOReturnSuccess ) |
779 | { |
780 | fgWarning( "error opening device interface" ); |
781 | return; |
782 | } |
783 | |
784 | props = getCFProperties( ioDevices[ joy->id ] ); |
785 | |
786 | /* recursively enumerate all the bits */ |
787 | CFTypeRef topLevelElement = |
788 | CFDictionaryGetValue( props, CFSTR( kIOHIDElementKey ) ); |
789 | enumerateElements( topLevelElement ); |
790 | |
791 | CFRelease( props ); |
792 | } |
793 | |
794 | |
795 | void fgPlatformJoystickInit( SFG_Joystick *fgJoystick[], int ident ) |
796 | { |
797 | fgJoystick[ ident ]->id = ident; |
798 | fgJoystick[ ident ]->error = GL_FALSE; |
799 | fgJoystick[ ident ]->num_axes = 0; |
800 | fgJoystick[ ident ]->num_buttons = 0; |
801 | |
802 | if( numDevices < 0 ) |
803 | { |
804 | /* do first-time init (since we can't over-ride jsInit, hmm */ |
805 | numDevices = 0; |
806 | |
807 | mach_port_t masterPort; |
808 | IOReturn rv = IOMasterPort( bootstrap_port, &masterPort ); |
809 | if( rv != kIOReturnSuccess ) |
810 | { |
811 | fgWarning( "error getting master Mach port" ); |
812 | return; |
813 | } |
814 | fghJoystickFindDevices( masterPort ); |
815 | } |
816 | |
817 | if ( ident >= numDevices ) |
818 | { |
819 | fgJoystick[ ident ]->error = GL_TRUE; |
820 | return; |
821 | } |
822 | |
823 | /* get the name now too */ |
824 | CFDictionaryRef properties = getCFProperties( ioDevices[ ident ] ); |
825 | CFTypeRef ref = CFDictionaryGetValue( properties, |
826 | CFSTR( kIOHIDProductKey ) ); |
827 | if (!ref) |
828 | ref = CFDictionaryGetValue(properties, CFSTR( "USB Product Name" ) ); |
829 | |
830 | if( !ref || |
831 | !CFStringGetCString( ( CFStringRef )ref, name, 128, |
832 | CFStringGetSystemEncoding( ) ) ) |
833 | { |
834 | fgWarning( "error getting device name" ); |
835 | name[ 0 ] = '\0'; |
836 | } |
837 | } |
838 | |
839 | |
840 | void fgPlatformJoystickClose ( int ident ) |
841 | { |
842 | ( *( fgJoystick[ ident ]->pJoystick.hidDev ) )-> |
843 | close( fgJoystick[ ident ]->pJoystick.hidDev ); |
844 | } |
845 | #endif |
846 | |
847 | |
848 | |
849 | |
850 | static void fghJoystickOpen( SFG_Joystick* joy ) |
851 | { |
852 | /* |
853 | * Default values (for no joystick -- each conditional will reset the |
854 | * error flag) |
855 | */ |
856 | joy->error = TRUE; |
857 | joy->num_axes = joy->num_buttons = 0; |
858 | joy->name[ 0 ] = '\0'; |
859 | |
860 | fgPlatformJoystickOpen ( joy ); |
861 | |
862 | } |
863 | |
864 | /* |
865 | * This function replaces the constructor method in the JS library. |
866 | */ |
867 | static void fghJoystickInit( int ident ) |
868 | { |
869 | if( ident >= MAX_NUM_JOYSTICKS ) |
870 | fgError( "Too large a joystick number: %d" , ident ); |
871 | |
872 | if( fgJoystick[ ident ] ) |
873 | fgError( "illegal attempt to initialize joystick device again" ); |
874 | |
875 | fgJoystick[ ident ] = |
876 | ( SFG_Joystick * )calloc( sizeof( SFG_Joystick ), 1 ); |
877 | |
878 | /* Set defaults */ |
879 | fgJoystick[ ident ]->num_axes = fgJoystick[ ident ]->num_buttons = 0; |
880 | fgJoystick[ ident ]->error = GL_TRUE; |
881 | |
882 | fgPlatformJoystickInit( fgJoystick, ident ); |
883 | |
884 | fghJoystickOpen( fgJoystick[ ident ] ); |
885 | } |
886 | |
887 | /* |
888 | * Try initializing all the joysticks (well, both of them) |
889 | */ |
890 | void fgInitialiseJoysticks ( void ) |
891 | { |
892 | if( !fgState.JoysticksInitialised ) |
893 | { |
894 | int ident ; |
895 | for ( ident = 0; ident < MAX_NUM_JOYSTICKS; ident++ ) |
896 | fghJoystickInit( ident ); |
897 | |
898 | fgState.JoysticksInitialised = GL_TRUE; |
899 | } |
900 | } |
901 | |
902 | |
903 | void fgJoystickClose( void ) |
904 | { |
905 | int ident ; |
906 | for( ident = 0; ident < MAX_NUM_JOYSTICKS; ident++ ) |
907 | { |
908 | if( fgJoystick[ ident ] ) |
909 | { |
910 | fgPlatformJoystickClose ( ident ); |
911 | |
912 | free( fgJoystick[ ident ] ); |
913 | fgJoystick[ ident ] = NULL; |
914 | /* show joystick has been deinitialized */ |
915 | } |
916 | } |
917 | } |
918 | |
919 | /* |
920 | * Polls the joystick and executes the joystick callback hooked to the |
921 | * window specified in the function's parameter: |
922 | */ |
923 | void fgJoystickPollWindow( SFG_Window* window ) |
924 | { |
925 | float axes[ _JS_MAX_AXES ]; |
926 | int buttons; |
927 | int ident; |
928 | |
929 | freeglut_return_if_fail( window ); |
930 | freeglut_return_if_fail( FETCH_WCB( *window, Joystick ) ); |
931 | |
932 | for( ident = 0; ident < MAX_NUM_JOYSTICKS; ident++ ) |
933 | { |
934 | if( fgJoystick[ident] ) |
935 | { |
936 | fghJoystickRead( fgJoystick[ident], &buttons, axes ); |
937 | |
938 | if( !fgJoystick[ident]->error ) |
939 | INVOKE_WCB( *window, Joystick, |
940 | ( buttons, |
941 | (int) ( axes[ 0 ] * 1000.0f ), |
942 | (int) ( axes[ 1 ] * 1000.0f ), |
943 | (int) ( axes[ 2 ] * 1000.0f ) ) |
944 | ); |
945 | } |
946 | } |
947 | } |
948 | |
949 | /* |
950 | * Implementation for glutDeviceGet(GLUT_HAS_JOYSTICK) |
951 | */ |
952 | int fgJoystickDetect( void ) |
953 | { |
954 | int ident; |
955 | |
956 | fgInitialiseJoysticks (); |
957 | |
958 | if ( !fgState.JoysticksInitialised ) |
959 | return 0; |
960 | |
961 | for( ident=0; ident<MAX_NUM_JOYSTICKS; ident++ ) |
962 | if( fgJoystick[ident] && !fgJoystick[ident]->error ) |
963 | return 1; |
964 | |
965 | return 0; |
966 | } |
967 | |
968 | /* |
969 | * Joystick information, setup and execution functions |
970 | */ |
971 | |
972 | /* |
973 | * Forces the joystick callback to be executed |
974 | */ |
975 | void FGAPIENTRY glutForceJoystickFunc( void ) |
976 | { |
977 | FREEGLUT_EXIT_IF_NOT_INITIALISED ( "glutForceJoystickFunc" ); |
978 | #if !defined(_WIN32_WCE) |
979 | freeglut_return_if_fail( fgStructure.CurrentWindow != NULL ); |
980 | freeglut_return_if_fail( FETCH_WCB( *( fgStructure.CurrentWindow ), Joystick ) ); |
981 | fgJoystickPollWindow( fgStructure.CurrentWindow ); |
982 | #endif /* !defined(_WIN32_WCE) */ |
983 | } |
984 | int glutJoystickGetNumAxes( int ident ) |
985 | { |
986 | FREEGLUT_EXIT_IF_NOT_INITIALISED ( "glutJoystickGetNumAxes" ); |
987 | return fgJoystick[ ident ]->num_axes; |
988 | } |
989 | int glutJoystickGetNumButtons( int ident ) |
990 | { |
991 | FREEGLUT_EXIT_IF_NOT_INITIALISED ( "glutJoystickGetNumButtons" ); |
992 | return fgJoystick[ ident ]->num_buttons; |
993 | } |
994 | int glutJoystickNotWorking( int ident ) |
995 | { |
996 | FREEGLUT_EXIT_IF_NOT_INITIALISED ( "glutJoystickNotWorking" ); |
997 | return fgJoystick[ ident ]->error; |
998 | } |
999 | |
1000 | float glutJoystickGetDeadBand( int ident, int axis ) |
1001 | { |
1002 | FREEGLUT_EXIT_IF_NOT_INITIALISED ( "glutJoystickGetDeadBand" ); |
1003 | return fgJoystick[ ident ]->dead_band [ axis ]; |
1004 | } |
1005 | void glutJoystickSetDeadBand( int ident, int axis, float db ) |
1006 | { |
1007 | FREEGLUT_EXIT_IF_NOT_INITIALISED ( "glutJoystickSetDeadBand" ); |
1008 | fgJoystick[ ident ]->dead_band[ axis ] = db; |
1009 | } |
1010 | |
1011 | float glutJoystickGetSaturation( int ident, int axis ) |
1012 | { |
1013 | FREEGLUT_EXIT_IF_NOT_INITIALISED ( "glutJoystickGetSaturation" ); |
1014 | return fgJoystick[ ident ]->saturate[ axis ]; |
1015 | } |
1016 | void glutJoystickSetSaturation( int ident, int axis, float st ) |
1017 | { |
1018 | FREEGLUT_EXIT_IF_NOT_INITIALISED ( "glutJoystickSetSaturation" ); |
1019 | fgJoystick[ ident ]->saturate [ axis ] = st; |
1020 | } |
1021 | |
1022 | void glutJoystickSetMinRange( int ident, float *axes ) |
1023 | { |
1024 | FREEGLUT_EXIT_IF_NOT_INITIALISED ( "glutJoystickSetMinRange" ); |
1025 | memcpy( fgJoystick[ ident ]->min, axes, |
1026 | fgJoystick[ ident ]->num_axes * sizeof( float ) ); |
1027 | } |
1028 | void glutJoystickSetMaxRange( int ident, float *axes ) |
1029 | { |
1030 | FREEGLUT_EXIT_IF_NOT_INITIALISED ( "glutJoystickSetMaxRange" ); |
1031 | memcpy( fgJoystick[ ident ]->max, axes, |
1032 | fgJoystick[ ident ]->num_axes * sizeof( float ) ); |
1033 | } |
1034 | void glutJoystickSetCenter( int ident, float *axes ) |
1035 | { |
1036 | FREEGLUT_EXIT_IF_NOT_INITIALISED ( "glutJoystickSetCenter" ); |
1037 | memcpy( fgJoystick[ ident ]->center, axes, |
1038 | fgJoystick[ ident ]->num_axes * sizeof( float ) ); |
1039 | } |
1040 | |
1041 | void glutJoystickGetMinRange( int ident, float *axes ) |
1042 | { |
1043 | FREEGLUT_EXIT_IF_NOT_INITIALISED ( "glutJoystickGetMinRange" ); |
1044 | memcpy( axes, fgJoystick[ ident ]->min, |
1045 | fgJoystick[ ident ]->num_axes * sizeof( float ) ); |
1046 | } |
1047 | void glutJoystickGetMaxRange( int ident, float *axes ) |
1048 | { |
1049 | FREEGLUT_EXIT_IF_NOT_INITIALISED ( "glutJoystickGetMaxRange" ); |
1050 | memcpy( axes, fgJoystick[ ident ]->max, |
1051 | fgJoystick[ ident ]->num_axes * sizeof( float ) ); |
1052 | } |
1053 | void glutJoystickGetCenter( int ident, float *axes ) |
1054 | { |
1055 | FREEGLUT_EXIT_IF_NOT_INITIALISED ( "glutJoystickGetCenter" ); |
1056 | memcpy( axes, fgJoystick[ ident ]->center, |
1057 | fgJoystick[ ident ]->num_axes * sizeof( float ) ); |
1058 | } |
1059 | |
1060 | /*** END OF FILE ***/ |
1061 | |