Skip to content

Commit 06388fd

Browse files
committed
Initial commit: RainingKeys core implementation
0 parents  commit 06388fd

8 files changed

Lines changed: 488 additions & 0 deletions

File tree

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
__pycache__/
2+
*.pyc
3+
implementation_plan.md
4+
task.md
5+
walkthrough.md

README.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# RainingKeys (Python)
2+
3+
**A high-performance, external rhythm game input visualizer.**
4+
5+
RainingKeys is a purely external overlay application that visualizes keyboard inputs as falling bars, similar to the "Rain" mode found in various rhythm game mods. It provides visual feedback on rhythm stability, micro-jitter, and input timing without injecting code into the game process.
6+
7+
> [!NOTE]
8+
> This project is a **standalone external tool**. It is **NOT** a game mod and does **NOT** perform DLL injection or memory hooking. It is safe to use with anti-cheat software that permits external overlays.
9+
10+
---
11+
12+
## Features
13+
14+
- **External Overlay**: Runs as a transparent, always-on-top, click-through window over any game.
15+
- **Accurate Timing**: Uses high-resolution monotonic clocks (`time.perf_counter`) for smooth, jitter-free falling animation.
16+
- **Lane System**: Configurable key-to-lane mapping (e.g., WASD, Space, Enter).
17+
- **Long Press Support**: Visualizes held keys with variable-length bars.
18+
- **Performance Optimized**: Implements object pooling and efficient rendering logic to minimize CPU/GPU usage.
19+
- **Fade Out**: Distance-based fade-out for visual clarity.
20+
- **Input Latency Compensation**: Configurable offset to visually align inputs with audio latency.
21+
22+
## Tech Stack
23+
24+
- **Python 3.10+**
25+
- **PySide6 (Qt)**: High-performance rendering and window management.
26+
- **pynput**: Global low-level keyboard hook.
27+
- **pywin32**: Windows API integration for transparency and click-through flags.
28+
29+
## Project Structure
30+
31+
```
32+
RainingKeysPython/
33+
├── core/
34+
│ ├── config.py # User configuration (Keys, speeds, colors)
35+
│ ├── input_mon.py # Global input listener
36+
│ └── overlay.py # Main rendering loop and window logic
37+
├── main.py # Application entry point
38+
└── requirements.txt # Dependencies
39+
```
40+
41+
## Installation
42+
43+
1. **Prerequisites**: Ensure you have Python 3.10 or newer installed.
44+
2. **Clone the repository** (or download source):
45+
```bash
46+
git clone https://github.com/your-username/RainingKeysPython.git
47+
cd RainingKeysPython
48+
```
49+
3. **Install dependencies**:
50+
```bash
51+
pip install -r requirements.txt
52+
```
53+
54+
## Usage
55+
56+
1. Run the application:
57+
```bash
58+
python main.py
59+
```
60+
2. The overlay will appear (default: full screen transparent window).
61+
3. Press the configured keys (Default: `a`, `s`, `l`, `;`) to see the visualization.
62+
4. The Debug Overlay in the top-left corner shows FPS and object pool stats.
63+
5. **To Exit**: Press `Ctrl+C` in the terminal window.
64+
65+
## Configuration
66+
67+
Edit `core/config.py` to customize the overlay:
68+
69+
| Parameter | Description |
70+
| :--- | :--- |
71+
| `SCROLL_SPEED` | Falling speed in pixels per second. |
72+
| `LANE_MAP` | Dictionary mapping specific keys (e.g., `'a'`, `'Key.space'`) to lane indices. |
73+
| `BAR_WIDTH` | Visual width of the falling notes. |
74+
| `INPUT_LATENCY_OFFSET` | Seconds to offset rendering (useful for audio sync). |
75+
| `MAX_BARS` | Soft limit for the object pool (prevents memory leaks). |
76+
| `COLORS` | RGBA values for bars and text. |
77+
78+
## Roadmap
79+
80+
- [ ] Interactive Configuration UI (GUI for settings)
81+
- [ ] Save/Load config from JSON/YAML
82+
- [ ] Multi-monitor support
83+
- [ ] Custom skins/textures for bars
84+
85+
## Disclaimer
86+
87+
This software is an unofficial community tool. It is not affiliated with, endorsed by, or connected to 7th Beat Games (developers of A Dance of Fire and Ice) or any other game developer. Use responsibly.
88+
89+
## License
90+
91+
MIT License. See `LICENSE` for details.

core/__init__.py

Whitespace-only changes.

core/config.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from PySide6.QtGui import QColor
2+
3+
class Config:
4+
# Visual Settings
5+
SCROLL_SPEED = 800 # Pixels per second
6+
BAR_WIDTH = 60 # Width of a single note bar
7+
BAR_HEIGHT = 20 # Visual thickness of the bar (does not affect timing)
8+
9+
# Lane Configuration
10+
# Maps key strings (pynput format) to lane indices (0-based)
11+
LANE_MAP = {
12+
"'a'": 0,
13+
"'s'": 1,
14+
"'l'": 2,
15+
"';'": 3
16+
}
17+
LANE_WIDTH = 70 # Horizontal spacing between lane starts
18+
LANE_START_X = 50 # Starting X offset for the first lane
19+
20+
# Performance & Logic
21+
MAX_BARS = 300 # Soft limit for object pool
22+
INPUT_LATENCY_OFFSET = 0.0 # Seconds to add/subtract to align visual with audio
23+
24+
# Fade Out Logic
25+
# Position Y where fade starts (e.g., 80% down the screen)
26+
FADE_START_Y = 800
27+
FADE_RANGE = 200 # Distance over which it fades to 0 opacity
28+
29+
# Debugging
30+
DEBUG_MODE = True
31+
32+
# Colors (R, G, B, A)
33+
COLOR_BAR = QColor(100, 200, 255, 200)
34+
COLOR_BAR_BORDER = QColor(255, 255, 255, 230)
35+
COLOR_DEBUG_TEXT = QColor(0, 255, 0, 255)

core/input_mon.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import time
2+
from PySide6.QtCore import QObject, Signal, QThread
3+
from pynput import keyboard
4+
from .config import Config
5+
6+
class InputWorker(QObject):
7+
"""
8+
Worker that runs the pynput listener in a separate thread.
9+
Emits (lane_index, timestamp) when a tracked key is pressed or released.
10+
"""
11+
key_pressed = Signal(int, float) # lane_index, timestamp
12+
key_released = Signal(int, float) # lane_index, timestamp
13+
14+
def __init__(self):
15+
super().__init__()
16+
self.listener = None
17+
self.running = False
18+
self.active_keys = set() # Track pressed keys to filter autorepeats
19+
20+
def start_monitoring(self):
21+
self.running = True
22+
self.active_keys.clear()
23+
self.listener = keyboard.Listener(on_press=self.on_press, on_release=self.on_release)
24+
self.listener.start()
25+
print("Input monitoring started.")
26+
27+
def stop_monitoring(self):
28+
self.running = False
29+
if self.listener:
30+
self.listener.stop()
31+
self.listener = None
32+
print("Input monitoring stopped.")
33+
34+
def _get_key_str(self, key):
35+
try:
36+
return f"'{key.char}'"
37+
except AttributeError:
38+
return str(key)
39+
40+
def on_press(self, key):
41+
if not self.running:
42+
return
43+
44+
k_str = self._get_key_str(key)
45+
46+
# Filter autorepeat: If key is already in active_keys, ignore it
47+
if k_str in self.active_keys:
48+
return
49+
50+
if k_str in Config.LANE_MAP:
51+
self.active_keys.add(k_str)
52+
timestamp = time.perf_counter()
53+
lane_idx = Config.LANE_MAP[k_str]
54+
self.key_pressed.emit(lane_idx, timestamp)
55+
56+
def on_release(self, key):
57+
if not self.running:
58+
return
59+
60+
k_str = self._get_key_str(key)
61+
62+
if k_str in self.active_keys:
63+
self.active_keys.remove(k_str)
64+
65+
if k_str in Config.LANE_MAP:
66+
timestamp = time.perf_counter()
67+
lane_idx = Config.LANE_MAP[k_str]
68+
self.key_released.emit(lane_idx, timestamp)
69+
70+
class InputMonitor(QThread):
71+
"""
72+
Thread wrapper for the worker.
73+
"""
74+
key_pressed = Signal(int, float)
75+
key_released = Signal(int, float)
76+
77+
def __init__(self):
78+
super().__init__()
79+
self.worker = InputWorker()
80+
self.worker.key_pressed.connect(self.key_pressed.emit)
81+
self.worker.key_released.connect(self.key_released.emit)
82+
83+
def run(self):
84+
self.worker.start_monitoring()
85+
self.exec()
86+
self.worker.stop_monitoring()

0 commit comments

Comments
 (0)