First public release
This commit is contained in:
		
							parent
							
								
									963cae3756
								
							
						
					
					
						commit
						64333fdb4a
					
				
					 14 changed files with 1580 additions and 0 deletions
				
			
		
							
								
								
									
										413
									
								
								joydance/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										413
									
								
								joydance/__init__.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,413 @@ | |||
| import asyncio | ||||
| import json | ||||
| import random | ||||
| import socket | ||||
| import ssl | ||||
| import time | ||||
| from enum import Enum | ||||
| 
 | ||||
| import aiohttp | ||||
| import websockets | ||||
| 
 | ||||
| from .constants import (ACCEL_ACQUISITION_FREQ_HZ, ACCEL_ACQUISITION_LATENCY, | ||||
|                         ACCEL_MAX_RANGE, ACCEL_SEND_RATE, JOYCON_UPDATE_RATE, | ||||
|                         SHORTCUT_MAPPING, UBI_APP_ID, UBI_SKU_ID, | ||||
|                         WS_SUBPROTOCOL, Command, JoyConButton) | ||||
| 
 | ||||
| 
 | ||||
| class PairingState(Enum): | ||||
|     IDLE = 0 | ||||
|     GETTING_TOKEN = 1 | ||||
|     PAIRING = 2 | ||||
|     CONNECTING = 3 | ||||
|     CONNECTED = 4 | ||||
|     DISCONNECTING = 5 | ||||
|     DISCONNECTED = 10 | ||||
| 
 | ||||
|     ERROR_JOYCON = 101 | ||||
|     ERROR_CONNECTION = 102 | ||||
|     ERROR_INVALID_PAIRING_CODE = 103 | ||||
|     ERROR_PUNCH_PAIRING = 104 | ||||
|     ERROR_HOLE_PUNCHING = 105 | ||||
|     ERROR_CONSOLE_CONNECTION = 106 | ||||
| 
 | ||||
| 
 | ||||
| class JoyDance: | ||||
|     def __init__( | ||||
|             self, | ||||
|             joycon, | ||||
|             pairing_code=None, | ||||
|             host_ip_addr=None, | ||||
|             console_ip_addr=None, | ||||
|             accel_acquisition_freq_hz=ACCEL_ACQUISITION_FREQ_HZ, | ||||
|             accel_acquisition_latency=ACCEL_ACQUISITION_LATENCY, | ||||
|             accel_max_range=ACCEL_MAX_RANGE, | ||||
|             on_state_changed=None): | ||||
|         self.joycon = joycon | ||||
|         self.joycon_is_left = joycon.is_left() | ||||
| 
 | ||||
|         if on_state_changed: | ||||
|             self.on_state_changed = on_state_changed | ||||
| 
 | ||||
|         self.pairing_code = pairing_code | ||||
|         self.host_ip_addr = host_ip_addr | ||||
|         self.console_ip_addr = console_ip_addr | ||||
|         self.host_port = self.get_random_port() | ||||
| 
 | ||||
|         self.accel_acquisition_freq_hz = accel_acquisition_freq_hz | ||||
|         self.accel_acquisition_latency = accel_acquisition_latency | ||||
|         self.accel_max_range = accel_max_range | ||||
| 
 | ||||
|         self.number_of_accels_sent = 0 | ||||
|         self.should_start_accelerometer = False | ||||
|         self.is_input_allowed = False | ||||
|         self.available_shortcuts = set() | ||||
| 
 | ||||
|         self.ws = None | ||||
|         self.disconnected = False | ||||
| 
 | ||||
|         self.headers = { | ||||
|             'Ubi-AppId': UBI_APP_ID, | ||||
|             'X-SkuId': UBI_SKU_ID, | ||||
|         } | ||||
| 
 | ||||
|         self.console_conn = None | ||||
| 
 | ||||
|     def get_random_port(self): | ||||
|         ''' Randomize a port number, to be used in hole_punching() later ''' | ||||
|         return random.randrange(39000, 39999) | ||||
| 
 | ||||
|     async def on_state_changed(state): | ||||
|         pass | ||||
| 
 | ||||
|     async def get_access_token(self): | ||||
|         ''' Log in using a guest account, pre-defined by Ubisoft ''' | ||||
|         headers = { | ||||
|             'Authorization': 'UbiMobile_v1 t=NTNjNWRjZGMtZjA2Yy00MTdmLWJkMjctOTNhZTcxNzU1OTkyOlcwM0N5eGZldlBTeFByK3hSa2hhQ05SMXZtdz06UjNWbGMzUmZaVzB3TjJOYTpNakF5TVMweE1DMHlOMVF3TVRvME5sbz0=', | ||||
|             'Ubi-AppId': UBI_APP_ID, | ||||
|             'User-Agent': 'UbiServices_SDK_Unity_Light_Mobile_2018.Release.16_ANDROID64_dynamic', | ||||
|             'Ubi-RequestedPlatformType': 'ubimobile', | ||||
|             'Content-Type': 'application/json', | ||||
|         } | ||||
| 
 | ||||
|         async with aiohttp.ClientSession(headers=headers) as session: | ||||
|             async with session.post('https://public-ubiservices.ubi.com/v1/profiles/sessions', json={}, ssl=False) as resp: | ||||
|                 if resp.status != 200: | ||||
|                     await self.on_state_changed(self.joycon.serial, PairingState.ERROR_CONNECTION) | ||||
|                     raise Exception('ERROR: Couldn\'t get access token!') | ||||
| 
 | ||||
|                 # Add ticket to headers | ||||
|                 json_body = await resp.json() | ||||
|                 self.headers['Authorization'] = 'Ubi_v1 ' + json_body['ticket'] | ||||
| 
 | ||||
|     async def send_pairing_code(self): | ||||
|         ''' Send pairing code to JD server ''' | ||||
|         url = 'https://prod.just-dance.com/sessions/v1/pairing-info' | ||||
| 
 | ||||
|         async with aiohttp.ClientSession(headers=self.headers) as session: | ||||
|             async with session.get(url, params={'code': self.pairing_code}, ssl=False) as resp: | ||||
|                 if resp.status != 200: | ||||
|                     await self.on_state_changed(self.joycon.serial, PairingState.ERROR_INVALID_PAIRING_CODE) | ||||
|                     raise Exception('ERROR: Invalid pairing code!') | ||||
| 
 | ||||
|                 json_body = await resp.json() | ||||
|                 self.pairing_url = json_body['pairingUrl'].replace('https://', 'wss://') + 'smartphone' | ||||
|                 self.requires_punch_pairing = json_body.get('requiresPunchPairing', False) | ||||
| 
 | ||||
|     async def send_initiate_punch_pairing(self): | ||||
|         ''' Tell console which IP address & port to connect to ''' | ||||
|         url = 'https://prod.just-dance.com/sessions/v1/initiate-punch-pairing' | ||||
|         json_payload = { | ||||
|             'pairingCode': self.pairing_code, | ||||
|             'mobileIP': self.host_ip_addr, | ||||
|             'mobilePort': self.host_port, | ||||
|         } | ||||
| 
 | ||||
|         async with aiohttp.ClientSession(headers=self.headers) as session: | ||||
|             async with session.post(url, json=json_payload, ssl=False) as resp: | ||||
|                 body = await resp.text() | ||||
|                 if body != 'OK': | ||||
|                     await self.on_state_changed(self.joycon.serial, PairingState.ERROR_PUNCH_PAIRING) | ||||
|                     raise Exception('ERROR: Couldn\'t initiate punch pairing!') | ||||
| 
 | ||||
|     async def hole_punching(self): | ||||
|         ''' Open a port on this machine so the console can connect to it ''' | ||||
|         try: | ||||
|             conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | ||||
|             conn.bind(('0.0.0.0', self.host_port)) | ||||
|             conn.listen(5) | ||||
| 
 | ||||
|             # Accept incoming connection from console | ||||
|             console_conn, addr = conn.accept() | ||||
|             self.console_conn = console_conn | ||||
|             print('Connected with {}:{}'.format(addr[0], addr[1])) | ||||
|         except Exception as e: | ||||
|             await self.on_state_changed(self.joycon.serial, PairingState.ERROR_HOLE_PUNCHING) | ||||
|             raise e | ||||
| 
 | ||||
|     async def send_message(self, __class, data={}): | ||||
|         ''' Send JSON message to server ''' | ||||
|         if __class != 'JD_PhoneScoringData': | ||||
|             print('>>>', __class, data) | ||||
| 
 | ||||
|         msg = {'root': {'__class': __class}} | ||||
|         if data: | ||||
|             msg['root'].update(data) | ||||
| 
 | ||||
|         # Remove extra spaces from JSON to reduce size | ||||
| 
 | ||||
|         try: | ||||
|             await self.ws.send(json.dumps(msg, separators=(',', ':'))) | ||||
|         except Exception: | ||||
|             await self.disconnect(close_ws=False) | ||||
| 
 | ||||
|     async def on_message(self, message): | ||||
|         message = json.loads(message) | ||||
|         print('<<<', message) | ||||
| 
 | ||||
|         __class = message['__class'] | ||||
|         if __class == 'JD_PhoneDataCmdHandshakeContinue': | ||||
|             await self.send_message('JD_PhoneDataCmdSync', {'phoneID': message['phoneID']}) | ||||
|         elif __class == 'JD_PhoneDataCmdSyncEnd': | ||||
|             await self.send_message('JD_PhoneDataCmdSyncEnd', {'phoneID': message['phoneID']}) | ||||
|             await self.on_state_changed(self.joycon.serial, PairingState.CONNECTED) | ||||
|         elif __class == 'JD_EnableAccelValuesSending_ConsoleCommandData': | ||||
|             self.should_start_accelerometer = True | ||||
|             self.number_of_accels_sent = 0 | ||||
|         elif __class == 'JD_DisableAccelValuesSending_ConsoleCommandData': | ||||
|             self.should_start_accelerometer = False | ||||
|         elif __class == 'InputSetup_ConsoleCommandData': | ||||
|             if message.get('isEnabled', 0) == 1: | ||||
|                 self.is_input_allowed = True | ||||
|         elif __class == 'EnableCarousel_ConsoleCommandData': | ||||
|             if message.get('isEnabled', 0) == 1: | ||||
|                 self.is_input_allowed = True | ||||
|         elif __class == 'JD_EnableLobbyStartbutton_ConsoleCommandData': | ||||
|             if message.get('isEnabled', 0) == 1: | ||||
|                 self.is_input_allowed = True | ||||
|         elif __class == 'ShortcutSetup_ConsoleCommandData': | ||||
|             if message.get('isEnabled', 0) == 1: | ||||
|                 self.is_input_allowed = True | ||||
|         elif __class == 'JD_PhoneUiShortcutData': | ||||
|             shortcuts = set() | ||||
|             for item in message.get('shortcuts', []): | ||||
|                 if item['__class'] == 'JD_PhoneAction_Shortcut': | ||||
|                     try: | ||||
|                         shortcuts.add(Command(item['shortcutType'])) | ||||
|                     except Exception as e: | ||||
|                         print('Unknown Command: ', e) | ||||
|             self.available_shortcuts = shortcuts | ||||
|         elif __class == 'JD_OpenPhoneKeyboard_ConsoleCommandData': | ||||
|             await asyncio.sleep(1) | ||||
|             await self.send_message('JD_CancelKeyboard_PhoneCommandData') | ||||
|         elif __class == 'JD_PhoneUiSetupData': | ||||
|             self.is_input_allowed = True | ||||
|             shortcuts = set() | ||||
|             if message.get('setupData', {}).get('gameplaySetup', {}).get('pauseSlider', {}): | ||||
|                 self.available_shortcuts.add(Command.PAUSE) | ||||
| 
 | ||||
|             if message['isPopup'] == 1: | ||||
|                 self.is_input_allowed = True | ||||
|             else: | ||||
|                 self.is_input_allowed = (message.get('inputSetup', {}).get('isEnabled', 0) == 1) | ||||
| 
 | ||||
|     async def send_hello(self): | ||||
|         print('Pairing...') | ||||
| 
 | ||||
|         await self.send_message('JD_PhoneDataCmdHandshakeHello', { | ||||
|             'accelAcquisitionFreqHz': float(self.accel_acquisition_freq_hz), | ||||
|             'accelAcquisitionLatency': float(self.accel_acquisition_latency), | ||||
|             'accelMaxRange': float(self.accel_max_range), | ||||
|         }) | ||||
| 
 | ||||
|         async for message in self.ws: | ||||
|             await self.on_message(message) | ||||
| 
 | ||||
|     async def send_accelerometer_data(self): | ||||
|         accel_data = [] | ||||
|         delta_time = 0 | ||||
| 
 | ||||
|         end = time.time() | ||||
| 
 | ||||
|         while True: | ||||
|             if self.disconnected: | ||||
|                 break | ||||
| 
 | ||||
|             if not self.should_start_accelerometer: | ||||
|                 await asyncio.sleep(0.5) | ||||
|                 continue | ||||
| 
 | ||||
|             start = time.time() | ||||
|             if delta_time > ACCEL_SEND_RATE: | ||||
|                 delta_time = 0 | ||||
|                 while len(accel_data) > 0: | ||||
|                     accels_num = min(len(accel_data), 10) | ||||
| 
 | ||||
|                     await self.send_message('JD_PhoneScoringData', { | ||||
|                         'accelData': accel_data[:accels_num], | ||||
|                         'timeStamp': self.number_of_accels_sent, | ||||
|                     }) | ||||
| 
 | ||||
|                     self.number_of_accels_sent += accels_num | ||||
|                     accel_data = accel_data[accels_num:] | ||||
| 
 | ||||
|             try: | ||||
|                 await asyncio.sleep(JOYCON_UPDATE_RATE) | ||||
|                 joycon_status = self.joycon.get_status() | ||||
|             except OSError: | ||||
|                 self.disconnect() | ||||
|                 return | ||||
| 
 | ||||
|             # Accelerator axes on phone & Joy-Con are different so we need to swap some axes | ||||
|             # https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering/blob/master/imu_sensor_notes.md | ||||
|             accel = joycon_status['accel'] | ||||
|             x = accel['y'] * -1 | ||||
|             y = accel['x'] | ||||
|             z = accel['z'] | ||||
| 
 | ||||
|             accel_data.append([x, y, z]) | ||||
| 
 | ||||
|             end = time.time() | ||||
|             delta_time += (end - start) * 1000 | ||||
| 
 | ||||
|     async def send_command(self): | ||||
|         ''' Capture Joycon's input and send to console ''' | ||||
|         while True: | ||||
|             try: | ||||
|                 if self.disconnected: | ||||
|                     return | ||||
| 
 | ||||
|                 if not self.is_input_allowed and not self.should_start_accelerometer: | ||||
|                     await asyncio.sleep(JOYCON_UPDATE_RATE * 5) | ||||
|                     continue | ||||
| 
 | ||||
|                 await asyncio.sleep(JOYCON_UPDATE_RATE * 5) | ||||
| 
 | ||||
|                 cmd = None | ||||
|                 # Get pressed button | ||||
|                 for event_type, status in self.joycon.events(): | ||||
|                     if status == 0:  # 0 = pressed, 1 = released | ||||
|                         continue | ||||
| 
 | ||||
|                     joycon_button = JoyConButton(event_type) | ||||
|                     if self.should_start_accelerometer:  # Can only send Pause command while playing | ||||
|                         if joycon_button == JoyConButton.PLUS or joycon_button == JoyConButton.MINUS: | ||||
|                             cmd = Command.PAUSE | ||||
|                     else: | ||||
|                         if joycon_button == JoyConButton.A or joycon_button == JoyConButton.RIGHT: | ||||
|                             cmd = Command.ACCEPT | ||||
|                         elif joycon_button == JoyConButton.B or joycon_button == JoyConButton.DOWN: | ||||
|                             cmd = Command.BACK | ||||
|                         elif joycon_button in SHORTCUT_MAPPING: | ||||
|                             # Get command depends on which button was pressed & which shortcuts are available | ||||
|                             for shortcut in SHORTCUT_MAPPING[joycon_button]: | ||||
|                                 if shortcut in self.available_shortcuts: | ||||
|                                     cmd = shortcut | ||||
|                                     break | ||||
| 
 | ||||
|                 # Get joystick direction | ||||
|                 if not self.should_start_accelerometer and not cmd: | ||||
|                     status = self.joycon.get_status() | ||||
| 
 | ||||
|                     # Check which Joycon (L/R) is being used | ||||
|                     stick = status['analog-sticks']['left'] if self.joycon_is_left else status['analog-sticks']['right'] | ||||
|                     vertical = stick['vertical'] | ||||
|                     horizontal = stick['horizontal'] | ||||
| 
 | ||||
|                     if vertical < -0.5: | ||||
|                         cmd = Command.DOWN | ||||
|                     elif vertical > 0.5: | ||||
|                         cmd = Command.UP | ||||
|                     elif horizontal < -0.5: | ||||
|                         cmd = Command.LEFT | ||||
|                     elif horizontal > 0.5: | ||||
|                         cmd = Command.RIGHT | ||||
| 
 | ||||
|                 # Send command to server | ||||
|                 if cmd: | ||||
|                     if cmd == Command.PAUSE: | ||||
|                         __class = 'JD_Pause_PhoneCommandData' | ||||
|                         data = {} | ||||
|                     elif type(cmd.value) == str: | ||||
|                         __class = 'JD_Custom_PhoneCommandData' | ||||
|                         data = { | ||||
|                             'identifier': cmd.value, | ||||
|                         } | ||||
|                     else: | ||||
|                         __class = 'JD_Input_PhoneCommandData' | ||||
|                         data = { | ||||
|                             'input': cmd.value, | ||||
|                         } | ||||
| 
 | ||||
|                     # Only send input when it's allowed to, otherwise we might get a disconnection | ||||
|                     if self.is_input_allowed: | ||||
|                         await self.send_message(__class, data) | ||||
|                         await asyncio.sleep(0.01) | ||||
|             except Exception as e: | ||||
|                 print(e) | ||||
|                 await self.disconnect() | ||||
| 
 | ||||
|     async def connect_ws(self): | ||||
|         ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) | ||||
|         ssl_context.set_ciphers('ALL') | ||||
|         ssl_context.options &= ~ssl.OP_NO_SSLv3 | ||||
|         ssl_context.check_hostname = False | ||||
|         ssl_context.verify_mode = ssl.CERT_NONE | ||||
| 
 | ||||
|         server_hostname = self.console_conn.getpeername()[0] if self.console_conn else None | ||||
|         try: | ||||
|             async with websockets.connect( | ||||
|                     self.pairing_url, | ||||
|                     subprotocols=[WS_SUBPROTOCOL], | ||||
|                     sock=self.console_conn, | ||||
|                     ssl=ssl_context, | ||||
|                     ping_timeout=None, | ||||
|                     server_hostname=server_hostname | ||||
|             ) as websocket: | ||||
|                 try: | ||||
|                     self.ws = websocket | ||||
|                     await asyncio.gather( | ||||
|                         self.send_hello(), | ||||
|                         self.send_accelerometer_data(), | ||||
|                         self.send_command(), | ||||
|                     ) | ||||
| 
 | ||||
|                 except websockets.ConnectionClosed: | ||||
|                     await self.on_state_changed(self.joycon.serial, PairingState.ERROR_CONSOLE_CONNECTION) | ||||
|                     await self.disconnect(close_ws=False) | ||||
|         except Exception: | ||||
|             await self.on_state_changed(self.joycon.serial, PairingState.ERROR_CONSOLE_CONNECTION) | ||||
|             await self.disconnect(close_ws=False) | ||||
| 
 | ||||
|     async def disconnect(self, close_ws=True): | ||||
|         print('disconnected') | ||||
|         self.disconnected = True | ||||
|         self.joycon.__del__() | ||||
| 
 | ||||
|         if close_ws and self.ws: | ||||
|             await self.ws.close() | ||||
| 
 | ||||
|     async def pair(self): | ||||
|         try: | ||||
|             if self.console_ip_addr: | ||||
|                 await self.on_state_changed(self.joycon.serial, PairingState.CONNECTING) | ||||
|                 self.pairing_url = 'wss://{}:8080/smartphone'.format(self.console_ip_addr) | ||||
|             else: | ||||
|                 await self.on_state_changed(self.joycon.serial, PairingState.GETTING_TOKEN) | ||||
|                 print('Getting authorication token...') | ||||
|                 await self.get_access_token() | ||||
| 
 | ||||
|                 await self.on_state_changed(self.joycon.serial, PairingState.PAIRING) | ||||
|                 print('Sending pairing code...') | ||||
|                 await self.send_pairing_code() | ||||
| 
 | ||||
|                 await self.on_state_changed(self.joycon.serial, PairingState.CONNECTING) | ||||
|                 print('Connecting with console...') | ||||
|                 if self.requires_punch_pairing: | ||||
|                     await self.send_initiate_punch_pairing() | ||||
|                     await self.hole_punching() | ||||
| 
 | ||||
|             await self.connect_ws() | ||||
|         except Exception as e: | ||||
|             await self.disconnect() | ||||
|             print(e) | ||||
							
								
								
									
										128
									
								
								joydance/constants.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								joydance/constants.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,128 @@ | |||
| from enum import Enum | ||||
| 
 | ||||
| 
 | ||||
| JOYDANCE_VERSION = '0.1' | ||||
| UBI_APP_ID = '210da0fb-d6a5-4ed1-9808-01e86f0de7fb' | ||||
| UBI_SKU_ID = 'jdcompanion-android' | ||||
| WS_SUBPROTOCOL = 'v2.phonescoring.jd.ubisoft.com' | ||||
| 
 | ||||
| JOYCON_UPDATE_RATE = 0.02  # 50Hz | ||||
| ACCEL_SEND_RATE = 40  # ms | ||||
| ACCEL_ACQUISITION_FREQ_HZ = 50  # Hz | ||||
| ACCEL_ACQUISITION_LATENCY = 20  # ms | ||||
| ACCEL_MAX_RANGE = 8  # ±G | ||||
| 
 | ||||
| DEFAULT_CONFIG = { | ||||
|     'pairing_method': 'default', | ||||
|     'host_ip_addr': '', | ||||
|     'console_ip_addr': '', | ||||
|     'pairing_code': '', | ||||
|     'accel_acquisition_freq_hz': ACCEL_ACQUISITION_FREQ_HZ, | ||||
|     'accel_acquisition_latency': ACCEL_ACQUISITION_LATENCY, | ||||
|     'accel_max_range': ACCEL_MAX_RANGE, | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| class Command(Enum): | ||||
|     UP = 3690595578 | ||||
|     RIGHT = 1099935642 | ||||
|     DOWN = 2467711647 | ||||
|     LEFT = 3652315484 | ||||
|     ACCEPT = 1084313942 | ||||
| 
 | ||||
|     PAUSE = 'PAUSE' | ||||
| 
 | ||||
|     BACK = 'SHORTCUT_BACK' | ||||
|     CHANGE_DANCERCARD = 'SHORTCUT_CHANGE_DANCERCARD' | ||||
|     FAVORITE = 'SHORTCUT_FAVORITE' | ||||
|     GOTO_SONGSTAB = 'SHORTCUT_GOTO_SONGSTAB' | ||||
|     SKIP = 'SHORTCUT_SKIP' | ||||
|     SORTING = 'SHORTCUT_SORTING' | ||||
|     SWAP_GENDER = 'SHORTCUT_SWAP_GENDER' | ||||
|     SWEAT_ACTIVATION = 'SHORTCUT_SWEAT_ACTIVATION' | ||||
|     TOGGLE_COOP = 'SHORTCUT_TOGGLE_COOP' | ||||
|     UPLAY = 'SHORTCUT_UPLAY' | ||||
| 
 | ||||
|     ACTIVATE_DANCERCARD = 'SHORTCUT_ACTIVATE_DANCERCARD' | ||||
|     DELETE_DANCERCARD = 'SHORTCUT_DELETE_DANCERCARD' | ||||
| 
 | ||||
|     DELETE_PLAYLIST = 'SHORTCUT_DELETE_PLAYLIST' | ||||
|     SAVE_PLAYLIST = 'SHORTCUT_SAVE_PLAYLIST' | ||||
|     PLAYLIST_RENAME = 'SHORTCUT_PLAYLIST_RENAME' | ||||
|     PLAYLIST_DELETE_SONG = 'SHORTCUT_PLAYLIST_DELETE_SONG' | ||||
|     PLAYLIST_MOVE_SONG_LEFT = 'SHORTCUT_PLAYLIST_MOVE_SONG_LEFT' | ||||
|     PLAYLIST_MOVE_SONG_RIGHT = 'SHORTCUT_PLAYLIST_MOVE_SONG_RIGHT' | ||||
| 
 | ||||
|     TIPS_NEXT = 'SHORTCUT_TIPS_NEXT' | ||||
|     TIPS_PREVIOUS = 'SHORTCUT_TIPS_PREVIOUS' | ||||
| 
 | ||||
| 
 | ||||
| class JoyConButton(Enum): | ||||
|     # Joy-Con (L) | ||||
|     UP = 'up' | ||||
|     RIGHT = 'right' | ||||
|     DOWN = 'down' | ||||
|     LEFT = 'left' | ||||
|     L = 'l' | ||||
|     ZL = 'zl' | ||||
|     MINUS = 'minus' | ||||
|     CAPTURE = 'capture' | ||||
|     LEFT_STICK = 'stick_l_btn' | ||||
|     LEFT_SR = 'left_sr' | ||||
|     LEFT_SL = 'left_sl' | ||||
| 
 | ||||
|     # Joy-Con (R) | ||||
|     A = 'a' | ||||
|     B = 'b' | ||||
|     X = 'x' | ||||
|     Y = 'y' | ||||
|     R = 'r' | ||||
|     ZR = 'zr' | ||||
|     PLUS = 'plus' | ||||
|     HOME = 'home' | ||||
|     RIGHT_STICK = 'stick_r_btn' | ||||
|     RIGHT_SL = 'right_sl' | ||||
|     RIGHT_SR = 'right_sr' | ||||
|     CHARGING_GRIP = 'charging-grip' | ||||
| 
 | ||||
| 
 | ||||
| # Assign buttons on Joy-Con (R) with commands | ||||
| SHORTCUT_MAPPING = { | ||||
|     JoyConButton.X: [ | ||||
|         Command.DELETE_DANCERCARD, | ||||
|         Command.DELETE_PLAYLIST, | ||||
|         Command.GOTO_SONGSTAB, | ||||
|         Command.PLAYLIST_DELETE_SONG, | ||||
|         Command.SKIP, | ||||
|         Command.SORTING, | ||||
|         Command.SWAP_GENDER, | ||||
|         Command.TOGGLE_COOP, | ||||
|     ], | ||||
|     JoyConButton.Y: [ | ||||
|         Command.ACTIVATE_DANCERCARD, | ||||
|         Command.CHANGE_DANCERCARD, | ||||
|         Command.SWEAT_ACTIVATION, | ||||
|         Command.UPLAY, | ||||
|     ], | ||||
|     JoyConButton.PLUS: [ | ||||
|         Command.FAVORITE, | ||||
|         Command.PAUSE, | ||||
|         Command.PLAYLIST_RENAME, | ||||
|         Command.SAVE_PLAYLIST, | ||||
|     ], | ||||
|     JoyConButton.R: [ | ||||
|         Command.PLAYLIST_MOVE_SONG_LEFT, | ||||
|         Command.TIPS_PREVIOUS, | ||||
|     ], | ||||
|     JoyConButton.ZR: [ | ||||
|         Command.PLAYLIST_MOVE_SONG_RIGHT, | ||||
|         Command.TIPS_NEXT, | ||||
|     ], | ||||
| } | ||||
| 
 | ||||
| # Same with Joy-Con (L) | ||||
| SHORTCUT_MAPPING[JoyConButton.UP] = JoyConButton.X | ||||
| SHORTCUT_MAPPING[JoyConButton.LEFT] = JoyConButton.Y | ||||
| SHORTCUT_MAPPING[JoyConButton.MINUS] = JoyConButton.PLUS | ||||
| SHORTCUT_MAPPING[JoyConButton.L] = JoyConButton.R | ||||
| SHORTCUT_MAPPING[JoyConButton.ZL] = JoyConButton.ZR | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue