Compare commits
156 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
342e2eef93 | ||
|
|
b8b076450e | ||
|
|
3bb7cad554 | ||
|
|
44ec540db3 | ||
|
|
e864f2a9f5 | ||
|
|
3c70e42819 | ||
|
|
9de8c91544 | ||
|
|
7f2e25458e | ||
|
|
95810d3735 | ||
|
|
df65b1ed07 | ||
|
|
a36f07d0c1 | ||
|
|
09d9911293 | ||
|
|
60d391b7a4 | ||
|
|
60ed9c1269 | ||
|
|
13cf35bdd8 | ||
|
|
79c90ac40c | ||
|
|
dd1b053efa | ||
|
|
f619fe9e1b | ||
|
|
781bfb8a67 | ||
|
|
9ece270a13 | ||
|
|
f647a03467 | ||
|
|
ecb719a97a | ||
|
|
1738ce7d19 | ||
|
|
354d07d6bf | ||
|
|
39ab01525f | ||
|
|
ceeeb8fe4b | ||
|
|
d029d82366 | ||
|
|
0435f7b3e8 | ||
|
|
5ef0dde50d | ||
|
|
bc3f0271e7 | ||
|
|
e2d7c4a908 | ||
|
|
c744704b63 | ||
|
|
213fd555e4 | ||
|
|
dd41028aab | ||
|
|
8ff1c2fab4 | ||
|
|
1f7b222b9c | ||
|
|
1842023224 | ||
|
|
45b873f462 | ||
|
|
88a78c489f | ||
|
|
8314bdc457 | ||
|
|
a6a9b177e9 | ||
|
|
f649862dd4 | ||
|
|
85804d6f84 | ||
|
|
f14871218d | ||
|
|
26f052d981 | ||
|
|
2d4cb394de | ||
|
|
163bf2b405 | ||
|
|
e968ce17ce | ||
|
|
f07e67b3fd | ||
|
|
fbf4849517 | ||
|
|
804ce02407 | ||
|
|
4a86d2b6ca | ||
|
|
c663b1a12c | ||
|
|
af42db6803 | ||
|
|
f9dc4cff9f | ||
|
|
37050f3d87 | ||
|
|
74717609ec | ||
|
|
24ae45896f | ||
|
|
8b29648c50 | ||
|
|
1eba008efe | ||
|
|
d48c545030 | ||
|
|
323ca466ad | ||
|
|
67e504ced6 | ||
|
|
ad967d5047 | ||
|
|
8fd0067ad1 | ||
|
|
43566be7ae | ||
|
|
cb8b254cbb | ||
|
|
340ca1d2f5 | ||
|
|
02c49852c0 | ||
|
|
02c9dce1b3 | ||
|
|
a3650642ca | ||
|
|
fbded57807 | ||
|
|
44cb05487e | ||
|
|
e1e2ce3eec | ||
|
|
4c7f444887 | ||
|
|
0311fc56a3 | ||
|
|
9902684990 | ||
|
|
967e66bdb3 | ||
|
|
ff21bfb8b3 | ||
|
|
c153636e24 | ||
|
|
efe4adf129 | ||
|
|
802266e500 | ||
|
|
373d8be1a0 | ||
|
|
bd5654128e | ||
|
|
9073658e12 | ||
|
|
7df4d09553 | ||
|
|
1ad60189f4 | ||
|
|
766c77b00a | ||
|
|
bcdf545188 | ||
|
|
3264d9ad55 | ||
|
|
3f35f7f936 | ||
|
|
0cddc33ac6 | ||
|
|
a1a12b1873 | ||
|
|
ae1e32391c | ||
|
|
97cc3ae151 | ||
|
|
2113252e2d | ||
|
|
2cae3c6d28 | ||
|
|
de261ae400 | ||
|
|
3dd0a557d4 | ||
|
|
51ae1b27b4 | ||
|
|
50e89492cf | ||
|
|
3d9ea1296c | ||
|
|
db69f76aa0 | ||
|
|
0ee3a8b6e8 | ||
|
|
b379f1d82c | ||
|
|
c83f9d8700 | ||
|
|
6289222b69 | ||
|
|
607a65e22e | ||
|
|
6bbb297c59 | ||
|
|
95866f7df0 | ||
|
|
aaa949cc2a | ||
|
|
ed2b7b7cb9 | ||
|
|
1ef3afd647 | ||
|
|
2746051a2a | ||
|
|
610aef4f7e | ||
|
|
a9ca58f154 | ||
|
|
7182ab387f | ||
|
|
0e46f6e122 | ||
|
|
01777c8c3e | ||
|
|
aa16b66332 | ||
|
|
2af373aed7 | ||
|
|
896f5e9c92 | ||
|
|
b319ed7e6d | ||
|
|
9990273b19 | ||
|
|
ea7f2d8932 | ||
|
|
38b00e55b0 | ||
|
|
ab673a81f0 | ||
|
|
287b9c7b4e | ||
|
|
126f5ab60a | ||
|
|
3c19f984a9 | ||
|
|
cf3b93464a | ||
|
|
edcea37148 | ||
|
|
b4e4de82ae | ||
|
|
cdfcfe8fd0 | ||
|
|
bf677856a2 | ||
|
|
b9d76d5e8f | ||
|
|
d292a830a1 | ||
|
|
745175210c | ||
|
|
6536daee7f | ||
|
|
9ce50f2622 | ||
|
|
ea0bb5d365 | ||
|
|
20488d2be8 | ||
|
|
7b7486cc93 | ||
|
|
e5a668b691 | ||
|
|
c298bb0dfd | ||
|
|
adabc4eb98 | ||
|
|
6e1bfc177d | ||
|
|
e491cc5cbc | ||
|
|
37d02f0f52 | ||
|
|
e832085fc2 | ||
|
|
8e92c9a6d6 | ||
|
|
1fc6d71db6 | ||
|
|
4f48ee805f | ||
|
|
f9ec874491 | ||
|
|
32c186e1e2 | ||
|
|
17b2891f9a |
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/mayhem-party
|
||||
**/*.sw*
|
||||
11
README.md
11
README.md
@@ -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
|
||||
|
||||
|
||||
16
go.mod
16
go.mod
@@ -3,6 +3,18 @@ module mayhem-party
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/go-yaml/yaml v2.1.0+incompatible // indirect
|
||||
github.com/micmonay/keybd_event v1.1.1 // indirect
|
||||
github.com/faiface/beep v1.1.0
|
||||
github.com/go-yaml/yaml v2.1.0+incompatible
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/evanphx/json-patch/v5 v5.6.0 // indirect
|
||||
github.com/hajimehoshi/oto v0.7.1 // indirect
|
||||
github.com/micmonay/keybd_event v1.1.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 // indirect
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067 // indirect
|
||||
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6 // indirect
|
||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 // indirect
|
||||
)
|
||||
|
||||
43
go.sum
43
go.sum
@@ -1,4 +1,47 @@
|
||||
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||
github.com/d4l3k/messagediff v1.2.2-0.20190829033028-7e0a312ae40b/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo=
|
||||
github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww=
|
||||
github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
|
||||
github.com/faiface/beep v1.1.0 h1:A2gWP6xf5Rh7RG/p9/VAW2jRSDEGQm5sbOb38sf5d4c=
|
||||
github.com/faiface/beep v1.1.0/go.mod h1:6I8p6kK2q4opL/eWb+kAkk38ehnTunWeToJB+s51sT4=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
|
||||
github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs=
|
||||
github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498=
|
||||
github.com/go-audio/wav v1.0.0/go.mod h1:3yoReyQOsiARkvPl3ERCi8JFjihzG6WhjYpZCf5zAWE=
|
||||
github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwnTLB6vQiq+o=
|
||||
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
|
||||
github.com/hajimehoshi/go-mp3 v0.3.0/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM=
|
||||
github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI=
|
||||
github.com/hajimehoshi/oto v0.7.1 h1:I7maFPz5MBCwiutOrz++DLdbr4rTzBsbBuV2VpgU9kk=
|
||||
github.com/hajimehoshi/oto v0.7.1/go.mod h1:wovJ8WWMfFKvP587mhHgot/MBr4DnNy9m6EepeVGnos=
|
||||
github.com/icza/bitio v1.0.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A=
|
||||
github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6/go.mod h1:xQig96I1VNBDIWGCdTt54nHt6EeI639SmHycLYL7FkA=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jfreymuth/oggvorbis v1.0.1/go.mod h1:NqS+K+UXKje0FUYUPosyQ+XTVvjmVjps1aEZH1sumIk=
|
||||
github.com/jfreymuth/vorbis v1.0.0/go.mod h1:8zy3lUAm9K/rJJk223RKy6vjCZTWC61NA2QD06bfOE0=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
|
||||
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mewkiz/flac v1.0.7/go.mod h1:yU74UH277dBUpqxPouHSQIar3G1X/QIclVbFahSd1pU=
|
||||
github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2/go.mod h1:3E2FUC/qYUfM8+r9zAwpeHJzqRVVMIYnpzD/clwWxyA=
|
||||
github.com/micmonay/keybd_event v1.1.1 h1:rv7omwXWYL9Lgf3PUq6uBgJI2k1yGkL/GD6dxc6nmSs=
|
||||
github.com/micmonay/keybd_event v1.1.1/go.mod h1:CGMWMDNgsfPljzrAWoybUOSKafQPZpv+rLigt2LzNGI=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 h1:idBdZTd9UioThJp8KpM/rTSinK/ChZFBE43/WtIy8zg=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067 h1:KYGJGHOQy8oSi1fDlSpcZF0+juKwk/hEMv5SiwHogR0=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6 h1:vyLBGJPIl9ZYbcQFM2USFmJBK6KI+t+z6jL0lbwjrnc=
|
||||
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 h1:9nuHUbU8dRnRRfj9KjWUVrJeoexdbeMjttk6Oh1rD10=
|
||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
|
||||
8
main.go
8
main.go
@@ -11,6 +11,14 @@ import (
|
||||
func main() {
|
||||
ctx, can := signal.NotifyContext(context.Background(), syscall.SIGINT)
|
||||
defer can()
|
||||
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Println("panic:", err)
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := src.Main(ctx); err != nil && ctx.Err() == nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
12
src/device/input/button/button.go
Normal file
12
src/device/input/button/button.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package button
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Button struct {
|
||||
Char byte
|
||||
Down bool
|
||||
}
|
||||
|
||||
func (button Button) String() string {
|
||||
return fmt.Sprintf("%c:%v", button.Char, button.Down)
|
||||
}
|
||||
@@ -2,28 +2,20 @@ package input
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"mayhem-party/src/device/input/button"
|
||||
"mayhem-party/src/device/input/parse"
|
||||
"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() wrap.Wrap {
|
||||
return parse.New(ctx, src)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package input_test
|
||||
import (
|
||||
"context"
|
||||
"mayhem-party/src/device/input"
|
||||
"mayhem-party/src/device/input/raw"
|
||||
"mayhem-party/src/device/input/wrap"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
@@ -30,11 +32,11 @@ func TestNewRemapped(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
os.Setenv("INPUT_REMAP_FILE", remap)
|
||||
os.Setenv("INPUT_RANDOM_WEIGHT_FILE", rand)
|
||||
wrap.FlagRemapFile = remap
|
||||
raw.FlagRawRandomWeightFile = rand
|
||||
t.Cleanup(func() {
|
||||
os.Unsetenv("INPUT_REMAP_FILE")
|
||||
os.Unsetenv("INPUT_RANDOM_WEIGHT_FILE")
|
||||
wrap.FlagRemapFile = ""
|
||||
raw.FlagRawRandomWeightFile = ""
|
||||
})
|
||||
|
||||
r := input.New(context.Background())
|
||||
@@ -50,9 +52,9 @@ func TestNewRemapped(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNewBuffered(t *testing.T) {
|
||||
os.Setenv("INPUT_BUFFERED", "true")
|
||||
wrap.FlagBuffered = true
|
||||
t.Cleanup(func() {
|
||||
os.Unsetenv("INPUT_BUFFERED")
|
||||
wrap.FlagBuffered = false
|
||||
})
|
||||
|
||||
r := input.New(context.Background())
|
||||
@@ -71,9 +73,9 @@ func TestNewRandomWeightFile(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
os.Setenv("INPUT_RANDOM_WEIGHT_FILE", p)
|
||||
raw.FlagRawRandomWeightFile = p
|
||||
t.Cleanup(func() {
|
||||
os.Unsetenv("INPUT_RANDOM_WEIGHT_FILE")
|
||||
raw.FlagRawRandomWeightFile = ""
|
||||
})
|
||||
|
||||
r := input.New(context.Background())
|
||||
|
||||
@@ -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{}
|
||||
}
|
||||
26
src/device/input/parse/parser.go
Normal file
26
src/device/input/parse/parser.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package parse
|
||||
|
||||
import (
|
||||
"context"
|
||||
"mayhem-party/src/device/input/button"
|
||||
v01 "mayhem-party/src/device/input/parse/v01"
|
||||
"mayhem-party/src/device/input/raw"
|
||||
"os"
|
||||
)
|
||||
|
||||
var (
|
||||
FlagParseV01 = os.Getenv("PARSE_V01") == "true"
|
||||
)
|
||||
|
||||
type Parser interface {
|
||||
Read() []button.Button
|
||||
Close()
|
||||
CloseWrap() raw.Raw
|
||||
}
|
||||
|
||||
func New(ctx context.Context, src raw.Raw) Parser {
|
||||
if FlagParseV01 {
|
||||
return v01.NewV01(ctx, src)
|
||||
}
|
||||
return NewPlaintext(src)
|
||||
}
|
||||
12
src/device/input/parse/parser_test.go
Normal file
12
src/device/input/parse/parser_test.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package parse_test
|
||||
|
||||
import (
|
||||
"mayhem-party/src/device/input/parse"
|
||||
v01 "mayhem-party/src/device/input/parse/v01"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParser(t *testing.T) {
|
||||
var _ parse.Parser = parse.Plaintext{}
|
||||
var _ parse.Parser = &v01.V01{}
|
||||
}
|
||||
48
src/device/input/parse/plaintext.go
Normal file
48
src/device/input/parse/plaintext.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package parse
|
||||
|
||||
import (
|
||||
"mayhem-party/src/device/input/button"
|
||||
"mayhem-party/src/device/input/raw"
|
||||
"os"
|
||||
)
|
||||
|
||||
var (
|
||||
FlagParsePlaintextRelease = os.Getenv("PARSE_PLAINTEXT_RELEASE")
|
||||
)
|
||||
|
||||
type Plaintext struct {
|
||||
src raw.Raw
|
||||
release byte
|
||||
}
|
||||
|
||||
func NewPlaintext(src raw.Raw) Plaintext {
|
||||
releaseChar := byte('!')
|
||||
if FlagParsePlaintextRelease != "" {
|
||||
releaseChar = byte(FlagParsePlaintextRelease[0])
|
||||
}
|
||||
return Plaintext{
|
||||
src: src,
|
||||
release: releaseChar,
|
||||
}
|
||||
}
|
||||
|
||||
func (p Plaintext) Close() { p.src.Close() }
|
||||
|
||||
func (p Plaintext) CloseWrap() raw.Raw { return p.src }
|
||||
|
||||
func (p Plaintext) Read() []button.Button {
|
||||
b := p.src.Read()
|
||||
buttons := make([]button.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.Button{Char: b[i], Down: down})
|
||||
}
|
||||
down = true
|
||||
}
|
||||
}
|
||||
return buttons
|
||||
}
|
||||
30
src/device/input/parse/plaintext_test.go
Normal file
30
src/device/input/parse/plaintext_test.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package parse_test
|
||||
|
||||
import (
|
||||
"mayhem-party/src/device/input/button"
|
||||
"mayhem-party/src/device/input/parse"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPlaintext(t *testing.T) {
|
||||
src := constSrc("c!b")
|
||||
p := parse.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)
|
||||
}
|
||||
74
src/device/input/parse/v01/config.go
Normal file
74
src/device/input/parse/v01/config.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package v01
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sync"
|
||||
|
||||
patch "github.com/evanphx/json-patch/v5"
|
||||
)
|
||||
|
||||
type (
|
||||
config struct {
|
||||
lock *sync.Mutex
|
||||
Feedback configFeedback
|
||||
Users map[string]configUser
|
||||
Players []configPlayer
|
||||
Quiet bool
|
||||
Broadcast configBroadcast
|
||||
}
|
||||
|
||||
configBroadcast struct {
|
||||
Message string
|
||||
}
|
||||
|
||||
configFeedback struct {
|
||||
Addr string
|
||||
TTSURL string
|
||||
}
|
||||
|
||||
configUser struct {
|
||||
Meta configUserMeta
|
||||
State configUserState
|
||||
}
|
||||
|
||||
configUserMeta struct {
|
||||
LastTSMS int64
|
||||
LastLag int64
|
||||
}
|
||||
|
||||
configUserState struct {
|
||||
Player int
|
||||
Message string
|
||||
GM configUserStateGM
|
||||
}
|
||||
|
||||
configUserStateGM struct {
|
||||
Alias string
|
||||
LastAlias string
|
||||
Vote string
|
||||
}
|
||||
|
||||
configPlayer struct {
|
||||
Transformation transformation
|
||||
}
|
||||
)
|
||||
|
||||
func (cfg config) WithPatch(v interface{}) config {
|
||||
cfg.lock.Lock()
|
||||
defer cfg.lock.Unlock()
|
||||
originalData, _ := json.Marshal(cfg)
|
||||
patchData, _ := json.Marshal(v)
|
||||
patcher, err := patch.DecodePatch(patchData)
|
||||
if err != nil {
|
||||
return cfg
|
||||
}
|
||||
patchedData, err := patcher.Apply(originalData)
|
||||
if err != nil {
|
||||
return cfg
|
||||
}
|
||||
var patched config
|
||||
if err := json.Unmarshal(patchedData, &patched); err != nil {
|
||||
return cfg
|
||||
}
|
||||
return patched
|
||||
}
|
||||
83
src/device/input/parse/v01/config_test.go
Normal file
83
src/device/input/parse/v01/config_test.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package v01
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConfigPatch(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
cfg config
|
||||
patch interface{}
|
||||
want config
|
||||
}{
|
||||
"nil patch": {
|
||||
cfg: config{Quiet: true},
|
||||
patch: nil,
|
||||
want: config{Quiet: true},
|
||||
},
|
||||
"[] patch": {
|
||||
cfg: config{Quiet: true},
|
||||
patch: []interface{}{},
|
||||
want: config{Quiet: true},
|
||||
},
|
||||
"set fake field": {
|
||||
cfg: config{Quiet: true},
|
||||
patch: []interface{}{
|
||||
map[string]interface{}{"op": "add", "path": "/Fake", "value": true},
|
||||
},
|
||||
want: config{Quiet: true},
|
||||
},
|
||||
"remove field": {
|
||||
cfg: config{Quiet: true},
|
||||
patch: []interface{}{
|
||||
map[string]interface{}{"op": "remove", "path": "/Quiet"},
|
||||
},
|
||||
want: config{Quiet: false},
|
||||
},
|
||||
"replace field with valid": {
|
||||
cfg: config{Quiet: true},
|
||||
patch: []interface{}{
|
||||
map[string]interface{}{"op": "replace", "path": "/Quiet", "value": false},
|
||||
},
|
||||
want: config{Quiet: false},
|
||||
},
|
||||
"replace field with invalid": {
|
||||
cfg: config{Quiet: true},
|
||||
patch: []interface{}{
|
||||
map[string]interface{}{"op": "replace", "path": "/Quiet", "value": "teehee"},
|
||||
},
|
||||
want: config{Quiet: true},
|
||||
},
|
||||
"test and noop": {
|
||||
cfg: config{Quiet: true},
|
||||
patch: []interface{}{
|
||||
map[string]interface{}{"op": "test", "path": "/Quiet", "value": false},
|
||||
map[string]interface{}{"op": "replace", "path": "/Quiet", "value": false},
|
||||
},
|
||||
want: config{Quiet: true},
|
||||
},
|
||||
"test and apply": {
|
||||
cfg: config{Quiet: true},
|
||||
patch: []interface{}{
|
||||
map[string]interface{}{"op": "test", "path": "/Quiet", "value": true},
|
||||
map[string]interface{}{"op": "replace", "path": "/Quiet", "value": false},
|
||||
},
|
||||
want: config{Quiet: false},
|
||||
},
|
||||
}
|
||||
|
||||
for name, d := range cases {
|
||||
c := d
|
||||
t.Run(name, func(t *testing.T) {
|
||||
c.cfg.lock = &sync.Mutex{}
|
||||
got := c.cfg.WithPatch(c.patch)
|
||||
got.lock = nil
|
||||
c.want.lock = nil
|
||||
if fmt.Sprintf("%+v", got) != fmt.Sprintf("%+v", c.want) {
|
||||
t.Errorf("(%+v).Patch(%+v) want %+v, got %+v", c.cfg, c.patch, c.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
27
src/device/input/parse/v01/message.go
Normal file
27
src/device/input/parse/v01/message.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package v01
|
||||
|
||||
import (
|
||||
"log"
|
||||
"mayhem-party/src/device/input/button"
|
||||
)
|
||||
|
||||
type message struct {
|
||||
T int64
|
||||
U string
|
||||
Y string
|
||||
N string
|
||||
}
|
||||
|
||||
func (msg message) buttons() []button.Button {
|
||||
buttons := make([]button.Button, len(msg.Y)+len(msg.N))
|
||||
for i := range msg.Y {
|
||||
buttons[i] = button.Button{Char: msg.Y[i], Down: true}
|
||||
}
|
||||
for i := range msg.N {
|
||||
buttons[len(msg.Y)+i] = button.Button{Char: msg.N[i], Down: false}
|
||||
}
|
||||
if FlagDebug {
|
||||
log.Printf("%+v", msg)
|
||||
}
|
||||
return buttons
|
||||
}
|
||||
363
src/device/input/parse/v01/server.go
Normal file
363
src/device/input/parse/v01/server.go
Normal file
@@ -0,0 +1,363 @@
|
||||
package v01
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math/rand"
|
||||
"mayhem-party/src/device/input/wrap"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func (v01 *V01) listen() {
|
||||
if v01.cfg.Feedback.Addr == "" {
|
||||
return
|
||||
}
|
||||
v01._listen()
|
||||
}
|
||||
|
||||
func (v01 *V01) _listen() {
|
||||
mutex := &sync.Mutex{}
|
||||
s := &http.Server{
|
||||
Addr: v01.cfg.Feedback.Addr,
|
||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
defer func() { log.Printf("%vms | %s %s", time.Since(start).Milliseconds(), r.Method, r.URL) }()
|
||||
v01.cfg.lock.Lock()
|
||||
defer v01.cfg.lock.Unlock()
|
||||
if r.Method == http.MethodGet {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
} else {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
}
|
||||
v01.ServeHTTP(w, r)
|
||||
v01.stashConfig() // TODO
|
||||
}),
|
||||
}
|
||||
go func() {
|
||||
<-v01.ctx.Done()
|
||||
log.Println("closing v01 server")
|
||||
s.Close()
|
||||
}()
|
||||
log.Println("starting v01 server")
|
||||
if err := s.ListenAndServe(); err != nil && v01.ctx.Err() == nil {
|
||||
log.Println("err with v01 server", err)
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (v01 *V01) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
r = r.WithContext(v01.ctx)
|
||||
v01.serveHTTP(w, r)
|
||||
v01.serveGlobalQueries(r)
|
||||
}
|
||||
|
||||
func (v01 *V01) serveHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
switch strings.Split(r.URL.Path[1:], "/")[0] {
|
||||
case "":
|
||||
v01.getUserFeedback(w, r)
|
||||
case "broadcast":
|
||||
v01.servePutBroadcast(w, r)
|
||||
case "config":
|
||||
v01.serveConfig(w, r)
|
||||
case "gm":
|
||||
v01.serveGM(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (v01 *V01) getUserFeedback(w http.ResponseWriter, r *http.Request) {
|
||||
user := v01.cfg.Users[r.URL.Query().Get("user")]
|
||||
|
||||
msg := user.State.Message
|
||||
if msg == "" {
|
||||
msg = v01.cfg.Broadcast.Message
|
||||
}
|
||||
|
||||
alias := user.State.GM.Alias
|
||||
if alias == "" {
|
||||
alias = user.State.GM.LastAlias
|
||||
}
|
||||
if alias != "" {
|
||||
msg = fmt.Sprintf("%s (Your secret word is '%s'. Make **someone else** say it!)", msg, alias)
|
||||
}
|
||||
|
||||
w.Write([]byte(msg))
|
||||
}
|
||||
|
||||
func (v01 *V01) servePutBroadcast(w http.ResponseWriter, r *http.Request) {
|
||||
b, _ := io.ReadAll(r.Body)
|
||||
v01.servePutBroadcastValue(string(b))
|
||||
}
|
||||
|
||||
func (v01 *V01) servePutBroadcastValue(v string) {
|
||||
v01.cfg.Broadcast.Message = v
|
||||
}
|
||||
|
||||
func (v01 *V01) serveConfig(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodGet {
|
||||
v01.serveGetConfig(w, r)
|
||||
} else {
|
||||
v01.servePatchConfig(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (v01 *V01) serveGetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
b, _ := json.Marshal(v01.cfg)
|
||||
w.Write(b)
|
||||
}
|
||||
|
||||
func (v01 *V01) servePatchConfig(w http.ResponseWriter, r *http.Request) {
|
||||
b, _ := io.ReadAll(r.Body)
|
||||
var v []interface{}
|
||||
if err := json.Unmarshal(b, &v); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
v01.cfg = v01.cfg.WithPatch(v)
|
||||
if err := v01.stashConfig(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func (v01 *V01) stashConfig() error {
|
||||
if b, err := yaml.Marshal(v01.cfg); err == nil && FlagParseV01Config != "" {
|
||||
if err := os.WriteFile(FlagParseV01Config, b, os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v01 *V01) serveGlobalQueries(r *http.Request) {
|
||||
v01.serveGlobalQuerySay(r)
|
||||
v01.serveGlobalQueryRefresh(r)
|
||||
}
|
||||
|
||||
func (v01 *V01) serveGlobalQuerySay(r *http.Request) {
|
||||
text := r.URL.Query().Get("say")
|
||||
if text == "" {
|
||||
text = r.Header.Get("say")
|
||||
}
|
||||
if text == "" {
|
||||
return
|
||||
}
|
||||
go v01.tts(text)
|
||||
}
|
||||
|
||||
func (v01 *V01) serveGlobalQueryRefresh(r *http.Request) {
|
||||
if _, ok := r.URL.Query()["refresh"]; !ok {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case wrap.ChSigUsr1 <- syscall.SIGUSR1:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (v01 *V01) serveGM(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/gm/rpc/status":
|
||||
v01.serveGMStatus(w, r)
|
||||
case "/gm/rpc/broadcastSomeoneSaidAlias":
|
||||
v01.serveGMSomeoneSaidAlias(w, r)
|
||||
case "/gm/rpc/fillNonPlayerAliases":
|
||||
v01.serveGMFillNonPlayerAliases(w, r)
|
||||
case "/gm/rpc/vote":
|
||||
v01.serveGMVote(w, r)
|
||||
case "/gm/rpc/elect":
|
||||
v01.serveGMElect(w, r)
|
||||
case "/gm/rpc/shuffle":
|
||||
v01.serveGMShuffle(r)
|
||||
case "/gm/rpc/swap":
|
||||
if errCode, err := v01.serveGMSwap(r.URL.Query().Get("a"), r.URL.Query().Get("b")); err != nil {
|
||||
http.Error(w, err.Error(), errCode)
|
||||
return
|
||||
}
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (v01 *V01) serveGMStatus(w http.ResponseWriter, r *http.Request) {
|
||||
users := map[string]struct {
|
||||
Lag time.Duration `yaml:"lag,omitempty"`
|
||||
Player int `yaml:"player,omitempty"`
|
||||
IdleFor time.Duration `yaml:"idle_for,omitempty"`
|
||||
}{}
|
||||
for k, v := range v01.cfg.Users {
|
||||
v2 := users[k]
|
||||
v2.Lag = time.Duration(v.Meta.LastLag) * time.Millisecond
|
||||
v2.Player = v.State.Player
|
||||
if v.Meta.LastTSMS > 0 {
|
||||
v2.IdleFor = time.Since(time.Unix(0, v.Meta.LastTSMS*int64(time.Millisecond)))
|
||||
}
|
||||
users[k] = v2
|
||||
}
|
||||
yaml.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"Players": len(v01.cfg.Players),
|
||||
"Users": users,
|
||||
})
|
||||
}
|
||||
|
||||
func (v01 *V01) serveGMSomeoneSaidAlias(w http.ResponseWriter, r *http.Request) {
|
||||
v01.cfg.Quiet = true
|
||||
for k, v := range v01.cfg.Users {
|
||||
v.State.GM.LastAlias = v.State.GM.Alias
|
||||
v.State.GM.Alias = ""
|
||||
v01.cfg.Users[k] = v
|
||||
}
|
||||
v01.servePutBroadcastValue(fmt.Sprintf("<<SOMEONE SAID %q>>", strings.ToUpper(r.URL.Query().Get("message"))))
|
||||
}
|
||||
|
||||
func (v01 *V01) serveGMFillNonPlayerAliases(w http.ResponseWriter, r *http.Request) {
|
||||
b, _ := io.ReadAll(r.Body)
|
||||
var pool []string
|
||||
yaml.Unmarshal(b, &pool)
|
||||
n := 0
|
||||
for _, v := range v01.cfg.Users {
|
||||
if v.State.Player == 0 {
|
||||
n += 1
|
||||
}
|
||||
}
|
||||
if n < 1 {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
if len(pool) < n {
|
||||
http.Error(w, fmt.Sprintf("request body must contain a list of %v options", n), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
for i := 0; i < 100; i++ {
|
||||
a, b := rand.Int()%len(pool), rand.Int()%len(pool)
|
||||
pool[a], pool[b] = pool[b], pool[a]
|
||||
}
|
||||
i := 0
|
||||
for k, v := range v01.cfg.Users {
|
||||
if v.State.Player == 0 {
|
||||
v.State.GM.Alias = pool[i]
|
||||
v01.cfg.Users[k] = v
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (v01 *V01) serveGMElect(w http.ResponseWriter, r *http.Request) {
|
||||
alias := r.URL.Query().Get("alias")
|
||||
aliasWinner := ""
|
||||
votes := map[string]int{}
|
||||
for k, v := range v01.cfg.Users {
|
||||
votes[v.State.GM.Vote] = votes[v.State.GM.Vote] + 1
|
||||
if v.State.GM.LastAlias == alias {
|
||||
aliasWinner = k
|
||||
}
|
||||
}
|
||||
if aliasWinner == "" {
|
||||
http.Error(w, "who is "+alias+"?", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
threshold := 0.1 + float64(len(votes))/2.0
|
||||
winner := ""
|
||||
for k, v := range votes {
|
||||
if float64(v) > threshold {
|
||||
winner = k
|
||||
}
|
||||
}
|
||||
if winner == "" {
|
||||
v01.serveGMShuffle(r)
|
||||
} else if _, err := v01.serveGMSwap(winner, aliasWinner); err != nil {
|
||||
v01.serveGMShuffle(r)
|
||||
}
|
||||
for k, v := range v01.cfg.Users {
|
||||
v.State.GM.Vote = ""
|
||||
v01.cfg.Users[k] = v
|
||||
}
|
||||
yaml.NewEncoder(w).Encode(votes)
|
||||
}
|
||||
|
||||
func (v01 *V01) serveGMVote(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Query().Get("payload") {
|
||||
case "":
|
||||
counts := map[string]string{}
|
||||
for k, v := range v01.cfg.Users {
|
||||
if v.State.GM.Vote != "" {
|
||||
counts[k] = "voted"
|
||||
} else {
|
||||
counts[k] = "voting"
|
||||
}
|
||||
}
|
||||
yaml.NewEncoder(w).Encode(counts)
|
||||
default:
|
||||
voter := r.URL.Query().Get("user")
|
||||
candidate := r.URL.Query().Get("payload")
|
||||
v, ok := v01.cfg.Users[voter]
|
||||
if _, ok2 := v01.cfg.Users[candidate]; !ok || !ok2 {
|
||||
http.Error(w, "bad voter/candidate", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
v.State.GM.Vote = candidate
|
||||
v01.cfg.Users[voter] = v
|
||||
}
|
||||
}
|
||||
|
||||
func (v01 *V01) serveGMShuffle(r *http.Request) {
|
||||
poolSize := len(v01.cfg.Users)
|
||||
if altSize := len(v01.cfg.Players); altSize > poolSize {
|
||||
poolSize = altSize
|
||||
}
|
||||
pool := make([]int, poolSize)
|
||||
if poolSize > 0 {
|
||||
for i := range v01.cfg.Players {
|
||||
pool[i] = i + 1
|
||||
}
|
||||
for i := 0; i < 30; i++ {
|
||||
l := rand.Int() % poolSize
|
||||
r := rand.Int() % poolSize
|
||||
pool[l], pool[r] = pool[r], pool[l]
|
||||
}
|
||||
}
|
||||
i := 0
|
||||
msg := []string{}
|
||||
for k, v := range v01.cfg.Users {
|
||||
v.State.Player = pool[i]
|
||||
v01.cfg.Users[k] = v
|
||||
if pool[i] > 0 {
|
||||
msg = append(msg, fmt.Sprintf("%s is now player %v", k, v.State.Player))
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
v01.servePutBroadcastValue(strings.Join(msg, ", "))
|
||||
v01.cfg.Quiet = false
|
||||
}
|
||||
|
||||
func (v01 *V01) serveGMSwap(userA, userB string) (int, error) {
|
||||
if userA == userB {
|
||||
return http.StatusConflict, errors.New("/spiderman-pointing")
|
||||
}
|
||||
_, okA := v01.cfg.Users[userA]
|
||||
_, okB := v01.cfg.Users[userB]
|
||||
if !okA || !okB {
|
||||
return http.StatusBadRequest, errors.New("who dat?")
|
||||
}
|
||||
a := v01.cfg.Users[userA]
|
||||
b := v01.cfg.Users[userB]
|
||||
a.State.Player, b.State.Player = b.State.Player, a.State.Player
|
||||
v01.cfg.Users[userA] = a
|
||||
v01.cfg.Users[userB] = b
|
||||
v01.cfg.Quiet = false
|
||||
v01.servePutBroadcastValue(fmt.Sprintf(`%s is now player %v and %s is now player %v`, userA, a.State.Player, userB, b.State.Player))
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
572
src/device/input/parse/v01/server_test.go
Normal file
572
src/device/input/parse/v01/server_test.go
Normal file
@@ -0,0 +1,572 @@
|
||||
package v01
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func TestPatchConfig(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
p := path.Join(dir, t.Name()+".yaml")
|
||||
cases := map[string]struct {
|
||||
was config
|
||||
patch string
|
||||
want config
|
||||
}{
|
||||
"replace entire doc": {
|
||||
was: config{
|
||||
Feedback: configFeedback{Addr: "a", TTSURL: "a"},
|
||||
Users: map[string]configUser{"a": configUser{State: configUserState{Player: 1, Message: "a"}}},
|
||||
Players: []configPlayer{configPlayer{Transformation: transformation{"a": "a"}}},
|
||||
Quiet: true,
|
||||
},
|
||||
patch: `[{"op": "replace", "path": "", "value": {
|
||||
"Feedback": {"Addr": "b", "TTSURL": "b"},
|
||||
"Users": {"b": {"State":{"Player": 2, "Message": "b"}}},
|
||||
"Players": [{"Transformation": {"b": "b"}}],
|
||||
"Quiet": false
|
||||
}}]`,
|
||||
want: config{
|
||||
Feedback: configFeedback{Addr: "b", TTSURL: "b"},
|
||||
Users: map[string]configUser{"b": configUser{State: configUserState{Player: 2, Message: "b"}}},
|
||||
Players: []configPlayer{configPlayer{Transformation: transformation{"b": "b"}}},
|
||||
Quiet: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, d := range cases {
|
||||
c := d
|
||||
for _, usesdisk := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("%s disk=%v", name, usesdisk), func(t *testing.T) {
|
||||
b, _ := yaml.Marshal(c.was)
|
||||
os.WriteFile(p, b, os.ModePerm)
|
||||
FlagParseV01Config = ""
|
||||
if usesdisk {
|
||||
FlagParseV01Config = p
|
||||
}
|
||||
v01 := &V01{cfg: c.was}
|
||||
v01.cfg.lock = &sync.Mutex{}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest(http.MethodPatch, "/config", strings.NewReader(c.patch))
|
||||
v01.servePatchConfig(w, r)
|
||||
if fmt.Sprintf("%+v", c.want) != fmt.Sprintf("%+v", v01.cfg) {
|
||||
t.Errorf("want \n\t%+v, got \n\t%+v", c.want, v01.cfg)
|
||||
}
|
||||
if usesdisk {
|
||||
b, _ := os.ReadFile(p)
|
||||
var got config
|
||||
yaml.Unmarshal(b, &got)
|
||||
if fmt.Sprintf("%+v", c.want) != fmt.Sprintf("%+v", v01.cfg) {
|
||||
t.Errorf("want \n\t%+v, got \n\t%+v", c.want, v01.cfg)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeGM(t *testing.T) {
|
||||
ctx, can := context.WithCancel(context.Background())
|
||||
defer can()
|
||||
|
||||
do := func(v01 *V01, path, body string, method ...string) *httptest.ResponseRecorder {
|
||||
m := http.MethodPost
|
||||
if len(method) > 0 {
|
||||
m = method[0]
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest(m, path, strings.NewReader(body))
|
||||
v01.ServeHTTP(w, r)
|
||||
return w
|
||||
}
|
||||
|
||||
t.Run("status", func(t *testing.T) {
|
||||
v01 := NewV01(ctx, nil)
|
||||
var result struct {
|
||||
Players int `yaml:"Players"`
|
||||
Users map[string]struct {
|
||||
Player int
|
||||
Lag string
|
||||
IdleFor string `yaml:"idle_for"`
|
||||
} `yaml:"Users"`
|
||||
}
|
||||
|
||||
t.Run("empty", func(t *testing.T) {
|
||||
resp := do(v01, "/gm/rpc/status", "")
|
||||
if resp.Code != http.StatusOK {
|
||||
t.Error(resp.Code)
|
||||
}
|
||||
t.Log(string(resp.Body.Bytes()))
|
||||
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.Players != 0 {
|
||||
t.Error(result.Players)
|
||||
}
|
||||
if len(result.Users) != 0 {
|
||||
t.Error(result.Users)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("full", func(t *testing.T) {
|
||||
v01.cfg.Players = []configPlayer{
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
}
|
||||
v01.cfg.Users = map[string]configUser{
|
||||
"bel": configUser{
|
||||
State: configUserState{Player: 3},
|
||||
Meta: configUserMeta{
|
||||
LastTSMS: time.Now().Add(-1*time.Minute).UnixNano() / int64(time.Millisecond),
|
||||
LastLag: int64(time.Second / time.Millisecond),
|
||||
},
|
||||
},
|
||||
"zach": configUser{},
|
||||
"chase": configUser{},
|
||||
"mason": configUser{},
|
||||
"nat": configUser{},
|
||||
"roxy": configUser{},
|
||||
"bill": configUser{},
|
||||
}
|
||||
resp := do(v01, "/gm/rpc/status", "")
|
||||
if resp.Code != http.StatusOK {
|
||||
t.Error(resp.Code)
|
||||
}
|
||||
t.Log(string(resp.Body.Bytes()))
|
||||
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.Players != 4 {
|
||||
t.Error(result.Players)
|
||||
}
|
||||
if len(result.Users) != 7 {
|
||||
t.Error(result.Users)
|
||||
}
|
||||
if d, err := time.ParseDuration(result.Users["bel"].Lag); err != nil {
|
||||
t.Error(err)
|
||||
} else if d != time.Second {
|
||||
t.Error(d)
|
||||
}
|
||||
if d, err := time.ParseDuration(result.Users["bel"].IdleFor); err != nil {
|
||||
t.Error(err)
|
||||
} else if d < time.Minute || d > 2*time.Minute {
|
||||
t.Error(d)
|
||||
}
|
||||
if result.Users["bel"].Player != 3 {
|
||||
t.Error(result.Users["bel"].Player)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("broadcastSomeoneSaidAlias", func(t *testing.T) {
|
||||
v01 := NewV01(ctx, nil)
|
||||
v01.cfg.Quiet = false
|
||||
v01.cfg.Users = map[string]configUser{
|
||||
"bel": configUser{State: configUserState{
|
||||
GM: configUserStateGM{
|
||||
Alias: "driver",
|
||||
},
|
||||
Message: "if someone else says 'driver', then you get to play",
|
||||
}},
|
||||
}
|
||||
v01.cfg.Broadcast.Message = ":)"
|
||||
do(v01, "/gm/rpc/broadcastSomeoneSaidAlias", "")
|
||||
if !v01.cfg.Quiet {
|
||||
t.Error(v01.cfg.Quiet)
|
||||
}
|
||||
if v := v01.cfg.Users["bel"]; v.State.GM.Alias != "" {
|
||||
t.Error(v.State.GM.Alias)
|
||||
} else if v.State.GM.LastAlias != "driver" {
|
||||
t.Error(v.State.GM.LastAlias)
|
||||
}
|
||||
if bc := v01.cfg.Broadcast.Message; bc == ":)" {
|
||||
t.Error(bc)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fillNonPlayerAliases", func(t *testing.T) {
|
||||
t.Run("empty", func(t *testing.T) {
|
||||
v01 := NewV01(ctx, nil)
|
||||
v01.cfg.Users = nil
|
||||
resp := do(v01, "/gm/rpc/fillNonPlayerAliases", "[qt]")
|
||||
if resp.Code != http.StatusNoContent {
|
||||
t.Error(resp.Code)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("not enough", func(t *testing.T) {
|
||||
v01 := NewV01(ctx, nil)
|
||||
v01.cfg.Users = map[string]configUser{
|
||||
"zach": configUser{State: configUserState{Player: 0}},
|
||||
}
|
||||
resp := do(v01, "/gm/rpc/fillNonPlayerAliases", "[]")
|
||||
if resp.Code != http.StatusBadRequest {
|
||||
t.Error(resp.Code)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("happy", func(t *testing.T) {
|
||||
v01 := NewV01(ctx, nil)
|
||||
v01.cfg.Users = map[string]configUser{
|
||||
"bel": configUser{State: configUserState{Player: 1}},
|
||||
"zach": configUser{State: configUserState{Player: 0}},
|
||||
}
|
||||
do(v01, "/gm/rpc/fillNonPlayerAliases", "[qt]")
|
||||
if v := v01.cfg.Users["bel"]; v.State.GM.Alias != "" {
|
||||
t.Error(v.State.GM.Alias)
|
||||
} else if v.State.Player != 1 {
|
||||
t.Error(v.State.Player)
|
||||
}
|
||||
if v := v01.cfg.Users["zach"]; v.State.GM.Alias != "qt" {
|
||||
t.Error(v.State.GM.Alias)
|
||||
} else if v.State.Player != 0 {
|
||||
t.Error(v.State.Player)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("vote", func(t *testing.T) {
|
||||
type result map[string]string
|
||||
|
||||
t.Run("cast bad vote", func(t *testing.T) {
|
||||
v01 := NewV01(ctx, nil)
|
||||
v01.cfg.Users = map[string]configUser{"bel": {}}
|
||||
resp := do(v01, "/gm/rpc/vote?user=bel&payload=?", "")
|
||||
if resp.Code != http.StatusBadRequest {
|
||||
t.Error(resp)
|
||||
}
|
||||
if v01.cfg.Users["bel"].State.Message != "" {
|
||||
t.Error(v01.cfg.Users["bel"].State.Message)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("cast vote", func(t *testing.T) {
|
||||
v01 := NewV01(ctx, nil)
|
||||
v01.cfg.Users = map[string]configUser{"bel": {}, "zach": {}}
|
||||
do(v01, "/gm/rpc/vote?user=bel&payload=zach", "")
|
||||
if v01.cfg.Users["bel"].State.GM.Vote != "zach" {
|
||||
t.Error(v01.cfg.Users["bel"].State.GM.Vote)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("get non vote", func(t *testing.T) {
|
||||
v01 := NewV01(ctx, nil)
|
||||
v01.cfg.Users = map[string]configUser{"bel": {}}
|
||||
resp := do(v01, "/gm/rpc/vote", "", "GET")
|
||||
var result result
|
||||
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if len(result) != 1 {
|
||||
t.Error(result)
|
||||
}
|
||||
if result["bel"] != "voting" {
|
||||
t.Error(result)
|
||||
}
|
||||
t.Logf("%+v", result)
|
||||
})
|
||||
|
||||
t.Run("get mid vote", func(t *testing.T) {
|
||||
v01 := NewV01(ctx, nil)
|
||||
v01.cfg.Users = map[string]configUser{"bel": {State: configUserState{GM: configUserStateGM{Vote: "zach"}, Message: "driver"}}}
|
||||
resp := do(v01, "/gm/rpc/vote", "", "GET")
|
||||
var result result
|
||||
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if len(result) != 1 {
|
||||
t.Error(result)
|
||||
}
|
||||
if result["bel"] != "voted" {
|
||||
t.Error(result)
|
||||
}
|
||||
t.Logf("%+v", result)
|
||||
})
|
||||
|
||||
t.Run("get empty", func(t *testing.T) {
|
||||
v01 := NewV01(ctx, nil)
|
||||
v01.cfg.Users = nil
|
||||
resp := do(v01, "/gm/rpc/vote", "", "GET")
|
||||
var result result
|
||||
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if len(result) != 0 {
|
||||
t.Error(result)
|
||||
}
|
||||
t.Logf("%+v", result)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("elect", func(t *testing.T) {
|
||||
type result map[string]int
|
||||
|
||||
t.Run("happy", func(t *testing.T) {
|
||||
v01 := NewV01(ctx, nil)
|
||||
v01.cfg.Users = map[string]configUser{
|
||||
"bel": configUser{State: configUserState{GM: configUserStateGM{Vote: "zach", LastAlias: "driver"}, Player: 1}},
|
||||
"zach": configUser{State: configUserState{GM: configUserStateGM{Vote: "bel", LastAlias: "pizza"}}},
|
||||
"bill": configUser{State: configUserState{GM: configUserStateGM{Vote: "bel"}, Player: 2}},
|
||||
}
|
||||
resp := do(v01, "/gm/rpc/elect?alias=pizza", "")
|
||||
var result result
|
||||
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
|
||||
t.Errorf("%s => %v", resp.Body.Bytes(), err)
|
||||
}
|
||||
if len(result) != 2 {
|
||||
t.Error(result)
|
||||
} else if result["bel"] != 2 {
|
||||
t.Error(result)
|
||||
} else if result["zach"] != 1 {
|
||||
t.Error(result)
|
||||
}
|
||||
if v01.cfg.Users["bel"].State.Player != 0 {
|
||||
t.Error(v01.cfg.Users["bel"].State.Player)
|
||||
} else if v01.cfg.Users["zach"].State.Player != 1 {
|
||||
t.Error(v01.cfg.Users["zach"].State.Player)
|
||||
}
|
||||
if v01.cfg.Broadcast.Message != `bel is now player 0 and zach is now player 1` {
|
||||
t.Error(v01.cfg.Broadcast)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("self", func(t *testing.T) {
|
||||
v01 := NewV01(ctx, nil)
|
||||
v01.cfg.Players = []configPlayer{{}}
|
||||
v01.cfg.Users = map[string]configUser{
|
||||
"bel": configUser{State: configUserState{GM: configUserStateGM{Vote: "zach", LastAlias: "driver"}, Player: 1}},
|
||||
"zach": configUser{State: configUserState{GM: configUserStateGM{Vote: "bel"}}},
|
||||
"bill": configUser{State: configUserState{GM: configUserStateGM{Vote: "bel"}}},
|
||||
}
|
||||
resp := do(v01, "/gm/rpc/elect?alias=driver", "")
|
||||
var result result
|
||||
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if len(result) != 2 {
|
||||
t.Error(result)
|
||||
} else if result["bel"] != 2 {
|
||||
t.Error(result)
|
||||
} else if result["zach"] != 1 {
|
||||
t.Error(result)
|
||||
}
|
||||
if !strings.HasSuffix(v01.cfg.Broadcast.Message, `is now player 1`) || strings.Contains(v01.cfg.Broadcast.Message, ",") {
|
||||
t.Error(v01.cfg.Broadcast.Message)
|
||||
}
|
||||
assignments := map[int]int{}
|
||||
for _, v := range v01.cfg.Users {
|
||||
assignments[v.State.Player] = assignments[v.State.Player] + 1
|
||||
}
|
||||
if len(assignments) != 2 {
|
||||
t.Error(assignments)
|
||||
} else if assignments[0] != 2 {
|
||||
t.Error(assignments[0])
|
||||
} else if assignments[1] != 1 {
|
||||
t.Error(assignments[1])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("tie", func(t *testing.T) {
|
||||
v01 := NewV01(ctx, nil)
|
||||
v01.cfg.Players = []configPlayer{{}}
|
||||
v01.cfg.Users = map[string]configUser{
|
||||
"bel": configUser{State: configUserState{GM: configUserStateGM{Vote: "zach", LastAlias: "driver"}, Player: 1}},
|
||||
"zach": configUser{State: configUserState{GM: configUserStateGM{Vote: "bel", LastAlias: "pizza"}}},
|
||||
}
|
||||
resp := do(v01, "/gm/rpc/elect?alias=pizza", "")
|
||||
var result result
|
||||
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if len(result) != 2 {
|
||||
t.Error(result)
|
||||
} else if result["bel"] != 1 {
|
||||
t.Error(result)
|
||||
} else if result["zach"] != 1 {
|
||||
t.Error(result)
|
||||
}
|
||||
if bc := v01.cfg.Broadcast.Message; !strings.HasSuffix(bc, `is now player 1`) || strings.Contains(bc, ",") {
|
||||
t.Error(bc)
|
||||
}
|
||||
assignments := map[int]int{}
|
||||
for _, v := range v01.cfg.Users {
|
||||
assignments[v.State.Player] = assignments[v.State.Player] + 1
|
||||
}
|
||||
if len(assignments) != 2 {
|
||||
t.Error(assignments)
|
||||
} else if assignments[0] != 1 {
|
||||
t.Error(assignments[0])
|
||||
} else if assignments[1] != 1 {
|
||||
t.Error(assignments[1])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("shuffle", func(t *testing.T) {
|
||||
t.Run("many 2u 2p", func(t *testing.T) {
|
||||
v01 := NewV01(ctx, nil)
|
||||
for i := 0; i < 100; i++ {
|
||||
v01.cfg.Quiet = true
|
||||
v01.cfg.Users = map[string]configUser{
|
||||
"bel": configUser{State: configUserState{Player: 1}},
|
||||
"zach": configUser{State: configUserState{Player: 2}},
|
||||
}
|
||||
v01.cfg.Players = []configPlayer{{}, {}}
|
||||
do(v01, "/gm/rpc/shuffle", "")
|
||||
if v01.cfg.Quiet {
|
||||
t.Error(v01.cfg.Quiet)
|
||||
}
|
||||
if len(v01.cfg.Users) != 2 {
|
||||
t.Error(v01.cfg.Users)
|
||||
} else if len(v01.cfg.Players) != 2 {
|
||||
t.Error(v01.cfg.Users)
|
||||
} else if bp := v01.cfg.Users["bel"].State.Player; bp != 1 && bp != 2 {
|
||||
t.Error(bp)
|
||||
} else if zp := v01.cfg.Users["zach"].State.Player; zp != 1 && zp != 2 {
|
||||
t.Error(zp)
|
||||
} else if bp == zp {
|
||||
t.Error(bp, zp)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
cases := map[string]struct {
|
||||
users int
|
||||
usersAssigned int
|
||||
players int
|
||||
}{
|
||||
"empty": {},
|
||||
"just users": {users: 2},
|
||||
"just players": {players: 2},
|
||||
"2 unassigned users and 2 players": {users: 2, players: 2},
|
||||
"2 users and 2 players": {users: 2, usersAssigned: 2, players: 2},
|
||||
"1 users and 2 players": {users: 1, usersAssigned: 1, players: 2},
|
||||
"1 unassigned users and 2 players": {users: 1, players: 2},
|
||||
"4 players for 7 users 0 assigned": {users: 7, players: 4},
|
||||
"4 players for 7 users 4 assigned": {users: 7, players: 4, usersAssigned: 4},
|
||||
}
|
||||
|
||||
for name, d := range cases {
|
||||
c := d
|
||||
t.Run(name, func(t *testing.T) {
|
||||
v01 := NewV01(ctx, nil)
|
||||
v01.cfg.Quiet = true
|
||||
v01.cfg.Users = map[string]configUser{}
|
||||
for i := 0; i < c.users; i++ {
|
||||
v01.cfg.Users[strconv.Itoa(i)] = configUser{}
|
||||
if i < c.usersAssigned {
|
||||
v01.cfg.Users[strconv.Itoa(i)] = configUser{State: configUserState{Player: i}}
|
||||
}
|
||||
}
|
||||
v01.cfg.Players = make([]configPlayer, c.players)
|
||||
|
||||
do(v01, "/gm/rpc/shuffle", "")
|
||||
if v01.cfg.Quiet {
|
||||
t.Error(v01.cfg.Quiet)
|
||||
}
|
||||
|
||||
if len(v01.cfg.Users) != c.users {
|
||||
t.Error(v01.cfg.Users)
|
||||
} else if len(v01.cfg.Players) != c.players {
|
||||
t.Error(v01.cfg.Users)
|
||||
}
|
||||
for i := 0; i < c.users; i++ {
|
||||
if _, ok := v01.cfg.Users[strconv.Itoa(i)]; !ok {
|
||||
t.Error(i)
|
||||
}
|
||||
}
|
||||
assignments := map[int]int{}
|
||||
for _, v := range v01.cfg.Users {
|
||||
if v.State.Player > 0 {
|
||||
assignments[v.State.Player] = assignments[v.State.Player] + 1
|
||||
}
|
||||
}
|
||||
lesser := c.users
|
||||
if c.players < lesser {
|
||||
lesser = c.players
|
||||
}
|
||||
if len(assignments) != lesser {
|
||||
t.Error(assignments)
|
||||
}
|
||||
for _, v := range assignments {
|
||||
if v != 1 {
|
||||
t.Error(v)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("swap", func(t *testing.T) {
|
||||
t.Run("self", func(t *testing.T) {
|
||||
v01 := NewV01(ctx, nil)
|
||||
v01.cfg.Quiet = true
|
||||
v01.cfg.Users = map[string]configUser{
|
||||
"bel": configUser{State: configUserState{Player: 1}},
|
||||
}
|
||||
resp := do(v01, "/gm/rpc/swap?a=bel&b=bel", "")
|
||||
if resp.Code != http.StatusConflict {
|
||||
t.Error(resp.Code)
|
||||
}
|
||||
if !v01.cfg.Quiet {
|
||||
t.Error(v01.cfg.Quiet)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("who", func(t *testing.T) {
|
||||
v01 := NewV01(ctx, nil)
|
||||
v01.cfg.Quiet = true
|
||||
resp := do(v01, "/gm/rpc/swap?a=bel", "")
|
||||
if resp.Code != http.StatusBadRequest {
|
||||
t.Error(resp.Code)
|
||||
}
|
||||
if !v01.cfg.Quiet {
|
||||
t.Error(v01.cfg.Quiet)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("happy", func(t *testing.T) {
|
||||
v01 := NewV01(ctx, nil)
|
||||
v01.cfg.Quiet = true
|
||||
v01.cfg.Users = map[string]configUser{
|
||||
"bel": configUser{State: configUserState{Player: 1}},
|
||||
"zach": configUser{State: configUserState{Player: 2}},
|
||||
}
|
||||
resp := do(v01, "/gm/rpc/swap?a=bel&b=zach", "")
|
||||
if resp.Code != http.StatusOK {
|
||||
t.Error(resp.Code)
|
||||
}
|
||||
if v01.cfg.Quiet {
|
||||
t.Error(v01.cfg.Quiet)
|
||||
}
|
||||
if v01.cfg.Users["bel"].State.Player != 2 {
|
||||
t.Error(v01.cfg.Users["bel"])
|
||||
} else if v01.cfg.Users["zach"].State.Player != 1 {
|
||||
t.Error(v01.cfg.Users["zach"])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("404", func(t *testing.T) {
|
||||
v01 := NewV01(ctx, nil)
|
||||
resp := do(v01, "/gm/teehee", "")
|
||||
if resp.Code != http.StatusNotFound {
|
||||
t.Error(resp.Code)
|
||||
}
|
||||
})
|
||||
}
|
||||
27
src/device/input/parse/v01/testdata/v01.yaml
vendored
Normal file
27
src/device/input/parse/v01/testdata/v01.yaml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
feedback:
|
||||
addr: :17071
|
||||
ttsurl: http://localhost:15002
|
||||
broadcast:
|
||||
message: hi
|
||||
users:
|
||||
bel:
|
||||
state:
|
||||
player: 0
|
||||
message: "hi"
|
||||
alias: driver
|
||||
meta:
|
||||
tsms: 1
|
||||
lastlag: 2
|
||||
players:
|
||||
- buttons:
|
||||
up: "w"
|
||||
down: "s"
|
||||
left: "a"
|
||||
right: "d"
|
||||
l: "q"
|
||||
r: "e"
|
||||
a: "1"
|
||||
b: "2"
|
||||
x: "3"
|
||||
y: "4"
|
||||
quiet: false
|
||||
14
src/device/input/parse/v01/transform.go
Normal file
14
src/device/input/parse/v01/transform.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package v01
|
||||
|
||||
type (
|
||||
transformation map[string]string
|
||||
)
|
||||
|
||||
func (t transformation) pipe(s string) string {
|
||||
for i := range s {
|
||||
if v := t[s[i:i+1]]; v != "" {
|
||||
s = s[:i] + v[:1] + s[i+1:]
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
76
src/device/input/parse/v01/tts.go
Normal file
76
src/device/input/parse/v01/tts.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package v01
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/faiface/beep"
|
||||
"github.com/faiface/beep/effects"
|
||||
"github.com/faiface/beep/speaker"
|
||||
"github.com/faiface/beep/wav"
|
||||
)
|
||||
|
||||
var (
|
||||
ttsLock = &sync.RWMutex{}
|
||||
)
|
||||
|
||||
func (v01 *V01) tts(text string) {
|
||||
if err := v01._tts(text); err != nil {
|
||||
log.Printf("failed to tts: %s: %v", text, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (v01 *V01) _tts(text string) error {
|
||||
if v01.cfg.Feedback.TTSURL == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
url, err := url.Parse(v01.cfg.Feedback.TTSURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(url.Path) < 2 {
|
||||
url.Path = "/api/tts"
|
||||
}
|
||||
q := url.Query()
|
||||
if q.Get("voice") == "" {
|
||||
q.Set("voice", "en-us/glados-glow_tts")
|
||||
}
|
||||
if q.Get("lengthScale") == "" {
|
||||
q.Set("lengthScale", "1")
|
||||
}
|
||||
q.Set("text", text)
|
||||
url.RawQuery = q.Encode()
|
||||
|
||||
resp, err := http.Get(url.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
b, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode != http.StatusOK || resp.Header.Get("Content-Type") != "audio/wav" {
|
||||
return fmt.Errorf("failed to call ttsurl: (%d) %s", resp.StatusCode, b)
|
||||
}
|
||||
|
||||
decoder, format, err := wav.Decode(bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ttsLock.Lock()
|
||||
defer ttsLock.Unlock()
|
||||
speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/30))
|
||||
speaker.Play(&effects.Volume{Streamer: beep.ResampleRatio(4, 1, &beep.Ctrl{Streamer: beep.Loop(1, decoder)})})
|
||||
duration := time.Duration(decoder.Len()) * format.SampleRate.D(1)
|
||||
select {
|
||||
case <-v01.ctx.Done():
|
||||
case <-time.After(duration):
|
||||
}
|
||||
return nil
|
||||
}
|
||||
125
src/device/input/parse/v01/v01.go
Normal file
125
src/device/input/parse/v01/v01.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package v01
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"mayhem-party/src/device/input/button"
|
||||
"mayhem-party/src/device/input/raw"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
var (
|
||||
FlagDebug = os.Getenv("DEBUG") == "true"
|
||||
FlagParseV01Config = os.Getenv("V01_CONFIG")
|
||||
)
|
||||
|
||||
type (
|
||||
V01 struct {
|
||||
ctx context.Context
|
||||
can context.CancelFunc
|
||||
src raw.Raw
|
||||
cfg config
|
||||
telemetryc chan message
|
||||
}
|
||||
)
|
||||
|
||||
func NewV01(ctx context.Context, src raw.Raw) *V01 {
|
||||
var cfg config
|
||||
cfg.lock = &sync.Mutex{}
|
||||
b, _ := ioutil.ReadFile(FlagParseV01Config)
|
||||
yaml.Unmarshal(b, &cfg)
|
||||
ctx, can := context.WithCancel(ctx)
|
||||
result := &V01{
|
||||
ctx: ctx,
|
||||
can: can,
|
||||
src: src,
|
||||
cfg: cfg,
|
||||
telemetryc: make(chan message),
|
||||
}
|
||||
go result.listen()
|
||||
go result.dotelemetry()
|
||||
return result
|
||||
}
|
||||
|
||||
func (v01 *V01) CloseWrap() raw.Raw {
|
||||
v01.can()
|
||||
return v01.src
|
||||
}
|
||||
|
||||
func (v01 *V01) Close() {
|
||||
v01.can()
|
||||
v01.src.Close()
|
||||
}
|
||||
|
||||
func (v01 *V01) Read() []button.Button {
|
||||
line := v01.src.Read()
|
||||
var msg message
|
||||
if err := json.Unmarshal(line, &msg); err != nil {
|
||||
log.Printf("%v: %s", err, line)
|
||||
}
|
||||
v01.telemetry(msg)
|
||||
|
||||
buttons := v01.transform(msg).buttons()
|
||||
if v01.cfg.Quiet {
|
||||
for i := range buttons {
|
||||
buttons[i].Down = false
|
||||
}
|
||||
}
|
||||
return buttons
|
||||
}
|
||||
|
||||
func (v01 *V01) dotelemetry() {
|
||||
for {
|
||||
select {
|
||||
case <-v01.ctx.Done():
|
||||
return
|
||||
case msg := <-v01.telemetryc:
|
||||
v01._telemetry(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (v01 *V01) telemetry(msg message) {
|
||||
select {
|
||||
case v01.telemetryc <- msg:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (v01 *V01) _telemetry(msg message) {
|
||||
// TODO oof
|
||||
v01.cfg.lock.Lock()
|
||||
defer v01.cfg.lock.Unlock()
|
||||
if v01.cfg.Users == nil {
|
||||
v01.cfg.Users = map[string]configUser{}
|
||||
}
|
||||
u := v01.cfg.Users[msg.U]
|
||||
u.Meta.LastLag = time.Now().UnixNano()/int64(time.Millisecond) - msg.T
|
||||
u.Meta.LastTSMS = msg.T
|
||||
if FlagDebug {
|
||||
log.Printf("%s|%dms", msg.U, u.Meta.LastLag)
|
||||
}
|
||||
v01.cfg.Users[msg.U] = u
|
||||
}
|
||||
|
||||
func (v01 *V01) transform(msg message) message {
|
||||
if len(v01.cfg.Players) == 0 {
|
||||
return msg
|
||||
}
|
||||
user := v01.cfg.Users[msg.U]
|
||||
if user.State.Player < 1 {
|
||||
msg.Y = ""
|
||||
msg.N = ""
|
||||
return msg
|
||||
}
|
||||
player := v01.cfg.Players[user.State.Player-1]
|
||||
msg.Y = player.Transformation.pipe(msg.Y)
|
||||
msg.N = player.Transformation.pipe(msg.N)
|
||||
return msg
|
||||
}
|
||||
192
src/device/input/parse/v01/v01_exported_test.go
Normal file
192
src/device/input/parse/v01/v01_exported_test.go
Normal file
@@ -0,0 +1,192 @@
|
||||
package v01_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"mayhem-party/src/device/input/button"
|
||||
v01 "mayhem-party/src/device/input/parse/v01"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestV01(t *testing.T) {
|
||||
src := constSrc(fmt.Sprintf(`{"T":%v,"U":"bel","Y":"abc","N":"cde"}`, time.Now().UnixNano()/int64(time.Millisecond)-50))
|
||||
t.Logf("(%v) %s", len(src), src.Read())
|
||||
v01 := v01.NewV01(context.Background(), src)
|
||||
defer v01.Close()
|
||||
got := v01.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])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestV01WithCfg(t *testing.T) {
|
||||
d := t.TempDir()
|
||||
p := path.Join(d, "cfg.yaml")
|
||||
os.WriteFile(p, []byte(`
|
||||
users:
|
||||
bel:
|
||||
state:
|
||||
player: 2
|
||||
players:
|
||||
- transformation:
|
||||
w: t
|
||||
- transformation:
|
||||
w: i
|
||||
`), os.ModePerm)
|
||||
v01.FlagParseV01Config = p
|
||||
|
||||
t.Run("unknown user ignored", func(t *testing.T) {
|
||||
v01 := v01.NewV01(context.Background(), constSrc(`{"U":"qt","Y":"w"}`))
|
||||
defer v01.Close()
|
||||
got := v01.Read()
|
||||
if len(got) != 0 {
|
||||
t.Error(got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("player2", func(t *testing.T) {
|
||||
v01 := v01.NewV01(context.Background(), constSrc(`{"U":"bel","Y":"w","N":"w"}`))
|
||||
defer v01.Close()
|
||||
got := v01.Read()
|
||||
if len(got) != 2 {
|
||||
t.Error(got)
|
||||
}
|
||||
if got[0] != (button.Button{Char: 'i', Down: true}) {
|
||||
t.Error(got[0])
|
||||
}
|
||||
if got[1] != (button.Button{Char: 'i', Down: false}) {
|
||||
t.Error(got[1])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestV01Feedback(t *testing.T) {
|
||||
d := t.TempDir()
|
||||
p := path.Join(d, "cfg.yaml")
|
||||
os.WriteFile(p, []byte(`
|
||||
feedback:
|
||||
addr: :27071
|
||||
ttsurl: http://localhost:15002
|
||||
users:
|
||||
bel:
|
||||
state:
|
||||
player: 2
|
||||
message: to bel
|
||||
broadcast:
|
||||
message: to everyone
|
||||
players:
|
||||
- transformation:
|
||||
w: t
|
||||
- transformation:
|
||||
w: i
|
||||
`), os.ModePerm)
|
||||
v01.FlagParseV01Config = p
|
||||
ctx, can := context.WithCancel(context.Background())
|
||||
defer can()
|
||||
|
||||
v01 := v01.NewV01(ctx, constSrc(`{"U":"qt","Y":"w"}`))
|
||||
defer v01.Close()
|
||||
|
||||
for {
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
resp, err := http.Get("http://localhost:27071?user=bel")
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
resp.Body.Close()
|
||||
break
|
||||
}
|
||||
|
||||
t.Run("specific user", func(t *testing.T) {
|
||||
resp, err := http.Get("http://localhost:27071?user=bel")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
b, _ := io.ReadAll(resp.Body)
|
||||
if string(b) != "to bel" {
|
||||
t.Error(b)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("broadcast", func(t *testing.T) {
|
||||
resp, err := http.Get("http://localhost:27071")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
b, _ := io.ReadAll(resp.Body)
|
||||
if string(b) != "to everyone" {
|
||||
t.Error(b)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("change broadcast", func(t *testing.T) {
|
||||
want := `my new broadcast`
|
||||
r, _ := http.NewRequest(http.MethodPut, "http://localhost:27071/broadcast", strings.NewReader(want))
|
||||
resp, err := http.DefaultClient.Do(r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
resp, err = http.Get("http://localhost:27071")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
b, _ := io.ReadAll(resp.Body)
|
||||
if string(b) != want {
|
||||
t.Error(string(b))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("tts", func(t *testing.T) {
|
||||
if os.Getenv("INTEGRATION_TTS") != "true" {
|
||||
t.Skip("$INTEGRATION_TTS is not true")
|
||||
}
|
||||
for i := 0; i < 2; i++ {
|
||||
resp, err := http.Get("http://localhost:27071/?say=hello%20world")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
}
|
||||
time.Sleep(time.Millisecond * 2500)
|
||||
r, _ := http.NewRequest(http.MethodGet, "http://localhost:27071", nil)
|
||||
r.Header.Set("say", "No, HTTP does not define any limit. However most web servers do limit size of headers they accept. For example in Apache default limit is 8KB, in IIS it's 16K. Server will return 413 Entity Too Large error if headers size exceeds that limit.")
|
||||
resp, err := http.DefaultClient.Do(r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
time.Sleep(time.Millisecond * 8500)
|
||||
})
|
||||
}
|
||||
|
||||
type constSrc string
|
||||
|
||||
func (c constSrc) Close() {}
|
||||
|
||||
func (c constSrc) Read() []byte {
|
||||
return []byte(c)
|
||||
}
|
||||
92
src/device/input/parse/v01/v01_test.go
Normal file
92
src/device/input/parse/v01/v01_test.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package v01
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestV01TransformationPipe(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
input string
|
||||
xform map[string]string
|
||||
want string
|
||||
}{
|
||||
"empty input": {
|
||||
xform: map[string]string{"a": "bc"},
|
||||
},
|
||||
"empty xform": {
|
||||
input: "aa",
|
||||
want: "aa",
|
||||
},
|
||||
"all": {
|
||||
input: "aa",
|
||||
xform: map[string]string{"a": "cc"},
|
||||
want: "cc",
|
||||
},
|
||||
"last": {
|
||||
input: "ba",
|
||||
xform: map[string]string{"a": "cc"},
|
||||
want: "bc",
|
||||
},
|
||||
"first": {
|
||||
input: "ab",
|
||||
xform: map[string]string{"a": "cc"},
|
||||
want: "cb",
|
||||
},
|
||||
"noop": {
|
||||
input: "bb",
|
||||
xform: map[string]string{"a": "bc"},
|
||||
want: "bb",
|
||||
},
|
||||
}
|
||||
|
||||
for name, d := range cases {
|
||||
c := d
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got := transformation(c.xform).pipe(c.input)
|
||||
if got != c.want {
|
||||
t.Errorf("%+v(%s) want %s got %s", c.xform, c.input, c.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestV01Quiet(t *testing.T) {
|
||||
ctx, can := context.WithCancel(context.Background())
|
||||
defer can()
|
||||
v01 := NewV01(ctx, constSrc(`{"Y":"a", "N":"b"}`))
|
||||
|
||||
v01.cfg.Quiet = false
|
||||
if got := v01.Read(); len(got) != 2 {
|
||||
t.Error(len(got))
|
||||
} else if got[0].Char != 'a' {
|
||||
t.Error(got[0].Char)
|
||||
} else if got[0].Down != true {
|
||||
t.Error(got[0].Down)
|
||||
} else if got[1].Char != 'b' {
|
||||
t.Error(got[1].Char)
|
||||
} else if got[1].Down != false {
|
||||
t.Error(got[1].Down)
|
||||
}
|
||||
|
||||
v01.cfg.Quiet = true
|
||||
if got := v01.Read(); len(got) != 2 {
|
||||
t.Error(len(got))
|
||||
} else if got[0].Char != 'a' {
|
||||
t.Error(got[0].Char)
|
||||
} else if got[0].Down != false {
|
||||
t.Error(got[0].Down)
|
||||
} else if got[1].Char != 'b' {
|
||||
t.Error(got[1].Char)
|
||||
} else if got[1].Down != false {
|
||||
t.Error(got[1].Down)
|
||||
}
|
||||
}
|
||||
|
||||
type constSrc string
|
||||
|
||||
func (c constSrc) Close() {}
|
||||
|
||||
func (c constSrc) Read() []byte {
|
||||
return []byte(c)
|
||||
}
|
||||
@@ -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 FlagDebug {
|
||||
log.Printf("raw.Keyboard.Read() %s", b[:n])
|
||||
}
|
||||
return result
|
||||
return b[:n]
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -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 {
|
||||
32
src/device/input/raw/raw.go
Normal file
32
src/device/input/raw/raw.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package raw
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var (
|
||||
FlagRawKeyboard = os.Getenv("RAW_KEYBOARD") == "true"
|
||||
FlagRawUDP = os.Getenv("RAW_UDP")
|
||||
FlagRawRandomWeightFile = os.Getenv("RAW_RANDOM_WEIGHT_FILE")
|
||||
)
|
||||
|
||||
type Raw interface {
|
||||
Read() []byte
|
||||
Close()
|
||||
}
|
||||
|
||||
func New(ctx context.Context) Raw {
|
||||
if FlagRawKeyboard {
|
||||
return NewKeyboard()
|
||||
}
|
||||
if port, _ := strconv.Atoi(FlagRawUDP); port != 0 {
|
||||
return NewUDP(ctx, port)
|
||||
}
|
||||
generator := randomCharFromRange('a', 'g')
|
||||
if FlagRawRandomWeightFile != "" {
|
||||
generator = randomCharFromWeightFile(FlagRawRandomWeightFile)
|
||||
}
|
||||
return NewRandom(generator)
|
||||
}
|
||||
9
src/device/input/raw/raw_test.go
Normal file
9
src/device/input/raw/raw_test.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package raw
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestRaw(t *testing.T) {
|
||||
var _ Raw = &Random{}
|
||||
var _ Raw = UDP{}
|
||||
var _ Raw = Keyboard{}
|
||||
}
|
||||
63
src/device/input/raw/udp.go
Normal file
63
src/device/input/raw/udp.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package raw
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var (
|
||||
FlagDebug = os.Getenv("DEBUG") == "true"
|
||||
)
|
||||
|
||||
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() {
|
||||
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 FlagDebug {
|
||||
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()
|
||||
}
|
||||
@@ -1,31 +1,42 @@
|
||||
package input
|
||||
package wrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"mayhem-party/src/device/input/button"
|
||||
"mayhem-party/src/device/input/raw"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
FlagBufferedStickyDuration = os.Getenv("WRAP_BUFFERED_STICKY_DURATION")
|
||||
)
|
||||
|
||||
type Buffered struct {
|
||||
ctx context.Context
|
||||
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(FlagBufferedStickyDuration); 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 +50,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()
|
||||
@@ -49,12 +62,17 @@ func (b *Buffered) listen() {
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Buffered) CloseWrap() raw.Raw {
|
||||
b.can()
|
||||
return b.input.CloseWrap()
|
||||
}
|
||||
|
||||
func (b *Buffered) Close() {
|
||||
b.input.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 +83,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
|
||||
64
src/device/input/wrap/refresh.go
Normal file
64
src/device/input/wrap/refresh.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package wrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"mayhem-party/src/device/input/button"
|
||||
"mayhem-party/src/device/input/raw"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
var (
|
||||
ChSigUsr1 = func() chan os.Signal {
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, syscall.SIGUSR1)
|
||||
return c
|
||||
}()
|
||||
)
|
||||
|
||||
type Refresh struct {
|
||||
can context.CancelFunc
|
||||
input Wrap
|
||||
}
|
||||
|
||||
func NewRefresh(ctx context.Context, newWrap func() Wrap) *Refresh {
|
||||
return NewRefreshWith(ctx, newWrap, ChSigUsr1)
|
||||
}
|
||||
|
||||
func NewRefreshWith(ctx context.Context, newWrap func() Wrap, ch <-chan os.Signal) *Refresh {
|
||||
ctx, can := context.WithCancel(ctx)
|
||||
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.CloseWrap()
|
||||
result.input = newWrap()
|
||||
}
|
||||
}
|
||||
}()
|
||||
return result
|
||||
}
|
||||
|
||||
func (r *Refresh) CloseWrap() raw.Raw {
|
||||
r.can()
|
||||
return r.input.CloseWrap()
|
||||
}
|
||||
|
||||
func (r *Refresh) Read() []button.Button {
|
||||
return r.input.Read()
|
||||
}
|
||||
|
||||
func (r *Refresh) Close() {
|
||||
r.can()
|
||||
r.input.Close()
|
||||
}
|
||||
101
src/device/input/wrap/refresh_test.go
Normal file
101
src/device/input/wrap/refresh_test.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package wrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"mayhem-party/src/device/input/button"
|
||||
"mayhem-party/src/device/input/raw"
|
||||
"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 := NewRefreshWith(context.Background(), 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'))
|
||||
})
|
||||
}
|
||||
|
||||
func TestRefreshDoesntCloseSources(t *testing.T) {
|
||||
src := &telemetrySrc{}
|
||||
newParsers := 0
|
||||
newParser := func() Wrap {
|
||||
newParsers += 1
|
||||
return src
|
||||
}
|
||||
ctx, can := context.WithCancel(context.Background())
|
||||
defer can()
|
||||
refresh := NewRefresh(ctx, newParser)
|
||||
if newParsers != 1 {
|
||||
t.Error(newParsers)
|
||||
}
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
refresh.Read()
|
||||
}
|
||||
if want := (telemetrySrc{reads: 5}); *src != want {
|
||||
t.Errorf("%+v", *src)
|
||||
} else if newParsers != 1 {
|
||||
t.Error(newParsers)
|
||||
}
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
ChSigUsr1 <- syscall.SIGINT
|
||||
}
|
||||
time.Sleep(time.Millisecond * 250)
|
||||
if want := (telemetrySrc{reads: 5, closeWraps: 5}); *src != want {
|
||||
t.Errorf("want %+v, got %+v", want, *src)
|
||||
} else if newParsers != 6 {
|
||||
t.Error(newParsers)
|
||||
}
|
||||
}
|
||||
|
||||
type telemetrySrc struct {
|
||||
closeWraps int
|
||||
closes int
|
||||
reads int
|
||||
}
|
||||
|
||||
func (src *telemetrySrc) CloseWrap() raw.Raw {
|
||||
src.closeWraps += 1
|
||||
return nil
|
||||
}
|
||||
|
||||
func (src *telemetrySrc) Close() {
|
||||
src.closes += 1
|
||||
}
|
||||
|
||||
func (src *telemetrySrc) Read() []button.Button {
|
||||
src.reads += 1
|
||||
return nil
|
||||
}
|
||||
@@ -1,17 +1,19 @@
|
||||
package input
|
||||
package wrap
|
||||
|
||||
import (
|
||||
"mayhem-party/src/device/input/button"
|
||||
"mayhem-party/src/device/input/raw"
|
||||
"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,18 +32,22 @@ 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,
|
||||
}
|
||||
}
|
||||
|
||||
func (re Remap) CloseWrap() raw.Raw {
|
||||
return re.input.CloseWrap()
|
||||
}
|
||||
|
||||
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 {
|
||||
45
src/device/input/wrap/wrap.go
Normal file
45
src/device/input/wrap/wrap.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package wrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"mayhem-party/src/device/input/button"
|
||||
"mayhem-party/src/device/input/raw"
|
||||
"os"
|
||||
)
|
||||
|
||||
var (
|
||||
FlagBuffered = os.Getenv("WRAP_BUFFERED") == "true"
|
||||
FlagRemapFile = os.Getenv("WRAP_REMAP_FILE")
|
||||
FlagRefreshOnSigUsr1 = os.Getenv("WRAP_REFRESH_ON_SIGUSR1") == "true"
|
||||
)
|
||||
|
||||
type Wrap interface {
|
||||
Read() []button.Button
|
||||
Close()
|
||||
CloseWrap() raw.Raw
|
||||
}
|
||||
|
||||
func New(ctx context.Context, parserFunc func() Wrap) Wrap {
|
||||
maker := func() Wrap {
|
||||
return parserFunc()
|
||||
}
|
||||
if FlagBuffered {
|
||||
oldMaker := maker
|
||||
maker = func() Wrap {
|
||||
return NewBuffered(ctx, oldMaker())
|
||||
}
|
||||
}
|
||||
if FlagRemapFile != "" {
|
||||
oldMaker := maker
|
||||
maker = func() Wrap {
|
||||
return NewRemapFromFile(oldMaker(), FlagRemapFile)
|
||||
}
|
||||
}
|
||||
if FlagRefreshOnSigUsr1 {
|
||||
oldMaker := maker
|
||||
maker = func() Wrap {
|
||||
return NewRefresh(ctx, oldMaker)
|
||||
}
|
||||
}
|
||||
return maker()
|
||||
}
|
||||
22
src/device/input/wrap/wrap_test.go
Normal file
22
src/device/input/wrap/wrap_test.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package wrap
|
||||
|
||||
import (
|
||||
"mayhem-party/src/device/input/button"
|
||||
"mayhem-party/src/device/input/raw"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWrap(t *testing.T) {
|
||||
var _ Wrap = dummyParser{}
|
||||
var _ Wrap = &Refresh{}
|
||||
var _ Wrap = &Buffered{}
|
||||
var _ Wrap = &Remap{}
|
||||
}
|
||||
|
||||
type dummyParser button.Button
|
||||
|
||||
func (d dummyParser) CloseWrap() raw.Raw { return nil }
|
||||
func (d dummyParser) Close() {}
|
||||
func (d dummyParser) Read() []button.Button {
|
||||
return []button.Button{button.Button(d)}
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -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' {
|
||||
|
||||
@@ -32,4 +32,29 @@ const (
|
||||
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)
|
||||
)
|
||||
|
||||
11
src/main.go
11
src/main.go
@@ -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"
|
||||
@@ -16,9 +17,9 @@ func Main(ctx context.Context) error {
|
||||
defer reader.Close()
|
||||
|
||||
interval := time.Millisecond * 50
|
||||
if intervalS, ok := os.LookupEnv("MAIN_INTERVAL_DURATION"); !ok {
|
||||
if intervalS := os.Getenv("MAIN_INTERVAL_DURATION"); intervalS == "" {
|
||||
} else if v, err := time.ParseDuration(intervalS); err != nil {
|
||||
panic(err)
|
||||
return err
|
||||
} else {
|
||||
interval = v
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -46,6 +50,9 @@ func Main(ctx context.Context) error {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
}
|
||||
if os.Getenv("DEBUG") == "true" {
|
||||
log.Printf("src.Main.writer.Press(%+v) (from %+v)", keys, delta)
|
||||
}
|
||||
writer.Press(keys...)
|
||||
}
|
||||
|
||||
|
||||
6
testdata/INPUT_REMAP_FILE.yaml
vendored
6
testdata/INPUT_REMAP_FILE.yaml
vendored
@@ -2,3 +2,9 @@ w: i
|
||||
a: j
|
||||
s: k
|
||||
d: l
|
||||
q: u
|
||||
e: o
|
||||
1: 0
|
||||
2: 9
|
||||
3: 8
|
||||
4: 7
|
||||
|
||||
4
testdata/keyboard_input.env
vendored
4
testdata/keyboard_input.env
vendored
@@ -1,2 +1,2 @@
|
||||
export INPUT_BUFFERED=true
|
||||
export INPUT_KEYBOARD=true
|
||||
export WRAP_BUFFERED=true
|
||||
export RAW_KEYBOARD=true
|
||||
|
||||
4
testdata/remapped_wasd.env
vendored
4
testdata/remapped_wasd.env
vendored
@@ -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
|
||||
|
||||
2
testdata/supermarioplay.com.env
vendored
2
testdata/supermarioplay.com.env
vendored
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user