Compare commits

...

3 Commits

Author SHA1 Message Date
Mel
92254a43d1 finished version 0.1.0 2025-12-13 04:37:03 +01:00
Mel
38a1e7de60 add .idea to gitignore 2025-12-11 23:02:30 +01:00
Mel
6c90998bb0 started to add VTS API code 2025-12-11 23:01:34 +01:00
11 changed files with 627 additions and 148 deletions

3
.gitignore vendored
View File

@@ -166,11 +166,10 @@ cython_debug/
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear # and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder. # option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/ .idea/
# Ruff stuff: # Ruff stuff:
.ruff_cache/ .ruff_cache/
# PyPI configuration file # PyPI configuration file
.pypirc .pypirc

View File

@@ -1,7 +0,0 @@
<component name="ProjectDictionaryState">
<dictionary name="project">
<words>
<w>deadzone</w>
</words>
</dictionary>
</component>

View File

@@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

7
.idea/misc.xml generated
View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="uv (vts-gamepad-input)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="uv (vts-gamepad-input)" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml generated
View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/vts-gamepad-input.iml" filepath="$PROJECT_DIR$/.idea/vts-gamepad-input.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="jdk" jdkName="uv (vts-gamepad-input)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

72
.idea/workspace.xml generated

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,33 @@
#! /usr/bin/env python #! /usr/bin/env python
import asyncio
import time
import inquirer import inquirer
from inputs import GamePad, devices from inputs import GamePad, devices
from vts_connection import VTSConnection
# initiate gamepad buttons
dpad_up, dpad_down, dpad_left, dpad_right = (0.0, 0.0, 0.0, 0.0)
action_up, action_down, action_left, action_right = (0.0, 0.0, 0.0, 0.0)
button_start, button_select = (0.0, 0.0)
button_l1, button_l2, button_l3 = (0.0, 0.0, 0.0)
button_r1, button_r2, button_r3 = (0.0, 0.0, 0.0)
left_stick_x_axis, left_stick_y_axis, right_stick_x_axis, right_stick_y_axis = (
0.0,
0.0,
0.0,
0.0,
)
left_button_pressed, right_button_pressed, left_button_hold, right_button_hold = (
0.0,
0.0,
0.0,
0.0,
)
left_thumb_stick, right_thumb_stick = (0.0, 0.0)
left_thumb_X, right_thumb_X, left_thumb_Y, right_thumb_Y = (0.0, 0.0, 0.0, 0.0)
left_index, right_index = (0.0, 0.0)
def _get_gamepads() -> list[GamePad]: def _get_gamepads() -> list[GamePad]:
@@ -37,32 +63,213 @@ def select_gamepad() -> GamePad | None:
return gamepads[0] return gamepads[0]
def _in_deadzone(analog_value: int, percentage: float = 5) -> bool: def _deadzone(analog_value: float, deadzone: float = 0.1) -> float:
center: float = 255 / 2 return 0 if abs(analog_value) < deadzone else analog_value
dead_range = center * (percentage / 100)
if center - dead_range < analog_value < center + dead_range:
return True
else:
return False
def main() -> None: def _normalize_analog_sticks(analog_value: float) -> float:
"""Normalize analog value to -1 - 1 range to comply with Nyarupad"""
minimum: int = 0
maximum: int = 255
formula = 2 * ((analog_value - minimum) / (maximum - minimum)) - 1
return formula
def _normalize_analog_trigger(analog_value: float) -> float:
minimum: int = 0
maximum: int = 255
formula = (analog_value - minimum) / (maximum - minimum)
return formula
def set_inputs(code: str, state: float) -> None:
global dpad_up, dpad_down, dpad_left, dpad_right
global action_up, action_down, action_left, action_right
global left_stick_x_axis, left_stick_y_axis, right_stick_x_axis, right_stick_y_axis
global button_start, button_l1, button_l2, button_l3
global button_select, button_r1, button_r2, button_r3
global left_thumb_stick, right_thumb_stick
global left_thumb_X, right_thumb_X, left_thumb_Y, right_thumb_Y
global left_index, right_index
global \
left_button_pressed, \
right_button_pressed, \
left_button_hold, \
right_button_hold
if code == "ABS_HAT0Y":
dpad_up = state * -1 if state < 0 else 0
dpad_down = state if state > 0 else 0
elif code == "ABS_HAT0X":
dpad_left = state * -1 if state < 0 else 0
dpad_right = state if state > 0 else 0
elif code == "ABS_X":
left_stick_x_axis = state
elif code == "ABS_Y":
left_stick_y_axis = state * -1
elif code == "ABS_RX":
right_stick_x_axis = state
elif code == "ABS_RY":
right_stick_y_axis = state * -1
elif code == "ABS_Z":
button_l2 = state
elif code == "ABS_RZ":
button_r2 = state
elif code == "BTN_TL":
button_l1 = state
elif code == "BTN_TR":
button_r1 = state
elif code == "BTN_SOUTH":
action_down = state
elif code == "BTN_WEST":
action_left = state
elif code == "BTN_EAST":
action_right = state
elif code == "BTN_NORTH":
action_up = state
elif code == "BTN_THUMBL":
button_l3 = state
elif code == "BTN_THUMBR":
button_r3 = state
elif code == "BTN_START":
button_start = state
elif code == "BTN_SELECT":
button_select = state
left_button_pressed = dpad_left + dpad_up + dpad_down + dpad_right
right_button_pressed = action_left + action_right + action_up + action_down
left_thumb_X = dpad_right - dpad_left if left_button_pressed >= 1 else 0
left_thumb_Y = dpad_up - dpad_down if left_button_pressed >= 1 else 0
right_thumb_X = action_right - action_left if right_button_pressed >= 1 else 0
right_thumb_Y = action_up - action_down if right_button_pressed >= 1 else 0
left_thumb_stick = (
1
if dpad_down == dpad_up == dpad_left == dpad_right == 0 or button_l3 == 1
else 0
)
right_thumb_stick = (
1
if action_up == action_down == action_left == action_right == 0
or button_r3 == 1
else 0
)
left_index = 1 if button_l1 == 0 else 0
right_index = 1 if button_r1 == 0 else 0
def create_vts_input_request() -> list[dict]:
params = [
{"id": "NP_ON", "value": 1.0, "weight": 1.0},
{"id": "NP_DPadDown", "value": dpad_down, "weight": 1.0},
{"id": "NP_DPadUp", "value": dpad_up, "weight": 1.0},
{"id": "NP_DPadLeft", "value": dpad_left, "weight": 1.0},
{"id": "NP_DPadRight", "value": dpad_right, "weight": 1.0},
{"id": "NP_ButtonA", "value": action_down, "weight": 1.0},
{"id": "NP_ButtonB", "value": action_right, "weight": 1.0},
{"id": "NP_ButtonX", "value": action_left, "weight": 1.0},
{"id": "NP_ButtonY", "value": action_up, "weight": 1.0},
{"id": "NP_StartDown", "value": button_start, "weight": 1.0},
{"id": "NP_SelectDown", "value": button_select, "weight": 1.0},
{"id": "NP_ButtonLS", "value": button_l3, "weight": 1.0},
{"id": "NP_ButtonRS", "value": button_r3, "weight": 1.0},
{"id": "NP_L1", "value": button_l1, "weight": 1.0},
{"id": "NP_R1", "value": button_r1, "weight": 1.0},
{"id": "NP_L2", "value": button_l2, "weight": 1.0},
{"id": "NP_R2", "value": button_r2, "weight": 1.0},
{"id": "NP_LStickX", "value": left_stick_x_axis, "weight": 1.0},
{"id": "NP_LStickY", "value": left_stick_y_axis, "weight": 1.0},
{"id": "NP_RStickX", "value": right_stick_x_axis, "weight": 1.0},
{"id": "NP_RStickY", "value": right_stick_y_axis, "weight": 1.0},
{"id": "NP_LButtonDown", "value": left_button_pressed, "weight": 1.0},
{"id": "NP_RButtonDown", "value": right_button_pressed, "weight": 1.0},
{"id": "NP_LButtonPress", "value": left_button_pressed, "weight": 1.0},
{"id": "NP_RButtonPress", "value": right_button_pressed, "weight": 1.0},
{"id": "NP_LThumbX", "value": left_thumb_X, "weight": 1.0},
{"id": "NP_LThumbY", "value": left_thumb_Y, "weight": 1.0},
{"id": "NP_RThumbX", "value": right_thumb_X, "weight": 1.0},
{"id": "NP_RThumbY", "value": right_thumb_Y, "weight": 1.0},
{"id": "NP_LOnStick", "value": left_thumb_stick, "weight": 1.0},
{"id": "NP_ROnStick", "value": right_thumb_stick, "weight": 1.0},
{"id": "NP_LIndexPos", "value": left_index, "weight": 1.0},
{"id": "NP_RIndexPos", "value": right_index, "weight": 1.0},
]
return params
async def main() -> None:
gamepad = select_gamepad() gamepad = select_gamepad()
vts = VTSConnection()
try:
await vts.connect()
except asyncio.CancelledError:
await vts.close()
axis_states = {
"ABS_X": 0.0,
"ABS_Y": 0.0,
"ABS_RX": 0.0,
"ABS_RY": 0.0,
}
trigger_states = {
"ABS_Z": 0.0,
"ABS_RZ": 0.0,
}
last_time = time.monotonic()
frequency = 1 / 30
while True: while True:
for event in gamepad.read(): for event in gamepad.read():
if event.code != "SYN_REPORT": if event.code != "SYN_REPORT":
# Add some deadzone of about 5% to analog stick # Add some deadzone of about 10% to analog stick
if event.code in ["ABS_X", "ABS_Y", "ABS_RX", "ABS_RY"]: if event.code in ["ABS_X", "ABS_Y", "ABS_RX", "ABS_RY"]:
if not _in_deadzone(analog_value=event.state): axis_states[event.code] = _deadzone(
print(event.ev_type, event.code, event.state) _normalize_analog_sticks(event.state)
)
elif event.code in ["ABS_Z", "ABS_RZ"]:
trigger_states[event.code] = _normalize_analog_trigger(event.state)
else: else:
print(event.ev_type, event.code, event.state) set_inputs(event.code, event.state)
now = time.monotonic()
if now - last_time >= frequency:
last_time = now
for key in trigger_states:
set_inputs(key, trigger_states[key])
for key in axis_states:
set_inputs(key, axis_states[key])
input_data = create_vts_input_request()
await vts.update_parameters(input_data)
await asyncio.sleep(0.001)
if __name__ == "__main__": if __name__ == "__main__":
try: try:
main() asyncio.run(main())
except KeyboardInterrupt: except KeyboardInterrupt:
exit(0) exit(0)

View File

@@ -1,16 +0,0 @@
import pyvts
import asyncio
plugin_info = {
"plugin_name": "vts-gampad-input",
"developer": "Melody LaFae",
"authentication_token_path": f"{os.environ['XDG_CONFIG_DIRS']}/.vts-gampad-authentication-token",
}
class VTSConnection:
def __init__(self):
pass
async def connect(self):
vts = pyvts.

View File

@@ -0,0 +1,405 @@
#! /usr/bin/env python
import asyncio
import os
import pyvts
AUTHOR = "Melody LaFae"
PLUGIN_NAME = "VTS Gamepad Input"
token_path = f"{os.environ['HOME']}/.vts-gamepad-input-token"
ip_path = f"{os.environ['HOME']}/.vts-gamepad-input-ip"
plugin_info = {
"plugin_name": PLUGIN_NAME,
"developer": AUTHOR,
"authentication_token_path": token_path,
}
class VTSConnection:
def __init__(self):
self.vts = pyvts.vts(
plugin_info=plugin_info, vts_api_info=self._get_vts_api_info()
)
self.request = pyvts.VTSRequest(plugin_info=plugin_info)
async def connect(self):
conn_success = await self._connect_to_api()
if conn_success:
await self._create_vts_params()
await self.close()
async def close(self):
await self.vts.close()
@staticmethod
def _get_vts_api_info() -> dict[str, str]:
host_uri = "ws://localhost:8001"
vts_api_info: dict[str, str] = {}
if os.path.exists(ip_path) and os.path.isfile(ip_path):
with open(ip_path, "r") as file:
host_uri = file.read()
else:
with open(ip_path, "w") as file:
file.write(host_uri)
host_url = host_uri.split(":")[1].replace("//", "")
host_port = host_uri.split(":")[2]
vts_api_info["host"] = host_url
vts_api_info["port"] = host_port
vts_api_info["name"] = "VTubeStudioPublicAPI"
vts_api_info["version"] = "1.0"
return vts_api_info
async def _connect_to_api(self) -> bool:
try:
await self.vts.connect()
except OSError:
print("Error: Make sure the VTS API is running")
exit(1)
await self.vts.request_authenticate_token()
await self.vts.request_authenticate()
if not self.vts.get_connection_status():
raise ConnectionError("Connection failed")
else:
return True
async def _create_vts_params(self) -> None:
# Param to enable or disable inputs
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_ON",
info="VTS Gamepad Input Enabled",
min=0.0,
max=1.0,
default_value=0.0,
)
)
# DPad params
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_DPadDown",
info="DPad Down pressed",
min=0.0,
max=1.0,
default_value=0.0,
)
)
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_DPadLeft",
info="DPad Left pressed",
min=0.0,
max=1.0,
default_value=0.0,
),
)
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_DPadUp",
info="DPad Up pressed",
min=0.0,
max=1.0,
default_value=0.0,
),
)
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_DPadRight",
info="DPad Right pressed",
min=0.0,
max=1.0,
default_value=0.0,
),
)
# Action Buttons params
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_ButtonA",
info="Down Action button pressed (XBox A, PS Cross)",
min=0.0,
max=1.0,
default_value=0.0,
),
)
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_ButtonB",
info="Right Action button pressed (XBox B, PS Circle)",
min=0.0,
max=1.0,
default_value=0.0,
),
)
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_ButtonY",
info="Up Action button pressed (XBox Y, PS Triangle)",
min=0.0,
max=1.0,
default_value=0.0,
),
)
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_ButtonX",
info="Left Action button pressed (XBox X, PS Square)",
min=0.0,
max=1.0,
default_value=0.0,
),
)
# Shoulder buttons params
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_L1",
info="Left front shoulder button pressed (XBox LB, PS L1, Switch L)",
min=0.0,
max=1.0,
default_value=0.0,
),
)
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_L2",
info="Left back shoulder button pressed (XBox LT, PS L2, Switch ZL)",
min=0.0,
max=1.0,
default_value=0.0,
),
)
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_R1",
info="Right front shoulder button pressed (XBox RB, PS R1, Switch R)",
min=0.0,
max=1.0,
default_value=0.0,
),
)
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_R2",
info="Right back shoulder button pressed (XBox RT, PS R2, Switch ZR)",
min=0.0,
max=1.0,
default_value=0.0,
),
)
# Start and select params
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_StartDown",
info="Start button pressed",
min=0.0,
max=1.0,
default_value=0.0,
),
)
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_SelectDown",
info="Select button pressed",
min=0.0,
max=1.0,
default_value=0.0,
),
)
# Left Analog stick param
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_LStickX",
info="Left stick moved on X axis",
min=-1.0,
max=1.0,
default_value=0.0,
),
)
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_LStickY",
info="Left stick moved on Y axis",
min=-1.0,
max=1.0,
default_value=0.0,
),
)
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_ButtonLS",
info="Left analog stick pressed",
min=0.0,
max=1.0,
default_value=0.0,
),
)
# Right Analog stick params
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_RStickX",
info="Right stick moved on X axis",
min=-1.0,
max=1.0,
default_value=0.0,
),
)
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_RStickY",
info="Right stick moved on Y axis",
min=-1.0,
max=1.0,
default_value=0.0,
),
)
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_ButtonRS",
info="Right analog stick pressed",
min=0.0,
max=1.0,
default_value=0.0,
),
)
## Rigging Logic params
# Thumb on sticks if no buttons are pressed
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_LOnStick",
info="Left thumb on left analog stick",
min=0.0,
max=1.0,
default_value=0.0,
),
)
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_ROnStick",
info="Right thumb on right analog stick",
min=0.0,
max=1.0,
default_value=0.0,
),
)
# Thumb locations
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_LThumbX",
info="Left thumb movement on X axis",
min=-1.0,
max=1.0,
default_value=0.0,
),
)
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_LThumbY",
info="Left thumb movement on Y axis",
min=-1.0,
max=1.0,
default_value=0.0,
),
)
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_RThumbX",
info="Right thumb movement on X axis",
min=-1.0,
max=1.0,
default_value=0.0,
),
)
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_RThumbY",
info="Right thumb movement on Y axis",
min=-1.0,
max=1.0,
default_value=0.0,
),
)
# Index finger location
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_LIndexPos",
info="Left index finger position (on L2/LT/ZL if L1 not pressed)",
min=0.0,
max=1.0,
default_value=0.0,
),
)
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_RIndexPos",
info="Right index finger position (on R2/RT/ZR if R1 not pressed)",
min=0.0,
max=1.0,
default_value=0.0,
),
)
# Button pressed or hold params
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_LButtonPress",
info="Left button pressed",
min=0.0,
max=1.0,
default_value=0.0,
),
)
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_RButtonPress",
info="Right button pressed",
min=0.0,
max=1.0,
default_value=0.0,
),
)
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_LButtonDown",
info="Left button hold",
min=0.0,
max=1.0,
default_value=0.0,
),
)
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_RButtonDown",
info="Right button hold",
min=0.0,
max=1.0,
default_value=0.0,
)
)
async def update_parameters(self, parameters: list[dict]) -> None:
await self._connect_to_api()
request = self.request.BaseRequest(
data={
"faceFound": False,
"mode": "set",
"parameterValues": parameters,
},
message_type="InjectParameterDataRequest",
)
await self.vts.request(request)
await self.close()
async def main():
vts_conn = VTSConnection()
await vts_conn.vts.connect()
await vts_conn.vts.request_authenticate_token()
# await vts_conn.vts.request_authenticate()
# await vts_conn.connect()
if __name__ == "__main__":
asyncio.run(main())