Skip to content

Instantly share code, notes, and snippets.

@catleeball
Last active May 18, 2021 22:20
Show Gist options
  • Save catleeball/d1ac3a3b176efc88ff22c88928cde59b to your computer and use it in GitHub Desktop.
Save catleeball/d1ac3a3b176efc88ff22c88928cde59b to your computer and use it in GitHub Desktop.
Twitch Plays Caves of Qud
// Go script to allow Twitch viewers to play Caves of Qud via Twitch chat.
//
// This is a really messy, hacky, bad script.
package main
import (
"fmt"
"regexp"
"strings"
"github.com/gempir/go-twitch-irc"
"github.com/go-vgo/robotgo"
)
func main() {
// 0. Open game and make sure it's the active window
// TODO: automate step 0
// 1. Connect to IRC
// 2. Listen to IRC messages
// 3. Parse message into keystrokes
// 4. Send commands to game
conntectIrc()
}
func conntectIrc() {
// TODO: get auth from config file
const twitchName string = "goplaytwitch"
const oauthToken string = "oauth:xxxxx"
fmt.Println("Connecting to IRC")
client := twitch.NewClient(twitchName, oauthToken)
client.OnNewMessage(ircMessageHandler)
client.Join(twitchName)
err := client.Connect()
if err != nil {
panic(err)
}
}
func ircMessageHandler(channel string, user twitch.User, message twitch.Message) {
username := user.Username
msg := strings.ToLower(message.Text)
if len(username) > 6 {
username = username[:6]
}
if len(msg) == 1 {
handleSingleChar(msg, username)
} else {
handleMultipleChars(msg, username)
}
}
func handleSingleChar(msg string, username string) {
unshiftedKey := unshiftKey(msg)
if unshiftedKey != "" {
sendShiftKey(username, unshiftedKey)
} else {
sendKey(username, msg)
}
}
func handleMultipleChars(msg string, username string) {
key, mods := parseModifiers(msg)
unshiftedKey := unshiftKey(msg)
if unshiftedKey != "" {
key = unshiftedKey
}
isAlias := false
if isKeyAlias(key) != "" {
isAlias = true
key = isKeyAlias(key)
}
if isKey(key) || (isAlphaNum(key) && len(key) == 1) || isAlias {
if len(mods) <= 0 {
sendKey(username, key)
} else {
sendKeys(username, key, mods)
}
}
}
func sendKey(user string, key string) {
fmt.Println(user + ": " + key)
if isEsc(key) {
robotgo.KeyTap(key)
robotgo.KeyTap(key)
} else {
robotgo.KeyTap(key)
}
}
func sendKeys(user string, key string, mods []string) {
if areKeysBlacklisted(key, mods) != true {
fmt.Println(user + ": " + key + "+" + strings.Join(mods, "+"))
robotgo.KeyTap(key, mods)
}
}
func sendShiftKey(user string, key string) {
fmt.Println(user + ": " + key + "+shift")
robotgo.KeyTap(key, "shift")
}
func parseModifiers(msg string) (string, []string) {
msgArr := strings.Fields(msg)
key := msgArr[0]
mods := []string{}
if len(msgArr) < 2 {
return key, mods
}
if areModsValid(msgArr) == true {
mods = msgArr[1:]
}
return key, mods
}
func isModifier(modifier string) bool {
modifierKeys := map[string]bool{
"control": true,
"alt": true,
"shift": true,
// "command": true,
}
return modifierKeys[modifier]
}
func areModsValid(msgArr []string) bool {
// We can't have more than three modifiers;
// we only have ctrl, alt, and shift.
valid := true
if len(msgArr) > 1 && len(msgArr) <= 3 {
for _, mod := range msgArr[1:] {
if isModifier(mod) != true {
valid = false
break
}
}
} else {
valid = false
}
return valid
}
func areKeysBlacklisted(key string, mods []string) bool {
if key == "f4" {
for _, mod := range mods {
if mod == "alt" {
return true
}
}
}
if key == "tab" {
for _, mod := range mods {
if mod == "alt" {
return true
}
}
}
if key == "delete" {
if len(mods) > 1 {
return true
}
}
return false
}
func isEsc(key string) bool {
if key == "escape" {
return true
}
return false
}
func unshiftKey(key string) string {
// Sending characters like '@' to
// robotgo.KeyTap doesn't work as expected,
// so we need to send robotgo.KeyTap("2", "shift")
shiftModChars := map[string]string{
"<": ",",
">": ".",
"?": "/",
":": ";",
"\"": "'",
"{": "[",
"}": "]",
"|": "\\",
"~": "`",
"!": "1",
"@": "2",
"#": "3",
"$": "4",
"%": "5",
"^": "6",
"&": "7",
"*": "8",
"(": "9",
")": "0",
"_": "-",
"+": "=",
}
return shiftModChars[key]
}
func isKeyAlias(key string) string {
// Friendly names for valid keys.
keyAliases := map[string]string{
"esc": "escape",
"del": "delete",
"return": "enter",
"pgup": "pageup",
"pgdown": "pagedown",
"pgdwn": "pagedown",
"pgdn": "pgdn",
"np0": "numpad_0",
"np1": "numpad_1",
"np2": "numpad_2",
"np3": "numpad_3",
"np4": "numpad_4",
"np5": "numpad_5",
"np6": "numpad_6",
"np7": "numpad_7",
"np8": "numpad_8",
"np9": "numpad_9",
"north": "up",
"south": "down",
"east": "right",
"west": "left",
"northwest": "numpad_7",
"northeast": "numpad_9",
"southwest": "numpad_1",
"southeast": "numpad_3",
"nw": "numpad_7",
"ne": "numpad_9",
"sw": "numpad_1",
"se": "numpad_3",
}
return keyAliases[key]
}
func isKey(key string) bool {
// All single-character keys are valid.
// This function is to determine if strings
// longer than one character are valid keys,
// such as "F5" or "Enter".
validKeys := map[string]bool{
"backspace": true,
"delete": true,
"enter": true,
"tab": true,
"up": true,
"down": true,
"right": true,
"left": true,
"home": true,
"end": true,
"pageup": true,
"pagedown": true,
"f1": true,
"f2": true,
"f3": true,
"f4": true,
"f5": true,
"f6": true,
"f7": true,
"f8": true,
"f9": true,
"f10": true,
"f11": true,
"f12": true,
"f13": true,
"f14": true,
"f15": true,
"f16": true,
"f17": true,
"f18": true,
"f19": true,
"f20": true,
"f21": true,
"f22": true,
"f23": true,
"f24": true,
"space": true,
"numpad_0": true,
"numpad_1": true,
"numpad_2": true,
"numpad_3": true,
"numpad_4": true,
"numpad_5": true,
"numpad_6": true,
"numpad_7": true,
"numpad_8": true,
"numpad_9": true,
"escape": true,
"printscreen": true,
//"insert": true,
//"menu": true,
//"command": true,
// "alt": true,
// "control": true,
// "shift": true,
// "right_shift": true,
}
return validKeys[key]
}
var isAlphaNum = regexp.MustCompile(`^[a-zA-Z0-9]+$`).MatchString
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment