Last active
May 18, 2021 22:20
-
-
Save catleeball/d1ac3a3b176efc88ff22c88928cde59b to your computer and use it in GitHub Desktop.
Twitch Plays Caves of Qud
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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