On Thu, Jan 21, 2021 at 12:05:59PM -0700, Thomas Frohwein wrote:
> On Sat, Jan 09, 2021 at 10:46:17AM -0700, Thomas Frohwein wrote:
> > Hi,
> > 
> > The next stepping stone in my project to improve out-of-the-box
> > gamecontroller support.
> 
> Haven't gotten much feedback since I sent this out. Below is a slightly
> adjusted diff that generates the GUID in BSD_JoystickOpen rather than
> BSD_JoystickInit, avoiding a duplicate open(2) - close(2) dance.
> 
> Some testing would be appreciated - I tested it on 4 controllers, but
> it would be good to see that this works with some more controllers.
> 
> Here the Cliff notes of the diff:
> - Enables using GUID to get joystick mapping from the included (or
>   game-specific) controller mapping.
> - This replaces the prior workaround that was much more primitive, with
>   a fallback to standard XBox360 mapping and customization with a hand-
>   crafter SDL_GAMECONTROLLERCONFIG environment variable.
> - The entire patch for SDL_gamecontroller.c can now be dropped.
> - This gets us much closer to what's done on other platforms (Linux,
>   MacOS) to appropriately map a large number of gamecontrollers.
> - The solution is functional, though not yet 100% ideal in regards to
>   the creation and storage of the GUID, as it's stored as the string
>   which results in conversion to the numerical and then back to the
>   string. We will figure out the solution to this eventually, but I
>   don't think this should hold up the transition as the current
>   solution does not adversely affect performance.
> 
> More details in the original email.
> 
> Any of {testing,feedback,oks} would be appreciated.
> 
> > 
> > Background: On other platforms (Linux, MacOSX), SDL2 uses a GUID for
> > gamecontrollers combined with a database [1] to provide the default
> > button/axis mappings. This hasn't worked on OpenBSD (or apparently any
> > *BSD) and the SDL2 code resorts to simply converting the first 16 chars
> > of the device name to the GUID, which doesn't help with making use of
> > the gamecontrollerdb [2]. Therefore, the way we have been handling this
> > so far is that the env var SDL_GAMECONTROLLERCONFIG was available to
> > hold a mapping string [3]. This poses a significant barrier, as users
> > have to learn the syntax of the mapping strings and find one fitting
> > their controller by trial and error mostly.
> > 
> > This diff fixes the issue. To accomplish that, a couple of tasks are
> > added:
> > 
> > - Calculate and store the GUID (Credit goes to brynet@ for finding the
> >   GUID calculation and writing the initial proof-of-concept code).
> > - Remove the workaround code in SDL_gamecontroller.c that would either
> >   read the mapping from SDL_GAMECONTROLLERCONFIG if present, or
> >   otherwise default to the (commonly used) XBox 360 controller layout.
> > - Enable the mappings in SDL_gamecontrollerdb.h for OpenBSD.
> > - Assign buttons correctly - not in the order that they appear in the
> >   HID report, but based on their numbering in the HID report descriptor.
> > - Invert Y/Ry axes for the newer XInput-type gamecontrollers. This has
> >   been a particularly annoying problem, as USB standards recommend the
> >   positive directions to be left to right and far to near, but these
> >   controllers just use near-to-far as the positive direction. [4]
> > 
> > The problem with the last bullet point is that the Y/Ry axis inversion
> > needs to be applied *only* to the XInput devices, and *not* to the
> > older DInput devices. I can't check for XBox360 and other interface
> > subclasses (as used by the kernel in uhidev.c), as
> > USB_GET_INTERFACE_DESC is not exposed in the uhid(4) driver (see
> > uhid_do_ioctl() in uhid.c).
> > The solution I came up with is admittedly a (minor) hack. It turns out
> > that the old DInput devices report their axis values as an unsigned
> > 8bit integer, while the newer ones use a signed 16bit integer. With my
> > collection of gamecontrollers, checking if hitem.logical_maximum is
> > greater than 255 allows to reliably pick the correct handling of the
> > Y/Ry axes.
> > 
> > Other caveats:
> > 
> > - With the newly enabled GUID detection, SDL2 checks for a matching
> >   controller in gamecontrollerdb.txt in the application directory.
> >   I found that with one game (Cryptark), this contained bogus mappings
> >   for the XBox One controller (see the post on tech@), so some buttons
> >   and the Dpad didn't work until I removed that file.
> > - For some reason, the GUID for DInput devices matches the MACOSX
> >   entries in SDL_gamecontrollerdb.h, but the XInput devices match the
> >   LINUX entries. It's gonna take me a bit of time to get to the bottom
> >   of this; in the meantime just allowing both sets of GUID mappings
> >   works for the devices here on my testing
> > - The BSD_JoystickGetDeviceGUID() is not ideal at this point, as it
> >   takes the GUID stored as a string in char **joyguids and converts it
> >   into the raw numerical. I haven't figured out how to do the GUID
> >   calculation to avoid this additional transformation step yet. In the
> >   meantime, I don't notice performance issues and I don't think it's
> >   worth holding up the GUID mapping support over this.
> >  
> > Testing:
> > 
> > I tested this with sdl2-jstest (from sdl-jstest package) and few
> > games (Owlboy, Cryptark) with these controllers that I have:
> > XBox 360 controller (XInput), XBox One controller (XInput) (see separate
> > mail on tech@), Logitech F310 (both DInput and XInput), Logitech Dual
> > Action (DInput)). This maps everything correctly without additional
> > configuration (as long as faulty gamecontrollerdb.txt in the game
> > directory is removed).
> > 
> > Testing with a variety of controllers would be useful to see if this
> > really works across most of them as intended. If you do that, please
> > let me know the mapping/functioning before and after the diff is
> > applied.
> > 
> > [1] 
> > https://hg.libsdl.org/SDL/file/2370522dac4c/src/joystick/SDL_gamecontrollerdb.h
> > [2] 
> > https://hg.libsdl.org/SDL/file/2370522dac4c/src/joystick/bsd/SDL_bsdjoystick.c#l700
> > [3] https://marc.info/?l=openbsd-ports-cvs&m=152080802204959&w=2
> > [4] https://www.usb.org/sites/default/files/documents/hid1_11.pdf (page 20)
> 
> Here the updated diff:
> 
> Index: Makefile
> ===================================================================
> RCS file: /cvs/ports/devel/sdl2/Makefile,v
> retrieving revision 1.32
> diff -u -p -r1.32 Makefile
> --- Makefile  6 Jan 2021 22:32:08 -0000       1.32
> +++ Makefile  21 Jan 2021 18:47:48 -0000
> @@ -5,6 +5,7 @@ COMMENT=      cross-platform multimedia libra
>  V=           2.0.14
>  DISTNAME=    SDL2-${V}
>  PKGNAME=     sdl2-${V}
> +REVISION=    0
>  CATEGORIES=  devel
>  MASTER_SITES=        https://www.libsdl.org/release/
>  
> Index: patches/patch-src_joystick_SDL_gamecontroller_c
> ===================================================================
> RCS file: patches/patch-src_joystick_SDL_gamecontroller_c
> diff -N patches/patch-src_joystick_SDL_gamecontroller_c
> --- patches/patch-src_joystick_SDL_gamecontroller_c   6 Jan 2021 22:32:08 
> -0000       1.7
> +++ /dev/null 1 Jan 1970 00:00:00 -0000
> @@ -1,50 +0,0 @@
> -$OpenBSD: patch-src_joystick_SDL_gamecontroller_c,v 1.7 2021/01/06 22:32:08 
> thfr Exp $
> -
> -enable GameController API the Linux fallback way (by posing as Xbox360
> -controller)
> -also disable checking string "Xbox 360 Wireless Receiver", so for now
> -everything will be Xbox360 controller (works with generic joysticks)
> -map to SDL_GAMECONTROLLERCONFIG envvar if available
> -Use layout for XBox360 controller to maximize compatibility because
> -many controllers use this mapping
> -
> -Index: src/joystick/SDL_gamecontroller.c
> ---- src/joystick/SDL_gamecontroller.c.orig
> -+++ src/joystick/SDL_gamecontroller.c
> -@@ -968,7 +968,7 @@ static char *SDL_PrivateGetControllerGUIDFromMappingSt
> -             SDL_memcpy(&pchGUID[8], &pchGUID[0], 4);
> -             SDL_memcpy(&pchGUID[0], "03000000", 8);
> -         }
> --#elif __MACOSX__
> -+#elif defined(__MACOSX__) || defined(__OpenBSD__)
> -         if (SDL_strlen(pchGUID) == 32 &&
> -             SDL_memcmp(&pchGUID[4], "000000000000", 12) == 0 &&
> -             SDL_memcmp(&pchGUID[20], "000000000000", 12) == 0) {
> -@@ -1131,17 +1131,21 @@ static ControllerMapping_t 
> *SDL_PrivateGetControllerMa
> -     ControllerMapping_t *mapping;
> - 
> -     mapping = SDL_PrivateGetControllerMappingForGUID(guid, SDL_FALSE);
> --#ifdef __LINUX__
> -+#if defined(__LINUX__) || defined(__OpenBSD__)
> -     if (!mapping && name) {
> --        if (SDL_strstr(name, "Xbox 360 Wireless Receiver")) {
> --            /* The Linux driver xpad.c maps the wireless dpad to buttons */
> --            SDL_bool existing;
> -+        /* The Linux driver xpad.c maps the wireless dpad to buttons */
> -+        SDL_bool existing;
> -+        char guid_str[1024];
> -+        SDL_JoystickGetGUIDString(guid, guid_str, sizeof(guid_str));
> -+        if (SDL_GetHint(SDL_HINT_GAMECONTROLLERCONFIG) == NULL) {
> -             mapping = SDL_PrivateAddMappingForGUID(guid,
> --"none,X360 Wireless 
> Controller,a:b0,b:b1,back:b6,dpdown:b14,dpleft:b11,dpright:b12,dpup:b13,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3",
> -+"none,XBox360 
> Controller,a:b7,b:b8,back:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b2,lefttrigger:a2,leftx:a0,lefty:a1~,rightshoulder:b5,rightstick:b3,righttrigger:a5,rightx:a3,righty:a4~,start:b0,x:b9,y:b10",
> -                           &existing, 
> SDL_CONTROLLER_MAPPING_PRIORITY_DEFAULT);
> -+        } else {
> -+            mapping = SDL_PrivateAddMappingForGUID(guid, 
> SDL_GetHint(SDL_HINT_GAMECONTROLLERCONFIG), &existing, 
> SDL_CONTROLLER_MAPPING_PRIORITY_DEFAULT);
> -         }
> -     }
> --#endif /* __LINUX__ */
> -+#endif /* __LINUX__ || __OpenBSD__ */
> - 
> -     if (!mapping && name && !SDL_IsJoystickWGI(guid)) {
> -         if (SDL_strstr(name, "Xbox") || SDL_strstr(name, "X-Box") || 
> SDL_strstr(name, "XBOX")) {
> Index: patches/patch-src_joystick_SDL_gamecontrollerdb_h
> ===================================================================
> RCS file: patches/patch-src_joystick_SDL_gamecontrollerdb_h
> diff -N patches/patch-src_joystick_SDL_gamecontrollerdb_h
> --- /dev/null 1 Jan 1970 00:00:00 -0000
> +++ patches/patch-src_joystick_SDL_gamecontrollerdb_h 21 Jan 2021 18:47:48 
> -0000
> @@ -0,0 +1,26 @@
> +$OpenBSD$
> +
> +enable controller detection by GUID on OpenBSD
> +use both LINUX and MACOSX guids to match both XInput and DInput devices
> +
> +Index: src/joystick/SDL_gamecontrollerdb.h
> +--- src/joystick/SDL_gamecontrollerdb.h.orig
> ++++ src/joystick/SDL_gamecontrollerdb.h
> +@@ -338,7 +338,7 @@ static const char *s_ControllerMappings [] =
> +     
> "030000004f04000003d0000000000000,run'n'drive,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b7,leftshoulder:a3,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:a4,rightstick:b11,righttrigger:b5,rightx:a2,righty:a5,start:b9,x:b0,y:b3,",
> +     "03000000101c0000171c000000000000,uRage 
> Gamepad,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,",
> + #endif
> +-#if defined(__MACOSX__)
> ++#if defined(__MACOSX__) || defined(__OpenBSD__)
> +     "03000000c82d00000090000001000000,8BitDo FC30 
> Pro,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a5,rightx:a2,righty:a3,start:b11,x:b3,y:b4,hint:SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,",
> +     "03000000c82d00000090000001000000,8BitDo FC30 
> Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a5,rightx:a2,righty:a3,start:b11,x:b4,y:b3,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,",
> +     "03000000c82d00001038000000010000,8BitDo FC30 
> Pro,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,hint:SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,",
> +@@ -479,7 +479,7 @@ static const char *s_ControllerMappings [] =
> +     "03000000830500006020000000010000,iBuffalo SNES 
> Controller,a:b1,b:b0,back:b6,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b5,start:b7,x:b3,y:b2,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,",
> +     "03000000830500006020000000000000,iBuffalo USB 2-axis 8-button 
> Gamepad,a:b1,b:b0,back:b6,leftshoulder:b4,leftx:a0,lefty:a1,rightshoulder:b5,start:b7,x:b3,y:b2,",
> + #endif
> +-#if defined(__LINUX__)
> ++#if defined(__LINUX__) || defined(__OpenBSD__)
> +     "03000000c82d00000090000011010000,8BitDo FC30 
> Pro,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a5,rightx:a2,righty:a3,start:b11,x:b3,y:b4,hint:SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,",
> +     "03000000c82d00000090000011010000,8BitDo FC30 
> Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a5,rightx:a2,righty:a3,start:b11,x:b4,y:b3,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,",
> +     "05000000c82d00001038000000010000,8BitDo FC30 
> Pro,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,hint:SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,",
> Index: patches/patch-src_joystick_bsd_SDL_bsdjoystick_c
> ===================================================================
> RCS file: patches/patch-src_joystick_bsd_SDL_bsdjoystick_c
> diff -N patches/patch-src_joystick_bsd_SDL_bsdjoystick_c
> --- /dev/null 1 Jan 1970 00:00:00 -0000
> +++ patches/patch-src_joystick_bsd_SDL_bsdjoystick_c  21 Jan 2021 18:47:48 
> -0000
> @@ -0,0 +1,125 @@
> +$OpenBSD$
> +
> +assign buttons correctly
> +get GUID using USB_GET_DEVICEINFO
> +detect newer (XInput-style) gamecontroller if hitem.logical_maximum is
> +> 255; if so invert y axes
> +
> +Index: src/joystick/bsd/SDL_bsdjoystick.c
> +--- src/joystick/bsd/SDL_bsdjoystick.c.orig
> ++++ src/joystick/bsd/SDL_bsdjoystick.c
> +@@ -83,6 +83,9 @@
> + 
> + #ifdef __OpenBSD__
> + 
> ++#define DEV_USB             3       /* needed to get GUID from 
> USB_GET_DEVICEINFO */
> ++#define GUID_LEN    32      /* GUID string has length 32 */
> ++
> + #define HUG_DPAD_UP         0x90
> + #define HUG_DPAD_DOWN       0x91
> + #define HUG_DPAD_RIGHT      0x92
> +@@ -192,6 +195,9 @@ struct joystick_hwdata
> + 
> + static char *joynames[MAX_JOYS];
> + static char *joydevnames[MAX_JOYS];
> ++#ifdef __OpenBSD__
> ++static char joyguids[MAX_JOYS][GUID_LEN];
> ++#endif
> + 
> + static int report_alloc(struct report *, struct report_desc *, int);
> + static void report_free(struct report *);
> +@@ -222,6 +228,9 @@ BSD_JoystickInit(void)
> + 
> +     SDL_memset(joynames, 0, sizeof(joynames));
> +     SDL_memset(joydevnames, 0, sizeof(joydevnames));
> ++#ifdef __OpenBSD__
> ++    SDL_memset(joyguids, 0, sizeof(char) * MAX_JOYS * GUID_LEN);
> ++#endif
> + 
> +     for (i = 0; i < MAX_UHID_JOYS; i++) {
> +         SDL_Joystick nj;
> +@@ -356,6 +365,9 @@ BSD_JoystickOpen(SDL_Joystick *joy, int device_index)
> + #endif
> +     int fd;
> +     int i;
> ++#ifdef __OpenBSD__
> ++    struct usb_device_info di;
> ++#endif
> + 
> +     fd = open(path, O_RDONLY);
> +     if (fd == -1) {
> +@@ -434,6 +446,17 @@ BSD_JoystickOpen(SDL_Joystick *joy, int device_index)
> +     }
> + desc_failed:
> + #endif
> ++#if defined(__OpenBSD__)
> ++    if (ioctl(fd, USB_GET_DEVICEINFO, &di) != -1) {
> ++        SDL_snprintf(joyguids[numjoysticks],
> ++                     SDL_arraysize(joyguids[device_index]),
> ++                     "%02x%02x0000%02x%02x0000%02x%02x0000%02x%02x0000",
> ++                     DEV_USB & 0xFF, DEV_USB >> 8,
> ++                     di.udi_vendorNo & 0xFF, di.udi_vendorNo >> 8,
> ++                     di.udi_productNo & 0xFF, di.udi_productNo >> 8,
> ++                     di.udi_releaseNo & 0xFF, di.udi_releaseNo >> 8);
> ++    }
> ++#endif
> +     if (report_alloc(rep, hw->repdesc, REPORT_INPUT) < 0) {
> +         goto usberr;
> +     }
> +@@ -544,6 +567,7 @@ BSD_JoystickUpdate(SDL_Joystick *joy)
> +     Sint32 v;
> + #ifdef __OpenBSD__
> +     Sint32 dpad[4] = {0, 0, 0, 0};
> ++    int actualbutton;
> + #endif
> + 
> + #if defined(__FREEBSD__) || SDL_JOYSTICK_USBHID_MACHINE_JOYSTICK_H || 
> defined(__FreeBSD_kernel__) || defined(__DragonFly_)
> +@@ -618,6 +642,18 @@ BSD_JoystickUpdate(SDL_Joystick *joy)
> +                             naxe = joy->hwdata->axis_map[joyaxe];
> +                             /* scaleaxe */
> +                             v = (Sint32) hid_get_data(REP_BUF_DATA(rep), 
> &hitem);
> ++#ifdef __OpenBSD__
> ++                            /* XInput controllermapping relies on inverted 
> Y axes.
> ++                             * These devices have a 16bit signed space, as 
> opposed
> ++                             * to older DInput devices (8bit unsigned), so
> ++                             * hitem.logical_maximum can be used to 
> differentiate them.
> ++                             */
> ++                            if ((joyaxe == JOYAXE_Y || joyaxe == JOYAXE_RY)
> ++                                && hitem.logical_maximum > 255) {
> ++                                if (v != 0)
> ++                                    v = ~v;
> ++                            }
> ++#endif
> +                             v -= (hitem.logical_maximum +
> +                                   hitem.logical_minimum + 1) / 2;
> +                             v *= 32768 /
> +@@ -652,7 +688,12 @@ BSD_JoystickUpdate(SDL_Joystick *joy)
> +                     }
> +                 case HUP_BUTTON:
> +                     v = (Sint32) hid_get_data(REP_BUF_DATA(rep), &hitem);
> ++#ifdef __OpenBSD__
> ++                    actualbutton = HID_USAGE(hitem.usage) - 1;      /* sdl 
> buttons are zero-based */
> ++                    SDL_PrivateJoystickButton(joy, actualbutton, v);
> ++#else
> +                     SDL_PrivateJoystickButton(joy, nbutton, v);
> ++#endif
> +                     nbutton++;
> +                     break;
> +                 default:
> +@@ -697,11 +738,16 @@ static SDL_JoystickGUID
> + BSD_JoystickGetDeviceGUID( int device_index )
> + {
> +     SDL_JoystickGUID guid;
> ++#ifdef __OpenBSD__
> ++    guid = SDL_JoystickGetGUIDFromString(joyguids[device_index]);
> ++    return guid;
> ++#else
> +     /* the GUID is just the first 16 chars of the name for now */
> +     const char *name = BSD_JoystickGetDeviceName( device_index );
> +     SDL_zero( guid );
> +     SDL_memcpy( &guid, name, SDL_min( sizeof(guid), SDL_strlen( name ) ) );
> +     return guid;
> ++#endif
> + }
> + 
> + static int

I tested this on my generic USB SNES controller (along with the ujoy
patch), and it works perfectly.

It even finds a better GUID than the one I had been using in my local
SDL_GAMECONTROLLERCONFIG override.

Found 1 joystick(s)

Joystick Name:     'Joystick (0)'
Joystick GUID:     030000001008000001e5000006010000
Joystick Number:    0
Number of Axes:     2
Number of Buttons: 10
Number of Hats:     0
Number of Balls:    0
GameController:
  Name:    'NEXT SNES Controller'
  Mapping: '030000001008000001e5000006010000,NEXT SNES
Controller,a:b2,b:b1,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b6,start:b9,x:b3,y:b0,'

Tested with:

uhidev4 at uhub0 port 12 configuration 1 interface 0 "vendor 0x0810 usb
gamepad" rev 1.00/1.06 addr 2
uhidev4: iclass 3/0, 1 report id
ujoy0 at uhidev4 reportid 1: input=7, output=0, feature=0

ok brynet@ (along with the /dev/ujoy/%d patch).

-Bryan.

Reply via email to