Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
W
wine-winehq
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
wine
wine-winehq
Commits
365c1a50
Commit
365c1a50
authored
Sep 02, 2005
by
Daniel Remenak
Committed by
Alexandre Julliard
Sep 02, 2005
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Added a linux input system force feedback effect implementation.
parent
d78888cc
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
926 additions
and
0 deletions
+926
-0
Makefile.in
dlls/dinput/Makefile.in
+1
-0
effect_linuxinput.c
dlls/dinput/effect_linuxinput.c
+925
-0
No files found.
dlls/dinput/Makefile.in
View file @
365c1a50
...
...
@@ -11,6 +11,7 @@ C_SRCS = \
data_formats.c
\
device.c
\
dinput_main.c
\
effect_linuxinput.c
\
joystick_linux.c
\
joystick_linuxinput.c
\
keyboard.c
\
...
...
dlls/dinput/effect_linuxinput.c
0 → 100644
View file @
365c1a50
/* DirectInput Linux Event Device Effect
*
* Copyright 2005 Daniel Remenak
*
* Thanks to Google's Summer of Code Program (2005)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#include "config.h"
#ifdef HAVE_STRUCT_FF_EFFECT_DIRECTION
#include <stdarg.h>
#include <string.h>
#ifdef HAVE_LINUX_INPUT_H
# include <linux/input.h>
#endif
#include <errno.h>
#include <unistd.h>
#include <math.h>
#include "wine/debug.h"
#include "wine/unicode.h"
#include "windef.h"
#include "winbase.h"
#include "winerror.h"
#include "dinput.h"
#include "device_private.h"
WINE_DEFAULT_DEBUG_CHANNEL
(
dinput
);
static
const
IDirectInputEffectVtbl
LinuxInputEffectVtbl
;
typedef
struct
LinuxInputEffectImpl
LinuxInputEffectImpl
;
struct
LinuxInputEffectImpl
{
const
void
*
lpVtbl
;
LONG
ref
;
GUID
guid
;
/* Effect data */
struct
ff_effect
effect
;
/* Parent device */
int
fd
;
};
/******************************************************************************
* DirectInputEffect Functional Helper
*/
static
DWORD
_typeFromGUID
(
REFGUID
guid
)
{
if
(
IsEqualGUID
(
guid
,
&
GUID_ConstantForce
))
{
return
DIEFT_CONSTANTFORCE
;
}
else
if
(
IsEqualGUID
(
guid
,
&
GUID_Square
)
||
IsEqualGUID
(
guid
,
&
GUID_Sine
)
||
IsEqualGUID
(
guid
,
&
GUID_Triangle
)
||
IsEqualGUID
(
guid
,
&
GUID_SawtoothUp
)
||
IsEqualGUID
(
guid
,
&
GUID_SawtoothDown
))
{
return
DIEFT_PERIODIC
;
}
else
if
(
IsEqualGUID
(
guid
,
&
GUID_RampForce
))
{
return
DIEFT_RAMPFORCE
;
}
else
if
(
IsEqualGUID
(
guid
,
&
GUID_Spring
)
||
IsEqualGUID
(
guid
,
&
GUID_Damper
)
||
IsEqualGUID
(
guid
,
&
GUID_Inertia
)
||
IsEqualGUID
(
guid
,
&
GUID_Friction
))
{
return
DIEFT_CONDITION
;
}
else
if
(
IsEqualGUID
(
guid
,
&
GUID_CustomForce
))
{
return
DIEFT_CUSTOMFORCE
;
}
else
{
WARN
(
"GUID (%s) is not a known force type
\n
"
,
_dump_dinput_GUID
(
guid
));
return
0
;
}
}
/******************************************************************************
* DirectInputEffect debug helpers
*/
static
void
_dump_DIEFFECT_flags
(
DWORD
dwFlags
)
{
if
(
TRACE_ON
(
dinput
))
{
unsigned
int
i
;
static
const
struct
{
DWORD
mask
;
const
char
*
name
;
}
flags
[]
=
{
#define FE(x) { x, #x}
FE
(
DIEFF_CARTESIAN
),
FE
(
DIEFF_OBJECTIDS
),
FE
(
DIEFF_OBJECTOFFSETS
),
FE
(
DIEFF_POLAR
),
FE
(
DIEFF_SPHERICAL
)
#undef FE
};
for
(
i
=
0
;
i
<
(
sizeof
(
flags
)
/
sizeof
(
flags
[
0
]));
i
++
)
if
(
flags
[
i
].
mask
&
dwFlags
)
DPRINTF
(
"%s "
,
flags
[
i
].
name
);
DPRINTF
(
"
\n
"
);
}
}
static
void
_dump_DIENVELOPE
(
LPDIENVELOPE
env
)
{
if
(
env
->
dwSize
!=
sizeof
(
DIENVELOPE
))
{
WARN
(
"Non-standard DIENVELOPE structure size (%ld instead of %d).
\n
"
,
env
->
dwSize
,
sizeof
(
DIENVELOPE
));
}
TRACE
(
"Envelope has attack (level: %ld time: %ld), fade (level: %ld time: %ld)
\n
"
,
env
->
dwAttackLevel
,
env
->
dwAttackTime
,
env
->
dwFadeLevel
,
env
->
dwFadeTime
);
}
static
void
_dump_DICONSTANTFORCE
(
LPDICONSTANTFORCE
frc
)
{
TRACE
(
"Constant force has magnitude %ld
\n
"
,
frc
->
lMagnitude
);
}
static
void
_dump_DIPERIODIC
(
LPDIPERIODIC
frc
)
{
TRACE
(
"Periodic force has magnitude %ld, offset %ld, phase %ld, period %ld
\n
"
,
frc
->
dwMagnitude
,
frc
->
lOffset
,
frc
->
dwPhase
,
frc
->
dwPeriod
);
}
static
void
_dump_DIRAMPFORCE
(
LPDIRAMPFORCE
frc
)
{
TRACE
(
"Ramp force has start %ld, end %ld
\n
"
,
frc
->
lStart
,
frc
->
lEnd
);
}
static
void
_dump_DICONDITION
(
LPDICONDITION
frc
)
{
TRACE
(
"Condition has offset %ld, pos/neg coefficients %ld and %ld, pos/neg saturations %ld and %ld, deadband %ld
\n
"
,
frc
->
lOffset
,
frc
->
lPositiveCoefficient
,
frc
->
lNegativeCoefficient
,
frc
->
dwPositiveSaturation
,
frc
->
dwNegativeSaturation
,
frc
->
lDeadBand
);
}
static
void
_dump_DICUSTOMFORCE
(
LPDICUSTOMFORCE
frc
)
{
unsigned
int
i
;
TRACE
(
"Custom force uses %ld channels, sample period %ld. Has %ld samples at %p.
\n
"
,
frc
->
cChannels
,
frc
->
dwSamplePeriod
,
frc
->
cSamples
,
frc
->
rglForceData
);
if
(
frc
->
cSamples
%
frc
->
cChannels
!=
0
)
WARN
(
"Custom force has a non-integral samples-per-channel count!
\n
"
);
if
(
TRACE_ON
(
dinput
))
{
DPRINTF
(
"Custom force data (time aligned, axes in order):
\n
"
);
for
(
i
=
1
;
i
<=
frc
->
cSamples
;
++
i
)
{
DPRINTF
(
"%ld "
,
frc
->
rglForceData
[
i
]);
if
(
i
%
frc
->
cChannels
==
0
)
DPRINTF
(
"
\n
"
);
}
}
}
static
void
_dump_DIEFFECT
(
LPCDIEFFECT
eff
,
REFGUID
guid
)
{
unsigned
int
i
;
DWORD
type
=
_typeFromGUID
(
guid
);
TRACE
(
"Dumping DIEFFECT structure:
\n
"
);
TRACE
(
" - dwSize: %ld
\n
"
,
eff
->
dwSize
);
if
((
eff
->
dwSize
!=
sizeof
(
DIEFFECT
))
&&
(
eff
->
dwSize
!=
sizeof
(
DIEFFECT_DX5
)))
{
WARN
(
"Non-standard DIEFFECT structure size (%ld instead of %d or %d).
\n
"
,
eff
->
dwSize
,
sizeof
(
DIEFFECT
),
sizeof
(
DIEFFECT_DX5
));
}
TRACE
(
" - dwFlags: %ld
\n
"
,
eff
->
dwFlags
);
TRACE
(
" "
);
_dump_DIEFFECT_flags
(
eff
->
dwFlags
);
TRACE
(
" - dwDuration: %ld
\n
"
,
eff
->
dwDuration
);
TRACE
(
" - dwGain: %ld
\n
"
,
eff
->
dwGain
);
if
((
eff
->
dwGain
>
10000
)
||
(
eff
->
dwGain
<
0
))
WARN
(
"dwGain is out of range (0 - 10,000)
\n
"
);
TRACE
(
" - dwTriggerButton: %ld
\n
"
,
eff
->
dwTriggerButton
);
TRACE
(
" - dwTriggerRepeatInterval: %ld
\n
"
,
eff
->
dwTriggerRepeatInterval
);
TRACE
(
" - cAxes: %ld
\n
"
,
eff
->
cAxes
);
TRACE
(
" - rgdwAxes: %p
\n
"
,
eff
->
rgdwAxes
);
if
(
TRACE_ON
(
dinput
))
{
TRACE
(
" "
);
for
(
i
=
0
;
i
<
eff
->
cAxes
;
++
i
)
DPRINTF
(
"%ld "
,
eff
->
rgdwAxes
[
i
]);
DPRINTF
(
"
\n
"
);
}
TRACE
(
" - rglDirection: %p
\n
"
,
eff
->
rglDirection
);
TRACE
(
" - lpEnvelope: %p
\n
"
,
eff
->
lpEnvelope
);
TRACE
(
" - cbTypeSpecificParams: %ld
\n
"
,
eff
->
cbTypeSpecificParams
);
TRACE
(
" - lpvTypeSpecificParams: %p
\n
"
,
eff
->
lpvTypeSpecificParams
);
if
(
eff
->
dwSize
>
sizeof
(
DIEFFECT_DX5
))
TRACE
(
" - dwStartDelay: %ld
\n
"
,
eff
->
dwStartDelay
);
if
(
eff
->
lpEnvelope
!=
NULL
)
_dump_DIENVELOPE
(
eff
->
lpEnvelope
);
if
(
type
==
DIEFT_CONSTANTFORCE
)
{
if
(
eff
->
cbTypeSpecificParams
!=
sizeof
(
DICONSTANTFORCE
))
{
WARN
(
"Effect claims to be a constant force but the type-specific params are the wrong size!
\n
"
);
}
else
{
_dump_DICONSTANTFORCE
(
eff
->
lpvTypeSpecificParams
);
}
}
else
if
(
type
==
DIEFT_PERIODIC
)
{
if
(
eff
->
cbTypeSpecificParams
!=
sizeof
(
DIPERIODIC
))
{
WARN
(
"Effect claims to be a periodic force but the type-specific params are the wrong size!
\n
"
);
}
else
{
_dump_DIPERIODIC
(
eff
->
lpvTypeSpecificParams
);
}
}
else
if
(
type
==
DIEFT_RAMPFORCE
)
{
if
(
eff
->
cbTypeSpecificParams
!=
sizeof
(
DIRAMPFORCE
))
{
WARN
(
"Effect claims to be a ramp force but the type-specific params are the wrong size!
\n
"
);
}
else
{
_dump_DIRAMPFORCE
(
eff
->
lpvTypeSpecificParams
);
}
}
else
if
(
type
==
DIEFT_CONDITION
)
{
if
(
eff
->
cbTypeSpecificParams
!=
sizeof
(
DICONDITION
))
{
WARN
(
"Effect claims to be a condition but the type-specific params are the wrong size!
\n
"
);
}
else
{
_dump_DICONDITION
(
eff
->
lpvTypeSpecificParams
);
}
}
else
if
(
type
==
DIEFT_CUSTOMFORCE
)
{
if
(
eff
->
cbTypeSpecificParams
!=
sizeof
(
DICUSTOMFORCE
))
{
WARN
(
"Effect claims to be a custom force but the type-specific params are the wrong size!
\n
"
);
}
else
{
_dump_DICUSTOMFORCE
(
eff
->
lpvTypeSpecificParams
);
}
}
}
/******************************************************************************
* LinuxInputEffectImpl
*/
static
ULONG
WINAPI
LinuxInputEffectImpl_AddRef
(
LPDIRECTINPUTEFFECT
iface
)
{
LinuxInputEffectImpl
*
This
=
(
LinuxInputEffectImpl
*
)
iface
;
return
InterlockedIncrement
(
&
(
This
->
ref
));
}
static
HRESULT
WINAPI
LinuxInputEffectImpl_Download
(
LPDIRECTINPUTEFFECT
iface
)
{
LinuxInputEffectImpl
*
This
=
(
LinuxInputEffectImpl
*
)
iface
;
TRACE
(
"(this=%p)
\n
"
,
This
);
if
(
ioctl
(
This
->
fd
,
EVIOCSFF
,
&
This
->
effect
)
==
-
1
)
{
if
(
errno
==
ENOMEM
)
{
return
DIERR_DEVICEFULL
;
}
else
{
FIXME
(
"Could not upload effect. Assuming a disconnected device.
\n
"
);
return
DIERR_INPUTLOST
;
}
}
return
DI_OK
;
}
static
HRESULT
WINAPI
LinuxInputEffectImpl_Escape
(
LPDIRECTINPUTEFFECT
iface
,
LPDIEFFESCAPE
pesc
)
{
WARN
(
"(this=%p,%p): invalid: no hardware-specific escape codes in this"
" driver!
\n
"
,
iface
,
pesc
);
return
DI_OK
;
}
static
HRESULT
WINAPI
LinuxInputEffectImpl_GetEffectGuid
(
LPDIRECTINPUTEFFECT
iface
,
LPGUID
pguid
)
{
LinuxInputEffectImpl
*
This
=
(
LinuxInputEffectImpl
*
)
iface
;
TRACE
(
"(this=%p,%p)
\n
"
,
This
,
pguid
);
pguid
=
&
This
->
guid
;
return
DI_OK
;
}
static
HRESULT
WINAPI
LinuxInputEffectImpl_GetEffectStatus
(
LPDIRECTINPUTEFFECT
iface
,
LPDWORD
pdwFlags
)
{
TRACE
(
"(this=%p,%p)
\n
"
,
iface
,
pdwFlags
);
/* linux sends the effect status through an event.
* that event is trapped by our parent joystick driver
* and there is no clean way to pass it back to us. */
FIXME
(
"Not enough information to provide a status.
\n
"
);
(
*
pdwFlags
)
=
0
;
return
DI_OK
;
}
static
HRESULT
WINAPI
LinuxInputEffectImpl_GetParameters
(
LPDIRECTINPUTEFFECT
iface
,
LPDIEFFECT
peff
,
DWORD
dwFlags
)
{
HRESULT
diErr
=
DI_OK
;
LinuxInputEffectImpl
*
This
=
(
LinuxInputEffectImpl
*
)
iface
;
TRACE
(
"(this=%p,%p,%ld)
\n
"
,
This
,
peff
,
dwFlags
);
/* Major conversion factors are:
* times: millisecond (linux) -> microsecond (windows) (x * 1000)
* forces: scale 0x7FFF (linux) -> scale 10000 (windows) approx ((x / 33) * 10)
* angles: scale 0x7FFF (linux) -> scale 35999 (windows) approx ((x / 33) * 36)
* angle bases: 0 -> -y (down) (linux) -> 0 -> +x (right) (windows)
*/
if
(
dwFlags
&
DIEP_AXES
)
{
if
(
peff
->
cAxes
<
2
/* linuxinput effects always use 2 axes, x and y */
)
diErr
=
DIERR_MOREDATA
;
peff
->
cAxes
=
2
;
if
(
diErr
)
return
diErr
;
else
{
peff
->
rgdwAxes
[
0
]
=
DIJOFS_X
;
peff
->
rgdwAxes
[
1
]
=
DIJOFS_Y
;
}
}
if
(
dwFlags
&
DIEP_DIRECTION
)
{
if
(
peff
->
cAxes
<
2
)
diErr
=
DIERR_MOREDATA
;
peff
->
cAxes
=
2
;
if
(
diErr
)
return
diErr
;
else
{
if
(
peff
->
dwFlags
&
DIEFF_CARTESIAN
)
{
peff
->
rglDirection
[
0
]
=
(
long
)(
sin
(
M_PI
*
3
*
This
->
effect
.
direction
/
0x7FFF
)
*
1000
);
peff
->
rglDirection
[
1
]
=
(
long
)(
cos
(
M_PI
*
3
*
This
->
effect
.
direction
/
0x7FFF
)
*
1000
);
}
else
{
/* Polar and spherical coordinates are the same for two or less
* axes.
* Note that we also use this case if NO flags are marked.
* According to MSDN, we should return the direction in the
* format that it was specified in, if no flags are marked.
*/
peff
->
rglDirection
[
0
]
=
(
This
->
effect
.
direction
/
33
)
*
36
+
9000
;
if
(
peff
->
rglDirection
[
0
]
>
35999
)
peff
->
rglDirection
[
0
]
-=
35999
;
}
}
}
if
(
dwFlags
&
DIEP_DURATION
)
{
peff
->
dwDuration
=
(
DWORD
)
This
->
effect
.
replay
.
length
*
1000
;
}
if
(
dwFlags
&
DIEP_ENVELOPE
)
{
struct
ff_envelope
*
env
;
if
(
This
->
effect
.
type
==
FF_CONSTANT
)
env
=
&
This
->
effect
.
u
.
constant
.
envelope
;
else
if
(
This
->
effect
.
type
==
FF_PERIODIC
)
env
=
&
This
->
effect
.
u
.
periodic
.
envelope
;
else
if
(
This
->
effect
.
type
==
FF_RAMP
)
env
=
&
This
->
effect
.
u
.
ramp
.
envelope
;
else
env
=
NULL
;
if
(
env
==
NULL
)
{
peff
->
lpEnvelope
=
NULL
;
}
else
if
(
peff
->
lpEnvelope
==
NULL
)
{
return
DIERR_INVALIDPARAM
;
}
else
{
peff
->
lpEnvelope
->
dwAttackLevel
=
(
env
->
attack_level
/
33
)
*
10
;
peff
->
lpEnvelope
->
dwAttackTime
=
env
->
attack_length
*
1000
;
peff
->
lpEnvelope
->
dwFadeLevel
=
(
env
->
fade_level
/
33
)
*
10
;
peff
->
lpEnvelope
->
dwFadeTime
=
env
->
fade_length
*
1000
;
}
}
if
(
dwFlags
&
DIEP_GAIN
)
{
/* the linux input ff driver apparently has no support
* for setting the device's gain. */
peff
->
dwGain
=
DI_FFNOMINALMAX
;
}
if
(
dwFlags
&
DIEP_SAMPLEPERIOD
)
{
/* the linux input ff driver has no support for setting
* the playback sample period. 0 means default. */
peff
->
dwSamplePeriod
=
0
;
}
if
(
dwFlags
&
DIEP_STARTDELAY
)
{
peff
->
dwStartDelay
=
This
->
effect
.
replay
.
delay
*
1000
;
}
if
(
dwFlags
&
DIEP_TRIGGERBUTTON
)
{
FIXME
(
"LinuxInput button mapping needs redoing; for now, assuming we're using an actual joystick.
\n
"
);
peff
->
dwTriggerButton
=
DIJOFS_BUTTON
(
This
->
effect
.
trigger
.
button
-
BTN_JOYSTICK
);
}
if
(
dwFlags
&
DIEP_TRIGGERREPEATINTERVAL
)
{
peff
->
dwTriggerRepeatInterval
=
This
->
effect
.
trigger
.
interval
*
1000
;
}
if
(
dwFlags
&
DIEP_TYPESPECIFICPARAMS
)
{
int
expectedsize
=
0
;
if
(
This
->
effect
.
type
==
FF_PERIODIC
)
{
expectedsize
=
sizeof
(
DIPERIODIC
);
}
else
if
(
This
->
effect
.
type
==
FF_CONSTANT
)
{
expectedsize
=
sizeof
(
DICONSTANTFORCE
);
}
else
if
(
This
->
effect
.
type
==
FF_SPRING
||
This
->
effect
.
type
==
FF_FRICTION
||
This
->
effect
.
type
==
FF_INERTIA
||
This
->
effect
.
type
==
FF_DAMPER
)
{
expectedsize
=
sizeof
(
DICONDITION
)
*
2
;
}
else
if
(
This
->
effect
.
type
==
FF_RAMP
)
{
expectedsize
=
sizeof
(
DIRAMPFORCE
);
}
if
(
expectedsize
>
peff
->
cbTypeSpecificParams
)
diErr
=
DIERR_MOREDATA
;
peff
->
cbTypeSpecificParams
=
expectedsize
;
if
(
diErr
)
return
diErr
;
else
{
if
(
This
->
effect
.
type
==
FF_PERIODIC
)
{
LPDIPERIODIC
tsp
=
(
LPDIPERIODIC
)(
peff
->
lpvTypeSpecificParams
);
tsp
->
dwMagnitude
=
(
This
->
effect
.
u
.
periodic
.
magnitude
/
33
)
*
10
;
tsp
->
lOffset
=
(
This
->
effect
.
u
.
periodic
.
offset
/
33
)
*
10
;
tsp
->
dwPhase
=
(
This
->
effect
.
u
.
periodic
.
phase
/
33
)
*
36
;
tsp
->
dwPeriod
=
(
This
->
effect
.
u
.
periodic
.
period
*
1000
);
}
else
if
(
This
->
effect
.
type
==
FF_CONSTANT
)
{
LPDICONSTANTFORCE
tsp
=
(
LPDICONSTANTFORCE
)(
peff
->
lpvTypeSpecificParams
);
tsp
->
lMagnitude
=
(
This
->
effect
.
u
.
constant
.
level
/
33
)
*
10
;
}
else
if
(
This
->
effect
.
type
==
FF_SPRING
||
This
->
effect
.
type
==
FF_FRICTION
||
This
->
effect
.
type
==
FF_INERTIA
||
This
->
effect
.
type
==
FF_DAMPER
)
{
LPDICONDITION
tsp
=
(
LPDICONDITION
)(
peff
->
lpvTypeSpecificParams
);
int
i
;
for
(
i
=
0
;
i
<
2
;
++
i
)
{
tsp
[
i
].
lOffset
=
(
This
->
effect
.
u
.
condition
[
i
].
center
/
33
)
*
10
;
tsp
[
i
].
lPositiveCoefficient
=
(
This
->
effect
.
u
.
condition
[
i
].
right_coeff
/
33
)
*
10
;
tsp
[
i
].
lNegativeCoefficient
=
(
This
->
effect
.
u
.
condition
[
i
].
left_coeff
/
33
)
*
10
;
tsp
[
i
].
dwPositiveSaturation
=
(
This
->
effect
.
u
.
condition
[
i
].
right_saturation
/
33
)
*
10
;
tsp
[
i
].
dwNegativeSaturation
=
(
This
->
effect
.
u
.
condition
[
i
].
left_saturation
/
33
)
*
10
;
tsp
[
i
].
lDeadBand
=
(
This
->
effect
.
u
.
condition
[
i
].
deadband
/
33
)
*
10
;
}
}
else
if
(
This
->
effect
.
type
==
FF_RAMP
)
{
LPDIRAMPFORCE
tsp
=
(
LPDIRAMPFORCE
)(
peff
->
lpvTypeSpecificParams
);
tsp
->
lStart
=
(
This
->
effect
.
u
.
ramp
.
start_level
/
33
)
*
10
;
tsp
->
lEnd
=
(
This
->
effect
.
u
.
ramp
.
end_level
/
33
)
*
10
;
}
}
}
return
diErr
;
}
static
HRESULT
WINAPI
LinuxInputEffectImpl_Initialize
(
LPDIRECTINPUTEFFECT
iface
,
HINSTANCE
hinst
,
DWORD
dwVersion
,
REFGUID
rguid
)
{
FIXME
(
"(this=%p,%p,%ld,%s): stub!
\n
"
,
iface
,
hinst
,
dwVersion
,
debugstr_guid
(
rguid
));
return
DI_OK
;
}
static
HRESULT
WINAPI
LinuxInputEffectImpl_QueryInterface
(
LPDIRECTINPUTEFFECT
iface
,
REFIID
riid
,
void
**
ppvObject
)
{
LinuxInputEffectImpl
*
This
=
(
LinuxInputEffectImpl
*
)
iface
;
TRACE
(
"(this=%p,%s,%p)
\n
"
,
This
,
debugstr_guid
(
riid
),
ppvObject
);
if
(
IsEqualGUID
(
&
IID_IUnknown
,
riid
)
||
IsEqualGUID
(
&
IID_IDirectInputEffect
,
riid
))
{
LinuxInputEffectImpl_AddRef
(
iface
);
*
ppvObject
=
This
;
return
0
;
}
TRACE
(
"Unsupported interface!
\n
"
);
return
E_FAIL
;
}
static
HRESULT
WINAPI
LinuxInputEffectImpl_Start
(
LPDIRECTINPUTEFFECT
iface
,
DWORD
dwIterations
,
DWORD
dwFlags
)
{
struct
input_event
event
;
LinuxInputEffectImpl
*
This
=
(
LinuxInputEffectImpl
*
)
iface
;
TRACE
(
"(this=%p,%ld,%ld)
\n
"
,
This
,
dwIterations
,
dwFlags
);
if
(
!
(
dwFlags
&
DIES_NODOWNLOAD
))
{
/* Download the effect if necessary */
if
(
This
->
effect
.
id
==
-
1
)
{
HRESULT
res
=
LinuxInputEffectImpl_Download
(
iface
);
if
(
res
!=
DI_OK
)
return
res
;
}
}
if
(
dwFlags
&
DIES_SOLO
)
{
FIXME
(
"Solo mode requested: should be stopping all effects here!
\n
"
);
}
event
.
type
=
EV_FF
;
event
.
code
=
This
->
effect
.
id
;
event
.
value
=
dwIterations
;
if
(
write
(
This
->
fd
,
&
event
,
sizeof
(
event
))
==
-
1
)
{
FIXME
(
"Unable to write event. Assuming device disconnected.
\n
"
);
return
DIERR_INPUTLOST
;
}
return
DI_OK
;
}
static
HRESULT
WINAPI
LinuxInputEffectImpl_SetParameters
(
LPDIRECTINPUTEFFECT
iface
,
LPCDIEFFECT
peff
,
DWORD
dwFlags
)
{
LinuxInputEffectImpl
*
This
=
(
LinuxInputEffectImpl
*
)
iface
;
DWORD
type
=
_typeFromGUID
(
&
This
->
guid
);
HRESULT
retval
=
DI_OK
;
TRACE
(
"(this=%p,%p,%ld)
\n
"
,
This
,
peff
,
dwFlags
);
_dump_DIEFFECT
(
peff
,
&
This
->
guid
);
if
((
dwFlags
&
!
DIEP_NORESTART
&
!
DIEP_NODOWNLOAD
&
!
DIEP_START
)
==
0
)
{
/* set everything */
dwFlags
=
DIEP_AXES
|
DIEP_DIRECTION
|
DIEP_DURATION
|
DIEP_ENVELOPE
|
DIEP_GAIN
|
DIEP_SAMPLEPERIOD
|
DIEP_STARTDELAY
|
DIEP_TRIGGERBUTTON
|
DIEP_TRIGGERREPEATINTERVAL
|
DIEP_TYPESPECIFICPARAMS
;
}
if
(
dwFlags
&
DIEP_AXES
)
{
/* the linux input effect system only supports one or two axes */
if
(
peff
->
cAxes
>
2
)
return
DIERR_INVALIDPARAM
;
else
if
(
peff
->
cAxes
<
1
)
return
DIERR_INCOMPLETEEFFECT
;
}
/* some of this may look funky, but it's 'cause the linux driver and directx have
* different opinions about which way direction "0" is. directx has 0 along the x
* axis (left), linux has it along the y axis (down). */
if
(
dwFlags
&
DIEP_DIRECTION
)
{
if
(
peff
->
cAxes
==
1
)
{
if
(
peff
->
dwFlags
&
DIEFF_CARTESIAN
)
{
if
(
dwFlags
&
DIEP_AXES
)
{
if
(
peff
->
rgdwAxes
[
0
]
==
DIJOFS_X
&&
peff
->
rglDirection
[
0
]
>=
0
)
This
->
effect
.
direction
=
0x4000
;
else
if
(
peff
->
rgdwAxes
[
0
]
==
DIJOFS_X
&&
peff
->
rglDirection
[
0
]
<
0
)
This
->
effect
.
direction
=
0xC000
;
else
if
(
peff
->
rgdwAxes
[
0
]
==
DIJOFS_Y
&&
peff
->
rglDirection
[
0
]
>=
0
)
This
->
effect
.
direction
=
0
;
else
if
(
peff
->
rgdwAxes
[
0
]
==
DIJOFS_Y
&&
peff
->
rglDirection
[
0
]
<
0
)
This
->
effect
.
direction
=
0x8000
;
}
}
else
{
/* one-axis effects must use cartesian coords */
return
DIERR_INVALIDPARAM
;
}
}
else
{
/* two axes */
if
(
peff
->
dwFlags
&
DIEFF_CARTESIAN
)
{
/* avoid divide-by-zero */
if
(
peff
->
rglDirection
[
1
]
==
0
)
{
if
(
peff
->
rglDirection
[
0
]
>=
0
)
This
->
effect
.
direction
=
0x4000
;
else
if
(
peff
->
rglDirection
[
0
]
<
0
)
This
->
effect
.
direction
=
0xC000
;
}
else
{
This
->
effect
.
direction
=
(
int
)(
atan
(
peff
->
rglDirection
[
0
]
/
peff
->
rglDirection
[
1
])
*
0x7FFF
/
(
3
*
M_PI
));
}
}
else
{
/* Polar and spherical are the same for 2 axes */
/* Precision is important here, so we do double math with exact constants */
This
->
effect
.
direction
=
(
int
)(((
double
)
peff
->
rglDirection
[
0
]
-
90
)
/
35999
)
*
0x7FFF
;
}
}
}
if
(
dwFlags
&
DIEP_DURATION
)
This
->
effect
.
replay
.
length
=
peff
->
dwDuration
/
1000
;
if
(
dwFlags
&
DIEP_ENVELOPE
)
{
struct
ff_envelope
*
env
;
if
(
This
->
effect
.
type
==
FF_CONSTANT
)
env
=
&
This
->
effect
.
u
.
constant
.
envelope
;
else
if
(
This
->
effect
.
type
==
FF_PERIODIC
)
env
=
&
This
->
effect
.
u
.
periodic
.
envelope
;
else
if
(
This
->
effect
.
type
==
FF_RAMP
)
env
=
&
This
->
effect
.
u
.
ramp
.
envelope
;
else
env
=
NULL
;
if
(
peff
->
lpEnvelope
==
NULL
)
{
/* if this type had an envelope, reset it
* note that length can never be zero, so we set it to something miniscule */
if
(
env
)
{
env
->
attack_length
=
0x10
;
env
->
attack_level
=
0x7FFF
;
env
->
fade_length
=
0x10
;
env
->
fade_level
=
0x7FFF
;
}
}
else
{
/* did we get passed an envelope for a type that doesn't even have one? */
if
(
!
env
)
return
DIERR_INVALIDPARAM
;
/* copy the envelope */
env
->
attack_length
=
peff
->
lpEnvelope
->
dwAttackTime
/
1000
;
env
->
attack_level
=
(
peff
->
lpEnvelope
->
dwAttackLevel
/
10
)
*
32
;
env
->
fade_length
=
peff
->
lpEnvelope
->
dwFadeTime
/
1000
;
env
->
fade_level
=
(
peff
->
lpEnvelope
->
dwFadeLevel
/
10
)
*
32
;
}
}
/* Gain and Sample Period settings are not supported by the linux
* event system */
if
(
dwFlags
&
DIEP_GAIN
)
TRACE
(
"Gain requested but no gain functionality present.
\n
"
);
if
(
dwFlags
&
DIEP_SAMPLEPERIOD
)
TRACE
(
"Sample period requested but no sample period functionality present.
\n
"
);
if
(
dwFlags
&
DIEP_STARTDELAY
)
This
->
effect
.
replay
.
delay
=
peff
->
dwStartDelay
/
1000
;
if
(
dwFlags
&
DIEP_TRIGGERBUTTON
)
{
if
(
peff
->
dwTriggerButton
!=
-
1
)
{
FIXME
(
"Linuxinput button mapping needs redoing, assuming we're using a joystick.
\n
"
);
FIXME
(
"Trigger button translation not yet implemented!
\n
"
);
}
This
->
effect
.
trigger
.
button
=
0
;
}
if
(
dwFlags
&
DIEP_TRIGGERREPEATINTERVAL
)
This
->
effect
.
trigger
.
interval
=
peff
->
dwTriggerRepeatInterval
/
1000
;
if
(
dwFlags
&
DIEP_TYPESPECIFICPARAMS
)
{
if
(
!
(
peff
->
lpvTypeSpecificParams
))
return
DIERR_INCOMPLETEEFFECT
;
if
(
type
==
DIEFT_PERIODIC
)
{
LPCDIPERIODIC
tsp
;
if
(
peff
->
cbTypeSpecificParams
!=
sizeof
(
DIPERIODIC
))
return
DIERR_INVALIDPARAM
;
tsp
=
(
LPCDIPERIODIC
)(
peff
->
lpvTypeSpecificParams
);
This
->
effect
.
u
.
periodic
.
magnitude
=
(
tsp
->
dwMagnitude
/
10
)
*
32
;
This
->
effect
.
u
.
periodic
.
offset
=
(
tsp
->
lOffset
/
10
)
*
32
;
This
->
effect
.
u
.
periodic
.
phase
=
(
tsp
->
dwPhase
/
9
)
*
8
;
/* == (/ 36 * 32) */
This
->
effect
.
u
.
periodic
.
period
=
tsp
->
dwPeriod
/
1000
;
}
else
if
(
type
==
DIEFT_CONSTANTFORCE
)
{
LPCDICONSTANTFORCE
tsp
;
if
(
peff
->
cbTypeSpecificParams
!=
sizeof
(
DICONSTANTFORCE
))
return
DIERR_INVALIDPARAM
;
tsp
=
(
LPCDICONSTANTFORCE
)(
peff
->
lpvTypeSpecificParams
);
This
->
effect
.
u
.
constant
.
level
=
(
tsp
->
lMagnitude
/
10
)
*
32
;
}
else
if
(
type
==
DIEFT_RAMPFORCE
)
{
LPCDIRAMPFORCE
tsp
;
if
(
peff
->
cbTypeSpecificParams
!=
sizeof
(
DIRAMPFORCE
))
return
DIERR_INVALIDPARAM
;
tsp
=
(
LPCDIRAMPFORCE
)(
peff
->
lpvTypeSpecificParams
);
This
->
effect
.
u
.
ramp
.
start_level
=
(
tsp
->
lStart
/
10
)
*
32
;
This
->
effect
.
u
.
ramp
.
end_level
=
(
tsp
->
lStart
/
10
)
*
32
;
}
else
if
(
type
==
DIEFT_CONDITION
)
{
LPCDICONDITION
tsp
=
(
LPCDICONDITION
)(
peff
->
lpvTypeSpecificParams
);
if
(
peff
->
cbTypeSpecificParams
==
sizeof
(
DICONDITION
))
{
/* One condition block. This needs to be rotated to direction,
* and expanded to separate x and y conditions. */
int
i
;
double
factor
[
2
];
factor
[
0
]
=
asin
((
This
->
effect
.
direction
*
3
.
0
*
M_PI
)
/
0x7FFF
);
factor
[
1
]
=
acos
((
This
->
effect
.
direction
*
3
.
0
*
M_PI
)
/
0x7FFF
);
for
(
i
=
0
;
i
<
2
;
++
i
)
{
This
->
effect
.
u
.
condition
[
i
].
center
=
(
int
)(
factor
[
i
]
*
(
tsp
->
lOffset
/
10
)
*
32
);
This
->
effect
.
u
.
condition
[
i
].
right_coeff
=
(
int
)(
factor
[
i
]
*
(
tsp
->
lPositiveCoefficient
/
10
)
*
32
);
This
->
effect
.
u
.
condition
[
i
].
left_coeff
=
(
int
)(
factor
[
i
]
*
(
tsp
->
lNegativeCoefficient
/
10
)
*
32
);
This
->
effect
.
u
.
condition
[
i
].
right_saturation
=
(
int
)(
factor
[
i
]
*
(
tsp
->
dwPositiveSaturation
/
10
)
*
32
);
This
->
effect
.
u
.
condition
[
i
].
left_saturation
=
(
int
)(
factor
[
i
]
*
(
tsp
->
dwNegativeSaturation
/
10
)
*
32
);
This
->
effect
.
u
.
condition
[
i
].
deadband
=
(
int
)(
factor
[
i
]
*
(
tsp
->
lDeadBand
/
10
)
*
32
);
}
}
else
if
(
peff
->
cbTypeSpecificParams
==
2
*
sizeof
(
DICONDITION
))
{
/* Two condition blocks. Direct parameter copy. */
int
i
;
for
(
i
=
0
;
i
<
2
;
++
i
)
{
This
->
effect
.
u
.
condition
[
i
].
center
=
(
tsp
[
i
].
lOffset
/
10
)
*
32
;
This
->
effect
.
u
.
condition
[
i
].
right_coeff
=
(
tsp
[
i
].
lPositiveCoefficient
/
10
)
*
32
;
This
->
effect
.
u
.
condition
[
i
].
left_coeff
=
(
tsp
[
i
].
lNegativeCoefficient
/
10
)
*
32
;
This
->
effect
.
u
.
condition
[
i
].
right_saturation
=
(
tsp
[
i
].
dwPositiveSaturation
/
10
)
*
32
;
This
->
effect
.
u
.
condition
[
i
].
left_saturation
=
(
tsp
[
i
].
dwNegativeSaturation
/
10
)
*
32
;
This
->
effect
.
u
.
condition
[
i
].
deadband
=
(
tsp
[
i
].
lDeadBand
/
10
)
*
32
;
}
}
else
{
return
DIERR_INVALIDPARAM
;
}
}
else
{
FIXME
(
"Custom force types are not supported
\n
"
);
return
DIERR_INVALIDPARAM
;
}
}
if
(
!
(
dwFlags
&
DIEP_NODOWNLOAD
))
retval
=
LinuxInputEffectImpl_Download
(
iface
);
if
(
retval
!=
DI_OK
)
return
retval
;
if
(
dwFlags
&
DIEP_NORESTART
)
TRACE
(
"DIEP_NORESTART: not handled (we have no control of that).
\n
"
);
if
(
dwFlags
&
DIEP_START
)
retval
=
LinuxInputEffectImpl_Start
(
iface
,
1
,
0
);
if
(
retval
!=
DI_OK
)
return
retval
;
return
DI_OK
;
}
static
ULONG
WINAPI
LinuxInputEffectImpl_Release
(
LPDIRECTINPUTEFFECT
iface
)
{
LinuxInputEffectImpl
*
This
=
(
LinuxInputEffectImpl
*
)
iface
;
ULONG
ref
=
InterlockedDecrement
(
&
(
This
->
ref
));
if
(
ref
==
0
)
HeapFree
(
GetProcessHeap
(),
0
,
This
);
return
ref
;
}
static
HRESULT
WINAPI
LinuxInputEffectImpl_Stop
(
LPDIRECTINPUTEFFECT
iface
)
{
struct
input_event
event
;
LinuxInputEffectImpl
*
This
=
(
LinuxInputEffectImpl
*
)
iface
;
TRACE
(
"(this=%p)
\n
"
,
This
);
event
.
type
=
EV_FF
;
event
.
code
=
This
->
effect
.
id
;
event
.
value
=
0
;
/* we don't care about the success or failure of this call */
write
(
This
->
fd
,
&
event
,
sizeof
(
event
));
return
DI_OK
;
}
static
HRESULT
WINAPI
LinuxInputEffectImpl_Unload
(
LPDIRECTINPUTEFFECT
iface
)
{
LinuxInputEffectImpl
*
This
=
(
LinuxInputEffectImpl
*
)
iface
;
TRACE
(
"(this=%p)
\n
"
,
This
);
/* Erase the downloaded effect */
if
(
ioctl
(
This
->
fd
,
EVIOCRMFF
,
This
->
effect
.
id
)
==
-
1
)
return
DIERR_INVALIDPARAM
;
/* Mark the effect as deallocated */
This
->
effect
.
id
=
-
1
;
return
DI_OK
;
}
/******************************************************************************
* LinuxInputEffect
*/
HRESULT
linuxinput_create_effect
(
int
fd
,
REFGUID
rguid
,
LPDIRECTINPUTEFFECT
*
peff
)
{
LinuxInputEffectImpl
*
newEffect
=
HeapAlloc
(
GetProcessHeap
(),
HEAP_ZERO_MEMORY
,
sizeof
(
LinuxInputEffectImpl
));
DWORD
type
=
_typeFromGUID
(
rguid
);
newEffect
->
lpVtbl
=
&
LinuxInputEffectVtbl
;
newEffect
->
ref
=
1
;
memcpy
(
&
(
newEffect
->
guid
),
rguid
,
sizeof
(
*
rguid
));
newEffect
->
fd
=
fd
;
/* set the type. this cannot be changed over the effect's life. */
switch
(
type
)
{
case
DIEFT_PERIODIC
:
newEffect
->
effect
.
type
=
FF_PERIODIC
;
if
(
IsEqualGUID
(
rguid
,
&
GUID_Sine
))
{
newEffect
->
effect
.
u
.
periodic
.
waveform
=
FF_SINE
;
}
else
if
(
IsEqualGUID
(
rguid
,
&
GUID_Triangle
))
{
newEffect
->
effect
.
u
.
periodic
.
waveform
=
FF_TRIANGLE
;
}
else
if
(
IsEqualGUID
(
rguid
,
&
GUID_Square
))
{
newEffect
->
effect
.
u
.
periodic
.
waveform
=
FF_SQUARE
;
}
else
if
(
IsEqualGUID
(
rguid
,
&
GUID_SawtoothUp
))
{
newEffect
->
effect
.
u
.
periodic
.
waveform
=
FF_SAW_UP
;
}
else
if
(
IsEqualGUID
(
rguid
,
&
GUID_SawtoothDown
))
{
newEffect
->
effect
.
u
.
periodic
.
waveform
=
FF_SAW_DOWN
;
}
break
;
case
DIEFT_CONSTANTFORCE
:
newEffect
->
effect
.
type
=
FF_CONSTANT
;
break
;
case
DIEFT_RAMPFORCE
:
newEffect
->
effect
.
type
=
FF_RAMP
;
break
;
case
DIEFT_CONDITION
:
if
(
IsEqualGUID
(
rguid
,
&
GUID_Spring
))
{
newEffect
->
effect
.
type
=
FF_SPRING
;
}
else
if
(
IsEqualGUID
(
rguid
,
&
GUID_Friction
))
{
newEffect
->
effect
.
type
=
FF_FRICTION
;
}
else
if
(
IsEqualGUID
(
rguid
,
&
GUID_Inertia
))
{
newEffect
->
effect
.
type
=
FF_INERTIA
;
}
else
if
(
IsEqualGUID
(
rguid
,
&
GUID_Damper
))
{
newEffect
->
effect
.
type
=
FF_DAMPER
;
}
break
;
case
DIEFT_CUSTOMFORCE
:
FIXME
(
"Custom forces are not supported.
\n
"
);
HeapFree
(
GetProcessHeap
(),
0
,
newEffect
);
return
DIERR_INVALIDPARAM
;
default:
FIXME
(
"Unkown force type.
\n
"
);
HeapFree
(
GetProcessHeap
(),
0
,
newEffect
);
return
DIERR_INVALIDPARAM
;
}
/* mark as non-uploaded */
newEffect
->
effect
.
id
=
-
1
;
*
peff
=
(
LPDIRECTINPUTEFFECT
)
newEffect
;
TRACE
(
"Creating linux input system effect (%p) with guid %s
\n
"
,
*
peff
,
_dump_dinput_GUID
(
rguid
));
return
DI_OK
;
}
HRESULT
linuxinput_get_info_A
(
int
fd
,
REFGUID
rguid
,
LPDIEFFECTINFOA
info
)
{
DWORD
type
=
_typeFromGUID
(
rguid
);
TRACE
(
"(%d, %s, %p) type=%ld
\n
"
,
fd
,
_dump_dinput_GUID
(
rguid
),
info
,
type
);
if
(
!
info
)
return
E_POINTER
;
if
(
info
->
dwSize
!=
sizeof
(
DIEFFECTINFOA
))
return
DIERR_INVALIDPARAM
;
info
->
guid
=
*
rguid
;
info
->
dwEffType
=
type
;
/* the event device API does not support querying for all these things
* therefore we assume that we have support for them
* that's not as dangerous as it sounds, since drivers are allowed to
* ignore parameters they claim to support anyway */
info
->
dwEffType
|=
DIEFT_DEADBAND
|
DIEFT_FFATTACK
|
DIEFT_FFFADE
|
DIEFT_POSNEGCOEFFICIENTS
|
DIEFT_POSNEGSATURATION
|
DIEFT_SATURATION
|
DIEFT_STARTDELAY
;
/* again, assume we have support for everything */
info
->
dwStaticParams
=
DIEP_ALLPARAMS
;
info
->
dwDynamicParams
=
info
->
dwStaticParams
;
/* yes, this is windows behavior (print the GUID_Name for name) */
strcpy
((
char
*
)
&
(
info
->
tszName
),
_dump_dinput_GUID
(
rguid
));
return
DI_OK
;
}
HRESULT
linuxinput_get_info_W
(
int
fd
,
REFGUID
rguid
,
LPDIEFFECTINFOW
info
)
{
DWORD
type
=
_typeFromGUID
(
rguid
);
TRACE
(
"(%d, %s, %p) type=%ld
\n
"
,
fd
,
_dump_dinput_GUID
(
rguid
),
info
,
type
);
if
(
!
info
)
return
E_POINTER
;
if
(
info
->
dwSize
!=
sizeof
(
DIEFFECTINFOW
))
return
DIERR_INVALIDPARAM
;
info
->
guid
=
*
rguid
;
info
->
dwEffType
=
type
;
/* the event device API does not support querying for all these things
* therefore we assume that we have support for them
* that's not as dangerous as it sounds, since drivers are allowed to
* ignore parameters they claim to support anyway */
info
->
dwEffType
|=
DIEFT_DEADBAND
|
DIEFT_FFATTACK
|
DIEFT_FFFADE
|
DIEFT_POSNEGCOEFFICIENTS
|
DIEFT_POSNEGSATURATION
|
DIEFT_SATURATION
|
DIEFT_STARTDELAY
;
/* again, assume we have support for everything */
info
->
dwStaticParams
=
DIEP_ALLPARAMS
;
info
->
dwDynamicParams
=
info
->
dwStaticParams
;
/* yes, this is windows behavior (print the GUID_Name for name) */
MultiByteToWideChar
(
CP_ACP
,
0
,
_dump_dinput_GUID
(
rguid
),
-
1
,
(
WCHAR
*
)
&
(
info
->
tszName
),
sizeof
(
WCHAR
)
*
MAX_PATH
);
return
DI_OK
;
}
static
const
IDirectInputEffectVtbl
LinuxInputEffectVtbl
=
{
LinuxInputEffectImpl_QueryInterface
,
LinuxInputEffectImpl_AddRef
,
LinuxInputEffectImpl_Release
,
LinuxInputEffectImpl_Initialize
,
LinuxInputEffectImpl_GetEffectGuid
,
LinuxInputEffectImpl_GetParameters
,
LinuxInputEffectImpl_SetParameters
,
LinuxInputEffectImpl_Start
,
LinuxInputEffectImpl_Stop
,
LinuxInputEffectImpl_GetEffectStatus
,
LinuxInputEffectImpl_Download
,
LinuxInputEffectImpl_Unload
,
LinuxInputEffectImpl_Escape
};
#endif
/* HAVE_STRUCT_FF_EFFECT_DIRECTION */
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment