Skip to content

Commit 7cf6ce3

Browse files
committed
Joy-Cons, Pro controller rumbles, battery status
1 parent 924f439 commit 7cf6ce3

File tree

4 files changed

+163
-52
lines changed

4 files changed

+163
-52
lines changed

README.RU.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,15 +80,16 @@
8080

8181

8282

83-
**Не работает вибрация на Nintendo контроллерах**<br>
84-
К сожалению, это пока не реализовано.
83+
**Не работает вибрация на Nintendo Pro контроллере**<br>
84+
Измените параметр `ProContollerRumble` на `1`, перезапустите программу и проверьте. Возможно заработает.
8585

8686
## Благодарности
8787
* Sony и Nintendo за самые продвинутые геймпады и инвестирование в инновации, а также за продвижение инноваций в игры.
8888
* [ViGEm](https://github.com/ViGEm) за возможность эмуляции разных геймпадов.
8989
* [HIDAPI library](https://github.com/signal11/hidapi), с [исправлениями](https://github.com/libusb/hidapi), за библиотеку для работы с USB устройства. В проекте используется этот [форк](https://github.com/r57zone/hidapi).
9090
* [JoyShockLibrary](https://github.com/JibbSmart/JoyShockLibrary) за классную библиотеку геймпадов, позволяющую легко получить вращение контроллера. Также используется некоторый код из этой библиотеки и [пример JibbSmart](https://gist.github.com/JibbSmart/8cbaba568c1c2e1193771459aa5385df) для прицеливания.
9191
* DS4Windows[[1]](https://github.com/Jays2Kings/DS4Windows)[[2]](https://github.com/Ryochan7/DS4Windows) за уровень заряда батареи.
92+
* [JoyCon-Driver](https://github.com/fossephate/JoyCon-Driver/blob/857e4e76e26f05d72400ae5d9f2a22cae88f3548/joycon-driver/include/Joycon.hpp) за вибрацию джойконов.
9293

9394
## Сборка
9495
1. Загрузите исходники и распакуйте.

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,15 +81,16 @@ In some games, for example, Max Payne or Crysis 2, unfortunately, this don'n wor
8181

8282

8383

84-
**Vibration does not work on Nintendo controllers**<br>
85-
Unfortunately, this is not yet implemented.
84+
**Rumble don't work on Nintendo Pro controller**<br>
85+
Change the `ProContollerRumble` parameter to `1`, restart the program and check. It might work.
8686

8787
## Credits
8888
* Sony and Nintendo for the most advanced gamepads and investment in innovation, and for driving innovation in games.
8989
* [ViGEm](https://github.com/ViGEm) for the ability to emulate different gamepads.
9090
* [HIDAPI library](https://github.com/signal11/hidapi) with [fixes](https://github.com/libusb/hidapi) for the library to work with a USB devices. The project uses this [fork](https://github.com/r57zone/hidapi).
9191
* [JoyShockLibrary](https://github.com/JibbSmart/JoyShockLibrary) for a cool gamepad library that makes it easy to get controller rotation. Also uses some code from this library and [JibbSmart snippet](https://gist.github.com/JibbSmart/8cbaba568c1c2e1193771459aa5385df) for aiming.
9292
* DS4Windows[[1]](https://github.com/Jays2Kings/DS4Windows)[[2]](https://github.com/Ryochan7/DS4Windows) for the battery level.
93+
* [JoyCon-Driver](https://github.com/fossephate/JoyCon-Driver/blob/857e4e76e26f05d72400ae5d9f2a22cae88f3548/joycon-driver/include/Joycon.hpp) for Joy-Cons rumble.
9394

9495
## Building
9596
1. Download the sources and unzip them.

Source/DSAdvance/DSAdvance.cpp

Lines changed: 152 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -42,19 +42,13 @@ EulerAngles QuaternionToEulerAngle(double qW, double qX, double qY, double qZ) /
4242
return resAngles;
4343
}
4444

45-
double RadToDeg(double Rad)
46-
{
47-
return Rad / 3.14159265358979323846 * 180.0;
48-
}
49-
50-
double OffsetYPR(float Angle1, float Angle2)
45+
float ClampFloat(float Value, float Min, float Max)
5146
{
52-
Angle1 -= Angle2;
53-
if (Angle1 < -180)
54-
Angle1 += 360;
55-
else if (Angle1 > 180)
56-
Angle1 -= 360;
57-
return Angle1;
47+
if (Value > Max)
48+
Value = Max;
49+
else if (Value < Min)
50+
Value = Min;
51+
return Value;
5852
}
5953

6054
void GamepadSetState(InputOutState OutState)
@@ -163,16 +157,48 @@ void GamepadSetState(InputOutState OutState)
163157

164158
hid_write(CurGamepad.HidHandle, &outputReport[1], 78);
165159
}
166-
//} else if (CurGamepad.ControllerType == NINTENDO_JOYCON_L || CurGamepad.ControllerType == NINTENDO_JOYCON_R) {
167-
/*unsigned char outputReport[10];
168-
memset(outputReport, 0, 10);
160+
}
161+
else if (CurGamepad.ControllerType == NINTENDO_JOYCONS) {
169162

163+
// Left Joycon
164+
unsigned char outputReport[64] = { 0 };
170165
outputReport[0] = 0x10;
171-
outputReport[1] = 0x01;
172-
166+
outputReport[1] = (++CurGamepad.RumbleOffCounter) & 0xF; if (CurGamepad.RumbleOffCounter > 0xF) CurGamepad.RumbleOffCounter = 0x0;
167+
outputReport[2] = std::clamp(OutState.SmallMotor - 0, 0, 229); // It seems that it is not possible to use the Nintendo Switch motors at 100%, so we will limit ourselves to 90%.
168+
//outputReport[9] = 0x1;
169+
//outputReport[13] = 0x1;
173170

174171
hid_write(CurGamepad.HidHandle, outputReport, 10);
175-
*/
172+
173+
// Right Joycon
174+
if (CurGamepad.HidHandle2 != NULL) {
175+
outputReport[6] = std::clamp(OutState.LargeMotor - 0, 0, 229);
176+
hid_write(CurGamepad.HidHandle2, outputReport, 10);
177+
}
178+
179+
if (OutState.SmallMotor == 0 && OutState.LargeMotor == 0) CurGamepad.RumbleOffCounter = 2; // Looks like Nintendo needs some "0" rumble packets to stop it
180+
181+
182+
} else if (CurGamepad.ControllerType == NINTENDO_SWITCH_PRO) { // Need test
183+
if (CurGamepad.TestRumbleProController) {
184+
unsigned char outputReport[64] = { 0 };
185+
outputReport[0] = 0x10;
186+
outputReport[1] = (++CurGamepad.RumbleOffCounter) & 0xF; if (CurGamepad.RumbleOffCounter > 0xF) CurGamepad.RumbleOffCounter = 0x0;
187+
outputReport[2] = std::clamp(OutState.SmallMotor - 0, 0, 229); // It seems that it is not possible to use the Nintendo Switch motors at 100%, so we will limit ourselves to 90%.
188+
outputReport[6] = std::clamp(OutState.LargeMotor - 0, 0, 229);
189+
//outputReport[9] = 0x1;
190+
//outputReport[13] = 0x1;
191+
192+
// USB?
193+
if (CurGamepad.USBConnection) {
194+
outputReport[0x00] = 0x80;
195+
outputReport[0x01] = 0x92;
196+
outputReport[0x03] = 0x31;
197+
outputReport[0x08] = 0x10;
198+
}
199+
200+
hid_write(CurGamepad.HidHandle, outputReport, 10);
201+
}
176202
} else {
177203
//if (JslGetControllerType(0) == JS_TYPE_DS || JslGetControllerType(0) == JS_TYPE_DS4)
178204
//JslSetLightColour(0, (std::clamp(OutState.LEDRed - OutState.LEDBrightness, 0, 255) << 16) + (std::clamp(OutState.LEDGreen - OutState.LEDBrightness, 0, 255) << 8) + std::clamp(OutState.LEDBlue - OutState.LEDBrightness, 0, 255)); // https://github.com/CyberPlaton/_Nautilus_/blob/master/Engine/PSGamepad.cpp
@@ -243,6 +269,37 @@ void GamepadSearch() {
243269
cur_dev = cur_dev->next;
244270
}
245271

272+
// Nintendo compatible controllers
273+
cur_dev = hid_enumerate(NINTENDO_VENDOR, 0x0);
274+
while (cur_dev) {
275+
if (cur_dev->product_id == NINTENDO_JOYCON_L)
276+
{
277+
CurGamepad.HidHandle = hid_open(cur_dev->vendor_id, cur_dev->product_id, cur_dev->serial_number);
278+
hid_set_nonblocking(CurGamepad.HidHandle, 1);
279+
CurGamepad.USBConnection = false;
280+
CurGamepad.ControllerType = NINTENDO_JOYCONS;
281+
} else if (cur_dev->product_id == NINTENDO_JOYCON_R) {
282+
CurGamepad.HidHandle2 = hid_open(cur_dev->vendor_id, cur_dev->product_id, cur_dev->serial_number);
283+
hid_set_nonblocking(CurGamepad.HidHandle2, 1);
284+
CurGamepad.USBConnection = false;
285+
CurGamepad.ControllerType = NINTENDO_JOYCONS;
286+
287+
} else if (cur_dev->product_id == NINTENDO_SWITCH_PRO) {
288+
CurGamepad.HidHandle = hid_open(cur_dev->vendor_id, cur_dev->product_id, cur_dev->serial_number);
289+
CurGamepad.ControllerType = NINTENDO_SWITCH_PRO;
290+
hid_set_nonblocking(CurGamepad.HidHandle, 1);
291+
CurGamepad.USBConnection = true;
292+
293+
// BT detection
294+
unsigned char buf[64];
295+
memset(buf, 0, sizeof(buf));
296+
int bytesRead = hid_read_timeout(CurGamepad.HidHandle, buf, sizeof(buf), 100);
297+
if (bytesRead > 0 && buf[0] == 0x11)
298+
CurGamepad.USBConnection = false;
299+
}
300+
cur_dev = cur_dev->next;
301+
}
302+
246303
if (cur_dev)
247304
hid_free_enumeration(cur_dev);
248305
}
@@ -274,12 +331,17 @@ void GetBatteryInfo() {
274331
CurGamepad.BatteryLevel = (buf[30] & DS_STATUS_BATTERY_CAPACITY) * 100 / DS4_USB_BATTERY_MAX;
275332
else
276333
CurGamepad.BatteryLevel = (buf[32] & DS_STATUS_BATTERY_CAPACITY) * 100 / DS_BATTERY_MAX;
277-
/*} else if (CurGamepad.ControllerType == NINTENDO_JOYCON_L || CurGamepad.ControllerType == NINTENDO_JOYCON_R) {
334+
} else if (CurGamepad.ControllerType == NINTENDO_JOYCONS || CurGamepad.ControllerType == NINTENDO_SWITCH_PRO) {
278335
unsigned char buf[64];
279336
memset(buf, 0, sizeof(buf));
280-
281337
hid_read(CurGamepad.HidHandle, buf, 64);
282-
CurGamepad.BatteryLevel = (buf[2] & 0x0F) * 100 / 5;*/
338+
CurGamepad.BatteryLevel = ((buf[2] >> 4) & 0x0F) * 100 / 8;
339+
340+
if (CurGamepad.HidHandle2 != NULL) {
341+
memset(buf, 0, sizeof(buf));
342+
hid_read(CurGamepad.HidHandle2, buf, 64);
343+
CurGamepad.BatteryLevel2 = ((buf[2] >> 4) & 0x0F) * 100 / 8;
344+
}
283345
}
284346
if (CurGamepad.BatteryLevel > 100) CurGamepad.BatteryLevel = 100; // It looks like something is not right, once it gave out 125%
285347
}
@@ -350,15 +412,6 @@ float DeadZoneAxis(float StickAxis, float DeadZoneValue) // Possibly wrong
350412
return StickAxis * 1 / (1 - DeadZoneValue); // 1 - max value of stick
351413
}
352414

353-
float ClampFloat(float Value, float Min, float Max)
354-
{
355-
if (Value > Max)
356-
Value = Max;
357-
else if (Value < Min)
358-
Value = Min;
359-
return Value;
360-
}
361-
362415
float accumulatedX = 0, accumulatedY = 0;
363416
void MouseMove(float x, float y) { // Implementation from https://github.com/JibbSmart/JoyShockMapper/blob/master/JoyShockMapper/src/win32/InputHelpers.cpp
364417
accumulatedX += x;
@@ -394,7 +447,12 @@ SHORT ToLeftStick(double Value, float WheelAngle)
394447
return LeftAxisX;
395448
}
396449

397-
double OffsetYPR2(double Angle1, double Angle2) // CalcMotionStick
450+
double RadToDeg(double Rad)
451+
{
452+
return Rad / 3.14159265358979323846 * 180.0;
453+
}
454+
455+
double OffsetYPR(double Angle1, double Angle2) // CalcMotionStick
398456
{
399457
Angle1 -= Angle2;
400458
if (Angle1 < -3.14159265358979323846)
@@ -407,7 +465,7 @@ double OffsetYPR2(double Angle1, double Angle2) // CalcMotionStick
407465
SHORT CalcMotionStick(float gravA, float gravB, float wheelAngle, float offsetAxis) {
408466
float angleRadians = wheelAngle * (3.14159f / 180.0f); // To radians
409467

410-
float normalizedValue = OffsetYPR2(atan2f(gravA, gravB), offsetAxis) / angleRadians;
468+
float normalizedValue = OffsetYPR(atan2f(gravA, gravB), offsetAxis) / angleRadians;
411469

412470
if (normalizedValue > 1.0f)
413471
normalizedValue = 1.0f;
@@ -482,12 +540,37 @@ void MainTextUpdate() {
482540
system("cls");
483541
if (AppStatus.ControllerCount < 1)
484542
printf("\n Connect DualSense, DualShock 4, Pro controller or Joycons and reset.");
543+
else
544+
switch (CurGamepad.ControllerType) {
545+
case SONY_DUALSENSE:
546+
printf("\n Sony DualSense connected.");
547+
break;
548+
case SONY_DUALSHOCK4:
549+
printf("\n Sony DualShock 4 connected.");
550+
break;
551+
case NINTENDO_JOYCONS:
552+
printf("\n Nintendo Joy-Cons (");
553+
if (CurGamepad.HidHandle != NULL && CurGamepad.HidHandle2 != NULL) printf("left & right");
554+
else if (CurGamepad.HidHandle != NULL) printf("left - not enough");
555+
else if (CurGamepad.HidHandle2 != NULL) printf("right - not enough");
556+
printf(") connected.");
557+
break;
558+
case NINTENDO_SWITCH_PRO:
559+
printf("\n Nintendo Switch Pro Controller connected.");
560+
break;
561+
}
485562
printf("\n Press \"CTRL + R\" or \"%s\" to reset controllers.\n", AppStatus.HotKeys.ResetKeyName.c_str());
486-
487-
if (AppStatus.ControllerCount > 0 && AppStatus.ShowBatteryStatus && (CurGamepad.ControllerType == SONY_DUALSENSE || CurGamepad.ControllerType == SONY_DUALSHOCK4)) {
563+
if (AppStatus.ControllerCount > 0 && AppStatus.ShowBatteryStatus) {
488564
printf(" Gamepad mode:");
489565
if (CurGamepad.USBConnection) printf(" wired"); else printf(" wireless");
490-
printf(", battery charge: %d\%%.", CurGamepad.BatteryLevel);
566+
if (CurGamepad.ControllerType != NINTENDO_JOYCONS)
567+
printf(", battery charge: %d\%%.", CurGamepad.BatteryLevel);
568+
else {
569+
if (CurGamepad.HidHandle != NULL && CurGamepad.HidHandle2 != NULL) printf(", battery charge: %d\%%, %d\%%.", CurGamepad.BatteryLevel, CurGamepad.BatteryLevel2);
570+
else if (CurGamepad.HidHandle != NULL) printf(", battery charge: %d\%%.", CurGamepad.BatteryLevel);
571+
else if (CurGamepad.HidHandle2 != NULL) printf(", battery charge: %d\%%.", CurGamepad.BatteryLevel2);
572+
}
573+
491574
if (CurGamepad.BatteryMode == 0x2)
492575
printf(" (charging)", CurGamepad.BatteryLevel);
493576
printf("\n");
@@ -536,7 +619,7 @@ void MainTextUpdate() {
536619
printf(", press \"ALT + X\" to switch.\n");
537620

538621
printf(" Press \"ALT + F9\" to get the sticks dead zones.\n");
539-
printf(" Press \"ALT + I\" to get the battery status (only Sony).\n");
622+
printf(" Press \"ALT + I\" to get the battery status.\n");
540623
printf(" Press \"ALT + Escape\" to exit.\n");
541624
}
542625

@@ -549,6 +632,12 @@ LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
549632
{
550633
AppStatus.ControllerCount = JslConnectDevices();
551634
JslGetConnectedDeviceHandles(CurGamepad.deviceID, AppStatus.ControllerCount);
635+
if (AppStatus.ControllerCount > 0) {
636+
//JslSetGyroSpace(CurGamepad.deviceID[0], 2);
637+
JslSetAutomaticCalibration(CurGamepad.deviceID[0], true);
638+
if (AppStatus.ControllerCount > 1)
639+
JslSetAutomaticCalibration(CurGamepad.deviceID[1], true);
640+
}
552641
GamepadSearch();
553642
GamepadSetState(GamepadOutState);
554643
AppStatus.Gamepad.BTReset = false;
@@ -569,7 +658,7 @@ LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
569658

570659
int main(int argc, char **argv)
571660
{
572-
SetConsoleTitle("DSAdvance 1.0");
661+
SetConsoleTitle("DSAdvance 1.0.1");
573662

574663
WNDCLASS AppWndClass = {};
575664
AppWndClass.lpfnWndProc = WindowProc;
@@ -581,6 +670,7 @@ int main(int argc, char **argv)
581670

582671
// Config parameters
583672
CIniReader IniFile("Config.ini");
673+
CurGamepad.TestRumbleProController = IniFile.ReadBoolean("Gamepad", "ProContollerRumble", false); //Temporary test
584674
CurGamepad.Sticks.InvertLeftX = IniFile.ReadBoolean("Gamepad", "InvertLeftStickX", false);
585675
CurGamepad.Sticks.InvertLeftY = IniFile.ReadBoolean("Gamepad", "InvertLeftStickY", false);
586676
CurGamepad.Sticks.InvertRightX = IniFile.ReadBoolean("Gamepad", "InvertRightStickX", false);
@@ -589,7 +679,7 @@ int main(int argc, char **argv)
589679
AppStatus.HotKeys.ResetKey = KeyNameToKeyCode(AppStatus.HotKeys.ResetKeyName);
590680
AppStatus.ShowBatteryStatusOnLightBar = IniFile.ReadBoolean("Gamepad", "ShowBatteryStatusOnLightBar", true);
591681
AppStatus.SleepTimeOut = IniFile.ReadInteger("Gamepad", "SleepTimeOut", 1);
592-
682+
593683
CurGamepad.Sticks.DeadZoneLeftX = IniFile.ReadFloat("Gamepad", "DeadZoneLeftStickX", 0);
594684
CurGamepad.Sticks.DeadZoneLeftY = IniFile.ReadFloat("Gamepad", "DeadZoneLeftStickY", 0);
595685
CurGamepad.Sticks.DeadZoneRightX = IniFile.ReadFloat("Gamepad", "DeadZoneRightStickX", 0);
@@ -698,8 +788,14 @@ int main(int argc, char **argv)
698788
EulerAngles MotionAngles, AnglesOffset;
699789

700790
AppStatus.ControllerCount = JslConnectDevices();
701-
702791
JslGetConnectedDeviceHandles(CurGamepad.deviceID, AppStatus.ControllerCount);
792+
793+
if (AppStatus.ControllerCount > 0) {
794+
//JslSetGyroSpace(CurGamepad.deviceID[0], 2);
795+
JslSetAutomaticCalibration(CurGamepad.deviceID[0], true);
796+
if (AppStatus.ControllerCount > 1)
797+
JslSetAutomaticCalibration(CurGamepad.deviceID[1], true);
798+
}
703799

704800
MainTextUpdate();
705801

@@ -744,6 +840,14 @@ int main(int argc, char **argv)
744840

745841
XUSB_REPORT_INIT(&report);
746842

843+
if (AppStatus.ControllerCount < 1) { // We do not process anything during idle time
844+
//InputState = { 0, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f };
845+
report.sThumbLX = 1; // helps with crash, maybe power saving turns off the controller
846+
ret = vigem_target_x360_update(client, x360, report); // Vigem always mode only
847+
Sleep(AppStatus.SleepTimeOut);
848+
continue;
849+
}
850+
747851
if (JslGetControllerType(CurGamepad.deviceID[0]) == JS_TYPE_DS || JslGetControllerType(CurGamepad.deviceID[0]) == JS_TYPE_DS4 || JslGetControllerType(CurGamepad.deviceID[0]) == JS_TYPE_PRO_CONTROLLER) {
748852
InputState = JslGetSimpleState(CurGamepad.deviceID[0]);
749853
MotionState = JslGetMotionState(CurGamepad.deviceID[0]);
@@ -771,14 +875,6 @@ int main(int argc, char **argv)
771875
}
772876
}
773877
}
774-
775-
if (AppStatus.ControllerCount < 1) { // We do not process anything during idle time
776-
//InputState = { 0, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f };
777-
report.sThumbLX = 1; // Maybe the crash is due to power saving? temporary test
778-
ret = vigem_target_x360_update(client, x360, report); // Vigem always mode only
779-
Sleep(AppStatus.SleepTimeOut);
780-
continue;
781-
}
782878

783879
MotionAngles = QuaternionToEulerAngle(MotionState.quatW, MotionState.quatZ, MotionState.quatX, MotionState.quatY);
784880
float velocityX, velocityY, velocityZ;
@@ -1221,6 +1317,15 @@ int main(int argc, char **argv)
12211317
// Battery level display
12221318
if (BackOutStateCounter > 0) { if (BackOutStateCounter == 1) { GamepadOutState.LEDBlue = 255; GamepadOutState.LEDRed = 0; GamepadOutState.LEDGreen = 0; GamepadOutState.PlayersCount = 0; if (AppStatus.ShowBatteryStatusOnLightBar) GamepadOutState.LEDBrightness = LastLEDBrightness; GamepadSetState(GamepadOutState); AppStatus.ShowBatteryStatus = false; MainTextUpdate(); } BackOutStateCounter--; }
12231319

1320+
if (CurGamepad.RumbleOffCounter > 0) {
1321+
CurGamepad.RumbleOffCounter--;
1322+
if (CurGamepad.RumbleOffCounter == 1) {
1323+
GamepadOutState.SmallMotor = 0;
1324+
GamepadOutState.LargeMotor = 0;
1325+
GamepadSetState(GamepadOutState);
1326+
}
1327+
}
1328+
12241329
if (SkipPollCount > 0) SkipPollCount--;
12251330
Sleep(AppStatus.SleepTimeOut);
12261331
}

Source/DSAdvance/DSAdvance.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
#define SONY_DUALSHOCK4 27
44
#define SONY_DUALSENSE 28
5+
#define NINTENDO_JOYCONS 29
56

67
#define SONY_VENDOR 0x054C
78
#define SONY_DS4_USB 0x05C4
@@ -115,15 +116,18 @@ int ProfileIndex = 0;
115116
struct Gamepad {
116117
int deviceID[4];
117118
hid_device *HidHandle;
119+
hid_device *HidHandle2;
118120
WORD ControllerType;
119121
bool USBConnection;
120122
unsigned char BatteryMode;
121123
unsigned char BatteryLevel;
122-
// unsigned char Battery2Level;
124+
unsigned char BatteryLevel2;
123125
unsigned char LEDBatteryLevel;
124126
wchar_t *serial_number;
125127
float AutoPressStickValue = 0;
126128
unsigned char DefaultLEDBrightness = 0;
129+
unsigned char RumbleOffCounter = 0;
130+
bool TestRumbleProController;
127131

128132
struct _Sticks
129133
{

0 commit comments

Comments
 (0)