Controller configs

This commit is contained in:
Joey Yakimowich-Payne 2025-05-16 09:22:03 -06:00
commit c43cb5e0ce
6 changed files with 1275 additions and 307 deletions

127
README.md
View file

@ -383,6 +383,133 @@ python main.py --token-cache /path/to/your/cache.json
!token
```
## Game Control Configuration (`data/config/game_controls.json`)
The `data/config/game_controls.json` file allows you to define custom game control schemes for different games. This enables the bot to adapt its input commands (keyboard or controller) based on the game currently being played.
### Structure
The JSON file has the following main structure:
```json
{
"default_game": "YourDefaultGameName",
"games": {
"YourDefaultGameName": {
// Configuration for YourDefaultGameName
},
"AnotherGameName": {
// Configuration for AnotherGameName
}
// ... more game configurations
}
}
```
- `"default_game"`: (String) Specifies the name of the game configuration that will be loaded by default when the bot starts. The command names from this game's configuration will be registered at startup.
- `"games"`: (Object) A dictionary where each key is a unique game name (e.g., "Apex Legends", "Minecraft", "MyCustomGame"). Each game name maps to an object containing its specific control configuration.
### Game Configuration Structure
Each game entry within the `"games"` object has the following structure:
```json
"GameName": {
"description": "A brief description of this game or control scheme.",
"keyboard": {
// Keyboard control mappings for GameName
},
"controller": {
// Controller control mappings for GameName
}
}
```
- `"description"`: (String) A human-readable description for this game configuration (e.g., "Apex Legends - Standard Controls", "Dark Souls - Fat Roll Setup"). This description is shown in `!gamehelp`.
- `"keyboard"`: (Object) Contains mappings for keyboard-based commands.
- `"controller"`: (Object) Contains mappings for virtual gamepad-based commands.
### Control Mapping Structure (Keyboard & Controller)
Inside both the `"keyboard"` and `"controller"` objects, you define individual control commands. The **keys** in these objects are the **actual command strings** users will type in Twitch chat (without the `!` prefix). For example, if you define `"w": {...}`, users will type `!w`.
Each command mapping has the following structure:
```json
"command_user_types": {
"action_type": "type_of_action_to_perform",
"params": {
// Parameters specific to the action_type
},
"description": "What this command does in this specific game (e.g., 'Move Forward', 'Jump', 'Primary Attack')."
}
```
- `"command_user_types"`: (String) The command string the user types (e.g., `"w"`, `"space"`, `"mouse_left"`, `"a_button"`, `"ls_up"`).
- `"action_type"`: (String) Defines the kind of input to simulate. This corresponds to methods available in `KeyboardController` or `VirtualController`.
- **Common Keyboard `action_type` values:**
- `"press_key"`: Simulates pressing a keyboard key.
- `params`: `{ "key": "key_name" }` (e.g., `"w"`, `"space"`, `"shift"`, `"ctrl"`, `"1"`)
- `"press_mouse_button"`: Simulates a mouse button press.
- `params`: `{ "button": "button_name" }` (e.g., `"left"`, `"right"`, `"middle"`)
- **Common Controller `action_type` values:**
- `"press_button"`: Simulates pressing a gamepad button.
- `params`: `{ "button_name": "name_of_button" }` (e.g., `"a"`, `"b"`, `"x"`, `"y"`, `"left_shoulder"`, `"right_shoulder"`, `"left_thumb"`, `"right_thumb"`, `"start"`, `"back"`)
- `"press_trigger"`: Simulates pressing a gamepad trigger.
- `params`: `{ "trigger_name": "left_or_right" }` (e.g., `"left"`, `"right"`)
- `"move_left_stick"` / `"move_right_stick"`: Simulates moving an analog stick.
- `params`: `{ "direction": "up/down/left/right" }` (e.g., `"up"`)
- *Note: For more precise stick control, the `VirtualController` might support direct x/y values in the future, which would require different params.*
- `"press_dpad"`: Simulates pressing a D-Pad direction.
- `params`: `{ "direction": "up/down/left/right" }`
- `"params"`: (Object) A dictionary of parameters required by the specified `action_type`. The keys and values within `params` depend directly on what the underlying controller methods (`KeyboardController` or `VirtualController` methods) expect.
- An optional `"duration"` (float, in seconds) can often be included in `params` to specify how long a key/button should be held. If omitted, a short default (e.g., 0.1 seconds) is typically used.
- `"description"`: (String) This is crucial. It explains what the command does in the context of *this specific game*. This description is shown to users via the `!gamehelp` command.
### Example Snippet
```json
{
"default_game": "ExampleFPS",
"games": {
"ExampleFPS": {
"description": "Standard FPS Controls",
"keyboard": {
"w": {"action_type": "press_key", "params": {"key": "w"}, "description": "Move Forward"},
"s": {"action_type": "press_key", "params": {"key": "s"}, "description": "Move Backward"},
"mouse_left": {"action_type": "press_mouse_button", "params": {"button": "left"}, "description": "Fire Weapon"}
},
"controller": {
"ls_up": {"action_type": "move_left_stick", "params": {"direction": "up"}, "description": "Move Forward (L-Stick)"},
"rt": {"action_type": "press_trigger", "params": {"trigger_name": "right"}, "description": "Fire Weapon (RT)"},
"a": {"action_type": "press_button", "params": {"button_name": "a"}, "description": "Jump (A Button)"}
}
},
"RacingSim": {
"description": "Basic Racing Controls",
"keyboard": {
"w": {"action_type": "press_key", "params": {"key": "w", "duration": 0.5}, "description": "Accelerate"},
"s": {"action_type": "press_key", "params": {"key": "s", "duration": 0.3}, "description": "Brake/Reverse"}
},
"controller": {
"rt": {"action_type": "press_trigger", "params": {"trigger_name": "right"}, "description": "Accelerate (RT)"},
"lt": {"action_type": "press_trigger", "params": {"trigger_name": "left"}, "description": "Brake/Reverse (LT)"}
}
}
}
}
```
### Activating a Game Configuration
- At startup, the commands defined in the `keyboard` and `controller` sections of the `default_game` are registered. Their descriptions (from the JSON) are used.
- An admin can change the active game configuration at any time using the `!setgame <GameName>` command (e.g., `!setgame RacingSim`).
- Once the game is switched:
- The `!gamehelp` command will display the controls for the newly activated game.
- When users type a command (e.g., `!w`), the bot will execute the action defined for `"w"` in the *currently active game's* configuration. If `"w"` is not defined for the active game, it will be treated as an unknown command for that game.
This system allows for a highly flexible and game-adaptive control scheme managed entirely through the `game_controls.json` file.
## License
This project is open source and available under the MIT License.

37
main.py
View file

@ -295,22 +295,28 @@ def main() -> None:
print("Keyboard/mouse control will be simulated but not actually perform any actions.")
print("Run 'pip install pynput' to enable actual keyboard/mouse control.")
game_controller: GameController = GameController(
bot,
mode=args.game_mode,
cooldown=args.cooldown,
input_type=args.input_type
)
if args.game_mode == "direct":
print(f"Commands will execute immediately with a {args.cooldown}s cooldown per user")
# Only initialize the GameController when NOT using queue mode
# When in queue mode, the controller is initialized in the queue consumer process
if not args.use_queue:
game_controller: GameController = GameController(
bot,
mode=args.game_mode,
cooldown=args.cooldown,
input_type=args.input_type
)
if args.game_mode == "direct":
print(f"Commands will execute immediately with a {args.cooldown}s cooldown per user")
else:
print("Commands will be collected through voting")
print("Streamer can start a vote with !startvote and end with !endvote")
print("Users vote with !vote [command]")
print("Use !gamehelp to see available game commands")
print("Check user stats with !gamestats [username]")
else:
print("Commands will be collected through voting")
print("Streamer can start a vote with !startvote and end with !endvote")
print("Users vote with !vote [command]")
print("Use !gamehelp to see available game commands")
print("Check user stats with !gamestats [username]")
print("Game control will be handled by the queue consumer process")
print("Use !gamehelp to see available game commands")
except ImportError as e:
print(f"Failed to import game control features: {e}")
@ -319,7 +325,6 @@ def main() -> None:
if args.input_type == 'controller':
print(" pip install vgamepad # For controller support")
print("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxinput type: ", args.input_type)
# Start the bot
print(f"Starting bot for channel #{channel}")
print("Basic commands: !hello, !dice [sides], !echo [message], !8ball")

View file

@ -7,13 +7,14 @@ import json
import logging
import requests
import webbrowser
import traceback
from typing import Dict, Any, Optional, Tuple
# Import the auth server functionality
from src.core.auth_server import start_oauth_flow
# Configure logging
logging.basicConfig(level=logging.INFO)
logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')
logger = logging.getLogger(__name__)
class TwitchAuth:
@ -128,7 +129,7 @@ class TwitchAuth:
return True
except Exception as e:
logger.error(f"Failed to refresh token: {e}")
logger.error(f"Failed to refresh token: {e}\n{traceback.format_exc()}")
# Clear refresh token on failure to force a new auth flow
self.refresh_token = None
return False
@ -172,7 +173,7 @@ class TwitchAuth:
return True, "Authentication successful"
except Exception as e:
logger.error(f"Failed to exchange authorization code: {e}")
logger.error(f"Failed to exchange authorization code: {e}\n{traceback.format_exc()}")
return False, f"Failed to exchange authorization code: {str(e)}"
def _setup_auth(self, manual_mode: bool = False) -> bool:
@ -258,7 +259,7 @@ class TwitchAuth:
print("=====================================\n")
return True
except Exception as e:
logger.error(f"Failed to validate token: {e}")
logger.error(f"Failed to validate token: {e}\n{traceback.format_exc()}")
print("\n===== Authentication Status =====")
print("Warning: Could not validate token")
print("============================\n")
@ -342,7 +343,7 @@ class TwitchAuth:
response.raise_for_status()
return response.json()
except Exception as e:
logger.error(f"Token validation failed: {e}")
logger.error(f"Token validation failed: {e}\n{traceback.format_exc()}")
raise ValueError(f"Token validation failed: {str(e)}")
def update_manually(self, access_token: str, refresh_token: str = None, expires_in: int = 14400) -> None:

View file

@ -6,6 +6,7 @@ import requests
import time
import json
import os
import traceback
from typing import Dict, Callable, List, Optional, Any, Tuple, Union, Match, Set
# Import the queue functionality
@ -203,7 +204,7 @@ class TwitchBot:
return False
except Exception as e:
logger.error(f"Failed to connect: {e}")
logger.error(f"Failed to connect: {e}\n{traceback.format_exc()}")
return False
def register_command(self, command: str, callback: CommandCallback) -> None:
@ -233,6 +234,10 @@ class TwitchBot:
"""Process commands from chat messages"""
if not message_data:
return
# Ignore messages from the bot itself
if message_data['username'].lower() == self.username.lower():
return
content: str = message_data['content'].strip()
username: str = message_data['username']
@ -262,9 +267,6 @@ class TwitchBot:
if not self.oauth_token:
self.oauth_token = self.get_oauth_token()
# Simply pass the command name and let the queue worker handle all processing
logger.info(f"Enqueueing command '{command}' for processing by queue worker")
enqueue_command(
username,
command,
@ -280,7 +282,7 @@ class TwitchBot:
try:
self.commands[command](username, args, self)
except Exception as e:
logger.error(f"Error executing command {command}: {e}")
logger.error(f"Error executing command {command}: {e}\n{traceback.format_exc()}")
def start(self, use_queue: bool = False) -> None:
"""Start the bot and listen for messages"""
@ -291,10 +293,13 @@ class TwitchBot:
# Try to connect with up to 3 attempts, possibly with token refreshes
max_attempts = 3
for attempt in range(1, max_attempts + 1):
logger.info(f"Connection attempt {attempt}/{max_attempts}")
if self.connect():
res = self.connect()
if res:
break
elif attempt < max_attempts:
logger.info(f"Connection attempt {attempt}/{max_attempts}")
if attempt < max_attempts:
# Wait a moment before retrying
time.sleep(2)
logger.info("Retrying connection...")
@ -370,3 +375,30 @@ class TwitchBot:
def get_queue_stats(self) -> Dict[str, Any]:
"""Get current queue statistics"""
return get_queue_stats()
# Add a standalone version of is_admin for use by other modules
def is_admin(username: str, channel: str = None, admin_users: List[str] = None) -> bool:
"""
Check if a user has admin privileges
Args:
username: The username to check
channel: The channel name (channel owner is always an admin)
admin_users: Optional list of additional admin users
Returns:
bool: True if the user is an admin, False otherwise
"""
# Normalize usernames to lowercase
username = username.lower()
# Create admin list - channel owner is always an admin
admins = []
if channel:
admins.append(channel.lower())
# Add additional admins if provided
if admin_users:
admins.extend([user.lower() for user in admin_users])
return username in admins

View file

@ -1,8 +1,13 @@
import time
import logging
import traceback
import threading
from typing import Optional, Union, Any, Literal, Dict, Tuple, List, Set
import ctypes
logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')
logger = logging.getLogger(__name__)
# Try to import vgamepad for Xbox controller emulation
try:
import vgamepad as vg
@ -69,7 +74,7 @@ class VirtualController:
self.gamepad = vg.VX360Gamepad()
print("Virtual Xbox 360 controller initialized successfully")
except Exception as e:
print(f"Failed to initialize virtual gamepad: {e}")
logger.error(f"Failed to initialize virtual gamepad: {e}\n{traceback.format_exc()}")
self.available = False
# Initialize SDL2 for physical controller if available

File diff suppressed because it is too large Load diff