Compare commits
181 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
b89ed62036 | |
|
|
67c93a9048 | |
|
|
8eae7ae9a6 | |
|
|
ce32620940 | |
|
|
24f4b6b8f5 | |
|
|
440191de0f | |
|
|
d5adc596ac | |
|
|
41a39c40d0 | |
|
|
9a38033b65 | |
|
|
6a4ad5ec36 | |
|
|
c2b8ab67f2 | |
|
|
9418cecdf5 | |
|
|
fb5da88774 | |
|
|
39f6bc8ed9 | |
|
|
f3cbfa1c48 | |
|
|
444245c0f5 | |
|
|
52ee1e5083 | |
|
|
934158b7a3 | |
|
|
87e63c27df | |
|
|
f98e417ba6 | |
|
|
d6a7ee3db0 | |
|
|
b814dabfd3 | |
|
|
0a91fc656d | |
|
|
5c3341e260 | |
|
|
0903c01b9a | |
|
|
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 |
|
|
@ -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
|
||||
|
||||
|
|
|
|||
17
go.mod
17
go.mod
|
|
@ -3,6 +3,19 @@ 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/gorilla/websocket v1.5.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
|
||||
)
|
||||
|
|
|
|||
45
go.sum
45
go.sum
|
|
@ -1,4 +1,49 @@
|
|||
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/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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{}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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{}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
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
|
||||
GM configGM
|
||||
}
|
||||
|
||||
configGM struct {
|
||||
Hotwords map[string]configGMHotword
|
||||
}
|
||||
|
||||
configGMHotword struct {
|
||||
Call string
|
||||
Args []string
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1,407 @@
|
|||
package v01
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math/rand"
|
||||
"mayhem-party/src/device/input/button"
|
||||
"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 + "\n\n"))
|
||||
v01.serveGMStatus(w)
|
||||
|
||||
if v01.cfg.Quiet {
|
||||
w.Write([]byte("\n\n"))
|
||||
v01.serveGMVoteRead(w)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
case "/gm/rpc/broadcastSomeoneSaidAlias":
|
||||
v01.serveGMSomeoneSaid(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 io.Writer) {
|
||||
users := map[string]string{}
|
||||
for k, v := range v01.cfg.Users {
|
||||
result := ""
|
||||
|
||||
if v.State.Player > 0 {
|
||||
result += fmt.Sprintf("Player %v ", v.State.Player)
|
||||
}
|
||||
|
||||
if ms := time.Duration(v.Meta.LastLag) * time.Millisecond; v.Meta.LastLag > 0 && ms < time.Minute {
|
||||
result += fmt.Sprintf("%s ", ms.String())
|
||||
}
|
||||
|
||||
if result == "" {
|
||||
result = "..."
|
||||
}
|
||||
users[k] = result
|
||||
}
|
||||
b, _ := yaml.Marshal(map[string]interface{}{
|
||||
"Players": len(v01.cfg.Players),
|
||||
"Users": users,
|
||||
})
|
||||
w.Write(b)
|
||||
}
|
||||
|
||||
func (v01 *V01) serveGMSomeoneSaid(w http.ResponseWriter, r *http.Request) {
|
||||
if gmHotword, ok := v01.cfg.GM.Hotwords[r.URL.Query().Get("message")]; ok {
|
||||
v01.serveGMSomeoneSaidGMHotword(w, r, gmHotword)
|
||||
}
|
||||
v01.serveGMSomeoneSaidAlias(w, r)
|
||||
}
|
||||
|
||||
func (v01 *V01) serveGMSomeoneSaidGMHotword(w http.ResponseWriter, r *http.Request, gmHotword configGMHotword) {
|
||||
switch gmHotword.Call {
|
||||
case "tap":
|
||||
args := append([]string{}, gmHotword.Args...)
|
||||
if len(args) < 1 || len(args[0]) < 1 {
|
||||
return
|
||||
}
|
||||
btn := args[0][0]
|
||||
go func() {
|
||||
v01.alt <- []button.Button{button.Button{Down: true, Char: btn}}
|
||||
v01.alt <- []button.Button{button.Button{Down: false, Char: btn}}
|
||||
}()
|
||||
r.URL.RawQuery = ""
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
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 "":
|
||||
v01.serveGMVoteRead(w)
|
||||
default:
|
||||
v01.serveGMVoteWrite(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (v01 *V01) serveGMVoteRead(w io.Writer) {
|
||||
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)
|
||||
}
|
||||
|
||||
func (v01 *V01) serveGMVoteWrite(w http.ResponseWriter, r *http.Request) {
|
||||
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
|
||||
}
|
||||
|
|
@ -0,0 +1,592 @@
|
|||
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]string `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 result.Users["bel"] == "" || result.Users["bel"] == "..." {
|
||||
t.Error(result.Users["bel"])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("broadcastSomeoneSaidAlias to hotword tap", func(t *testing.T) {
|
||||
v01 := NewV01(ctx, nil)
|
||||
v01.cfg.GM = configGM{
|
||||
Hotwords: map[string]configGMHotword{
|
||||
"hotword": configGMHotword{
|
||||
Call: "tap",
|
||||
Args: []string{"a"},
|
||||
},
|
||||
},
|
||||
}
|
||||
do(v01, "/gm/rpc/broadcastSomeoneSaidAlias?message=hotword", "")
|
||||
for i := 0; i < 2; i++ {
|
||||
select {
|
||||
case btn := <-v01.alt:
|
||||
if len(btn) != 1 {
|
||||
t.Error(btn)
|
||||
} else if btn[0].Down != (i == 0) {
|
||||
t.Error(btn[0])
|
||||
} else if btn[0].Char != 'a' {
|
||||
t.Error(btn[0].Char)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("nothing in alt")
|
||||
}
|
||||
}
|
||||
do(v01, "/gm/rpc/broadcastSomeoneSaidAlias?message=hotword", "")
|
||||
time.Sleep(time.Millisecond * 150)
|
||||
if got := v01.Read(); len(got) != 1 || !got[0].Down || got[0].Char != 'a' {
|
||||
t.Error(got)
|
||||
} else if got := v01.Read(); len(got) != 1 || got[0].Down || got[0].Char != 'a' {
|
||||
t.Error(got)
|
||||
}
|
||||
})
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
feedback:
|
||||
addr: :17071
|
||||
ttsurl: http://localhost:15002
|
||||
users:
|
||||
bel:
|
||||
meta:
|
||||
lasttsms: 1681062770999
|
||||
lastlag: 12
|
||||
state:
|
||||
player: 0
|
||||
message: hi
|
||||
gm:
|
||||
alias: ""
|
||||
lastalias: ""
|
||||
vote: ""
|
||||
players:
|
||||
- transformation: {}
|
||||
quiet: false
|
||||
broadcast:
|
||||
message: hi
|
||||
gm:
|
||||
hotwords:
|
||||
coin:
|
||||
call: tap
|
||||
args: ['!']
|
||||
star:
|
||||
call: tap
|
||||
args: ['?']
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
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
|
||||
alt chan []button.Button
|
||||
}
|
||||
)
|
||||
|
||||
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),
|
||||
alt: make(chan []button.Button, 2),
|
||||
}
|
||||
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 {
|
||||
select {
|
||||
case alt := <-v01.alt:
|
||||
return alt
|
||||
default:
|
||||
}
|
||||
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
|
||||
}
|
||||
|
|
@ -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 !strings.HasPrefix(string(b), "to bel") {
|
||||
t.Error(string(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 !strings.HasPrefix(string(b), "to everyone") {
|
||||
t.Error(string(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 !strings.HasPrefix(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)
|
||||
}
|
||||
|
|
@ -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]
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
// a key map of allowed keys
|
||||
var allowedKeys = {
|
||||
37: 'left',
|
||||
38: 'up',
|
||||
39: 'right',
|
||||
40: 'down',
|
||||
65: 'a',
|
||||
66: 'b'
|
||||
};
|
||||
|
||||
// the 'official' Konami Code sequence
|
||||
var konamiCode = ['up', 'up', 'down', 'down', 'left', 'right', 'left', 'right', 'b', 'a'];
|
||||
|
||||
// a variable to remember the 'position' the user has reached so far.
|
||||
var konamiCodePosition = 0;
|
||||
|
||||
// add keydown event listener
|
||||
document.addEventListener('keydown', function(e) {
|
||||
// get the value of the key code from the key map
|
||||
var key = allowedKeys[e.keyCode];
|
||||
// get the value of the required key from the konami code
|
||||
var requiredKey = konamiCode[konamiCodePosition];
|
||||
|
||||
// compare the key with the required key
|
||||
if (key == requiredKey) {
|
||||
|
||||
// move to the next key in the konami code sequence
|
||||
konamiCodePosition++;
|
||||
|
||||
// if the last key is reached, activate cheats
|
||||
if (konamiCodePosition == konamiCode.length) {
|
||||
showSecrets();
|
||||
konamiCodePosition = 0;
|
||||
}
|
||||
} else {
|
||||
konamiCodePosition = 0;
|
||||
}
|
||||
});
|
||||
|
||||
function showSecrets() {
|
||||
var element = document.getElementById("konami")
|
||||
element.style = "display:block"
|
||||
var e = new Event("onKonami")
|
||||
element.dispatchEvent(new Event("onKonami"))
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
function onYouTubeIframeAPIReady() {
|
||||
|
||||
var player;
|
||||
|
||||
player = new YT.Player('konami', {
|
||||
videoId: 'V4oJ62xrFZo', // 👈 video id.
|
||||
width: 560,
|
||||
height: 316,
|
||||
playerVars: {
|
||||
'autoplay': 1,
|
||||
'controls': 1,
|
||||
'showinfo': 0,
|
||||
'modestbranding': 0,
|
||||
'loop': 1,
|
||||
'fs': 0,
|
||||
'cc_load_policty': 0,
|
||||
'iv_load_policy': 3
|
||||
},
|
||||
events: {
|
||||
'onReady': function (e) {
|
||||
e.target.setVolume(33); // For max value, set value to 100.
|
||||
document.getElementById("konami").addEventListener("onKonami", () => {e.target.playVideo()})
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<header>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/water.css@2/out/dark.css">
|
||||
<script src="konami.js"></script>
|
||||
<script src="lowerVolume.js"></script>
|
||||
<script async src="https://www.youtube.com/iframe_api"></script>
|
||||
<script>
|
||||
function formsay(message) {
|
||||
console.log(`say '${message}'`)
|
||||
http("GET", `/proxy?user=${document.getElementById("user").value}&say=${message}`, noopcallback, null)
|
||||
}
|
||||
function formsend(message) {
|
||||
console.log(`send '${message}'`)
|
||||
http("GET", `/proxy/gm/rpc/vote?user=${document.getElementById("user").value}&payload=${message}`, noopcallback, null)
|
||||
}
|
||||
function http(method, remote, callback, body) {
|
||||
var xmlhttp = new XMLHttpRequest();
|
||||
xmlhttp.onreadystatechange = function() {
|
||||
if (xmlhttp.readyState == XMLHttpRequest.DONE) {
|
||||
callback(xmlhttp.responseText, xmlhttp.status)
|
||||
}
|
||||
};
|
||||
xmlhttp.open(method, remote, true);
|
||||
if (typeof body == "undefined") {
|
||||
body = null
|
||||
}
|
||||
xmlhttp.send(body);
|
||||
}
|
||||
function noopcallback(responseBody, responseStatus) {
|
||||
}
|
||||
setInterval(() => {
|
||||
http("GET", `/proxy?user=${document.getElementById("user").value}`, (b, s) => {
|
||||
if (s != 200)
|
||||
return
|
||||
document.getElementById("ntfy").innerHTML = `<pre>${b}</pre>`
|
||||
}, null)
|
||||
}, 1500)
|
||||
|
||||
|
||||
</script>
|
||||
</header>
|
||||
<body>
|
||||
<div>
|
||||
<form>
|
||||
<h3>WHO AM I</h3>
|
||||
<select id="user">
|
||||
<option>bel</option>
|
||||
<option>zach</option>
|
||||
<option>chase</option>
|
||||
<option>mason</option>
|
||||
<option>nat</option>
|
||||
<option>roxy</option>
|
||||
<option>bill</option>
|
||||
</select>
|
||||
</form>
|
||||
<div>
|
||||
<form action="" onsubmit="formsay(this.children.say.value); return false;" style="display: inline-block;">
|
||||
<h3>SAY</h3>
|
||||
<input type="text" name="say">
|
||||
<input type="submit" value="say">
|
||||
</form>
|
||||
<form action="" onsubmit="formsend(this.children.send.value); return false;" style="display: inline-block;">
|
||||
<h3>SEND</h3>
|
||||
<select name="send">
|
||||
<option>bel</option>
|
||||
<option>zach</option>
|
||||
<option>chase</option>
|
||||
<option>mason</option>
|
||||
<option>nat</option>
|
||||
<option>roxy</option>
|
||||
<option>bill</option>
|
||||
</select>
|
||||
<input type="submit" value="send">
|
||||
</form>
|
||||
</div>
|
||||
<details>
|
||||
<summary>CONTROLS</summary>
|
||||
<form id="controls">
|
||||
<div style="display: flex; flex-wrap: wrap;">
|
||||
<div>
|
||||
<label for="input-up">Up</label>
|
||||
<input id="input-up" type="text" maxLength=1 value="w" name="w" placeholder="up" onchange="recontrol()">
|
||||
<label for="input-down">Down</label>
|
||||
<input id="input-down" type="text" maxLength=1 value="s" name="s" placeholder="down" onchange="recontrol()">
|
||||
<label for="input-left">Left</label>
|
||||
<input id="input-left" type="text" maxLength=1 value="a" name="a" placeholder="left" onchange="recontrol()">
|
||||
<label for="input-right">Right</label>
|
||||
<input id="input-right" type="text" maxLength=1 value="d" name="d" placeholder="right" onchange="recontrol()">
|
||||
</div>
|
||||
<div>
|
||||
<label for="input-start">Start</label>
|
||||
<input id="input-start" type="text" maxLength=1 value="5" name="5" placeholder="start" onchange="recontrol()">
|
||||
<label for="input-left-bumper">Left Bumper</label>
|
||||
<input id="input-left-bumper" type="text" maxLength=1 value="q" name="q" placeholder="l" onchange="recontrol()">
|
||||
<label for="input-right-bumper">Right Bumper</label>
|
||||
<input id="input-right-bumper" type="text" maxLength=1 value="e" name="e" placeholder="r" onchange="recontrol()">
|
||||
</div>
|
||||
<div>
|
||||
<label for="input-a">A</label>
|
||||
<input id="input-a" type="text" maxLength=1 value="1" name="1" placeholder="a" onchange="recontrol()">
|
||||
<label for="input-b">B</label>
|
||||
<input id="input-b" type="text" maxLength=1 value="2" name="2" placeholder="b" onchange="recontrol()">
|
||||
<label for="input-x">X</label>
|
||||
<input id="input-x" type="text" maxLength=1 value="3" name="3" placeholder="x" onchange="recontrol()">
|
||||
<label for="input-y">Y</label>
|
||||
<input id="input-y" type="text" maxLength=1 value="4" name="4" placeholder="y" onchange="recontrol()">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</details>
|
||||
</div>
|
||||
<div id="ntfy"></div>
|
||||
<div id="ws"></div>
|
||||
<div id="konami" style="display:none"></div>
|
||||
</body>
|
||||
<footer>
|
||||
<script>
|
||||
var socket = new WebSocket("ws://"+window.location.host+"/api/ws")
|
||||
|
||||
function nosend(data) {
|
||||
}
|
||||
function dosend(data) {
|
||||
console.log(JSON.stringify(data))
|
||||
socket.send(JSON.stringify(data))
|
||||
}
|
||||
send = nosend
|
||||
|
||||
socket.addEventListener("open", (_) => {
|
||||
console.log("ws open")
|
||||
send = dosend
|
||||
})
|
||||
socket.addEventListener("message", (event) => console.log("ws recv:", event.data))
|
||||
socket.addEventListener("close", (event) => console.log("ws closed"))
|
||||
|
||||
keys = {}
|
||||
document.addEventListener('keydown', (event) => {
|
||||
var name = controls[event.key]
|
||||
if (!name)
|
||||
return
|
||||
if (keys[name])
|
||||
return
|
||||
keys[name] = true
|
||||
sendKeys(name, "")
|
||||
})
|
||||
document.addEventListener('keyup', (event) => {
|
||||
var name = controls[event.key]
|
||||
if (!name)
|
||||
return
|
||||
keys[name] = false
|
||||
sendKeys("", name)
|
||||
})
|
||||
function sendKeys(y, n) {
|
||||
send({
|
||||
T: new Date().getTime(),
|
||||
U: document.getElementById("user").value,
|
||||
Y: y,
|
||||
N: n,
|
||||
})
|
||||
}
|
||||
|
||||
var controls = {}
|
||||
function recontrol() {
|
||||
for (var k in controls)
|
||||
controls[k] = false
|
||||
for (var e of document.getElementById("controls").getElementsByTagName("input"))
|
||||
controls[e.value] = e.name
|
||||
}
|
||||
recontrol()
|
||||
</script>
|
||||
</footer>
|
||||
</html>
|
||||
|
|
@ -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 {
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package raw
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var (
|
||||
FlagRawKeyboard = os.Getenv("RAW_KEYBOARD") == "true"
|
||||
FlagRawUDP = os.Getenv("RAW_UDP")
|
||||
FlagRawWS = os.Getenv("RAW_WS")
|
||||
FlagDebug = os.Getenv("DEBUG") != ""
|
||||
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(FlagRawWS); port != 0 {
|
||||
return NewWS(ctx, port)
|
||||
}
|
||||
if port, _ := strconv.Atoi(FlagRawUDP); port != 0 {
|
||||
return NewUDP(ctx, port)
|
||||
}
|
||||
generator := randomCharFromRange('a', 'g')
|
||||
if FlagRawRandomWeightFile != "" {
|
||||
generator = randomCharFromWeightFile(FlagRawRandomWeightFile)
|
||||
}
|
||||
return NewRandom(generator)
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package raw
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestRaw(t *testing.T) {
|
||||
var _ Raw = &Random{}
|
||||
var _ Raw = UDP{}
|
||||
var _ Raw = Keyboard{}
|
||||
var _ Raw = WS{}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package raw
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net"
|
||||
"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() {
|
||||
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()
|
||||
}
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
package raw
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
var (
|
||||
FlagWSProxy = os.Getenv("RAW_WS_PROXY_URL")
|
||||
FlagWSDebug = os.Getenv("RAW_WS_DEBUG") != ""
|
||||
)
|
||||
|
||||
type WS struct {
|
||||
ctx context.Context
|
||||
can context.CancelFunc
|
||||
ch chan []byte
|
||||
}
|
||||
|
||||
func NewWS(ctx context.Context, port int) WS {
|
||||
ctx, can := context.WithCancel(ctx)
|
||||
ws := WS{ctx: ctx, can: can, ch: make(chan []byte, 256)}
|
||||
go ws.listen(port)
|
||||
return ws
|
||||
}
|
||||
|
||||
func (ws WS) Read() []byte {
|
||||
select {
|
||||
case v := <-ws.ch:
|
||||
return v
|
||||
case <-ws.ctx.Done():
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (ws WS) Close() {
|
||||
ws.can()
|
||||
}
|
||||
|
||||
func (ws WS) listen(port int) {
|
||||
server := &http.Server{
|
||||
Addr: fmt.Sprintf(":%d", port),
|
||||
Handler: ws,
|
||||
}
|
||||
go func() {
|
||||
if err := server.ListenAndServe(); err != nil && ws.ctx.Err() == nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
log.Println("WS on", port)
|
||||
<-ws.ctx.Done()
|
||||
server.Close()
|
||||
}
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
}
|
||||
|
||||
func (ws WS) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
r = r.WithContext(ws.ctx)
|
||||
if err := ws.serveHTTP(w, r); err != nil {
|
||||
log.Println(err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func (ws WS) serveHTTP(w http.ResponseWriter, r *http.Request) error {
|
||||
switch r.URL.Path {
|
||||
case "/api/ws":
|
||||
return ws.serveWS(w, r)
|
||||
}
|
||||
if strings.HasPrefix(r.URL.Path, "/proxy") {
|
||||
return ws.serveProxy(w, r)
|
||||
}
|
||||
return ws.serveStaticFile(w, r)
|
||||
}
|
||||
|
||||
func (ws WS) serveProxy(w http.ResponseWriter, r *http.Request) error {
|
||||
u, err := url.Parse(FlagWSProxy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/proxy")
|
||||
if r.URL.Path == "" {
|
||||
r.URL.Path = "/"
|
||||
}
|
||||
proxy := httputil.NewSingleHostReverseProxy(u)
|
||||
proxy.ServeHTTP(w, r)
|
||||
return nil
|
||||
}
|
||||
|
||||
//go:embed public/*
|
||||
var staticFiles embed.FS
|
||||
|
||||
func (ws WS) serveStaticFile(w http.ResponseWriter, r *http.Request) error {
|
||||
if FlagWSDebug {
|
||||
b, _ := os.ReadFile("src/device/input/raw/public/root.html")
|
||||
w.Write(b)
|
||||
return nil
|
||||
}
|
||||
if r.URL.Path == "/" {
|
||||
r.URL.Path = "root.html"
|
||||
}
|
||||
r.URL.Path = path.Join("public", r.URL.Path)
|
||||
http.FileServer(http.FS(staticFiles)).ServeHTTP(w, r)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ws WS) serveWS(w http.ResponseWriter, r *http.Request) error {
|
||||
if err := ws._serveWS(w, r); err != nil {
|
||||
log.Println("_serveWS:", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ws WS) _serveWS(w http.ResponseWriter, r *http.Request) error {
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
for ws.ctx.Err() == nil {
|
||||
msgType, p, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
if websocket.IsCloseError(err) || websocket.IsUnexpectedCloseError(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if msgType == websocket.TextMessage {
|
||||
log.Println(string(p))
|
||||
ws.ch <- p
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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,33 @@ 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,
|
||||
'<': PageDown,
|
||||
'>': PageUp,
|
||||
}
|
||||
keyToChar = func() map[Key]byte {
|
||||
result := map[Key]byte{}
|
||||
|
|
@ -47,7 +74,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' {
|
||||
|
|
|
|||
|
|
@ -5,31 +5,58 @@ 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)
|
||||
PageUp = Key(keybd_event.VK_PAGEUP)
|
||||
PageDown = Key(keybd_event.VK_PAGEDOWN)
|
||||
)
|
||||
|
|
|
|||
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...)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,3 +2,9 @@ w: i
|
|||
a: j
|
||||
s: k
|
||||
d: l
|
||||
q: u
|
||||
e: o
|
||||
1: 0
|
||||
2: 9
|
||||
3: 8
|
||||
4: 7
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
export INPUT_BUFFERED=true
|
||||
export INPUT_KEYBOARD=true
|
||||
export WRAP_BUFFERED=true
|
||||
export RAW_KEYBOARD=true
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue