44 Commits

Author SHA1 Message Date
bel
1ef3afd647 whitespace 2023-03-24 22:26:34 -06:00
bel
2746051a2a typing 2023-03-24 22:24:08 -06:00
bel
610aef4f7e rebuild parser on refresh 2023-03-24 22:15:36 -06:00
bel
a9ca58f154 v1 complete 2023-03-24 22:10:37 -06:00
bel
7182ab387f test button.plaintext parser 2023-03-24 21:38:06 -06:00
bel
0e46f6e122 todo 2023-03-24 21:17:01 -06:00
bel
01777c8c3e clean shutdown with udp 2023-03-24 21:14:27 -06:00
bel
aa16b66332 udp in bg thread 2023-03-24 21:02:47 -06:00
bel
2af373aed7 integrate button.Parser 2023-03-24 20:52:44 -06:00
bel
896f5e9c92 wrap accepts button.Parser 2023-03-24 20:50:58 -06:00
bel
b319ed7e6d split wrap protocol parsing into input.button 2023-03-24 20:25:15 -06:00
bel
9990273b19 protocol should be pkg 2023-03-24 20:05:55 -06:00
bel
ea7f2d8932 change testdata env to new RAW_ and WRAP_ 2023-03-24 19:53:55 -06:00
bel
38b00e55b0 split src/devices/input into src/devices/input/{raw,wrap} 2023-03-24 19:51:38 -06:00
bel
ab673a81f0 update host config based on mayhem-party having its own udp server now 2023-03-24 18:55:44 -06:00
Bel LaPointe
287b9c7b4e udp input except no clean shutdown 2023-03-24 18:48:54 -06:00
bel
126f5ab60a har we go 2023-03-24 18:35:16 -06:00
bel
3c19f984a9 add required field to rusty configs 2023-03-24 15:39:40 -06:00
bel
cf3b93464a update readme for compile 2023-03-24 15:29:33 -06:00
bel
edcea37148 drop .[] chars as they dont work on linux and macos for me out of the box 2023-03-24 15:28:17 -06:00
Bel LaPointe
b4e4de82ae almost 2023-03-24 15:13:17 -06:00
Bel LaPointe
cdfcfe8fd0 make rotate.sh first class citizen for receiving signal 2023-03-24 14:44:47 -06:00
Bel LaPointe
bf677856a2 script generating player offset files 2023-03-24 14:40:39 -06:00
Bel LaPointe
b9d76d5e8f create mvp files to run mayhem party without remap to stdout when hosting 2023-03-24 14:16:05 -06:00
Bel LaPointe
d292a830a1 set up players 1..5 rusty-pipe.yamls 2023-03-24 14:09:31 -06:00
Bel LaPointe
745175210c mv README#host to host.d 2023-03-24 14:01:44 -06:00
Bel LaPointe
6536daee7f support alpha, numeric, f0..10, punctuation, math keys 2023-03-24 13:49:12 -06:00
Bel LaPointe
9ce50f2622 todo 2023-03-24 13:39:29 -06:00
Bel LaPointe
ea0bb5d365 revert back to case sensitive because you cant hold A and a at the same time 2023-03-24 13:39:17 -06:00
Bel LaPointe
20488d2be8 no wait shift means sideaffecting 2023-03-24 13:37:53 -06:00
Bel LaPointe
7b7486cc93 keys support case 2023-03-24 13:30:46 -06:00
Bel LaPointe
e5a668b691 howtohost 2023-03-24 13:05:45 -06:00
Bel LaPointe
c298bb0dfd todo 2023-03-24 12:57:31 -06:00
Bel LaPointe
adabc4eb98 input.New refactor and test 2023-03-24 12:05:50 -06:00
Bel LaPointe
6e1bfc177d fix random weighted char because %sum can never get last value of sum 2023-03-24 11:51:29 -06:00
Bel LaPointe
e491cc5cbc refresher input 2023-03-24 11:46:55 -06:00
bel
37d02f0f52 debugs 2023-03-23 21:05:30 -06:00
bel
e832085fc2 todo 2023-03-23 20:55:58 -06:00
bel
8e92c9a6d6 keyboard supports !a to indicate release a 2023-03-23 20:39:57 -06:00
Bel LaPointe
1fc6d71db6 $INPUT_BUFFERED_STICKY_DURATION 2023-03-23 17:00:49 -06:00
Bel LaPointe
4f48ee805f whoops nums bitwised 2023-03-23 16:14:25 -06:00
Bel LaPointe
f9ec874491 support numbers as well 2023-03-23 16:10:31 -06:00
Bel LaPointe
32c186e1e2 input ignores newline chars 2023-03-23 15:53:01 -06:00
Bel LaPointe
17b2891f9a readme 2023-03-02 15:38:59 -07:00
49 changed files with 1136 additions and 114 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/mayhem-party
**/*.sw*

View File

@@ -9,3 +9,14 @@ Think Dug's Twitch Chat Plays
* multiplayer engine
* server; https://github.com/LizardByte/Sunshine
* client; https://moonlight-stream.org/
## DONE
* input
* random from weighted file
* buffered
* remapped from file
* output
* to keyboard
* to stderr

51
host.d/README.md Normal file
View File

@@ -0,0 +1,51 @@
# Hosting a Mayhem Party
## Requirements
1. [`rusty-pipe`](https://gogs.inhome.blapointe.com/bel/rusty-pipe)
1. [`mayhem-party`](https://gogs.inhome.blapointe.com/bel/mayhem-party)
1. [`stt`](https://gogs.inhome.blapointe.com/bel/stt)
# Clients
## Distribute `rusty-pipe`
```bash
rustup target add x86_64-pc-windows-gnu
echo windows
cargo build --target x86_64-pc-windows-gnu && ls target/x86_64-pc-windows-gnu/release/rusty-pipe.exe
echo local
cargo install --path ./
```
Each client needs 1 executable and 1 env file with a unique set of buttons
> 10 buttons per player
> `go doc key Undef | grep Key | grep -v Undef | wc -l` total (51)
The server cannot be a client because math. Maybe a VM on the client as a server would work tho.
See `./config.d/rusty-pipe.d`
# Server
## `mayhem-party`
### Configs
`cd ./config.d/mayhem-party.d/remap.d; bash ./rotate.sh`
> rotate anytime via stdin or `pkill -SIGUSR1 -f rotate.sh`
### Binary
`bash -c 'true; source ./config.d/mayhem-party.d/env.env; mayhem-party'`
## Game Playing
Foreground
## `stt`
TODO pipe stt detecting relevant strings -> change the `./config.d/mayhem-party.d/remap.d/live.yaml` link -> `SIGUSR1` to `mayhem-party`

View File

@@ -0,0 +1,6 @@
export DEBUG=1
export RAW_UDP=17070
export WRAP_REFRESH_ON_SIGUSR1=true
export WRAP_REMAP_FILE=./config.d/mayhem-party.d/remap.d/live.yaml
export MAIN_INTERVAL_DURATION=5ms
export OUTPUT_KEYBOARD=false

View File

@@ -0,0 +1,31 @@
import yaml
from sys import argv
from sys import stderr
def log(*args):
print(*args, file=stderr)
def main(args):
players = []
for i in range(1, 5+1):
with open(f"../../rusty-pipe.d/{i}.yaml", "r") as f:
players.append(yaml.safe_load(f)["streams"]["input"]["engine"]["gui"]["buttons"])
log(players[-1])
for arg in args:
offset = int(arg)
for i in range(len(players)):
j = (i+offset)%len(players)
if i == j:
break
keys = players[i]
values = players[j]
log(f"player {i+1} plays as player {j+1}")
print(f"# player {i+1} controls {j+1}")
for k in keys:
key = keys[k]
value = values[k]
print(f"'{key}': '{value}'")
if __name__ == "__main__":
from sys import argv
main(argv[1:])

View File

@@ -0,0 +1 @@
players_offset_0.yaml

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1,55 @@
# player 1 controls 2
'1': 'q'
'2': 'w'
'3': 'e'
'4': 'r'
'5': 't'
'6': 'y'
'7': 'u'
'8': 'i'
'9': 'o'
'0': 'p'
# player 2 controls 3
'q': 'a'
'w': 's'
'e': 'd'
'r': 'f'
't': 'g'
'y': 'h'
'u': 'j'
'i': 'k'
'o': 'l'
'p': ';'
# player 3 controls 4
'a': 'z'
's': 'x'
'd': 'c'
'f': 'v'
'g': 'b'
'h': 'n'
'j': 'm'
'k': ','
'l': '-'
';': '/'
# player 4 controls 5
'z': '!'
'x': '@'
'c': '#'
'v': '$'
'b': '%'
'n': '^'
'm': '&'
',': '*'
'-': '('
'/': ')'
# player 5 controls 1
'!': '1'
'@': '2'
'#': '3'
'$': '4'
'%': '5'
'^': '6'
'&': '7'
'*': '8'
'(': '9'
')': '0'

View File

@@ -0,0 +1,55 @@
# player 1 controls 3
'1': 'a'
'2': 's'
'3': 'd'
'4': 'f'
'5': 'g'
'6': 'h'
'7': 'j'
'8': 'k'
'9': 'l'
'0': ';'
# player 2 controls 4
'q': 'z'
'w': 'x'
'e': 'c'
'r': 'v'
't': 'b'
'y': 'n'
'u': 'm'
'i': ','
'o': '-'
'p': '/'
# player 3 controls 5
'a': '!'
's': '@'
'd': '#'
'f': '$'
'g': '%'
'h': '^'
'j': '&'
'k': '*'
'l': '('
';': ')'
# player 4 controls 1
'z': '1'
'x': '2'
'c': '3'
'v': '4'
'b': '5'
'n': '6'
'm': '7'
',': '8'
'-': '9'
'/': '0'
# player 5 controls 2
'!': 'q'
'@': 'w'
'#': 'e'
'$': 'r'
'%': 't'
'^': 'y'
'&': 'u'
'*': 'i'
'(': 'o'
')': 'p'

View File

@@ -0,0 +1,55 @@
# player 1 controls 4
'1': 'z'
'2': 'x'
'3': 'c'
'4': 'v'
'5': 'b'
'6': 'n'
'7': 'm'
'8': ','
'9': '-'
'0': '/'
# player 2 controls 5
'q': '!'
'w': '@'
'e': '#'
'r': '$'
't': '%'
'y': '^'
'u': '&'
'i': '*'
'o': '('
'p': ')'
# player 3 controls 1
'a': '1'
's': '2'
'd': '3'
'f': '4'
'g': '5'
'h': '6'
'j': '7'
'k': '8'
'l': '9'
';': '0'
# player 4 controls 2
'z': 'q'
'x': 'w'
'c': 'e'
'v': 'r'
'b': 't'
'n': 'y'
'm': 'u'
',': 'i'
'-': 'o'
'/': 'p'
# player 5 controls 3
'!': 'a'
'@': 's'
'#': 'd'
'$': 'f'
'%': 'g'
'^': 'h'
'&': 'j'
'*': 'k'
'(': 'l'
')': ';'

View File

@@ -0,0 +1,55 @@
# player 1 controls 5
'1': '!'
'2': '@'
'3': '#'
'4': '$'
'5': '%'
'6': '^'
'7': '&'
'8': '*'
'9': '('
'0': ')'
# player 2 controls 1
'q': '1'
'w': '2'
'e': '3'
'r': '4'
't': '5'
'y': '6'
'u': '7'
'i': '8'
'o': '9'
'p': '0'
# player 3 controls 2
'a': 'q'
's': 'w'
'd': 'e'
'f': 'r'
'g': 't'
'h': 'y'
'j': 'u'
'k': 'i'
'l': 'o'
';': 'p'
# player 4 controls 3
'z': 'a'
'x': 's'
'c': 'd'
'v': 'f'
'b': 'g'
'n': 'h'
'm': 'j'
',': 'k'
'-': 'l'
'/': ';'
# player 5 controls 4
'!': 'z'
'@': 'x'
'#': 'c'
'$': 'v'
'%': 'b'
'^': 'n'
'&': 'm'
'*': ','
'(': '-'
')': '/'

View File

@@ -0,0 +1,21 @@
#! /bin/bash
mayhem_party_rotate() {
local currently=$(realpath live.yaml | grep -o '[0-9].yaml$' | grep -o '^[0-9]')
local next=${NEXT:-$((RANDOM%5))}
while [ -z "$NEXT" ] && [ "$next" == "$currently" ]; do
next=$((RANDOM%5))
done
rm live.yaml
ln -s players_offset_$next.yaml live.yaml
pkill -SIGUSR1 -f mayhem-party
}
trap mayhem_party_rotate SIGUSR1
if [ "$0" == "$BASH_SOURCE" ]; then
NEXT=0 mayhem_party_rotate
while read -p "$(date) > [press enter to rotate]"; do
mayhem_party_rotate
done
fi

View File

@@ -0,0 +1,26 @@
streams:
input:
debug: false
engine:
name: gui
gui:
press: {prefix: "", suffix: ""}
release: {prefix: "!", suffix: ""}
buttons:
up: '1'
down: '2'
left: '3'
right: '4'
l: '5'
r: '6'
a: '7'
b: '8'
x: '9'
y: '0'
output:
debug: false
engine:
name: udp
udp:
host: mayhem-party.home.blapointe.com
port: 17070

View File

@@ -0,0 +1,26 @@
streams:
input:
debug: false
engine:
name: gui
gui:
press: {prefix: "", suffix: ""}
release: {prefix: "!", suffix: ""}
buttons:
up: 'q'
down: 'w'
left: 'e'
right: 'r'
l: 't'
r: 'y'
a: 'u'
b: 'i'
x: 'o'
y: 'p'
output:
debug: false
engine:
name: udp
udp:
host: mayhem-party.home.blapointe.com
port: 17070

View File

@@ -0,0 +1,26 @@
streams:
input:
debug: false
engine:
name: gui
gui:
press: {prefix: "", suffix: ""}
release: {prefix: "!", suffix: ""}
buttons:
up: 'a'
down: 's'
left: 'd'
right: 'f'
l: 'g'
r: 'h'
a: 'j'
b: 'k'
x: 'l'
y: ';'
output:
debug: false
engine:
name: udp
udp:
host: mayhem-party.home.blapointe.com
port: 17070

View File

@@ -0,0 +1,26 @@
streams:
input:
debug: false
engine:
name: gui
gui:
press: {prefix: "", suffix: ""}
release: {prefix: "!", suffix: ""}
buttons:
up: 'z'
down: 'x'
left: 'c'
right: 'v'
l: 'b'
r: 'n'
a: 'm'
b: ','
x: '-'
y: '/'
output:
debug: false
engine:
name: udp
udp:
host: mayhem-party.home.blapointe.com
port: 17070

View File

@@ -0,0 +1,26 @@
streams:
input:
debug: false
engine:
name: gui
gui:
press: {prefix: "", suffix: ""}
release: {prefix: "!", suffix: ""}
buttons:
up: '!'
down: '@'
left: '#'
right: '$'
l: '%'
r: '^'
a: '&'
b: '*'
x: '('
y: ')'
output:
debug: false
engine:
name: udp
udp:
host: mayhem-party.home.blapointe.com
port: 17070

View File

@@ -0,0 +1,6 @@
package button
type Button struct {
Char byte
Down bool
}

View File

@@ -0,0 +1,19 @@
package button
import (
"context"
"mayhem-party/src/device/input/raw"
"os"
)
type Parser interface {
Read() []Button
Close()
}
func New(ctx context.Context, src raw.Raw) Parser {
if os.Getenv("BUTTON_PARSER_V1") == "true" {
return NewV1(src)
}
return NewPlaintext(src)
}

View File

@@ -0,0 +1,11 @@
package button_test
import (
"mayhem-party/src/device/input/button"
"testing"
)
func TestParser(t *testing.T) {
var _ button.Parser = button.Plaintext{}
var _ button.Parser = button.V1{}
}

View File

@@ -0,0 +1,41 @@
package button
import (
"mayhem-party/src/device/input/raw"
"os"
)
type Plaintext struct {
src raw.Raw
release byte
}
func NewPlaintext(src raw.Raw) Plaintext {
releaseChar := byte('!')
if v := os.Getenv("BUTTON_PLAINTEXT_RELEASE"); v != "" {
releaseChar = byte(v[0])
}
return Plaintext{
src: src,
release: releaseChar,
}
}
func (p Plaintext) Close() { p.src.Close() }
func (p Plaintext) Read() []Button {
b := p.src.Read()
buttons := make([]Button, 0, len(b))
down := true
for i := range b {
if b[i] == p.release {
down = false
} else {
if b[i] != '\n' {
buttons = append(buttons, Button{Char: b[i], Down: down})
}
down = true
}
}
return buttons
}

View File

@@ -0,0 +1,29 @@
package button_test
import (
"mayhem-party/src/device/input/button"
"testing"
)
func TestPlaintext(t *testing.T) {
src := constSrc("c!b")
p := button.NewPlaintext(src)
got := p.Read()
if len(got) != 2 {
t.Fatal(len(got))
}
if got[0] != (button.Button{Char: 'c', Down: true}) {
t.Error(got[0])
}
if got[1] != (button.Button{Char: 'b', Down: false}) {
t.Error(got[1])
}
}
type constSrc string
func (c constSrc) Close() {}
func (c constSrc) Read() []byte {
return []byte(c)
}

View File

@@ -0,0 +1,55 @@
package button
import (
"encoding/json"
"log"
"mayhem-party/src/device/input/raw"
"os"
)
var debugging = os.Getenv("DEBUG") == "true"
type (
V1 struct {
src raw.Raw
}
v1Msg struct {
T int64
U string
Y string
N string
}
)
func NewV1(src raw.Raw) V1 {
return V1{
src: src,
}
}
func (v1 V1) Close() {
v1.src.Close()
}
func (v1 V1) Read() []Button {
line := v1.src.Read()
var msg v1Msg
if err := json.Unmarshal(line, &msg); err != nil {
log.Printf("%v: %s", err, line)
}
return msg.buttons()
}
func (msg v1Msg) buttons() []Button {
buttons := make([]Button, len(msg.Y)+len(msg.N))
for i := range msg.Y {
buttons[i] = Button{Char: msg.Y[i], Down: true}
}
for i := range msg.N {
buttons[len(msg.Y)+i] = Button{Char: msg.N[i], Down: false}
}
if debugging {
log.Printf("%+v", msg)
}
return buttons
}

View File

@@ -0,0 +1,29 @@
package button_test
import (
"mayhem-party/src/device/input/button"
"testing"
)
func TestV1(t *testing.T) {
src := constSrc(`{"T":1,"U":"bel","Y":"abc","N":"cde"}`)
t.Logf("(%v) %s", len(src), src.Read())
v1 := button.NewV1(src)
got := v1.Read()
want := []button.Button{
{Down: true, Char: 'a'},
{Down: true, Char: 'b'},
{Down: true, Char: 'c'},
{Down: false, Char: 'c'},
{Down: false, Char: 'd'},
{Down: false, Char: 'e'},
}
if len(got) != len(want) {
t.Fatal(len(want), len(got))
}
for i := range got {
if got[i] != want[i] {
t.Errorf("[%d] want %+v got %+v", i, want[i], got[i])
}
}
}

View File

@@ -2,28 +2,19 @@ package input
import (
"context"
"os"
"mayhem-party/src/device/input/button"
"mayhem-party/src/device/input/raw"
"mayhem-party/src/device/input/wrap"
)
type Input interface {
Read() []Button
Read() []button.Button
Close()
}
func New(ctx context.Context) Input {
foo := randomCharFromRange('a', 'g')
if p, ok := os.LookupEnv("INPUT_RANDOM_WEIGHT_FILE"); ok && len(p) > 0 {
foo = randomCharFromWeightFile(p)
}
var result Input = NewRandom(foo)
if os.Getenv("INPUT_KEYBOARD") == "true" {
result = NewKeyboard()
}
if os.Getenv("INPUT_BUFFERED") == "true" {
result = NewBuffered(ctx, result)
}
if p, ok := os.LookupEnv("INPUT_REMAP_FILE"); ok && len(p) > 0 {
result = NewRemapFromFile(result, p)
}
return result
src := raw.New(ctx)
return wrap.New(ctx, func() button.Parser {
return button.New(ctx, src)
})
}

View File

@@ -30,11 +30,11 @@ func TestNewRemapped(t *testing.T) {
t.Fatal(err)
}
os.Setenv("INPUT_REMAP_FILE", remap)
os.Setenv("INPUT_RANDOM_WEIGHT_FILE", rand)
os.Setenv("WRAP_REMAP_FILE", remap)
os.Setenv("RAW_RANDOM_WEIGHT_FILE", rand)
t.Cleanup(func() {
os.Unsetenv("INPUT_REMAP_FILE")
os.Unsetenv("INPUT_RANDOM_WEIGHT_FILE")
os.Unsetenv("WRAP_REMAP_FILE")
os.Unsetenv("RAW_RANDOM_WEIGHT_FILE")
})
r := input.New(context.Background())
@@ -50,9 +50,9 @@ func TestNewRemapped(t *testing.T) {
}
func TestNewBuffered(t *testing.T) {
os.Setenv("INPUT_BUFFERED", "true")
os.Setenv("WRAP_BUFFERED", "true")
t.Cleanup(func() {
os.Unsetenv("INPUT_BUFFERED")
os.Unsetenv("WRAP_BUFFERED")
})
r := input.New(context.Background())
@@ -71,9 +71,9 @@ func TestNewRandomWeightFile(t *testing.T) {
t.Fatal(err)
}
os.Setenv("INPUT_RANDOM_WEIGHT_FILE", p)
os.Setenv("RAW_RANDOM_WEIGHT_FILE", p)
t.Cleanup(func() {
os.Unsetenv("INPUT_RANDOM_WEIGHT_FILE")
os.Unsetenv("RAW_RANDOM_WEIGHT_FILE")
})
r := input.New(context.Background())

View File

@@ -1,10 +0,0 @@
package input
import "testing"
func TestInput(t *testing.T) {
var _ Input = &Random{}
var _ Input = Keyboard{}
var _ Input = &Buffered{}
var _ Input = Remap{}
}

View File

@@ -1,7 +1,8 @@
package input
package raw
import (
"io"
"log"
"os"
"os/exec"
"runtime"
@@ -43,16 +44,14 @@ func (kb Keyboard) Close() {
}
}
func (kb Keyboard) Read() []Button {
func (kb Keyboard) Read() []byte {
b := make([]byte, 5)
n, err := os.Stdin.Read(b)
if err != nil && err != io.EOF {
panic(err)
}
result := make([]Button, n)
for i := range result {
result[i] = Button{Char: b[i], Down: true}
if os.Getenv("DEBUG") == "true" {
log.Printf("raw.Keyboard.Read() %s", b[:n])
}
return result
return b[:n]
}

View File

@@ -1,4 +1,4 @@
package input
package raw
import (
"bytes"
@@ -6,19 +6,15 @@ import (
"io"
"math/rand"
"os"
"sort"
"time"
"github.com/go-yaml/yaml"
)
type Button struct {
Char byte
Down bool
}
type Random struct {
generator func() byte
down []Button
down []byte
}
func NewRandom(generator func() byte) *Random {
@@ -29,19 +25,8 @@ func NewRandom(generator func() byte) *Random {
func (r *Random) Close() {
}
func (r *Random) Read() []Button {
if len(r.down) > 0 && rand.Int()%2 == 0 {
was := r.down
for i := range was {
was[i].Down = false
}
r.down = r.down[:0]
return was
} else {
c := Button{Char: r.generator(), Down: true}
r.down = append(r.down, c)
return []Button{c}
}
func (r *Random) Read() []byte {
return []byte{r.generator()}
}
func randomCharFromRange(start, stop byte) func() byte {
@@ -87,11 +72,15 @@ func randomCharFromWeights(m map[byte]int) func() byte {
}
sum += v
}
sort.Slice(result, func(i, j int) bool {
return result[i].i < result[j].i
})
if sum <= 0 {
panic("weights must total nonzero")
}
return func() byte {
n := rand.Int() % sum
r := rand.Int()
n := r % (sum + 1)
for _, v := range result {
n -= v.i
if n <= 0 {

View File

@@ -1,4 +1,4 @@
package input
package raw
import (
"strings"
@@ -8,7 +8,7 @@ import (
func TestRandomCharFromWeights(t *testing.T) {
weights := map[byte]int{
'a': 1,
'b': 99,
'b': 20,
}
foo := randomCharFromWeights(weights)
for {

View File

@@ -0,0 +1,26 @@
package raw
import (
"context"
"os"
"strconv"
)
type Raw interface {
Read() []byte
Close()
}
func New(ctx context.Context) Raw {
if os.Getenv("RAW_KEYBOARD") == "true" {
return NewKeyboard()
}
if port, _ := strconv.Atoi(os.Getenv("RAW_UDP")); port != 0 {
return NewUDP(ctx, port)
}
generator := randomCharFromRange('a', 'g')
if p, ok := os.LookupEnv("RAW_RANDOM_WEIGHT_FILE"); ok && len(p) > 0 {
generator = randomCharFromWeightFile(p)
}
return NewRandom(generator)
}

View File

@@ -0,0 +1,9 @@
package raw
import "testing"
func TestRaw(t *testing.T) {
var _ Raw = &Random{}
var _ Raw = UDP{}
var _ Raw = Keyboard{}
}

View File

@@ -0,0 +1,60 @@
package raw
import (
"context"
"log"
"net"
"os"
"strconv"
)
type UDP struct {
conn net.PacketConn
c chan []byte
ctx context.Context
}
func NewUDP(ctx context.Context, port int) UDP {
conn, err := net.ListenPacket("udp", ":"+strconv.Itoa(port))
if err != nil {
panic(err)
}
result := UDP{
conn: conn,
c: make(chan []byte, 8),
ctx: ctx,
}
go result.listen()
return result
}
func (udp UDP) listen() {
debugging := os.Getenv("DEBUG") == "true"
for udp.ctx.Err() == nil {
buff := make([]byte, 256)
n, _, err := udp.conn.ReadFrom(buff)
if err != nil && udp.ctx.Err() == nil {
panic(err)
}
if debugging {
log.Printf("raw.UDP.Read() => %s", buff[:n])
}
select {
case udp.c <- buff[:n]:
case <-udp.ctx.Done():
}
}
}
func (udp UDP) Read() []byte {
select {
case v := <-udp.c:
return v
case <-udp.ctx.Done():
return []byte{}
}
}
func (udp UDP) Close() {
udp.conn.Close()
}

View File

@@ -1,7 +1,9 @@
package input
package wrap
import (
"context"
"mayhem-party/src/device/input/button"
"os"
"sync"
"time"
)
@@ -11,21 +13,25 @@ type Buffered struct {
can context.CancelFunc
lock sync.Mutex
keys map[byte]int64
input Input
input Wrap
listenInterval time.Duration
expirationInterval time.Duration
}
func NewBuffered(ctx context.Context, input Input) *Buffered {
func NewBuffered(ctx context.Context, input Wrap) *Buffered {
ctx, can := context.WithCancel(ctx)
expirationInterval := time.Millisecond * 125
if d, err := time.ParseDuration(os.Getenv("WRAP_BUFFERED_STICKY_DURATION")); err == nil {
expirationInterval = d
}
result := &Buffered{
input: input,
ctx: ctx,
can: can,
lock: sync.Mutex{},
keys: map[byte]int64{},
listenInterval: time.Millisecond * 20,
expirationInterval: time.Millisecond * 100,
listenInterval: time.Millisecond * 10,
expirationInterval: expirationInterval,
}
go result.listen()
return result
@@ -39,6 +45,8 @@ func (b *Buffered) listen() {
for i := range buttons {
if buttons[i].Down {
b.keys[buttons[i].Char] = time.Now().UnixNano()
} else {
b.keys[buttons[i].Char] = 0
}
}
b.lock.Unlock()
@@ -54,7 +62,7 @@ func (b *Buffered) Close() {
b.can()
}
func (b *Buffered) Read() []Button {
func (b *Buffered) Read() []button.Button {
for b.ctx.Err() == nil {
result := b.read()
if len(result) > 0 {
@@ -65,19 +73,19 @@ func (b *Buffered) Read() []Button {
case <-time.After(b.listenInterval):
}
}
return []Button{}
return []button.Button{}
}
func (b *Buffered) read() []Button {
func (b *Buffered) read() []button.Button {
b.lock.Lock()
defer b.lock.Unlock()
result := make([]Button, 0, len(b.keys))
result := make([]button.Button, 0, len(b.keys))
for k, v := range b.keys {
isFresh := v > 0
isStale := v < 0 && time.Since(time.Unix(0, -1*v)) > b.expirationInterval
if isFresh || isStale {
result = append(result, Button{Char: k, Down: isFresh})
result = append(result, button.Button{Char: k, Down: isFresh})
}
if isFresh {
b.keys[k] = -1 * v

View File

@@ -0,0 +1,24 @@
package wrap
import (
"mayhem-party/src/device/input/button"
"mayhem-party/src/device/input/raw"
)
type Protocol struct {
src raw.Raw
}
func NewProtocol(src raw.Raw) Protocol {
return Protocol{
src: src,
}
}
func (p Protocol) Close() {
p.src.Close()
}
func (p Protocol) Read() []button.Button {
panic(nil)
}

View File

@@ -0,0 +1,50 @@
package wrap
import (
"context"
"log"
"mayhem-party/src/device/input/button"
"os"
"os/signal"
)
type Refresh struct {
can context.CancelFunc
input Wrap
}
func NewRefreshCh(sig os.Signal) <-chan os.Signal {
c := make(chan os.Signal, 1)
signal.Notify(c, sig)
return c
}
func NewRefresh(newWrap func() Wrap, ch <-chan os.Signal) *Refresh {
ctx, can := context.WithCancel(context.Background())
result := &Refresh{
can: can,
input: newWrap(),
}
go func() {
defer log.Println("refreshing done")
for {
select {
case <-ctx.Done():
return
case sig := <-ch:
log.Println("refreshing for", sig)
result.input = newWrap()
}
}
}()
return result
}
func (r *Refresh) Read() []button.Button {
return r.input.Read()
}
func (r *Refresh) Close() {
r.can()
r.input.Close()
}

View File

@@ -0,0 +1,44 @@
package wrap
import (
"os"
"syscall"
"testing"
"time"
)
func TestRefresh(t *testing.T) {
b := byte('a')
generator := func() Wrap {
b += byte(1)
return dummyParser{Char: b}
}
ch := make(chan os.Signal, 1)
defer close(ch)
refresh := NewRefresh(generator, ch)
defer refresh.Close()
assertIts := func(t *testing.T, b byte) {
for i := 0; i < 10; i++ {
some := false
for j, button := range refresh.Read() {
some = true
if button.Char != b {
t.Error(i, j, b, button)
}
}
if !some {
t.Error("empty read")
}
}
}
t.Run("called once on init", func(t *testing.T) {
assertIts(t, byte('b'))
})
ch <- syscall.SIGUSR1
time.Sleep(time.Millisecond * 450)
t.Run("called once on signal", func(t *testing.T) {
assertIts(t, byte('c'))
})
}

View File

@@ -1,17 +1,18 @@
package input
package wrap
import (
"mayhem-party/src/device/input/button"
"os"
"github.com/go-yaml/yaml"
)
type Remap struct {
input Input
input Wrap
m map[byte]byte
}
func NewRemapFromFile(input Input, p string) Remap {
func NewRemapFromFile(input Wrap, p string) Remap {
b, err := os.ReadFile(p)
if err != nil {
panic(err)
@@ -30,7 +31,7 @@ func NewRemapFromFile(input Input, p string) Remap {
return NewRemap(input, remap)
}
func NewRemap(input Input, m map[byte]byte) Remap {
func NewRemap(input Wrap, m map[byte]byte) Remap {
return Remap{
input: input,
m: m,
@@ -41,7 +42,7 @@ func (re Remap) Close() {
re.input.Close()
}
func (re Remap) Read() []Button {
func (re Remap) Read() []button.Button {
result := re.input.Read()
for i := range result {
if v, ok := re.m[result[i].Char]; ok {

View File

@@ -0,0 +1,39 @@
package wrap
import (
"context"
"mayhem-party/src/device/input/button"
"os"
"syscall"
)
type Wrap interface {
Read() []button.Button
Close()
}
func New(ctx context.Context, srcFunc func() button.Parser) Wrap {
maker := func() Wrap {
return srcFunc()
}
if os.Getenv("WRAP_BUFFERED") == "true" {
oldMaker := maker
maker = func() Wrap {
return NewBuffered(ctx, oldMaker())
}
}
if p := os.Getenv("WRAP_REMAP_FILE"); p != "" {
oldMaker := maker
maker = func() Wrap {
return NewRemapFromFile(oldMaker(), p)
}
}
if os.Getenv("WRAP_REFRESH_ON_SIGUSR1") != "" {
oldMaker := maker
c := NewRefreshCh(syscall.SIGUSR1)
maker = func() Wrap {
return NewRefresh(oldMaker, c)
}
}
return maker()
}

View File

@@ -0,0 +1,21 @@
package wrap
import (
"mayhem-party/src/device/input/button"
"testing"
)
func TestWrap(t *testing.T) {
var _ Wrap = dummyParser{}
var _ Wrap = &Refresh{}
var _ Wrap = &Buffered{}
var _ Wrap = &Remap{}
var _ Wrap = Protocol{}
}
type dummyParser button.Button
func (d dummyParser) Close() {}
func (d dummyParser) Read() []button.Button {
return []button.Button{button.Button(d)}
}

View File

@@ -28,6 +28,31 @@ var (
'x': X,
'y': Y,
'z': Z,
'1': N1,
'2': N2,
'3': N3,
'4': N4,
'5': N5,
'6': N6,
'7': N7,
'8': N8,
'9': N9,
'0': N0,
'!': F1,
'@': F2,
'#': F3,
'$': F4,
'%': F5,
'^': F6,
'&': F7,
'*': F8,
'(': F9,
')': F10,
',': PComma,
'/': PFSlash,
';': PSemicolon,
'-': PMinus,
'=': PEqual,
}
keyToChar = func() map[Key]byte {
result := map[Key]byte{}
@@ -47,7 +72,7 @@ func ToChar(k Key) byte {
}
func FromChar(b byte) Key {
if b < 'a' {
if 'A' <= b && b <= 'Z' {
b += 'a' - 'A'
}
v, ok := charToKey[b]

View File

@@ -3,13 +3,16 @@ package key
import "testing"
func TestFromChar(t *testing.T) {
if got := FromChar('1'); got != N1 {
t.Error(got)
}
if got := FromChar('a'); got != A {
t.Error(got)
}
if got := FromChar('A'); got != A {
t.Error(got)
}
if got := FromChar('!'); got != Undef {
if got := FromChar(byte(0)); got != Undef {
t.Error(got)
}
if got := ToChar(A); got != 'a' {

View File

@@ -5,31 +5,56 @@ import "github.com/micmonay/keybd_event"
type Key int
const (
Undef = Key(keybd_event.VK_SP11)
A = Key(keybd_event.VK_A)
B = Key(keybd_event.VK_B)
C = Key(keybd_event.VK_C)
D = Key(keybd_event.VK_D)
E = Key(keybd_event.VK_E)
F = Key(keybd_event.VK_F)
G = Key(keybd_event.VK_G)
H = Key(keybd_event.VK_H)
I = Key(keybd_event.VK_I)
J = Key(keybd_event.VK_J)
K = Key(keybd_event.VK_K)
L = Key(keybd_event.VK_L)
M = Key(keybd_event.VK_M)
N = Key(keybd_event.VK_N)
O = Key(keybd_event.VK_O)
P = Key(keybd_event.VK_P)
Q = Key(keybd_event.VK_Q)
R = Key(keybd_event.VK_R)
S = Key(keybd_event.VK_S)
T = Key(keybd_event.VK_T)
U = Key(keybd_event.VK_U)
V = Key(keybd_event.VK_V)
W = Key(keybd_event.VK_W)
X = Key(keybd_event.VK_X)
Y = Key(keybd_event.VK_Y)
Z = Key(keybd_event.VK_Z)
Undef = Key(keybd_event.VK_SP11)
A = Key(keybd_event.VK_A)
B = Key(keybd_event.VK_B)
C = Key(keybd_event.VK_C)
D = Key(keybd_event.VK_D)
E = Key(keybd_event.VK_E)
F = Key(keybd_event.VK_F)
G = Key(keybd_event.VK_G)
H = Key(keybd_event.VK_H)
I = Key(keybd_event.VK_I)
J = Key(keybd_event.VK_J)
K = Key(keybd_event.VK_K)
L = Key(keybd_event.VK_L)
M = Key(keybd_event.VK_M)
N = Key(keybd_event.VK_N)
O = Key(keybd_event.VK_O)
P = Key(keybd_event.VK_P)
Q = Key(keybd_event.VK_Q)
R = Key(keybd_event.VK_R)
S = Key(keybd_event.VK_S)
T = Key(keybd_event.VK_T)
U = Key(keybd_event.VK_U)
V = Key(keybd_event.VK_V)
W = Key(keybd_event.VK_W)
X = Key(keybd_event.VK_X)
Y = Key(keybd_event.VK_Y)
Z = Key(keybd_event.VK_Z)
N1 = Key(keybd_event.VK_1)
N2 = Key(keybd_event.VK_2)
N3 = Key(keybd_event.VK_3)
N4 = Key(keybd_event.VK_4)
N5 = Key(keybd_event.VK_5)
N6 = Key(keybd_event.VK_6)
N7 = Key(keybd_event.VK_7)
N8 = Key(keybd_event.VK_8)
N9 = Key(keybd_event.VK_9)
N0 = Key(keybd_event.VK_0)
F1 = Key(keybd_event.VK_F1)
F2 = Key(keybd_event.VK_F2)
F3 = Key(keybd_event.VK_F3)
F4 = Key(keybd_event.VK_F4)
F5 = Key(keybd_event.VK_F5)
F6 = Key(keybd_event.VK_F6)
F7 = Key(keybd_event.VK_F7)
F8 = Key(keybd_event.VK_F8)
F9 = Key(keybd_event.VK_F9)
F10 = Key(keybd_event.VK_F10)
PComma = Key(keybd_event.VK_COMMA)
PFSlash = Key(keybd_event.VK_BACKSLASH)
PSemicolon = Key(keybd_event.VK_SEMICOLON)
PMinus = Key(keybd_event.VK_MINUS)
PEqual = Key(keybd_event.VK_EQUAL)
)

View File

@@ -2,6 +2,7 @@ package src
import (
"context"
"log"
"mayhem-party/src/device/input"
"mayhem-party/src/device/output"
"mayhem-party/src/device/output/key"
@@ -37,6 +38,9 @@ func Main(ctx context.Context) error {
state := map[key.Key]bool{}
for block() {
delta := reader.Read()
if os.Getenv("DEBUG") == "true" {
log.Printf("src.Main.reader.Read(): %+v", delta)
}
for _, button := range delta {
state[key.FromChar(button.Char)] = button.Down
}

View File

@@ -2,3 +2,9 @@ w: i
a: j
s: k
d: l
q: u
e: o
1: 0
2: 9
3: 8
4: 7

View File

@@ -1,2 +1,2 @@
export INPUT_BUFFERED=true
export INPUT_KEYBOARD=true
export WRAP_BUFFERED=true
export RAW_KEYBOARD=true

View File

@@ -1,2 +1,2 @@
export INPUT_RANDOM_WEIGHT_FILE=testdata/INPUT_RANDOM_WEIGHT_FILE.yaml
export INPUT_REMAP_FILE=testdata/INPUT_REMAP_FILE.yaml
export RAW_RANDOM_WEIGHT_FILE=testdata/INPUT_RANDOM_WEIGHT_FILE.yaml
export WRAP_REMAP_FILE=testdata/INPUT_REMAP_FILE.yaml

View File

@@ -1,3 +1,3 @@
export OUTPUT_KEYBOARD=true
export MAIN_INTERVAL_DURATION=250ms
export INPUT_RANDOM_WEIGHT_FILE=testdata/INPUT_RANDOM_WEIGHT_FILE.yaml
export RAW_RANDOM_WEIGHT_FILE=testdata/INPUT_RANDOM_WEIGHT_FILE.yaml

44
todo.yaml Executable file
View File

@@ -0,0 +1,44 @@
todo:
- change from 'a','b','c' from rust to just 11,21,31,41 so playerName is known implicitly
- lag via UDP formatted inputs as space-delimited TS PID buttonIdx buttonIdx buttonIdx
- input.MayhemParty as a logical wrapper from mod10 but then gotta translate back
to char for keyboard things somewhere; space delimited?
- todo: rusty configs have "name" for each client
details: |
'if name == server_broadcasted_name { debug_print_in_gui(server_broadcasted_message) }'
- todo: rotation triggers
subtasks:
- minigame end
- random word from cur wikipedia page
- each person has their own hotword
- only spectators have hotwords and must get a player to speak it
- tribunal to vote who said it
- we have 7 players oooooof
scheduled: []
done:
- todo: sticky keyboard input mode for enable/disable explicitly
ts: Thu Mar 23 20:55:52 MDT 2023
- todo: case-sensitive
ts: Fri Mar 24 13:39:26 MDT 2023
- todo: rusty configs have "name" for each client so "if name == server_broadcasted_name
{ debug_print_in_gui(server_broadcasted_message) }
ts: Fri Mar 24 16:40:09 MDT 2023
- todo: change from 'a','b','c' from rust to just 10,11,12 so playerName is known
implicitly but then gotta translate back to char for keyboard things somewhere;
space delimited?
ts: Fri Mar 24 17:00:55 MDT 2023
- todo: '"Button" to interface or strings'
ts: Fri Mar 24 17:01:01 MDT 2023
- todo: input.UDP as a raw provider
ts: Fri Mar 24 19:58:59 MDT 2023
- todo: input.MayhemParty as a logical wrapper
ts: Fri Mar 24 19:58:59 MDT 2023
- todo: change from 'a','b','c' from rust to just 11,21,31,41 so playerName is known
implicitly from %10 but then gotta translate back to char for keyboard things
somewhere; space delimited?
ts: Fri Mar 24 19:58:59 MDT 2023
- todo: input."Button" to interface or strings
ts: Fri Mar 24 21:16:39 MDT 2023
- todo: input.MayhemParty as a logical wrapper from %10 but then gotta translate back
to char for keyboard things somewhere; space delimited?
ts: Fri Mar 24 21:16:39 MDT 2023