OSC control bug

hi i have a script that copilot and gemini generated for me. it is supposed to make osc port control across three different vr chat instances. i want the first instance to “be the leader” and i want the second instance to just copy the leader’s input. and i want the third instance to also, “follow the leader”. so far, the jumping seems to work well in the script. with the leader window in focus, pressing the jump button on the controller causes the other two instances to also jump, through OSC control. however this is where the bug occurs, with movement and camera rotation. pressing the left joystick causes the “leader” instance to move around uncontrollably. its like there is random joystick movement across all three OSC instances. you cant control movement. its just aimless and constant. the same thing happens with right joystick camera control, it just gets stuck in a rotation loop where you cant control camera rotation. camera rotation goes uncontrollably. what is wrong with my script and how do i fix it? i have several versiions of the scriipt and copilot and gemeni have tried several variations of the script. following are a few variations of the script. if i recall correctly, one of the scripts has no movement working at all, and another variant has uncontrollable camera rotation and uncontrollable movement input. how do i properly implement an OSC script to have three instances of vrchat follow each other around?

import tkinter as tk
from threading import Thread
from inputs import get_gamepad
from pythonosc.udp_client import SimpleUDPClient

OSC targets

osc_targets = [
SimpleUDPClient(“127.0.0.1”, 9000),
SimpleUDPClient(“127.0.0.1”, 9002),
SimpleUDPClient(“127.0.0.1”, 9004)
]

Configurable settings

DEAD_ZONE = 50 # Increased dead zone for smoother control
INVERT_LOOK_Y = True
INVERT_LOOK_X = False
INVERT_MOVE_Y = True

Button state tracking

button_states = {
“BTN_SOUTH”: 0, # A (Jump)
“BTN_EAST”: 0, # B (Crouch)
“ABS_Y”: 128, # Left stick vertical
“ABS_X”: 128, # Left stick horizontal
“ABS_RY”: 128, # Right stick vertical
“ABS_RX”: 128 # Right stick horizontal
}

def send_osc_to_all(address, value):
for client in osc_targets:
client.send_message(address, value)

def controller_loop(log_callback):
while True:
events = get_gamepad()
for event in events:
code = event.code
state = event.state

        # Jump (A button)
        if code == "BTN_SOUTH":
            if state != button_states[code]:
                send_osc_to_all("/input/Jump", state)
                log_callback(f"Jump: {state}")
                button_states[code] = state

        # Crouch (B button)
        elif code == "BTN_EAST":
            if state != button_states[code]:
                send_osc_to_all("/input/Crouch", state)
                log_callback(f"Crouch: {state}")
                button_states[code] = state

        # Movement - Forward/Backward (Y-axis)
        elif code == "ABS_Y":
            if INVERT_MOVE_Y:
                forward = 1 if state > (128 + DEAD_ZONE) else 0
                backward = 1 if state < (128 - DEAD_ZONE) else 0
            else:
                forward = 1 if state < (128 - DEAD_ZONE) else 0
                backward = 1 if state > (128 + DEAD_ZONE) else 0

            send_osc_to_all("/input/MoveForward", forward)
            send_osc_to_all("/input/MoveBackward", backward)
            log_callback(f"Forward: {forward}, Backward: {backward}")
            button_states[code] = state

        # Movement - Left/Right
        elif code == "ABS_X":
            left = 1 if state < (128 - DEAD_ZONE) else 0
            right = 1 if state > (128 + DEAD_ZONE) else 0
            send_osc_to_all("/input/MoveLeft", left)
            send_osc_to_all("/input/MoveRight", right)
            log_callback(f"Left: {left}, Right: {right}")
            button_states[code] = state

        # Look - Up/Down
        elif code == "ABS_RY":
            if INVERT_LOOK_Y:
                look_up = 1 if state > (128 + DEAD_ZONE) else 0
                look_down = 1 if state < (128 - DEAD_ZONE) else 0
            else:
                look_up = 1 if state < (128 - DEAD_ZONE) else 0
                look_down = 1 if state > (128 + DEAD_ZONE) else 0

            send_osc_to_all("/input/LookUp", look_up)
            send_osc_to_all("/input/LookDown", look_down)
            log_callback(f"LookUp: {look_up}, LookDown: {look_down}")
            button_states[code] = state

        # Look - Left/Right
        elif code == "ABS_RX":
            if INVERT_LOOK_X:
                look_left = 1 if state > (128 + DEAD_ZONE) else 0
                look_right = 1 if state < (128 - DEAD_ZONE) else 0
            else:
                look_left = 1 if state < (128 - DEAD_ZONE) else 0
                look_right = 1 if state > (128 + DEAD_ZONE) else 0

            send_osc_to_all("/input/LookLeft", look_left)
            send_osc_to_all("/input/LookRight", look_right)
            log_callback(f"LookLeft: {look_left}, LookRight: {look_right}")
            button_states[code] = state

GUI setup

def start_gui():
root = tk.Tk()
root.title(“VRChat Controller Router”)
root.geometry(“360x220”)
root.resizable(False, False)

log_box = tk.Text(root, height=12, width=45)
log_box.pack(pady=10)

def log(msg):
    log_box.insert(tk.END, msg + "\n")
    log_box.see(tk.END)

Thread(target=controller_loop, args=(log,), daemon=True).start()
root.mainloop()

Run GUI

start_gui()

import tkinter as tk
from threading import Thread
from inputs import get_gamepad
from pythonosc.udp_client import SimpleUDPClient

OSC targets

osc_targets = [
SimpleUDPClient(“127.0.0.1”, 9000),
SimpleUDPClient(“127.0.0.1”, 9002),
SimpleUDPClient(“127.0.0.1”, 9004)
]

Configurable settings

DEAD_ZONE = 50
INVERT_LOOK_Y = True
INVERT_LOOK_X = False
INVERT_MOVE_Y = True

Button state tracking

button_states = {
“BTN_SOUTH”: 0,
“BTN_EAST”: 0,
“ABS_Y”: 128,
“ABS_X”: 128,
“ABS_RY”: 128,
“ABS_RX”: 128
}

def send_osc_to_all(address, value):
for client in osc_targets:
client.send_message(address, value)

def controller_loop(log_callback):
while True:
events = get_gamepad()
for event in events:
code = event.code
state = event.state

        # Jump
        if code == "BTN_SOUTH":
            if state != button_states[code]:
                send_osc_to_all("/input/Jump", state)
                log_callback(f"Jump: {state}")
                button_states[code] = state

        # Crouch
        elif code == "BTN_EAST":
            if state != button_states[code]:
                send_osc_to_all("/input/Crouch", state)
                log_callback(f"Crouch: {state}")
                button_states[code] = state

        # Movement - Forward/Backward
        elif code == "ABS_Y":
            forward = backward = 0
            if INVERT_MOVE_Y:
                if state > (128 + DEAD_ZONE):
                    forward = 1
                elif state < (128 - DEAD_ZONE):
                    backward = 1
            else:
                if state < (128 - DEAD_ZONE):
                    forward = 1
                elif state > (128 + DEAD_ZONE):
                    backward = 1

            send_osc_to_all("/input/MoveForward", forward)
            send_osc_to_all("/input/MoveBackward", backward)
            log_callback(f"Forward: {forward}, Backward: {backward}")
            button_states[code] = state

        # Movement - Left/Right
        elif code == "ABS_X":
            left = right = 0
            if state < (128 - DEAD_ZONE):
                left = 1
            elif state > (128 + DEAD_ZONE):
                right = 1

            send_osc_to_all("/input/MoveLeft", left)
            send_osc_to_all("/input/MoveRight", right)
            log_callback(f"Left: {left}, Right: {right}")
            button_states[code] = state

        # Look - Up/Down
        elif code == "ABS_RY":
            look_up = look_down = 0
            if INVERT_LOOK_Y:
                if state > (128 + DEAD_ZONE):
                    look_up = 1
                elif state < (128 - DEAD_ZONE):
                    look_down = 1
            else:
                if state < (128 - DEAD_ZONE):
                    look_up = 1
                elif state > (128 + DEAD_ZONE):
                    look_down = 1

            send_osc_to_all("/input/LookUp", look_up)
            send_osc_to_all("/input/LookDown", look_down)
            log_callback(f"LookUp: {look_up}, LookDown: {look_down}")
            button_states[code] = state

        # Look - Left/Right
        elif code == "ABS_RX":
            look_left = look_right = 0
            if INVERT_LOOK_X:
                if state > (128 + DEAD_ZONE):
                    look_left = 1
                elif state < (128 - DEAD_ZONE):
                    look_right = 1
            else:
                if state < (128 - DEAD_ZONE):
                    look_left = 1
                elif state > (128 + DEAD_ZONE):
                    look_right = 1

            send_osc_to_all("/input/LookLeft", look_left)
            send_osc_to_all("/input/LookRight", look_right)
            log_callback(f"LookLeft: {look_left}, LookRight: {look_right}")
            button_states[code] = state

GUI setup

def start_gui():
root = tk.Tk()
root.title(“VRChat Controller Router”)
root.geometry(“360x220”)
root.resizable(False, False)

log_box = tk.Text(root, height=12, width=45)
log_box.pack(pady=10)

def log(msg):
    log_box.insert(tk.END, msg + "\n")
    log_box.see(tk.END)

Thread(target=controller_loop, args=(log,), daemon=True).start()
root.mainloop()

Run GUI

start_gui()

import tkinter as tk
from threading import Thread
from inputs import get_gamepad
from pythonosc.udp_client import SimpleUDPClient

OSC targets

osc_targets = [
SimpleUDPClient(“127.0.0.1”, 9000),
SimpleUDPClient(“127.0.0.1”, 9002),
SimpleUDPClient(“127.0.0.1”, 9004)
]

— Configurable Settings —

DEAD_ZONE changed to 108, which is approx 85% of the 0-127.5 half-axis range.

DEAD_ZONE = 108

Set to True if movement/look is reversed in VRChat

INVERT_LOOK_Y = True
INVERT_LOOK_X = False
INVERT_MOVE_Y = True

— VRChat Axis Mapping Configuration —

Set to True to use alternative VRChat addresses (/input/Vertical, etc.)

USE_ALTERNATIVE_AXES = True

if USE_ALTERNATIVE_AXES:
AXIS_MAPPING = {
“ABS_Y”: “/input/Vertical”, # Left Stick Vertical (Fwd/Bwd)
“ABS_X”: “/input/Horizontal”, # Left Stick Horizontal (Lft/Rgt)
“ABS_RY”: “/input/LookVertical”, # Right Stick Vertical (Up/Down)
“ABS_RX”: “/input/LookHorizontal”, # Right Stick Horizontal (Lft/Rgt)
}
else:
AXIS_MAPPING = {
“ABS_Y”: “/input/AxisY”, # Left Stick Vertical (Fwd/Bwd)
“ABS_X”: “/input/AxisX”, # Left Stick Horizontal (Lft/Rgt)
“ABS_RY”: “/input/VerticalLookAxis”,# Right Stick Vertical (Up/Down)
“ABS_RX”: “/input/LookAxis”, # Right Stick Horizontal (Lft/Rgt)
}

Button/Axis state tracking

button_states = {
“BTN_SOUTH”: 0, “BTN_EAST”: 0,
“ABS_Y”: 128, “ABS_X”: 128,
“ABS_RY”: 128, “ABS_RX”: 128,
“ABS_Z”: 0, “ABS_RZ”: 0
}

def send_osc_to_all(address, value):
if isinstance(value, (int, float)):
value = float(value)

for client in osc_targets:
    try:
        client.send_message(address, value)
    except Exception as e:
        print(f"Error sending OSC message to client: {e}") 

def scale_axis_input(raw_state, dead_zone):
“”"
Scales the raw 0-255 axis state (Sticks) to a float from -1.0 to 1.0,
applying the dead zone.
“”"
center = 127.5
normalized_state = raw_state - center

if abs(normalized_state) < dead_zone:
    return 0.0 # Within dead zone

direction = 1.0 if normalized_state > 0 else -1.0
raw_magnitude = abs(normalized_state) - dead_zone
max_scale_range = 127.5 - dead_zone 

if max_scale_range <= 0:
    return 0.0

scaled_magnitude = min(1.0, raw_magnitude / max_scale_range)

return scaled_magnitude * direction

def scale_trigger_input(raw_state, max_val=255):
“”"
Scales the raw 0-Max trigger state to a float from 0.0 to 1.0.
“”"
return min(1.0, float(raw_state) / float(max_val))

def controller_loop(log_callback):
mode = “ALTERNATIVE” if USE_ALTERNATIVE_AXES else “PRIMARY”
log_callback(f"— USING {mode} VRChat AXIS INPUTS —")

while True:
    events = get_gamepad()
    for event in events:
        code = event.code
        state = event.state

        # --- Digital Buttons ---
        if code == "BTN_SOUTH":
            if state != button_states[code]:
                send_osc_to_all("/input/Jump", state)
                log_callback(f"Jump: {state}")
                button_states[code] = state

        elif code == "BTN_EAST":
            if state != button_states[code]:
                send_osc_to_all("/input/Crouch", state)
                log_callback(f"Crouch: {state}")
                button_states[code] = state

        # --- Analog Axes & Triggers ---
        
        # Filter noise on analog inputs
        if code in button_states and code.startswith("ABS"):
            noise_threshold = 3 if code.endswith(('X', 'Y', 'RY', 'RX')) else 5 
            if abs(state - button_states[code]) < noise_threshold:
                continue
            button_states[code] = state
        
        # --- STICKS (Move & Look) ---
        
        # Movement - Forward/Backward (ABS_Y - Left Stick Vertical)
        if code == "ABS_Y":
            move_value = scale_axis_input(state, DEAD_ZONE)
            if INVERT_MOVE_Y:
                move_value = -move_value
            
            address = AXIS_MAPPING[code]
            send_osc_to_all(address, move_value)
            # The log is crucial: check if it shows smooth floats (e.g., 0.10, 0.25, 0.50)
            log_callback(f"{address}: {move_value:.2f}")

        # Movement - Left/Right (ABS_X - Left Stick Horizontal)
        elif code == "ABS_X":
            move_value = scale_axis_input(state, DEAD_ZONE)
            
            address = AXIS_MAPPING[code]
            send_osc_to_all(address, move_value)
            log_callback(f"{address}: {move_value:.2f}")

        # Look - Up/Down (ABS_RY - Right Stick Vertical)
        elif code == "ABS_RY":
            look_value = scale_axis_input(state, DEAD_ZONE)
            if INVERT_LOOK_Y:
                look_value = -look_value
            
            address = AXIS_MAPPING[code]
            send_osc_to_all(address, look_value)
            log_callback(f"{address}: {look_value:.2f}")

        # Look - Left/Right (ABS_RX - Right Stick Horizontal)
        elif code == "ABS_RX":
            look_value = scale_axis_input(state, DEAD_ZONE)
            if INVERT_LOOK_X:
                look_value = -look_value
            
            address = AXIS_MAPPING[code]
            send_osc_to_all(address, look_value)
            log_callback(f"{address}: {look_value:.2f}")

        # --- TRIGGERS ---
        
        # Left Trigger (ABS_Z) -> VRChat Run/Sprint
        elif code == "ABS_Z":
            run_value = scale_trigger_input(state, max_val=255) 
            send_osc_to_all("/input/Run", run_value)
            log_callback(f"/input/Run (Left Trigger): {run_value:.2f}")
            
        # Right Trigger (ABS_RZ) -> VRChat Use
        elif code == "ABS_RZ":
            use_value = scale_trigger_input(state, max_val=255) 
            send_osc_to_all("/input/Use", use_value) 
            log_callback(f"/input/Use (Right Trigger): {use_value:.2f}")

GUI setup

def start_gui():
root = tk.Tk()
root.title(“VRChat Controller Router”)
root.geometry(“400x300”)
root.resizable(False, False)

log_frame = tk.Frame(root)
log_frame.pack(pady=10, padx=10, fill="both", expand=True)

scrollbar = tk.Scrollbar(log_frame)
log_box = tk.Text(log_frame, height=15, width=50, yscrollcommand=scrollbar.set)
scrollbar.config(command=log_box.yview)

scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
log_box.pack(side=tk.LEFT, fill="both", expand=True)

def log(msg):
    log_box.insert(tk.END, msg + "\n")
    log_box.see(tk.END)

log(f"Starting controller loop...")
log(f"Targets: 9000, 9002, 9004")
log(f"Dead Zone: {DEAD_ZONE}")

# Log the Axis Addresses being used
mode = "ALTERNATIVE" if USE_ALTERNATIVE_AXES else "PRIMARY"
log(f"--- USING {mode} VRChat AXIS INPUTS ---")

Thread(target=controller_loop, args=(log,), daemon=True).start()

def on_closing():
    root.quit()
    
root.protocol("WM_DELETE_WINDOW", on_closing)

root.mainloop()

Run GUI

start_gui()