Commit 01baa898 authored by Sam Lantinga's avatar Sam Lantinga

Date: Sun, 18 Apr 2004 16:09:53 -0400 (EDT)

From: David MacCormack
Subject: [SDL] Linux joystick patch

I recently got myself a PS2 -> USB converter (a super joybox 5).  It
accepts 4 PSX/PS2 controllers.  It's implemented as a HID, which is nice
because it doesn't require its own driver, but the problem is that it's
implemented as a *single* HID -- that is, it shows up as a single
joystick with 19 axes, 4 hats, and 48 buttons.  This poses a problem for a
number of apps which use SDL (stella, fce ultra, zsnes, to name a few) and
see only a single (physical) joystick even though there are really 4
(logical) joysticks.  There are a number of these types of devices on the
market, and I've seen others post messages (in the zsnes forum, for
example) with the same problem, so I came up with what I think is a pretty
generic solution.

I patched src/joystick/linux/SDL_sysjoystic.c to include support for
logical joysticks; basically, it's a static array and supporting functions
that map a single physical joystick to multiple logical joysticks.  The
attached patch has the new code.  It's wrapped inside #ifndef
statements so that you can get the old behavior if you want.

--HG--
extra : convert_revision : svn%3Ac70aab31-4412-0410-b14c-859654838e24/trunk%40893
parent 19f49cc2
...@@ -67,11 +67,102 @@ static struct { ...@@ -67,11 +67,102 @@ static struct {
{ "Saitek Saitek X45", 6, 1, 0 } { "Saitek Saitek X45", 6, 1, 0 }
}; };
#ifndef NO_LOGICAL_JOYSTICKS
static struct joystick_logical_values {
int njoy;
int nthing;
} joystick_logical_values[] = {
/* +0 */
/* MP-8800 axes map - map to {logical joystick #, logical axis #} */
{0,0},{0,1},{0,2},{1,0},{1,1},{0,3},{1,2},{1,3},{2,0},{2,1},{2,2},{2,3},
{3,0},{3,1},{3,2},{3,3},{0,4},{1,4},{2,4},
/* +19 */
/* MP-8800 hat map - map to {logical joystick #, logical hat #} */
{0,0},{1,0},{2,0},{3,0},
/* +23 */
/* MP-8800 button map - map to {logical joystick #, logical button #} */
{0,0},{0,1},{0,2},{0,3},{0,4},{0,5},{0,6},{0,7},{0,8},{0,9},{0,10},{0,11},
{1,0},{1,1},{1,2},{1,3},{1,4},{1,5},{1,6},{1,7},{1,8},{1,9},{1,10},{1,11},
{2,0},{2,1},{2,2},{2,3},{2,4},{2,5},{2,6},{2,7},{2,8},{2,9},{2,10},{2,11},
{3,0},{3,1},{3,2},{3,3},{3,4},{3,5},{3,6},{3,7},{3,8},{3,9},{3,10},{3,11}
};
static struct joystick_logical_layout {
int naxes;
int nhats;
int nballs;
int nbuttons;
} joystick_logical_layout[] = {
/* MP-8800 logical layout */
{5, 1, 0, 12},
{5, 1, 0, 12},
{5, 1, 0, 12},
{4, 1, 0, 12}
};
/*
Some USB HIDs show up as a single joystick even though they actually
control 2 or more joysticks. This array sets up a means of mapping
a single physical joystick to multiple logical joysticks. (djm)
njoys
the number of logical joysticks
layouts
an array of layout structures, one to describe each logical joystick
axes, hats, balls, buttons
arrays that map a physical thingy to a logical thingy
*/
static struct joystick_logicalmap {
const char *name;
int njoys;
struct joystick_logical_layout *layouts;
struct joystick_logical_values *axes;
struct joystick_logical_values *hats;
struct joystick_logical_values *balls;
struct joystick_logical_values *buttons;
} joystick_logicalmap[] = {
{"WiseGroup.,Ltd MP-8800 Quad USB Joypad", 4, joystick_logical_layout,
joystick_logical_values, joystick_logical_values+19, NULL,
joystick_logical_values+23}
};
/* find the head of a linked list, given a point in it
*/
#define SDL_joylist_head(i, start)\
for(i = start; SDL_joylist[i].fname == NULL;) i = SDL_joylist[i].prev;
#define SDL_logical_joydecl(d) d
#else
#define SDL_logical_joydecl(d)
#endif /* USE_LOGICAL_JOYSTICKS */
/* The maximum number of joysticks we'll detect */ /* The maximum number of joysticks we'll detect */
#define MAX_JOYSTICKS 32 #define MAX_JOYSTICKS 32
/* A list of available joysticks */ /* A list of available joysticks */
static char *SDL_joylist[MAX_JOYSTICKS]; static struct
{
char* fname;
#ifndef NO_LOGICAL_JOYSTICKS
SDL_Joystick* joy;
struct joystick_logicalmap* map;
int prev;
int next;
int logicalno;
#endif /* USE_LOGICAL_JOYSTICKS */
} SDL_joylist[MAX_JOYSTICKS];
/* The private structure used to keep track of a joystick */ /* The private structure used to keep track of a joystick */
struct joystick_hwdata { struct joystick_hwdata {
...@@ -108,6 +199,73 @@ static char *mystrdup(const char *string) ...@@ -108,6 +199,73 @@ static char *mystrdup(const char *string)
return(newstring); return(newstring);
} }
#ifndef NO_LOGICAL_JOYSTICKS
static int CountLogicalJoysticks(int max)
{
register int i, j, k, ret, prev;
const char* name;
ret = 0;
for(i = 0; i < max; i++) {
name = SDL_SYS_JoystickName(i);
if (name) {
for(j = 0; j < SDL_TABLESIZE(joystick_logicalmap); j++) {
if (!strcmp(name, joystick_logicalmap[j].name)) {
prev = i;
SDL_joylist[prev].map = joystick_logicalmap+j;
for(k = 1; k < joystick_logicalmap[j].njoys; k++) {
SDL_joylist[prev].next = max + ret;
if (prev != i)
SDL_joylist[max+ret].prev = prev;
prev = max + ret;
SDL_joylist[prev].logicalno = k;
SDL_joylist[prev].map = joystick_logicalmap+j;
ret++;
}
break;
}
}
}
}
return ret;
}
static void LogicalSuffix(int logicalno, char* namebuf, int len)
{
register int slen;
const static char suffixs[] =
"01020304050607080910111213141516171819"
"20212223242526272829303132";
const char* suffix;
slen = strlen(namebuf);
suffix = NULL;
if (logicalno*2<sizeof(suffixs))
suffix = suffixs + (logicalno*2);
if (slen + 4 < len && suffix) {
namebuf[slen++] = ' ';
namebuf[slen++] = '#';
namebuf[slen++] = suffix[0];
namebuf[slen++] = suffix[1];
namebuf[slen++] = 0;
}
}
#endif /* USE_LOGICAL_JOYSTICKS */
#ifdef USE_INPUT_EVENTS #ifdef USE_INPUT_EVENTS
#define test_bit(nr, addr) \ #define test_bit(nr, addr) \
(((1UL << ((nr) & 31)) & (((const unsigned int *) addr)[(nr) >> 5])) != 0) (((1UL << ((nr) & 31)) & (((const unsigned int *) addr)[(nr) >> 5])) != 0)
...@@ -160,8 +318,8 @@ int SDL_SYS_JoystickInit(void) ...@@ -160,8 +318,8 @@ int SDL_SYS_JoystickInit(void)
fd = open(path, O_RDONLY, 0); fd = open(path, O_RDONLY, 0);
if ( fd >= 0 ) { if ( fd >= 0 ) {
/* Assume the user knows what they're doing. */ /* Assume the user knows what they're doing. */
SDL_joylist[numjoysticks] = mystrdup(path); SDL_joylist[numjoysticks].fname =mystrdup(path);
if ( SDL_joylist[numjoysticks] ) { if ( SDL_joylist[numjoysticks].fname ) {
dev_nums[numjoysticks] = sb.st_rdev; dev_nums[numjoysticks] = sb.st_rdev;
++numjoysticks; ++numjoysticks;
} }
...@@ -208,8 +366,8 @@ int SDL_SYS_JoystickInit(void) ...@@ -208,8 +366,8 @@ int SDL_SYS_JoystickInit(void)
close(fd); close(fd);
/* We're fine, add this joystick */ /* We're fine, add this joystick */
SDL_joylist[numjoysticks] = mystrdup(path); SDL_joylist[numjoysticks].fname =mystrdup(path);
if ( SDL_joylist[numjoysticks] ) { if ( SDL_joylist[numjoysticks].fname ) {
dev_nums[numjoysticks] = sb.st_rdev; dev_nums[numjoysticks] = sb.st_rdev;
++numjoysticks; ++numjoysticks;
} }
...@@ -229,6 +387,9 @@ int SDL_SYS_JoystickInit(void) ...@@ -229,6 +387,9 @@ int SDL_SYS_JoystickInit(void)
break; break;
#endif #endif
} }
#ifndef NO_LOGICAL_JOYSTICKS
numjoysticks += CountLogicalJoysticks(numjoysticks);
#endif
return(numjoysticks); return(numjoysticks);
} }
...@@ -239,20 +400,29 @@ const char *SDL_SYS_JoystickName(int index) ...@@ -239,20 +400,29 @@ const char *SDL_SYS_JoystickName(int index)
int fd; int fd;
static char namebuf[128]; static char namebuf[128];
char *name; char *name;
SDL_logical_joydecl(int oindex = index);
#ifndef NO_LOGICAL_JOYSTICKS
SDL_joylist_head(index, index);
#endif
name = NULL; name = NULL;
fd = open(SDL_joylist[index], O_RDONLY, 0); fd = open(SDL_joylist[index].fname, O_RDONLY, 0);
if ( fd >= 0 ) { if ( fd >= 0 ) {
if ( if (
#ifdef USE_INPUT_EVENTS #ifdef USE_INPUT_EVENTS
(ioctl(fd, EVIOCGNAME(sizeof(namebuf)), namebuf) <= 0) && (ioctl(fd, EVIOCGNAME(sizeof(namebuf)), namebuf) <= 0) &&
#endif #endif
(ioctl(fd, JSIOCGNAME(sizeof(namebuf)), namebuf) <= 0) ) { (ioctl(fd, JSIOCGNAME(sizeof(namebuf)), namebuf) <= 0) ) {
name = SDL_joylist[index]; name = SDL_joylist[index].fname;
} else { } else {
name = namebuf; name = namebuf;
} }
close(fd); close(fd);
#ifndef NO_LOGICAL_JOYSTICKS
if (SDL_joylist[oindex].prev || SDL_joylist[oindex].next)
LogicalSuffix(SDL_joylist[oindex].logicalno, namebuf, 128);
#endif
} }
return name; return name;
} }
...@@ -479,6 +649,22 @@ static SDL_bool EV_ConfigJoystick(SDL_Joystick *joystick, int fd) ...@@ -479,6 +649,22 @@ static SDL_bool EV_ConfigJoystick(SDL_Joystick *joystick, int fd)
#endif /* USE_INPUT_EVENTS */ #endif /* USE_INPUT_EVENTS */
#ifndef NO_LOGICAL_JOYSTICKS
static void ConfigLogicalJoystick(SDL_Joystick *joystick)
{
struct joystick_logical_layout* layout;
layout = SDL_joylist[joystick->index].map->layouts +
SDL_joylist[joystick->index].logicalno;
joystick->nbuttons = layout->nbuttons;
joystick->nhats = layout->nhats;
joystick->naxes = layout->naxes;
joystick->nballs = layout->nballs;
}
#endif
/* Function to open a joystick for use. /* Function to open a joystick for use.
The joystick to open is specified by the index field of the joystick. The joystick to open is specified by the index field of the joystick.
This should fill the nbuttons and naxes fields of the joystick structure. This should fill the nbuttons and naxes fields of the joystick structure.
...@@ -487,9 +673,28 @@ static SDL_bool EV_ConfigJoystick(SDL_Joystick *joystick, int fd) ...@@ -487,9 +673,28 @@ static SDL_bool EV_ConfigJoystick(SDL_Joystick *joystick, int fd)
int SDL_SYS_JoystickOpen(SDL_Joystick *joystick) int SDL_SYS_JoystickOpen(SDL_Joystick *joystick)
{ {
int fd; int fd;
SDL_logical_joydecl(int realindex);
SDL_logical_joydecl(SDL_Joystick *realjoy = NULL);
/* Open the joystick and set the joystick file descriptor */ /* Open the joystick and set the joystick file descriptor */
fd = open(SDL_joylist[joystick->index], O_RDONLY, 0); #ifndef NO_LOGICAL_JOYSTICKS
if (SDL_joylist[joystick->index].fname == NULL) {
SDL_joylist_head(realindex, joystick->index);
realjoy = SDL_JoystickOpen(realindex);
if (realjoy == NULL)
return(-1);
fd = realjoy->hwdata->fd;
} else {
fd = open(SDL_joylist[joystick->index].fname, O_RDONLY, 0);
}
SDL_joylist[joystick->index].joy = joystick;
#else
fd = open(SDL_joylist[joystick->index].fname, O_RDONLY, 0);
#endif
if ( fd < 0 ) { if ( fd < 0 ) {
SDL_SetError("Unable to open %s\n", SDL_SetError("Unable to open %s\n",
SDL_joylist[joystick->index]); SDL_joylist[joystick->index]);
...@@ -509,6 +714,11 @@ int SDL_SYS_JoystickOpen(SDL_Joystick *joystick) ...@@ -509,6 +714,11 @@ int SDL_SYS_JoystickOpen(SDL_Joystick *joystick)
fcntl(fd, F_SETFL, O_NONBLOCK); fcntl(fd, F_SETFL, O_NONBLOCK);
/* Get the number of buttons and axes on the joystick */ /* Get the number of buttons and axes on the joystick */
#ifndef NO_LOGICAL_JOYSTICKS
if (realjoy)
ConfigLogicalJoystick(joystick);
else
#endif
#ifdef USE_INPUT_EVENTS #ifdef USE_INPUT_EVENTS
if ( ! EV_ConfigJoystick(joystick, fd) ) if ( ! EV_ConfigJoystick(joystick, fd) )
#endif #endif
...@@ -517,6 +727,84 @@ int SDL_SYS_JoystickOpen(SDL_Joystick *joystick) ...@@ -517,6 +727,84 @@ int SDL_SYS_JoystickOpen(SDL_Joystick *joystick)
return(0); return(0);
} }
#ifndef NO_LOGICAL_JOYSTICKS
static SDL_Joystick* FindLogicalJoystick(
SDL_Joystick *joystick, struct joystick_logical_values* v)
{
SDL_Joystick *logicaljoy;
register int i;
i = joystick->index;
logicaljoy = NULL;
/* get the fake joystick that will receive the event
*/
for(;;) {
if (SDL_joylist[i].logicalno == v->njoy) {
logicaljoy = SDL_joylist[i].joy;
break;
}
if (SDL_joylist[i].next == 0)
break;
i = SDL_joylist[i].next;
}
return logicaljoy;
}
static int LogicalJoystickButton(
SDL_Joystick *joystick, Uint8 button, Uint8 state){
struct joystick_logical_values* buttons;
SDL_Joystick *logicaljoy = NULL;
/* if there's no map then this is just a regular joystick
*/
if (SDL_joylist[joystick->index].map == NULL)
return 0;
/* get the logical joystick that will receive the event
*/
buttons = SDL_joylist[joystick->index].map->buttons+button;
logicaljoy = FindLogicalJoystick(joystick, buttons);
if (logicaljoy == NULL)
return 1;
SDL_PrivateJoystickButton(logicaljoy, buttons->nthing, state);
return 1;
}
static int LogicalJoystickAxis(
SDL_Joystick *joystick, Uint8 axis, Sint16 value)
{
struct joystick_logical_values* axes;
SDL_Joystick *logicaljoy = NULL;
/* if there's no map then this is just a regular joystick
*/
if (SDL_joylist[joystick->index].map == NULL)
return 0;
/* get the logical joystick that will receive the event
*/
axes = SDL_joylist[joystick->index].map->axes+axis;
logicaljoy = FindLogicalJoystick(joystick, axes);
if (logicaljoy == NULL)
return 1;
SDL_PrivateJoystickAxis(logicaljoy, axes->nthing, value);
return 1;
}
#endif /* USE_LOGICAL_JOYSTICKS */
static __inline__ static __inline__
void HandleHat(SDL_Joystick *stick, Uint8 hat, int axis, int value) void HandleHat(SDL_Joystick *stick, Uint8 hat, int axis, int value)
{ {
...@@ -526,6 +814,8 @@ void HandleHat(SDL_Joystick *stick, Uint8 hat, int axis, int value) ...@@ -526,6 +814,8 @@ void HandleHat(SDL_Joystick *stick, Uint8 hat, int axis, int value)
{ SDL_HAT_LEFT, SDL_HAT_CENTERED, SDL_HAT_RIGHT }, { SDL_HAT_LEFT, SDL_HAT_CENTERED, SDL_HAT_RIGHT },
{ SDL_HAT_LEFTDOWN, SDL_HAT_DOWN, SDL_HAT_RIGHTDOWN } { SDL_HAT_LEFTDOWN, SDL_HAT_DOWN, SDL_HAT_RIGHTDOWN }
}; };
SDL_logical_joydecl(SDL_Joystick *logicaljoy = NULL);
SDL_logical_joydecl(struct joystick_logical_values* hats = NULL);
the_hat = &stick->hwdata->hats[hat]; the_hat = &stick->hwdata->hats[hat];
if ( value < 0 ) { if ( value < 0 ) {
...@@ -539,6 +829,24 @@ void HandleHat(SDL_Joystick *stick, Uint8 hat, int axis, int value) ...@@ -539,6 +829,24 @@ void HandleHat(SDL_Joystick *stick, Uint8 hat, int axis, int value)
} }
if ( value != the_hat->axis[axis] ) { if ( value != the_hat->axis[axis] ) {
the_hat->axis[axis] = value; the_hat->axis[axis] = value;
#ifndef NO_LOGICAL_JOYSTICKS
/* if there's no map then this is just a regular joystick
*/
if (SDL_joylist[stick->index].map != NULL) {
/* get the fake joystick that will receive the event
*/
hats = SDL_joylist[stick->index].map->hats+hat;
logicaljoy = FindLogicalJoystick(stick, hats);
}
if (logicaljoy) {
stick = logicaljoy;
hat = hats->nthing;
}
#endif /* USE_LOGICAL_JOYSTICKS */
SDL_PrivateJoystickHat(stick, hat, SDL_PrivateJoystickHat(stick, hat,
position_map[the_hat->axis[1]][the_hat->axis[0]]); position_map[the_hat->axis[1]][the_hat->axis[0]]);
} }
...@@ -561,12 +869,23 @@ static __inline__ void JS_HandleEvents(SDL_Joystick *joystick) ...@@ -561,12 +869,23 @@ static __inline__ void JS_HandleEvents(SDL_Joystick *joystick)
int i, len; int i, len;
Uint8 other_axis; Uint8 other_axis;
#ifndef NO_LOGICAL_JOYSTICKS
if (SDL_joylist[joystick->index].fname == NULL) {
SDL_joylist_head(i, joystick->index);
return JS_HandleEvents(SDL_joylist[i].joy);
}
#endif
while ((len=read(joystick->hwdata->fd, events, (sizeof events))) > 0) { while ((len=read(joystick->hwdata->fd, events, (sizeof events))) > 0) {
len /= sizeof(events[0]); len /= sizeof(events[0]);
for ( i=0; i<len; ++i ) { for ( i=0; i<len; ++i ) {
switch (events[i].type & ~JS_EVENT_INIT) { switch (events[i].type & ~JS_EVENT_INIT) {
case JS_EVENT_AXIS: case JS_EVENT_AXIS:
if ( events[i].number < joystick->naxes ) { if ( events[i].number < joystick->naxes ) {
#ifndef NO_LOGICAL_JOYSTICKS
if (!LogicalJoystickAxis(joystick,
events[i].number, events[i].value))
#endif
SDL_PrivateJoystickAxis(joystick, SDL_PrivateJoystickAxis(joystick,
events[i].number, events[i].value); events[i].number, events[i].value);
break; break;
...@@ -589,6 +908,10 @@ static __inline__ void JS_HandleEvents(SDL_Joystick *joystick) ...@@ -589,6 +908,10 @@ static __inline__ void JS_HandleEvents(SDL_Joystick *joystick)
} }
break; break;
case JS_EVENT_BUTTON: case JS_EVENT_BUTTON:
#ifndef NO_LOGICAL_JOYSTICKS
if (!LogicalJoystickButton(joystick,
events[i].number, events[i].value))
#endif
SDL_PrivateJoystickButton(joystick, SDL_PrivateJoystickButton(joystick,
events[i].number, events[i].value); events[i].number, events[i].value);
break; break;
...@@ -631,6 +954,13 @@ static __inline__ void EV_HandleEvents(SDL_Joystick *joystick) ...@@ -631,6 +954,13 @@ static __inline__ void EV_HandleEvents(SDL_Joystick *joystick)
int i, len; int i, len;
int code; int code;
#ifndef NO_LOGICAL_JOYSTICKS
if (SDL_joylist[joystick->index].fname == NULL) {
SDL_joylist_head(i, joystick->index);
return EV_HandleEvents(SDL_joylist[i].joy);
}
#endif
while ((len=read(joystick->hwdata->fd, events, (sizeof events))) > 0) { while ((len=read(joystick->hwdata->fd, events, (sizeof events))) > 0) {
len /= sizeof(events[0]); len /= sizeof(events[0]);
for ( i=0; i<len; ++i ) { for ( i=0; i<len; ++i ) {
...@@ -639,6 +969,11 @@ static __inline__ void EV_HandleEvents(SDL_Joystick *joystick) ...@@ -639,6 +969,11 @@ static __inline__ void EV_HandleEvents(SDL_Joystick *joystick)
case EV_KEY: case EV_KEY:
if ( code >= BTN_MISC ) { if ( code >= BTN_MISC ) {
code -= BTN_MISC; code -= BTN_MISC;
#ifndef NO_LOGICAL_JOYSTICKS
if (!LogicalJoystickButton(joystick,
joystick->hwdata->key_map[code],
events[i].value))
#endif
SDL_PrivateJoystickButton(joystick, SDL_PrivateJoystickButton(joystick,
joystick->hwdata->key_map[code], joystick->hwdata->key_map[code],
events[i].value); events[i].value);
...@@ -660,6 +995,11 @@ static __inline__ void EV_HandleEvents(SDL_Joystick *joystick) ...@@ -660,6 +995,11 @@ static __inline__ void EV_HandleEvents(SDL_Joystick *joystick)
break; break;
default: default:
events[i].value = EV_AxisCorrect(joystick, code, events[i].value); events[i].value = EV_AxisCorrect(joystick, code, events[i].value);
#ifndef NO_LOGICAL_JOYSTICKS
if (!LogicalJoystickAxis(joystick,
joystick->hwdata->abs_map[code],
events[i].value))
#endif
SDL_PrivateJoystickAxis(joystick, SDL_PrivateJoystickAxis(joystick,
joystick->hwdata->abs_map[code], joystick->hwdata->abs_map[code],
events[i].value); events[i].value);
...@@ -714,7 +1054,18 @@ void SDL_SYS_JoystickUpdate(SDL_Joystick *joystick) ...@@ -714,7 +1054,18 @@ void SDL_SYS_JoystickUpdate(SDL_Joystick *joystick)
/* Function to close a joystick after use */ /* Function to close a joystick after use */
void SDL_SYS_JoystickClose(SDL_Joystick *joystick) void SDL_SYS_JoystickClose(SDL_Joystick *joystick)
{ {
#ifndef NO_LOGICAL_JOYSTICKS
register int i;
if (SDL_joylist[joystick->index].fname == NULL) {
SDL_joylist_head(i, joystick->index);
SDL_JoystickClose(SDL_joylist[i].joy);
}
#endif
if ( joystick->hwdata ) { if ( joystick->hwdata ) {
#ifndef NO_LOGICAL_JOYSTICKS
if (SDL_joylist[joystick->index].fname != NULL)
#endif
close(joystick->hwdata->fd); close(joystick->hwdata->fd);
if ( joystick->hwdata->hats ) { if ( joystick->hwdata->hats ) {
free(joystick->hwdata->hats); free(joystick->hwdata->hats);
...@@ -732,9 +1083,9 @@ void SDL_SYS_JoystickQuit(void) ...@@ -732,9 +1083,9 @@ void SDL_SYS_JoystickQuit(void)
{ {
int i; int i;
for ( i=0; SDL_joylist[i]; ++i ) { for ( i=0; SDL_joylist[i].fname; ++i ) {
free(SDL_joylist[i]); free(SDL_joylist[i].fname);
} }
SDL_joylist[0] = NULL; SDL_joylist[0].fname = NULL;
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment