Compare commits
108 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
16
go.mod
16
go.mod
@@ -3,6 +3,18 @@ module mayhem-party
|
|||||||
go 1.19
|
go 1.19
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/go-yaml/yaml v2.1.0+incompatible // indirect
|
github.com/faiface/beep v1.1.0
|
||||||
github.com/micmonay/keybd_event v1.1.1 // indirect
|
github.com/go-yaml/yaml v2.1.0+incompatible
|
||||||
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/evanphx/json-patch/v5 v5.6.0 // indirect
|
||||||
|
github.com/hajimehoshi/oto v0.7.1 // indirect
|
||||||
|
github.com/micmonay/keybd_event v1.1.1 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 // indirect
|
||||||
|
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067 // indirect
|
||||||
|
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6 // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
43
go.sum
43
go.sum
@@ -1,4 +1,47 @@
|
|||||||
|
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||||
|
github.com/d4l3k/messagediff v1.2.2-0.20190829033028-7e0a312ae40b/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo=
|
||||||
|
github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww=
|
||||||
|
github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
|
||||||
|
github.com/faiface/beep v1.1.0 h1:A2gWP6xf5Rh7RG/p9/VAW2jRSDEGQm5sbOb38sf5d4c=
|
||||||
|
github.com/faiface/beep v1.1.0/go.mod h1:6I8p6kK2q4opL/eWb+kAkk38ehnTunWeToJB+s51sT4=
|
||||||
|
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||||
|
github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
|
||||||
|
github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs=
|
||||||
|
github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498=
|
||||||
|
github.com/go-audio/wav v1.0.0/go.mod h1:3yoReyQOsiARkvPl3ERCi8JFjihzG6WhjYpZCf5zAWE=
|
||||||
github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwnTLB6vQiq+o=
|
github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwnTLB6vQiq+o=
|
||||||
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
|
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
|
||||||
|
github.com/hajimehoshi/go-mp3 v0.3.0/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM=
|
||||||
|
github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI=
|
||||||
|
github.com/hajimehoshi/oto v0.7.1 h1:I7maFPz5MBCwiutOrz++DLdbr4rTzBsbBuV2VpgU9kk=
|
||||||
|
github.com/hajimehoshi/oto v0.7.1/go.mod h1:wovJ8WWMfFKvP587mhHgot/MBr4DnNy9m6EepeVGnos=
|
||||||
|
github.com/icza/bitio v1.0.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A=
|
||||||
|
github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6/go.mod h1:xQig96I1VNBDIWGCdTt54nHt6EeI639SmHycLYL7FkA=
|
||||||
|
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||||
|
github.com/jfreymuth/oggvorbis v1.0.1/go.mod h1:NqS+K+UXKje0FUYUPosyQ+XTVvjmVjps1aEZH1sumIk=
|
||||||
|
github.com/jfreymuth/vorbis v1.0.0/go.mod h1:8zy3lUAm9K/rJJk223RKy6vjCZTWC61NA2QD06bfOE0=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
|
||||||
|
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||||
|
github.com/mewkiz/flac v1.0.7/go.mod h1:yU74UH277dBUpqxPouHSQIar3G1X/QIclVbFahSd1pU=
|
||||||
|
github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2/go.mod h1:3E2FUC/qYUfM8+r9zAwpeHJzqRVVMIYnpzD/clwWxyA=
|
||||||
github.com/micmonay/keybd_event v1.1.1 h1:rv7omwXWYL9Lgf3PUq6uBgJI2k1yGkL/GD6dxc6nmSs=
|
github.com/micmonay/keybd_event v1.1.1 h1:rv7omwXWYL9Lgf3PUq6uBgJI2k1yGkL/GD6dxc6nmSs=
|
||||||
github.com/micmonay/keybd_event v1.1.1/go.mod h1:CGMWMDNgsfPljzrAWoybUOSKafQPZpv+rLigt2LzNGI=
|
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=
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
# Hosting a Mayhem Party
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
1. [`rusty-pipe`](https://gogs.inhome.blapointe.com/bel/rusty-pipe)
|
|
||||||
1. [`mayhem-party`](https://gogs.inhome.blapointe.com/bel/mayhem-party)
|
|
||||||
1. [`stt`](https://gogs.inhome.blapointe.com/bel/stt)
|
|
||||||
|
|
||||||
# Clients
|
|
||||||
|
|
||||||
## Distribute `rusty-pipe`
|
|
||||||
|
|
||||||
```bash
|
|
||||||
rustup target add x86_64-pc-windows-gnu
|
|
||||||
echo windows
|
|
||||||
cargo build --target x86_64-pc-windows-gnu && ls target/x86_64-pc-windows-gnu/release/rusty-pipe.exe
|
|
||||||
echo local
|
|
||||||
cargo install --path ./
|
|
||||||
```
|
|
||||||
|
|
||||||
Each client needs 1 executable and 1 env file with a unique set of buttons
|
|
||||||
|
|
||||||
> 10 buttons per player
|
|
||||||
> `go doc key Undef | grep Key | grep -v Undef | wc -l` total (51)
|
|
||||||
|
|
||||||
The server cannot be a client because math. Maybe a VM on the client as a server would work tho.
|
|
||||||
|
|
||||||
See `./config.d/rusty-pipe.d`
|
|
||||||
|
|
||||||
# Server
|
|
||||||
|
|
||||||
## `mayhem-party`
|
|
||||||
|
|
||||||
### Configs
|
|
||||||
|
|
||||||
`cd ./config.d/mayhem-party.d/remap.d; bash ./rotate.sh`
|
|
||||||
|
|
||||||
> rotate anytime via stdin or `pkill -SIGUSR1 -f rotate.sh`
|
|
||||||
|
|
||||||
### Binary
|
|
||||||
|
|
||||||
`bash -c 'true; source ./config.d/mayhem-party.d/env.env; mayhem-party'`
|
|
||||||
|
|
||||||
## Game Playing
|
|
||||||
|
|
||||||
Foreground
|
|
||||||
|
|
||||||
## `stt`
|
|
||||||
|
|
||||||
TODO pipe stt detecting relevant strings -> change the `./config.d/mayhem-party.d/remap.d/live.yaml` link -> `SIGUSR1` to `mayhem-party`
|
|
||||||
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
export DEBUG=1
|
|
||||||
export RAW_UDP=17070
|
|
||||||
export WRAP_REFRESH_ON_SIGUSR1=true
|
|
||||||
export WRAP_REMAP_FILE=./config.d/mayhem-party.d/remap.d/live.yaml
|
|
||||||
export MAIN_INTERVAL_DURATION=5ms
|
|
||||||
export OUTPUT_KEYBOARD=false
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import yaml
|
|
||||||
from sys import argv
|
|
||||||
from sys import stderr
|
|
||||||
|
|
||||||
def log(*args):
|
|
||||||
print(*args, file=stderr)
|
|
||||||
|
|
||||||
def main(args):
|
|
||||||
players = []
|
|
||||||
for i in range(1, 5+1):
|
|
||||||
with open(f"../../rusty-pipe.d/{i}.yaml", "r") as f:
|
|
||||||
players.append(yaml.safe_load(f)["streams"]["input"]["engine"]["gui"]["buttons"])
|
|
||||||
log(players[-1])
|
|
||||||
for arg in args:
|
|
||||||
offset = int(arg)
|
|
||||||
for i in range(len(players)):
|
|
||||||
j = (i+offset)%len(players)
|
|
||||||
if i == j:
|
|
||||||
break
|
|
||||||
keys = players[i]
|
|
||||||
values = players[j]
|
|
||||||
log(f"player {i+1} plays as player {j+1}")
|
|
||||||
print(f"# player {i+1} controls {j+1}")
|
|
||||||
for k in keys:
|
|
||||||
key = keys[k]
|
|
||||||
value = values[k]
|
|
||||||
print(f"'{key}': '{value}'")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
from sys import argv
|
|
||||||
main(argv[1:])
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
players_offset_0.yaml
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
# player 1 controls 2
|
|
||||||
'1': 'q'
|
|
||||||
'2': 'w'
|
|
||||||
'3': 'e'
|
|
||||||
'4': 'r'
|
|
||||||
'5': 't'
|
|
||||||
'6': 'y'
|
|
||||||
'7': 'u'
|
|
||||||
'8': 'i'
|
|
||||||
'9': 'o'
|
|
||||||
'0': 'p'
|
|
||||||
# player 2 controls 3
|
|
||||||
'q': 'a'
|
|
||||||
'w': 's'
|
|
||||||
'e': 'd'
|
|
||||||
'r': 'f'
|
|
||||||
't': 'g'
|
|
||||||
'y': 'h'
|
|
||||||
'u': 'j'
|
|
||||||
'i': 'k'
|
|
||||||
'o': 'l'
|
|
||||||
'p': ';'
|
|
||||||
# player 3 controls 4
|
|
||||||
'a': 'z'
|
|
||||||
's': 'x'
|
|
||||||
'd': 'c'
|
|
||||||
'f': 'v'
|
|
||||||
'g': 'b'
|
|
||||||
'h': 'n'
|
|
||||||
'j': 'm'
|
|
||||||
'k': ','
|
|
||||||
'l': '-'
|
|
||||||
';': '/'
|
|
||||||
# player 4 controls 5
|
|
||||||
'z': '!'
|
|
||||||
'x': '@'
|
|
||||||
'c': '#'
|
|
||||||
'v': '$'
|
|
||||||
'b': '%'
|
|
||||||
'n': '^'
|
|
||||||
'm': '&'
|
|
||||||
',': '*'
|
|
||||||
'-': '('
|
|
||||||
'/': ')'
|
|
||||||
# player 5 controls 1
|
|
||||||
'!': '1'
|
|
||||||
'@': '2'
|
|
||||||
'#': '3'
|
|
||||||
'$': '4'
|
|
||||||
'%': '5'
|
|
||||||
'^': '6'
|
|
||||||
'&': '7'
|
|
||||||
'*': '8'
|
|
||||||
'(': '9'
|
|
||||||
')': '0'
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
# player 1 controls 3
|
|
||||||
'1': 'a'
|
|
||||||
'2': 's'
|
|
||||||
'3': 'd'
|
|
||||||
'4': 'f'
|
|
||||||
'5': 'g'
|
|
||||||
'6': 'h'
|
|
||||||
'7': 'j'
|
|
||||||
'8': 'k'
|
|
||||||
'9': 'l'
|
|
||||||
'0': ';'
|
|
||||||
# player 2 controls 4
|
|
||||||
'q': 'z'
|
|
||||||
'w': 'x'
|
|
||||||
'e': 'c'
|
|
||||||
'r': 'v'
|
|
||||||
't': 'b'
|
|
||||||
'y': 'n'
|
|
||||||
'u': 'm'
|
|
||||||
'i': ','
|
|
||||||
'o': '-'
|
|
||||||
'p': '/'
|
|
||||||
# player 3 controls 5
|
|
||||||
'a': '!'
|
|
||||||
's': '@'
|
|
||||||
'd': '#'
|
|
||||||
'f': '$'
|
|
||||||
'g': '%'
|
|
||||||
'h': '^'
|
|
||||||
'j': '&'
|
|
||||||
'k': '*'
|
|
||||||
'l': '('
|
|
||||||
';': ')'
|
|
||||||
# player 4 controls 1
|
|
||||||
'z': '1'
|
|
||||||
'x': '2'
|
|
||||||
'c': '3'
|
|
||||||
'v': '4'
|
|
||||||
'b': '5'
|
|
||||||
'n': '6'
|
|
||||||
'm': '7'
|
|
||||||
',': '8'
|
|
||||||
'-': '9'
|
|
||||||
'/': '0'
|
|
||||||
# player 5 controls 2
|
|
||||||
'!': 'q'
|
|
||||||
'@': 'w'
|
|
||||||
'#': 'e'
|
|
||||||
'$': 'r'
|
|
||||||
'%': 't'
|
|
||||||
'^': 'y'
|
|
||||||
'&': 'u'
|
|
||||||
'*': 'i'
|
|
||||||
'(': 'o'
|
|
||||||
')': 'p'
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
# player 1 controls 4
|
|
||||||
'1': 'z'
|
|
||||||
'2': 'x'
|
|
||||||
'3': 'c'
|
|
||||||
'4': 'v'
|
|
||||||
'5': 'b'
|
|
||||||
'6': 'n'
|
|
||||||
'7': 'm'
|
|
||||||
'8': ','
|
|
||||||
'9': '-'
|
|
||||||
'0': '/'
|
|
||||||
# player 2 controls 5
|
|
||||||
'q': '!'
|
|
||||||
'w': '@'
|
|
||||||
'e': '#'
|
|
||||||
'r': '$'
|
|
||||||
't': '%'
|
|
||||||
'y': '^'
|
|
||||||
'u': '&'
|
|
||||||
'i': '*'
|
|
||||||
'o': '('
|
|
||||||
'p': ')'
|
|
||||||
# player 3 controls 1
|
|
||||||
'a': '1'
|
|
||||||
's': '2'
|
|
||||||
'd': '3'
|
|
||||||
'f': '4'
|
|
||||||
'g': '5'
|
|
||||||
'h': '6'
|
|
||||||
'j': '7'
|
|
||||||
'k': '8'
|
|
||||||
'l': '9'
|
|
||||||
';': '0'
|
|
||||||
# player 4 controls 2
|
|
||||||
'z': 'q'
|
|
||||||
'x': 'w'
|
|
||||||
'c': 'e'
|
|
||||||
'v': 'r'
|
|
||||||
'b': 't'
|
|
||||||
'n': 'y'
|
|
||||||
'm': 'u'
|
|
||||||
',': 'i'
|
|
||||||
'-': 'o'
|
|
||||||
'/': 'p'
|
|
||||||
# player 5 controls 3
|
|
||||||
'!': 'a'
|
|
||||||
'@': 's'
|
|
||||||
'#': 'd'
|
|
||||||
'$': 'f'
|
|
||||||
'%': 'g'
|
|
||||||
'^': 'h'
|
|
||||||
'&': 'j'
|
|
||||||
'*': 'k'
|
|
||||||
'(': 'l'
|
|
||||||
')': ';'
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
# player 1 controls 5
|
|
||||||
'1': '!'
|
|
||||||
'2': '@'
|
|
||||||
'3': '#'
|
|
||||||
'4': '$'
|
|
||||||
'5': '%'
|
|
||||||
'6': '^'
|
|
||||||
'7': '&'
|
|
||||||
'8': '*'
|
|
||||||
'9': '('
|
|
||||||
'0': ')'
|
|
||||||
# player 2 controls 1
|
|
||||||
'q': '1'
|
|
||||||
'w': '2'
|
|
||||||
'e': '3'
|
|
||||||
'r': '4'
|
|
||||||
't': '5'
|
|
||||||
'y': '6'
|
|
||||||
'u': '7'
|
|
||||||
'i': '8'
|
|
||||||
'o': '9'
|
|
||||||
'p': '0'
|
|
||||||
# player 3 controls 2
|
|
||||||
'a': 'q'
|
|
||||||
's': 'w'
|
|
||||||
'd': 'e'
|
|
||||||
'f': 'r'
|
|
||||||
'g': 't'
|
|
||||||
'h': 'y'
|
|
||||||
'j': 'u'
|
|
||||||
'k': 'i'
|
|
||||||
'l': 'o'
|
|
||||||
';': 'p'
|
|
||||||
# player 4 controls 3
|
|
||||||
'z': 'a'
|
|
||||||
'x': 's'
|
|
||||||
'c': 'd'
|
|
||||||
'v': 'f'
|
|
||||||
'b': 'g'
|
|
||||||
'n': 'h'
|
|
||||||
'm': 'j'
|
|
||||||
',': 'k'
|
|
||||||
'-': 'l'
|
|
||||||
'/': ';'
|
|
||||||
# player 5 controls 4
|
|
||||||
'!': 'z'
|
|
||||||
'@': 'x'
|
|
||||||
'#': 'c'
|
|
||||||
'$': 'v'
|
|
||||||
'%': 'b'
|
|
||||||
'^': 'n'
|
|
||||||
'&': 'm'
|
|
||||||
'*': ','
|
|
||||||
'(': '-'
|
|
||||||
')': '/'
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
#! /bin/bash
|
|
||||||
|
|
||||||
mayhem_party_rotate() {
|
|
||||||
local currently=$(realpath live.yaml | grep -o '[0-9].yaml$' | grep -o '^[0-9]')
|
|
||||||
local next=${NEXT:-$((RANDOM%5))}
|
|
||||||
while [ -z "$NEXT" ] && [ "$next" == "$currently" ]; do
|
|
||||||
next=$((RANDOM%5))
|
|
||||||
done
|
|
||||||
rm live.yaml
|
|
||||||
ln -s players_offset_$next.yaml live.yaml
|
|
||||||
pkill -SIGUSR1 -f mayhem-party
|
|
||||||
}
|
|
||||||
|
|
||||||
trap mayhem_party_rotate SIGUSR1
|
|
||||||
|
|
||||||
if [ "$0" == "$BASH_SOURCE" ]; then
|
|
||||||
NEXT=0 mayhem_party_rotate
|
|
||||||
while read -p "$(date) > [press enter to rotate]"; do
|
|
||||||
mayhem_party_rotate
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
streams:
|
|
||||||
input:
|
|
||||||
debug: false
|
|
||||||
engine:
|
|
||||||
name: gui
|
|
||||||
gui:
|
|
||||||
press: {prefix: "", suffix: ""}
|
|
||||||
release: {prefix: "!", suffix: ""}
|
|
||||||
buttons:
|
|
||||||
up: '1'
|
|
||||||
down: '2'
|
|
||||||
left: '3'
|
|
||||||
right: '4'
|
|
||||||
l: '5'
|
|
||||||
r: '6'
|
|
||||||
a: '7'
|
|
||||||
b: '8'
|
|
||||||
x: '9'
|
|
||||||
y: '0'
|
|
||||||
output:
|
|
||||||
debug: false
|
|
||||||
engine:
|
|
||||||
name: udp
|
|
||||||
udp:
|
|
||||||
host: mayhem-party.home.blapointe.com
|
|
||||||
port: 17070
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
streams:
|
|
||||||
input:
|
|
||||||
debug: false
|
|
||||||
engine:
|
|
||||||
name: gui
|
|
||||||
gui:
|
|
||||||
press: {prefix: "", suffix: ""}
|
|
||||||
release: {prefix: "!", suffix: ""}
|
|
||||||
buttons:
|
|
||||||
up: 'q'
|
|
||||||
down: 'w'
|
|
||||||
left: 'e'
|
|
||||||
right: 'r'
|
|
||||||
l: 't'
|
|
||||||
r: 'y'
|
|
||||||
a: 'u'
|
|
||||||
b: 'i'
|
|
||||||
x: 'o'
|
|
||||||
y: 'p'
|
|
||||||
output:
|
|
||||||
debug: false
|
|
||||||
engine:
|
|
||||||
name: udp
|
|
||||||
udp:
|
|
||||||
host: mayhem-party.home.blapointe.com
|
|
||||||
port: 17070
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
streams:
|
|
||||||
input:
|
|
||||||
debug: false
|
|
||||||
engine:
|
|
||||||
name: gui
|
|
||||||
gui:
|
|
||||||
press: {prefix: "", suffix: ""}
|
|
||||||
release: {prefix: "!", suffix: ""}
|
|
||||||
buttons:
|
|
||||||
up: 'a'
|
|
||||||
down: 's'
|
|
||||||
left: 'd'
|
|
||||||
right: 'f'
|
|
||||||
l: 'g'
|
|
||||||
r: 'h'
|
|
||||||
a: 'j'
|
|
||||||
b: 'k'
|
|
||||||
x: 'l'
|
|
||||||
y: ';'
|
|
||||||
output:
|
|
||||||
debug: false
|
|
||||||
engine:
|
|
||||||
name: udp
|
|
||||||
udp:
|
|
||||||
host: mayhem-party.home.blapointe.com
|
|
||||||
port: 17070
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
streams:
|
|
||||||
input:
|
|
||||||
debug: false
|
|
||||||
engine:
|
|
||||||
name: gui
|
|
||||||
gui:
|
|
||||||
press: {prefix: "", suffix: ""}
|
|
||||||
release: {prefix: "!", suffix: ""}
|
|
||||||
buttons:
|
|
||||||
up: 'z'
|
|
||||||
down: 'x'
|
|
||||||
left: 'c'
|
|
||||||
right: 'v'
|
|
||||||
l: 'b'
|
|
||||||
r: 'n'
|
|
||||||
a: 'm'
|
|
||||||
b: ','
|
|
||||||
x: '-'
|
|
||||||
y: '/'
|
|
||||||
output:
|
|
||||||
debug: false
|
|
||||||
engine:
|
|
||||||
name: udp
|
|
||||||
udp:
|
|
||||||
host: mayhem-party.home.blapointe.com
|
|
||||||
port: 17070
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
streams:
|
|
||||||
input:
|
|
||||||
debug: false
|
|
||||||
engine:
|
|
||||||
name: gui
|
|
||||||
gui:
|
|
||||||
press: {prefix: "", suffix: ""}
|
|
||||||
release: {prefix: "!", suffix: ""}
|
|
||||||
buttons:
|
|
||||||
up: '!'
|
|
||||||
down: '@'
|
|
||||||
left: '#'
|
|
||||||
right: '$'
|
|
||||||
l: '%'
|
|
||||||
r: '^'
|
|
||||||
a: '&'
|
|
||||||
b: '*'
|
|
||||||
x: '('
|
|
||||||
y: ')'
|
|
||||||
output:
|
|
||||||
debug: false
|
|
||||||
engine:
|
|
||||||
name: udp
|
|
||||||
udp:
|
|
||||||
host: mayhem-party.home.blapointe.com
|
|
||||||
port: 17070
|
|
||||||
8
main.go
8
main.go
@@ -11,6 +11,14 @@ import (
|
|||||||
func main() {
|
func main() {
|
||||||
ctx, can := signal.NotifyContext(context.Background(), syscall.SIGINT)
|
ctx, can := signal.NotifyContext(context.Background(), syscall.SIGINT)
|
||||||
defer can()
|
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 {
|
if err := src.Main(ctx); err != nil && ctx.Err() == nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
package button
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"mayhem-party/src/device/input/raw"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Parser interface {
|
|
||||||
Read() []Button
|
|
||||||
Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(ctx context.Context, src raw.Raw) Parser {
|
|
||||||
if os.Getenv("BUTTON_PARSER_V1") == "true" {
|
|
||||||
return NewV1(src)
|
|
||||||
}
|
|
||||||
return NewPlaintext(src)
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package button_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"mayhem-party/src/device/input/button"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestParser(t *testing.T) {
|
|
||||||
var _ button.Parser = button.Plaintext{}
|
|
||||||
var _ button.Parser = button.V1{}
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
package button
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"log"
|
|
||||||
"mayhem-party/src/device/input/raw"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
var debugging = os.Getenv("DEBUG") == "true"
|
|
||||||
|
|
||||||
type (
|
|
||||||
V1 struct {
|
|
||||||
src raw.Raw
|
|
||||||
}
|
|
||||||
v1Msg struct {
|
|
||||||
T int64
|
|
||||||
U string
|
|
||||||
Y string
|
|
||||||
N string
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewV1(src raw.Raw) V1 {
|
|
||||||
return V1{
|
|
||||||
src: src,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v1 V1) Close() {
|
|
||||||
v1.src.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v1 V1) Read() []Button {
|
|
||||||
line := v1.src.Read()
|
|
||||||
var msg v1Msg
|
|
||||||
if err := json.Unmarshal(line, &msg); err != nil {
|
|
||||||
log.Printf("%v: %s", err, line)
|
|
||||||
}
|
|
||||||
return msg.buttons()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (msg v1Msg) buttons() []Button {
|
|
||||||
buttons := make([]Button, len(msg.Y)+len(msg.N))
|
|
||||||
for i := range msg.Y {
|
|
||||||
buttons[i] = Button{Char: msg.Y[i], Down: true}
|
|
||||||
}
|
|
||||||
for i := range msg.N {
|
|
||||||
buttons[len(msg.Y)+i] = Button{Char: msg.N[i], Down: false}
|
|
||||||
}
|
|
||||||
if debugging {
|
|
||||||
log.Printf("%+v", msg)
|
|
||||||
}
|
|
||||||
return buttons
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package button_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"mayhem-party/src/device/input/button"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestV1(t *testing.T) {
|
|
||||||
src := constSrc(`{"T":1,"U":"bel","Y":"abc","N":"cde"}`)
|
|
||||||
t.Logf("(%v) %s", len(src), src.Read())
|
|
||||||
v1 := button.NewV1(src)
|
|
||||||
got := v1.Read()
|
|
||||||
want := []button.Button{
|
|
||||||
{Down: true, Char: 'a'},
|
|
||||||
{Down: true, Char: 'b'},
|
|
||||||
{Down: true, Char: 'c'},
|
|
||||||
{Down: false, Char: 'c'},
|
|
||||||
{Down: false, Char: 'd'},
|
|
||||||
{Down: false, Char: 'e'},
|
|
||||||
}
|
|
||||||
if len(got) != len(want) {
|
|
||||||
t.Fatal(len(want), len(got))
|
|
||||||
}
|
|
||||||
for i := range got {
|
|
||||||
if got[i] != want[i] {
|
|
||||||
t.Errorf("[%d] want %+v got %+v", i, want[i], got[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,6 +3,7 @@ package input
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"mayhem-party/src/device/input/button"
|
"mayhem-party/src/device/input/button"
|
||||||
|
"mayhem-party/src/device/input/parse"
|
||||||
"mayhem-party/src/device/input/raw"
|
"mayhem-party/src/device/input/raw"
|
||||||
"mayhem-party/src/device/input/wrap"
|
"mayhem-party/src/device/input/wrap"
|
||||||
)
|
)
|
||||||
@@ -14,7 +15,7 @@ type Input interface {
|
|||||||
|
|
||||||
func New(ctx context.Context) Input {
|
func New(ctx context.Context) Input {
|
||||||
src := raw.New(ctx)
|
src := raw.New(ctx)
|
||||||
return wrap.New(ctx, func() button.Parser {
|
return wrap.New(ctx, func() wrap.Wrap {
|
||||||
return button.New(ctx, src)
|
return parse.New(ctx, src)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package input_test
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"mayhem-party/src/device/input"
|
"mayhem-party/src/device/input"
|
||||||
|
"mayhem-party/src/device/input/raw"
|
||||||
|
"mayhem-party/src/device/input/wrap"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -30,11 +32,11 @@ func TestNewRemapped(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
os.Setenv("WRAP_REMAP_FILE", remap)
|
wrap.FlagRemapFile = remap
|
||||||
os.Setenv("RAW_RANDOM_WEIGHT_FILE", rand)
|
raw.FlagRawRandomWeightFile = rand
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
os.Unsetenv("WRAP_REMAP_FILE")
|
wrap.FlagRemapFile = ""
|
||||||
os.Unsetenv("RAW_RANDOM_WEIGHT_FILE")
|
raw.FlagRawRandomWeightFile = ""
|
||||||
})
|
})
|
||||||
|
|
||||||
r := input.New(context.Background())
|
r := input.New(context.Background())
|
||||||
@@ -50,9 +52,9 @@ func TestNewRemapped(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestNewBuffered(t *testing.T) {
|
func TestNewBuffered(t *testing.T) {
|
||||||
os.Setenv("WRAP_BUFFERED", "true")
|
wrap.FlagBuffered = true
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
os.Unsetenv("WRAP_BUFFERED")
|
wrap.FlagBuffered = false
|
||||||
})
|
})
|
||||||
|
|
||||||
r := input.New(context.Background())
|
r := input.New(context.Background())
|
||||||
@@ -71,9 +73,9 @@ func TestNewRandomWeightFile(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
os.Setenv("RAW_RANDOM_WEIGHT_FILE", p)
|
raw.FlagRawRandomWeightFile = p
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
os.Unsetenv("RAW_RANDOM_WEIGHT_FILE")
|
raw.FlagRawRandomWeightFile = ""
|
||||||
})
|
})
|
||||||
|
|
||||||
r := input.New(context.Background())
|
r := input.New(context.Background())
|
||||||
|
|||||||
26
src/device/input/parse/parser.go
Normal file
26
src/device/input/parse/parser.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package parse
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"mayhem-party/src/device/input/button"
|
||||||
|
v01 "mayhem-party/src/device/input/parse/v01"
|
||||||
|
"mayhem-party/src/device/input/raw"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
FlagParseV01 = os.Getenv("PARSE_V01") == "true"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Parser interface {
|
||||||
|
Read() []button.Button
|
||||||
|
Close()
|
||||||
|
CloseWrap() raw.Raw
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(ctx context.Context, src raw.Raw) Parser {
|
||||||
|
if FlagParseV01 {
|
||||||
|
return v01.NewV01(ctx, src)
|
||||||
|
}
|
||||||
|
return NewPlaintext(src)
|
||||||
|
}
|
||||||
12
src/device/input/parse/parser_test.go
Normal file
12
src/device/input/parse/parser_test.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package parse_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"mayhem-party/src/device/input/parse"
|
||||||
|
v01 "mayhem-party/src/device/input/parse/v01"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParser(t *testing.T) {
|
||||||
|
var _ parse.Parser = parse.Plaintext{}
|
||||||
|
var _ parse.Parser = &v01.V01{}
|
||||||
|
}
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
package button
|
package parse
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"mayhem-party/src/device/input/button"
|
||||||
"mayhem-party/src/device/input/raw"
|
"mayhem-party/src/device/input/raw"
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
FlagParsePlaintextRelease = os.Getenv("PARSE_PLAINTEXT_RELEASE")
|
||||||
|
)
|
||||||
|
|
||||||
type Plaintext struct {
|
type Plaintext struct {
|
||||||
src raw.Raw
|
src raw.Raw
|
||||||
release byte
|
release byte
|
||||||
@@ -12,8 +17,8 @@ type Plaintext struct {
|
|||||||
|
|
||||||
func NewPlaintext(src raw.Raw) Plaintext {
|
func NewPlaintext(src raw.Raw) Plaintext {
|
||||||
releaseChar := byte('!')
|
releaseChar := byte('!')
|
||||||
if v := os.Getenv("BUTTON_PLAINTEXT_RELEASE"); v != "" {
|
if FlagParsePlaintextRelease != "" {
|
||||||
releaseChar = byte(v[0])
|
releaseChar = byte(FlagParsePlaintextRelease[0])
|
||||||
}
|
}
|
||||||
return Plaintext{
|
return Plaintext{
|
||||||
src: src,
|
src: src,
|
||||||
@@ -23,16 +28,18 @@ func NewPlaintext(src raw.Raw) Plaintext {
|
|||||||
|
|
||||||
func (p Plaintext) Close() { p.src.Close() }
|
func (p Plaintext) Close() { p.src.Close() }
|
||||||
|
|
||||||
func (p Plaintext) Read() []Button {
|
func (p Plaintext) CloseWrap() raw.Raw { return p.src }
|
||||||
|
|
||||||
|
func (p Plaintext) Read() []button.Button {
|
||||||
b := p.src.Read()
|
b := p.src.Read()
|
||||||
buttons := make([]Button, 0, len(b))
|
buttons := make([]button.Button, 0, len(b))
|
||||||
down := true
|
down := true
|
||||||
for i := range b {
|
for i := range b {
|
||||||
if b[i] == p.release {
|
if b[i] == p.release {
|
||||||
down = false
|
down = false
|
||||||
} else {
|
} else {
|
||||||
if b[i] != '\n' {
|
if b[i] != '\n' {
|
||||||
buttons = append(buttons, Button{Char: b[i], Down: down})
|
buttons = append(buttons, button.Button{Char: b[i], Down: down})
|
||||||
}
|
}
|
||||||
down = true
|
down = true
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
package button_test
|
package parse_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"mayhem-party/src/device/input/button"
|
"mayhem-party/src/device/input/button"
|
||||||
|
"mayhem-party/src/device/input/parse"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPlaintext(t *testing.T) {
|
func TestPlaintext(t *testing.T) {
|
||||||
src := constSrc("c!b")
|
src := constSrc("c!b")
|
||||||
p := button.NewPlaintext(src)
|
p := parse.NewPlaintext(src)
|
||||||
got := p.Read()
|
got := p.Read()
|
||||||
if len(got) != 2 {
|
if len(got) != 2 {
|
||||||
t.Fatal(len(got))
|
t.Fatal(len(got))
|
||||||
74
src/device/input/parse/v01/config.go
Normal file
74
src/device/input/parse/v01/config.go
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
package v01
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
patch "github.com/evanphx/json-patch/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
config struct {
|
||||||
|
lock *sync.Mutex
|
||||||
|
Feedback configFeedback
|
||||||
|
Users map[string]configUser
|
||||||
|
Players []configPlayer
|
||||||
|
Quiet bool
|
||||||
|
Broadcast configBroadcast
|
||||||
|
}
|
||||||
|
|
||||||
|
configBroadcast struct {
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
configFeedback struct {
|
||||||
|
Addr string
|
||||||
|
TTSURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
configUser struct {
|
||||||
|
Meta configUserMeta
|
||||||
|
State configUserState
|
||||||
|
}
|
||||||
|
|
||||||
|
configUserMeta struct {
|
||||||
|
LastTSMS int64
|
||||||
|
LastLag int64
|
||||||
|
}
|
||||||
|
|
||||||
|
configUserState struct {
|
||||||
|
Player int
|
||||||
|
Message string
|
||||||
|
GM configUserStateGM
|
||||||
|
}
|
||||||
|
|
||||||
|
configUserStateGM struct {
|
||||||
|
Alias string
|
||||||
|
LastAlias string
|
||||||
|
Vote string
|
||||||
|
}
|
||||||
|
|
||||||
|
configPlayer struct {
|
||||||
|
Transformation transformation
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (cfg config) WithPatch(v interface{}) config {
|
||||||
|
cfg.lock.Lock()
|
||||||
|
defer cfg.lock.Unlock()
|
||||||
|
originalData, _ := json.Marshal(cfg)
|
||||||
|
patchData, _ := json.Marshal(v)
|
||||||
|
patcher, err := patch.DecodePatch(patchData)
|
||||||
|
if err != nil {
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
patchedData, err := patcher.Apply(originalData)
|
||||||
|
if err != nil {
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
var patched config
|
||||||
|
if err := json.Unmarshal(patchedData, &patched); err != nil {
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
return patched
|
||||||
|
}
|
||||||
83
src/device/input/parse/v01/config_test.go
Normal file
83
src/device/input/parse/v01/config_test.go
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
package v01
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfigPatch(t *testing.T) {
|
||||||
|
cases := map[string]struct {
|
||||||
|
cfg config
|
||||||
|
patch interface{}
|
||||||
|
want config
|
||||||
|
}{
|
||||||
|
"nil patch": {
|
||||||
|
cfg: config{Quiet: true},
|
||||||
|
patch: nil,
|
||||||
|
want: config{Quiet: true},
|
||||||
|
},
|
||||||
|
"[] patch": {
|
||||||
|
cfg: config{Quiet: true},
|
||||||
|
patch: []interface{}{},
|
||||||
|
want: config{Quiet: true},
|
||||||
|
},
|
||||||
|
"set fake field": {
|
||||||
|
cfg: config{Quiet: true},
|
||||||
|
patch: []interface{}{
|
||||||
|
map[string]interface{}{"op": "add", "path": "/Fake", "value": true},
|
||||||
|
},
|
||||||
|
want: config{Quiet: true},
|
||||||
|
},
|
||||||
|
"remove field": {
|
||||||
|
cfg: config{Quiet: true},
|
||||||
|
patch: []interface{}{
|
||||||
|
map[string]interface{}{"op": "remove", "path": "/Quiet"},
|
||||||
|
},
|
||||||
|
want: config{Quiet: false},
|
||||||
|
},
|
||||||
|
"replace field with valid": {
|
||||||
|
cfg: config{Quiet: true},
|
||||||
|
patch: []interface{}{
|
||||||
|
map[string]interface{}{"op": "replace", "path": "/Quiet", "value": false},
|
||||||
|
},
|
||||||
|
want: config{Quiet: false},
|
||||||
|
},
|
||||||
|
"replace field with invalid": {
|
||||||
|
cfg: config{Quiet: true},
|
||||||
|
patch: []interface{}{
|
||||||
|
map[string]interface{}{"op": "replace", "path": "/Quiet", "value": "teehee"},
|
||||||
|
},
|
||||||
|
want: config{Quiet: true},
|
||||||
|
},
|
||||||
|
"test and noop": {
|
||||||
|
cfg: config{Quiet: true},
|
||||||
|
patch: []interface{}{
|
||||||
|
map[string]interface{}{"op": "test", "path": "/Quiet", "value": false},
|
||||||
|
map[string]interface{}{"op": "replace", "path": "/Quiet", "value": false},
|
||||||
|
},
|
||||||
|
want: config{Quiet: true},
|
||||||
|
},
|
||||||
|
"test and apply": {
|
||||||
|
cfg: config{Quiet: true},
|
||||||
|
patch: []interface{}{
|
||||||
|
map[string]interface{}{"op": "test", "path": "/Quiet", "value": true},
|
||||||
|
map[string]interface{}{"op": "replace", "path": "/Quiet", "value": false},
|
||||||
|
},
|
||||||
|
want: config{Quiet: false},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, d := range cases {
|
||||||
|
c := d
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
c.cfg.lock = &sync.Mutex{}
|
||||||
|
got := c.cfg.WithPatch(c.patch)
|
||||||
|
got.lock = nil
|
||||||
|
c.want.lock = nil
|
||||||
|
if fmt.Sprintf("%+v", got) != fmt.Sprintf("%+v", c.want) {
|
||||||
|
t.Errorf("(%+v).Patch(%+v) want %+v, got %+v", c.cfg, c.patch, c.want, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/device/input/parse/v01/message.go
Normal file
27
src/device/input/parse/v01/message.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package v01
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"mayhem-party/src/device/input/button"
|
||||||
|
)
|
||||||
|
|
||||||
|
type message struct {
|
||||||
|
T int64
|
||||||
|
U string
|
||||||
|
Y string
|
||||||
|
N string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msg message) buttons() []button.Button {
|
||||||
|
buttons := make([]button.Button, len(msg.Y)+len(msg.N))
|
||||||
|
for i := range msg.Y {
|
||||||
|
buttons[i] = button.Button{Char: msg.Y[i], Down: true}
|
||||||
|
}
|
||||||
|
for i := range msg.N {
|
||||||
|
buttons[len(msg.Y)+i] = button.Button{Char: msg.N[i], Down: false}
|
||||||
|
}
|
||||||
|
if FlagDebug {
|
||||||
|
log.Printf("%+v", msg)
|
||||||
|
}
|
||||||
|
return buttons
|
||||||
|
}
|
||||||
359
src/device/input/parse/v01/server.go
Normal file
359
src/device/input/parse/v01/server.go
Normal file
@@ -0,0 +1,359 @@
|
|||||||
|
package v01
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"math/rand"
|
||||||
|
"mayhem-party/src/device/input/wrap"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (v01 *V01) listen() {
|
||||||
|
if v01.cfg.Feedback.Addr == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
v01._listen()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v01 *V01) _listen() {
|
||||||
|
mutex := &sync.Mutex{}
|
||||||
|
s := &http.Server{
|
||||||
|
Addr: v01.cfg.Feedback.Addr,
|
||||||
|
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
start := time.Now()
|
||||||
|
defer func() { log.Printf("%vms | %s %s", time.Since(start).Milliseconds(), r.Method, r.URL) }()
|
||||||
|
v01.cfg.lock.Lock()
|
||||||
|
defer v01.cfg.lock.Unlock()
|
||||||
|
if r.Method == http.MethodGet {
|
||||||
|
mutex.Lock()
|
||||||
|
defer mutex.Unlock()
|
||||||
|
} else {
|
||||||
|
mutex.Lock()
|
||||||
|
defer mutex.Unlock()
|
||||||
|
}
|
||||||
|
v01.ServeHTTP(w, r)
|
||||||
|
v01.stashConfig() // TODO
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
<-v01.ctx.Done()
|
||||||
|
log.Println("closing v01 server")
|
||||||
|
s.Close()
|
||||||
|
}()
|
||||||
|
log.Println("starting v01 server")
|
||||||
|
if err := s.ListenAndServe(); err != nil && v01.ctx.Err() == nil {
|
||||||
|
log.Println("err with v01 server", err)
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v01 *V01) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r = r.WithContext(v01.ctx)
|
||||||
|
v01.serveHTTP(w, r)
|
||||||
|
v01.serveGlobalQueries(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v01 *V01) serveHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch strings.Split(r.URL.Path[1:], "/")[0] {
|
||||||
|
case "":
|
||||||
|
v01.getUserFeedback(w, r)
|
||||||
|
case "broadcast":
|
||||||
|
v01.servePutBroadcast(w, r)
|
||||||
|
case "config":
|
||||||
|
v01.serveConfig(w, r)
|
||||||
|
case "gm":
|
||||||
|
v01.serveGM(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v01 *V01) getUserFeedback(w http.ResponseWriter, r *http.Request) {
|
||||||
|
user := v01.cfg.Users[r.URL.Query().Get("user")]
|
||||||
|
|
||||||
|
msg := user.State.Message
|
||||||
|
if msg == "" {
|
||||||
|
msg = v01.cfg.Broadcast.Message
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.State.GM.Alias != "" {
|
||||||
|
msg = fmt.Sprintf("%s (Your secret word is '%s'. Make **someone else** say it!)", msg, user.State.GM.Alias)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Write([]byte(msg))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v01 *V01) servePutBroadcast(w http.ResponseWriter, r *http.Request) {
|
||||||
|
b, _ := io.ReadAll(r.Body)
|
||||||
|
v01.servePutBroadcastValue(string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v01 *V01) servePutBroadcastValue(v string) {
|
||||||
|
v01.cfg.Broadcast.Message = v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v01 *V01) serveConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method == http.MethodGet {
|
||||||
|
v01.serveGetConfig(w, r)
|
||||||
|
} else {
|
||||||
|
v01.servePatchConfig(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v01 *V01) serveGetConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
|
b, _ := json.Marshal(v01.cfg)
|
||||||
|
w.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v01 *V01) servePatchConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
|
b, _ := io.ReadAll(r.Body)
|
||||||
|
var v []interface{}
|
||||||
|
if err := json.Unmarshal(b, &v); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
v01.cfg = v01.cfg.WithPatch(v)
|
||||||
|
if err := v01.stashConfig(); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v01 *V01) stashConfig() error {
|
||||||
|
if b, err := yaml.Marshal(v01.cfg); err == nil && FlagParseV01Config != "" {
|
||||||
|
if err := os.WriteFile(FlagParseV01Config, b, os.ModePerm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v01 *V01) serveGlobalQueries(r *http.Request) {
|
||||||
|
v01.serveGlobalQuerySay(r)
|
||||||
|
v01.serveGlobalQueryRefresh(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v01 *V01) serveGlobalQuerySay(r *http.Request) {
|
||||||
|
text := r.URL.Query().Get("say")
|
||||||
|
if text == "" {
|
||||||
|
text = r.Header.Get("say")
|
||||||
|
}
|
||||||
|
if text == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go v01.tts(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v01 *V01) serveGlobalQueryRefresh(r *http.Request) {
|
||||||
|
if _, ok := r.URL.Query()["refresh"]; !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case wrap.ChSigUsr1 <- syscall.SIGUSR1:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v01 *V01) serveGM(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/gm/rpc/status":
|
||||||
|
v01.serveGMStatus(w, r)
|
||||||
|
case "/gm/rpc/broadcastSomeoneSaidAlias":
|
||||||
|
v01.serveGMSomeoneSaidAlias(w, r)
|
||||||
|
case "/gm/rpc/fillNonPlayerAliases":
|
||||||
|
v01.serveGMFillNonPlayerAliases(w, r)
|
||||||
|
case "/gm/rpc/vote":
|
||||||
|
v01.serveGMVote(w, r)
|
||||||
|
case "/gm/rpc/elect":
|
||||||
|
v01.serveGMElect(w, r)
|
||||||
|
case "/gm/rpc/shuffle":
|
||||||
|
v01.serveGMShuffle(r)
|
||||||
|
case "/gm/rpc/swap":
|
||||||
|
if errCode, err := v01.serveGMSwap(r.URL.Query().Get("a"), r.URL.Query().Get("b")); err != nil {
|
||||||
|
http.Error(w, err.Error(), errCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v01 *V01) serveGMStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
|
users := map[string]struct {
|
||||||
|
Lag time.Duration `yaml:"lag,omitempty"`
|
||||||
|
Player int `yaml:"player,omitempty"`
|
||||||
|
IdleFor time.Duration `yaml:"idle_for,omitempty"`
|
||||||
|
}{}
|
||||||
|
for k, v := range v01.cfg.Users {
|
||||||
|
v2 := users[k]
|
||||||
|
v2.Lag = time.Duration(v.Meta.LastLag) * time.Millisecond
|
||||||
|
v2.Player = v.State.Player
|
||||||
|
if v.Meta.LastTSMS > 0 {
|
||||||
|
v2.IdleFor = time.Since(time.Unix(0, v.Meta.LastTSMS*int64(time.Millisecond)))
|
||||||
|
}
|
||||||
|
users[k] = v2
|
||||||
|
}
|
||||||
|
yaml.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
|
"Players": len(v01.cfg.Players),
|
||||||
|
"Users": users,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v01 *V01) serveGMSomeoneSaidAlias(w http.ResponseWriter, r *http.Request) {
|
||||||
|
v01.cfg.Quiet = true
|
||||||
|
for k, v := range v01.cfg.Users {
|
||||||
|
v.State.GM.LastAlias = v.State.GM.Alias
|
||||||
|
v.State.GM.Alias = ""
|
||||||
|
v01.cfg.Users[k] = v
|
||||||
|
}
|
||||||
|
v01.servePutBroadcastValue(fmt.Sprintf("<<SOMEONE SAID %q>>", strings.ToUpper(r.URL.Query().Get("message"))))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v01 *V01) serveGMFillNonPlayerAliases(w http.ResponseWriter, r *http.Request) {
|
||||||
|
b, _ := io.ReadAll(r.Body)
|
||||||
|
var pool []string
|
||||||
|
yaml.Unmarshal(b, &pool)
|
||||||
|
n := 0
|
||||||
|
for _, v := range v01.cfg.Users {
|
||||||
|
if v.State.Player == 0 {
|
||||||
|
n += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if n < 1 {
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(pool) < n {
|
||||||
|
http.Error(w, fmt.Sprintf("request body must contain a list of %v options", n), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
a, b := rand.Int()%len(pool), rand.Int()%len(pool)
|
||||||
|
pool[a], pool[b] = pool[b], pool[a]
|
||||||
|
}
|
||||||
|
i := 0
|
||||||
|
for k, v := range v01.cfg.Users {
|
||||||
|
if v.State.Player == 0 {
|
||||||
|
v.State.GM.Alias = pool[i]
|
||||||
|
v01.cfg.Users[k] = v
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v01 *V01) serveGMElect(w http.ResponseWriter, r *http.Request) {
|
||||||
|
alias := r.URL.Query().Get("alias")
|
||||||
|
aliasWinner := ""
|
||||||
|
votes := map[string]int{}
|
||||||
|
for k, v := range v01.cfg.Users {
|
||||||
|
votes[v.State.GM.Vote] = votes[v.State.GM.Vote] + 1
|
||||||
|
if v.State.GM.LastAlias == alias {
|
||||||
|
aliasWinner = k
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if aliasWinner == "" {
|
||||||
|
http.Error(w, "who is "+alias+"?", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
threshold := 0.1 + float64(len(votes))/2.0
|
||||||
|
winner := ""
|
||||||
|
for k, v := range votes {
|
||||||
|
if float64(v) > threshold {
|
||||||
|
winner = k
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if winner == "" {
|
||||||
|
v01.serveGMShuffle(r)
|
||||||
|
} else if _, err := v01.serveGMSwap(winner, aliasWinner); err != nil {
|
||||||
|
v01.serveGMShuffle(r)
|
||||||
|
}
|
||||||
|
for k, v := range v01.cfg.Users {
|
||||||
|
v.State.GM.Vote = ""
|
||||||
|
v01.cfg.Users[k] = v
|
||||||
|
}
|
||||||
|
yaml.NewEncoder(w).Encode(votes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v01 *V01) serveGMVote(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.URL.Query().Get("payload") {
|
||||||
|
case "":
|
||||||
|
counts := map[string]string{}
|
||||||
|
for k, v := range v01.cfg.Users {
|
||||||
|
if v.State.GM.Vote != "" {
|
||||||
|
counts[k] = "voted"
|
||||||
|
} else {
|
||||||
|
counts[k] = "voting"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
yaml.NewEncoder(w).Encode(counts)
|
||||||
|
default:
|
||||||
|
voter := r.URL.Query().Get("user")
|
||||||
|
candidate := r.URL.Query().Get("payload")
|
||||||
|
v, ok := v01.cfg.Users[voter]
|
||||||
|
if _, ok2 := v01.cfg.Users[candidate]; !ok || !ok2 {
|
||||||
|
http.Error(w, "bad voter/candidate", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
v.State.GM.Vote = candidate
|
||||||
|
v01.cfg.Users[voter] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v01 *V01) serveGMShuffle(r *http.Request) {
|
||||||
|
poolSize := len(v01.cfg.Users)
|
||||||
|
if altSize := len(v01.cfg.Players); altSize > poolSize {
|
||||||
|
poolSize = altSize
|
||||||
|
}
|
||||||
|
pool := make([]int, poolSize)
|
||||||
|
if poolSize > 0 {
|
||||||
|
for i := range v01.cfg.Players {
|
||||||
|
pool[i] = i + 1
|
||||||
|
}
|
||||||
|
for i := 0; i < 30; i++ {
|
||||||
|
l := rand.Int() % poolSize
|
||||||
|
r := rand.Int() % poolSize
|
||||||
|
pool[l], pool[r] = pool[r], pool[l]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i := 0
|
||||||
|
msg := []string{}
|
||||||
|
for k, v := range v01.cfg.Users {
|
||||||
|
v.State.Player = pool[i]
|
||||||
|
v01.cfg.Users[k] = v
|
||||||
|
if pool[i] > 0 {
|
||||||
|
msg = append(msg, fmt.Sprintf("%s is now player %v", k, v.State.Player))
|
||||||
|
}
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
v01.servePutBroadcastValue(strings.Join(msg, ", "))
|
||||||
|
v01.cfg.Quiet = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v01 *V01) serveGMSwap(userA, userB string) (int, error) {
|
||||||
|
if userA == userB {
|
||||||
|
return http.StatusConflict, errors.New("/spiderman-pointing")
|
||||||
|
}
|
||||||
|
_, okA := v01.cfg.Users[userA]
|
||||||
|
_, okB := v01.cfg.Users[userB]
|
||||||
|
if !okA || !okB {
|
||||||
|
return http.StatusBadRequest, errors.New("who dat?")
|
||||||
|
}
|
||||||
|
a := v01.cfg.Users[userA]
|
||||||
|
b := v01.cfg.Users[userB]
|
||||||
|
a.State.Player, b.State.Player = b.State.Player, a.State.Player
|
||||||
|
v01.cfg.Users[userA] = a
|
||||||
|
v01.cfg.Users[userB] = b
|
||||||
|
v01.cfg.Quiet = false
|
||||||
|
v01.servePutBroadcastValue(fmt.Sprintf(`%s is now player %v and %s is now player %v`, userA, a.State.Player, userB, b.State.Player))
|
||||||
|
return http.StatusOK, nil
|
||||||
|
}
|
||||||
572
src/device/input/parse/v01/server_test.go
Normal file
572
src/device/input/parse/v01/server_test.go
Normal file
@@ -0,0 +1,572 @@
|
|||||||
|
package v01
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPatchConfig(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
p := path.Join(dir, t.Name()+".yaml")
|
||||||
|
cases := map[string]struct {
|
||||||
|
was config
|
||||||
|
patch string
|
||||||
|
want config
|
||||||
|
}{
|
||||||
|
"replace entire doc": {
|
||||||
|
was: config{
|
||||||
|
Feedback: configFeedback{Addr: "a", TTSURL: "a"},
|
||||||
|
Users: map[string]configUser{"a": configUser{State: configUserState{Player: 1, Message: "a"}}},
|
||||||
|
Players: []configPlayer{configPlayer{Transformation: transformation{"a": "a"}}},
|
||||||
|
Quiet: true,
|
||||||
|
},
|
||||||
|
patch: `[{"op": "replace", "path": "", "value": {
|
||||||
|
"Feedback": {"Addr": "b", "TTSURL": "b"},
|
||||||
|
"Users": {"b": {"State":{"Player": 2, "Message": "b"}}},
|
||||||
|
"Players": [{"Transformation": {"b": "b"}}],
|
||||||
|
"Quiet": false
|
||||||
|
}}]`,
|
||||||
|
want: config{
|
||||||
|
Feedback: configFeedback{Addr: "b", TTSURL: "b"},
|
||||||
|
Users: map[string]configUser{"b": configUser{State: configUserState{Player: 2, Message: "b"}}},
|
||||||
|
Players: []configPlayer{configPlayer{Transformation: transformation{"b": "b"}}},
|
||||||
|
Quiet: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, d := range cases {
|
||||||
|
c := d
|
||||||
|
for _, usesdisk := range []bool{false, true} {
|
||||||
|
t.Run(fmt.Sprintf("%s disk=%v", name, usesdisk), func(t *testing.T) {
|
||||||
|
b, _ := yaml.Marshal(c.was)
|
||||||
|
os.WriteFile(p, b, os.ModePerm)
|
||||||
|
FlagParseV01Config = ""
|
||||||
|
if usesdisk {
|
||||||
|
FlagParseV01Config = p
|
||||||
|
}
|
||||||
|
v01 := &V01{cfg: c.was}
|
||||||
|
v01.cfg.lock = &sync.Mutex{}
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r := httptest.NewRequest(http.MethodPatch, "/config", strings.NewReader(c.patch))
|
||||||
|
v01.servePatchConfig(w, r)
|
||||||
|
if fmt.Sprintf("%+v", c.want) != fmt.Sprintf("%+v", v01.cfg) {
|
||||||
|
t.Errorf("want \n\t%+v, got \n\t%+v", c.want, v01.cfg)
|
||||||
|
}
|
||||||
|
if usesdisk {
|
||||||
|
b, _ := os.ReadFile(p)
|
||||||
|
var got config
|
||||||
|
yaml.Unmarshal(b, &got)
|
||||||
|
if fmt.Sprintf("%+v", c.want) != fmt.Sprintf("%+v", v01.cfg) {
|
||||||
|
t.Errorf("want \n\t%+v, got \n\t%+v", c.want, v01.cfg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServeGM(t *testing.T) {
|
||||||
|
ctx, can := context.WithCancel(context.Background())
|
||||||
|
defer can()
|
||||||
|
|
||||||
|
do := func(v01 *V01, path, body string, method ...string) *httptest.ResponseRecorder {
|
||||||
|
m := http.MethodPost
|
||||||
|
if len(method) > 0 {
|
||||||
|
m = method[0]
|
||||||
|
}
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r := httptest.NewRequest(m, path, strings.NewReader(body))
|
||||||
|
v01.ServeHTTP(w, r)
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("status", func(t *testing.T) {
|
||||||
|
v01 := NewV01(ctx, nil)
|
||||||
|
var result struct {
|
||||||
|
Players int `yaml:"Players"`
|
||||||
|
Users map[string]struct {
|
||||||
|
Player int
|
||||||
|
Lag string
|
||||||
|
IdleFor string `yaml:"idle_for"`
|
||||||
|
} `yaml:"Users"`
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("empty", func(t *testing.T) {
|
||||||
|
resp := do(v01, "/gm/rpc/status", "")
|
||||||
|
if resp.Code != http.StatusOK {
|
||||||
|
t.Error(resp.Code)
|
||||||
|
}
|
||||||
|
t.Log(string(resp.Body.Bytes()))
|
||||||
|
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if result.Players != 0 {
|
||||||
|
t.Error(result.Players)
|
||||||
|
}
|
||||||
|
if len(result.Users) != 0 {
|
||||||
|
t.Error(result.Users)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("full", func(t *testing.T) {
|
||||||
|
v01.cfg.Players = []configPlayer{
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
}
|
||||||
|
v01.cfg.Users = map[string]configUser{
|
||||||
|
"bel": configUser{
|
||||||
|
State: configUserState{Player: 3},
|
||||||
|
Meta: configUserMeta{
|
||||||
|
LastTSMS: time.Now().Add(-1*time.Minute).UnixNano() / int64(time.Millisecond),
|
||||||
|
LastLag: int64(time.Second / time.Millisecond),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"zach": configUser{},
|
||||||
|
"chase": configUser{},
|
||||||
|
"mason": configUser{},
|
||||||
|
"nat": configUser{},
|
||||||
|
"roxy": configUser{},
|
||||||
|
"bill": configUser{},
|
||||||
|
}
|
||||||
|
resp := do(v01, "/gm/rpc/status", "")
|
||||||
|
if resp.Code != http.StatusOK {
|
||||||
|
t.Error(resp.Code)
|
||||||
|
}
|
||||||
|
t.Log(string(resp.Body.Bytes()))
|
||||||
|
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if result.Players != 4 {
|
||||||
|
t.Error(result.Players)
|
||||||
|
}
|
||||||
|
if len(result.Users) != 7 {
|
||||||
|
t.Error(result.Users)
|
||||||
|
}
|
||||||
|
if d, err := time.ParseDuration(result.Users["bel"].Lag); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
} else if d != time.Second {
|
||||||
|
t.Error(d)
|
||||||
|
}
|
||||||
|
if d, err := time.ParseDuration(result.Users["bel"].IdleFor); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
} else if d < time.Minute || d > 2*time.Minute {
|
||||||
|
t.Error(d)
|
||||||
|
}
|
||||||
|
if result.Users["bel"].Player != 3 {
|
||||||
|
t.Error(result.Users["bel"].Player)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("broadcastSomeoneSaidAlias", func(t *testing.T) {
|
||||||
|
v01 := NewV01(ctx, nil)
|
||||||
|
v01.cfg.Quiet = false
|
||||||
|
v01.cfg.Users = map[string]configUser{
|
||||||
|
"bel": configUser{State: configUserState{
|
||||||
|
GM: configUserStateGM{
|
||||||
|
Alias: "driver",
|
||||||
|
},
|
||||||
|
Message: "if someone else says 'driver', then you get to play",
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
v01.cfg.Broadcast.Message = ":)"
|
||||||
|
do(v01, "/gm/rpc/broadcastSomeoneSaidAlias", "")
|
||||||
|
if !v01.cfg.Quiet {
|
||||||
|
t.Error(v01.cfg.Quiet)
|
||||||
|
}
|
||||||
|
if v := v01.cfg.Users["bel"]; v.State.GM.Alias != "" {
|
||||||
|
t.Error(v.State.GM.Alias)
|
||||||
|
} else if v.State.GM.LastAlias != "driver" {
|
||||||
|
t.Error(v.State.GM.LastAlias)
|
||||||
|
}
|
||||||
|
if bc := v01.cfg.Broadcast.Message; bc == ":)" {
|
||||||
|
t.Error(bc)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("fillNonPlayerAliases", func(t *testing.T) {
|
||||||
|
t.Run("empty", func(t *testing.T) {
|
||||||
|
v01 := NewV01(ctx, nil)
|
||||||
|
v01.cfg.Users = nil
|
||||||
|
resp := do(v01, "/gm/rpc/fillNonPlayerAliases", "[qt]")
|
||||||
|
if resp.Code != http.StatusNoContent {
|
||||||
|
t.Error(resp.Code)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("not enough", func(t *testing.T) {
|
||||||
|
v01 := NewV01(ctx, nil)
|
||||||
|
v01.cfg.Users = map[string]configUser{
|
||||||
|
"zach": configUser{State: configUserState{Player: 0}},
|
||||||
|
}
|
||||||
|
resp := do(v01, "/gm/rpc/fillNonPlayerAliases", "[]")
|
||||||
|
if resp.Code != http.StatusBadRequest {
|
||||||
|
t.Error(resp.Code)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("happy", func(t *testing.T) {
|
||||||
|
v01 := NewV01(ctx, nil)
|
||||||
|
v01.cfg.Users = map[string]configUser{
|
||||||
|
"bel": configUser{State: configUserState{Player: 1}},
|
||||||
|
"zach": configUser{State: configUserState{Player: 0}},
|
||||||
|
}
|
||||||
|
do(v01, "/gm/rpc/fillNonPlayerAliases", "[qt]")
|
||||||
|
if v := v01.cfg.Users["bel"]; v.State.GM.Alias != "" {
|
||||||
|
t.Error(v.State.GM.Alias)
|
||||||
|
} else if v.State.Player != 1 {
|
||||||
|
t.Error(v.State.Player)
|
||||||
|
}
|
||||||
|
if v := v01.cfg.Users["zach"]; v.State.GM.Alias != "qt" {
|
||||||
|
t.Error(v.State.GM.Alias)
|
||||||
|
} else if v.State.Player != 0 {
|
||||||
|
t.Error(v.State.Player)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("vote", func(t *testing.T) {
|
||||||
|
type result map[string]string
|
||||||
|
|
||||||
|
t.Run("cast bad vote", func(t *testing.T) {
|
||||||
|
v01 := NewV01(ctx, nil)
|
||||||
|
v01.cfg.Users = map[string]configUser{"bel": {}}
|
||||||
|
resp := do(v01, "/gm/rpc/vote?user=bel&payload=?", "")
|
||||||
|
if resp.Code != http.StatusBadRequest {
|
||||||
|
t.Error(resp)
|
||||||
|
}
|
||||||
|
if v01.cfg.Users["bel"].State.Message != "" {
|
||||||
|
t.Error(v01.cfg.Users["bel"].State.Message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("cast vote", func(t *testing.T) {
|
||||||
|
v01 := NewV01(ctx, nil)
|
||||||
|
v01.cfg.Users = map[string]configUser{"bel": {}, "zach": {}}
|
||||||
|
do(v01, "/gm/rpc/vote?user=bel&payload=zach", "")
|
||||||
|
if v01.cfg.Users["bel"].State.GM.Vote != "zach" {
|
||||||
|
t.Error(v01.cfg.Users["bel"].State.GM.Vote)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("get non vote", func(t *testing.T) {
|
||||||
|
v01 := NewV01(ctx, nil)
|
||||||
|
v01.cfg.Users = map[string]configUser{"bel": {}}
|
||||||
|
resp := do(v01, "/gm/rpc/vote", "", "GET")
|
||||||
|
var result result
|
||||||
|
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if len(result) != 1 {
|
||||||
|
t.Error(result)
|
||||||
|
}
|
||||||
|
if result["bel"] != "voting" {
|
||||||
|
t.Error(result)
|
||||||
|
}
|
||||||
|
t.Logf("%+v", result)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("get mid vote", func(t *testing.T) {
|
||||||
|
v01 := NewV01(ctx, nil)
|
||||||
|
v01.cfg.Users = map[string]configUser{"bel": {State: configUserState{GM: configUserStateGM{Vote: "zach"}, Message: "driver"}}}
|
||||||
|
resp := do(v01, "/gm/rpc/vote", "", "GET")
|
||||||
|
var result result
|
||||||
|
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if len(result) != 1 {
|
||||||
|
t.Error(result)
|
||||||
|
}
|
||||||
|
if result["bel"] != "voted" {
|
||||||
|
t.Error(result)
|
||||||
|
}
|
||||||
|
t.Logf("%+v", result)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("get empty", func(t *testing.T) {
|
||||||
|
v01 := NewV01(ctx, nil)
|
||||||
|
v01.cfg.Users = nil
|
||||||
|
resp := do(v01, "/gm/rpc/vote", "", "GET")
|
||||||
|
var result result
|
||||||
|
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if len(result) != 0 {
|
||||||
|
t.Error(result)
|
||||||
|
}
|
||||||
|
t.Logf("%+v", result)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("elect", func(t *testing.T) {
|
||||||
|
type result map[string]int
|
||||||
|
|
||||||
|
t.Run("happy", func(t *testing.T) {
|
||||||
|
v01 := NewV01(ctx, nil)
|
||||||
|
v01.cfg.Users = map[string]configUser{
|
||||||
|
"bel": configUser{State: configUserState{GM: configUserStateGM{Vote: "zach", LastAlias: "driver"}, Player: 1}},
|
||||||
|
"zach": configUser{State: configUserState{GM: configUserStateGM{Vote: "bel", LastAlias: "pizza"}}},
|
||||||
|
"bill": configUser{State: configUserState{GM: configUserStateGM{Vote: "bel"}, Player: 2}},
|
||||||
|
}
|
||||||
|
resp := do(v01, "/gm/rpc/elect?alias=pizza", "")
|
||||||
|
var result result
|
||||||
|
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
|
||||||
|
t.Errorf("%s => %v", resp.Body.Bytes(), err)
|
||||||
|
}
|
||||||
|
if len(result) != 2 {
|
||||||
|
t.Error(result)
|
||||||
|
} else if result["bel"] != 2 {
|
||||||
|
t.Error(result)
|
||||||
|
} else if result["zach"] != 1 {
|
||||||
|
t.Error(result)
|
||||||
|
}
|
||||||
|
if v01.cfg.Users["bel"].State.Player != 0 {
|
||||||
|
t.Error(v01.cfg.Users["bel"].State.Player)
|
||||||
|
} else if v01.cfg.Users["zach"].State.Player != 1 {
|
||||||
|
t.Error(v01.cfg.Users["zach"].State.Player)
|
||||||
|
}
|
||||||
|
if v01.cfg.Broadcast.Message != `bel is now player 0 and zach is now player 1` {
|
||||||
|
t.Error(v01.cfg.Broadcast)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("self", func(t *testing.T) {
|
||||||
|
v01 := NewV01(ctx, nil)
|
||||||
|
v01.cfg.Players = []configPlayer{{}}
|
||||||
|
v01.cfg.Users = map[string]configUser{
|
||||||
|
"bel": configUser{State: configUserState{GM: configUserStateGM{Vote: "zach", LastAlias: "driver"}, Player: 1}},
|
||||||
|
"zach": configUser{State: configUserState{GM: configUserStateGM{Vote: "bel"}}},
|
||||||
|
"bill": configUser{State: configUserState{GM: configUserStateGM{Vote: "bel"}}},
|
||||||
|
}
|
||||||
|
resp := do(v01, "/gm/rpc/elect?alias=driver", "")
|
||||||
|
var result result
|
||||||
|
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if len(result) != 2 {
|
||||||
|
t.Error(result)
|
||||||
|
} else if result["bel"] != 2 {
|
||||||
|
t.Error(result)
|
||||||
|
} else if result["zach"] != 1 {
|
||||||
|
t.Error(result)
|
||||||
|
}
|
||||||
|
if !strings.HasSuffix(v01.cfg.Broadcast.Message, `is now player 1`) || strings.Contains(v01.cfg.Broadcast.Message, ",") {
|
||||||
|
t.Error(v01.cfg.Broadcast.Message)
|
||||||
|
}
|
||||||
|
assignments := map[int]int{}
|
||||||
|
for _, v := range v01.cfg.Users {
|
||||||
|
assignments[v.State.Player] = assignments[v.State.Player] + 1
|
||||||
|
}
|
||||||
|
if len(assignments) != 2 {
|
||||||
|
t.Error(assignments)
|
||||||
|
} else if assignments[0] != 2 {
|
||||||
|
t.Error(assignments[0])
|
||||||
|
} else if assignments[1] != 1 {
|
||||||
|
t.Error(assignments[1])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("tie", func(t *testing.T) {
|
||||||
|
v01 := NewV01(ctx, nil)
|
||||||
|
v01.cfg.Players = []configPlayer{{}}
|
||||||
|
v01.cfg.Users = map[string]configUser{
|
||||||
|
"bel": configUser{State: configUserState{GM: configUserStateGM{Vote: "zach", LastAlias: "driver"}, Player: 1}},
|
||||||
|
"zach": configUser{State: configUserState{GM: configUserStateGM{Vote: "bel", LastAlias: "pizza"}}},
|
||||||
|
}
|
||||||
|
resp := do(v01, "/gm/rpc/elect?alias=pizza", "")
|
||||||
|
var result result
|
||||||
|
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if len(result) != 2 {
|
||||||
|
t.Error(result)
|
||||||
|
} else if result["bel"] != 1 {
|
||||||
|
t.Error(result)
|
||||||
|
} else if result["zach"] != 1 {
|
||||||
|
t.Error(result)
|
||||||
|
}
|
||||||
|
if bc := v01.cfg.Broadcast.Message; !strings.HasSuffix(bc, `is now player 1`) || strings.Contains(bc, ",") {
|
||||||
|
t.Error(bc)
|
||||||
|
}
|
||||||
|
assignments := map[int]int{}
|
||||||
|
for _, v := range v01.cfg.Users {
|
||||||
|
assignments[v.State.Player] = assignments[v.State.Player] + 1
|
||||||
|
}
|
||||||
|
if len(assignments) != 2 {
|
||||||
|
t.Error(assignments)
|
||||||
|
} else if assignments[0] != 1 {
|
||||||
|
t.Error(assignments[0])
|
||||||
|
} else if assignments[1] != 1 {
|
||||||
|
t.Error(assignments[1])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("shuffle", func(t *testing.T) {
|
||||||
|
t.Run("many 2u 2p", func(t *testing.T) {
|
||||||
|
v01 := NewV01(ctx, nil)
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
v01.cfg.Quiet = true
|
||||||
|
v01.cfg.Users = map[string]configUser{
|
||||||
|
"bel": configUser{State: configUserState{Player: 1}},
|
||||||
|
"zach": configUser{State: configUserState{Player: 2}},
|
||||||
|
}
|
||||||
|
v01.cfg.Players = []configPlayer{{}, {}}
|
||||||
|
do(v01, "/gm/rpc/shuffle", "")
|
||||||
|
if v01.cfg.Quiet {
|
||||||
|
t.Error(v01.cfg.Quiet)
|
||||||
|
}
|
||||||
|
if len(v01.cfg.Users) != 2 {
|
||||||
|
t.Error(v01.cfg.Users)
|
||||||
|
} else if len(v01.cfg.Players) != 2 {
|
||||||
|
t.Error(v01.cfg.Users)
|
||||||
|
} else if bp := v01.cfg.Users["bel"].State.Player; bp != 1 && bp != 2 {
|
||||||
|
t.Error(bp)
|
||||||
|
} else if zp := v01.cfg.Users["zach"].State.Player; zp != 1 && zp != 2 {
|
||||||
|
t.Error(zp)
|
||||||
|
} else if bp == zp {
|
||||||
|
t.Error(bp, zp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
cases := map[string]struct {
|
||||||
|
users int
|
||||||
|
usersAssigned int
|
||||||
|
players int
|
||||||
|
}{
|
||||||
|
"empty": {},
|
||||||
|
"just users": {users: 2},
|
||||||
|
"just players": {players: 2},
|
||||||
|
"2 unassigned users and 2 players": {users: 2, players: 2},
|
||||||
|
"2 users and 2 players": {users: 2, usersAssigned: 2, players: 2},
|
||||||
|
"1 users and 2 players": {users: 1, usersAssigned: 1, players: 2},
|
||||||
|
"1 unassigned users and 2 players": {users: 1, players: 2},
|
||||||
|
"4 players for 7 users 0 assigned": {users: 7, players: 4},
|
||||||
|
"4 players for 7 users 4 assigned": {users: 7, players: 4, usersAssigned: 4},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, d := range cases {
|
||||||
|
c := d
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
v01 := NewV01(ctx, nil)
|
||||||
|
v01.cfg.Quiet = true
|
||||||
|
v01.cfg.Users = map[string]configUser{}
|
||||||
|
for i := 0; i < c.users; i++ {
|
||||||
|
v01.cfg.Users[strconv.Itoa(i)] = configUser{}
|
||||||
|
if i < c.usersAssigned {
|
||||||
|
v01.cfg.Users[strconv.Itoa(i)] = configUser{State: configUserState{Player: i}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v01.cfg.Players = make([]configPlayer, c.players)
|
||||||
|
|
||||||
|
do(v01, "/gm/rpc/shuffle", "")
|
||||||
|
if v01.cfg.Quiet {
|
||||||
|
t.Error(v01.cfg.Quiet)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(v01.cfg.Users) != c.users {
|
||||||
|
t.Error(v01.cfg.Users)
|
||||||
|
} else if len(v01.cfg.Players) != c.players {
|
||||||
|
t.Error(v01.cfg.Users)
|
||||||
|
}
|
||||||
|
for i := 0; i < c.users; i++ {
|
||||||
|
if _, ok := v01.cfg.Users[strconv.Itoa(i)]; !ok {
|
||||||
|
t.Error(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assignments := map[int]int{}
|
||||||
|
for _, v := range v01.cfg.Users {
|
||||||
|
if v.State.Player > 0 {
|
||||||
|
assignments[v.State.Player] = assignments[v.State.Player] + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lesser := c.users
|
||||||
|
if c.players < lesser {
|
||||||
|
lesser = c.players
|
||||||
|
}
|
||||||
|
if len(assignments) != lesser {
|
||||||
|
t.Error(assignments)
|
||||||
|
}
|
||||||
|
for _, v := range assignments {
|
||||||
|
if v != 1 {
|
||||||
|
t.Error(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("swap", func(t *testing.T) {
|
||||||
|
t.Run("self", func(t *testing.T) {
|
||||||
|
v01 := NewV01(ctx, nil)
|
||||||
|
v01.cfg.Quiet = true
|
||||||
|
v01.cfg.Users = map[string]configUser{
|
||||||
|
"bel": configUser{State: configUserState{Player: 1}},
|
||||||
|
}
|
||||||
|
resp := do(v01, "/gm/rpc/swap?a=bel&b=bel", "")
|
||||||
|
if resp.Code != http.StatusConflict {
|
||||||
|
t.Error(resp.Code)
|
||||||
|
}
|
||||||
|
if !v01.cfg.Quiet {
|
||||||
|
t.Error(v01.cfg.Quiet)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("who", func(t *testing.T) {
|
||||||
|
v01 := NewV01(ctx, nil)
|
||||||
|
v01.cfg.Quiet = true
|
||||||
|
resp := do(v01, "/gm/rpc/swap?a=bel", "")
|
||||||
|
if resp.Code != http.StatusBadRequest {
|
||||||
|
t.Error(resp.Code)
|
||||||
|
}
|
||||||
|
if !v01.cfg.Quiet {
|
||||||
|
t.Error(v01.cfg.Quiet)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("happy", func(t *testing.T) {
|
||||||
|
v01 := NewV01(ctx, nil)
|
||||||
|
v01.cfg.Quiet = true
|
||||||
|
v01.cfg.Users = map[string]configUser{
|
||||||
|
"bel": configUser{State: configUserState{Player: 1}},
|
||||||
|
"zach": configUser{State: configUserState{Player: 2}},
|
||||||
|
}
|
||||||
|
resp := do(v01, "/gm/rpc/swap?a=bel&b=zach", "")
|
||||||
|
if resp.Code != http.StatusOK {
|
||||||
|
t.Error(resp.Code)
|
||||||
|
}
|
||||||
|
if v01.cfg.Quiet {
|
||||||
|
t.Error(v01.cfg.Quiet)
|
||||||
|
}
|
||||||
|
if v01.cfg.Users["bel"].State.Player != 2 {
|
||||||
|
t.Error(v01.cfg.Users["bel"])
|
||||||
|
} else if v01.cfg.Users["zach"].State.Player != 1 {
|
||||||
|
t.Error(v01.cfg.Users["zach"])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("404", func(t *testing.T) {
|
||||||
|
v01 := NewV01(ctx, nil)
|
||||||
|
resp := do(v01, "/gm/teehee", "")
|
||||||
|
if resp.Code != http.StatusNotFound {
|
||||||
|
t.Error(resp.Code)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
27
src/device/input/parse/v01/testdata/v01.yaml
vendored
Normal file
27
src/device/input/parse/v01/testdata/v01.yaml
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
feedback:
|
||||||
|
addr: :17071
|
||||||
|
ttsurl: http://localhost:15002
|
||||||
|
broadcast:
|
||||||
|
message: hi
|
||||||
|
users:
|
||||||
|
bel:
|
||||||
|
state:
|
||||||
|
player: 0
|
||||||
|
message: "hi"
|
||||||
|
alias: driver
|
||||||
|
meta:
|
||||||
|
tsms: 1
|
||||||
|
lastlag: 2
|
||||||
|
players:
|
||||||
|
- buttons:
|
||||||
|
up: "w"
|
||||||
|
down: "s"
|
||||||
|
left: "a"
|
||||||
|
right: "d"
|
||||||
|
l: "q"
|
||||||
|
r: "e"
|
||||||
|
a: "1"
|
||||||
|
b: "2"
|
||||||
|
x: "3"
|
||||||
|
y: "4"
|
||||||
|
quiet: false
|
||||||
14
src/device/input/parse/v01/transform.go
Normal file
14
src/device/input/parse/v01/transform.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package v01
|
||||||
|
|
||||||
|
type (
|
||||||
|
transformation map[string]string
|
||||||
|
)
|
||||||
|
|
||||||
|
func (t transformation) pipe(s string) string {
|
||||||
|
for i := range s {
|
||||||
|
if v := t[s[i:i+1]]; v != "" {
|
||||||
|
s = s[:i] + v[:1] + s[i+1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
76
src/device/input/parse/v01/tts.go
Normal file
76
src/device/input/parse/v01/tts.go
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
package v01
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/faiface/beep"
|
||||||
|
"github.com/faiface/beep/effects"
|
||||||
|
"github.com/faiface/beep/speaker"
|
||||||
|
"github.com/faiface/beep/wav"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ttsLock = &sync.RWMutex{}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (v01 *V01) tts(text string) {
|
||||||
|
if err := v01._tts(text); err != nil {
|
||||||
|
log.Printf("failed to tts: %s: %v", text, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v01 *V01) _tts(text string) error {
|
||||||
|
if v01.cfg.Feedback.TTSURL == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
url, err := url.Parse(v01.cfg.Feedback.TTSURL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(url.Path) < 2 {
|
||||||
|
url.Path = "/api/tts"
|
||||||
|
}
|
||||||
|
q := url.Query()
|
||||||
|
if q.Get("voice") == "" {
|
||||||
|
q.Set("voice", "en-us/glados-glow_tts")
|
||||||
|
}
|
||||||
|
if q.Get("lengthScale") == "" {
|
||||||
|
q.Set("lengthScale", "1")
|
||||||
|
}
|
||||||
|
q.Set("text", text)
|
||||||
|
url.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
resp, err := http.Get(url.String())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
b, _ := io.ReadAll(resp.Body)
|
||||||
|
if resp.StatusCode != http.StatusOK || resp.Header.Get("Content-Type") != "audio/wav" {
|
||||||
|
return fmt.Errorf("failed to call ttsurl: (%d) %s", resp.StatusCode, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
decoder, format, err := wav.Decode(bytes.NewReader(b))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ttsLock.Lock()
|
||||||
|
defer ttsLock.Unlock()
|
||||||
|
speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/30))
|
||||||
|
speaker.Play(&effects.Volume{Streamer: beep.ResampleRatio(4, 1, &beep.Ctrl{Streamer: beep.Loop(1, decoder)})})
|
||||||
|
duration := time.Duration(decoder.Len()) * format.SampleRate.D(1)
|
||||||
|
select {
|
||||||
|
case <-v01.ctx.Done():
|
||||||
|
case <-time.After(duration):
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
125
src/device/input/parse/v01/v01.go
Normal file
125
src/device/input/parse/v01/v01.go
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
package v01
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"mayhem-party/src/device/input/button"
|
||||||
|
"mayhem-party/src/device/input/raw"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
FlagDebug = os.Getenv("DEBUG") == "true"
|
||||||
|
FlagParseV01Config = os.Getenv("V01_CONFIG")
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
V01 struct {
|
||||||
|
ctx context.Context
|
||||||
|
can context.CancelFunc
|
||||||
|
src raw.Raw
|
||||||
|
cfg config
|
||||||
|
telemetryc chan message
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewV01(ctx context.Context, src raw.Raw) *V01 {
|
||||||
|
var cfg config
|
||||||
|
cfg.lock = &sync.Mutex{}
|
||||||
|
b, _ := ioutil.ReadFile(FlagParseV01Config)
|
||||||
|
yaml.Unmarshal(b, &cfg)
|
||||||
|
ctx, can := context.WithCancel(ctx)
|
||||||
|
result := &V01{
|
||||||
|
ctx: ctx,
|
||||||
|
can: can,
|
||||||
|
src: src,
|
||||||
|
cfg: cfg,
|
||||||
|
telemetryc: make(chan message),
|
||||||
|
}
|
||||||
|
go result.listen()
|
||||||
|
go result.dotelemetry()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v01 *V01) CloseWrap() raw.Raw {
|
||||||
|
v01.can()
|
||||||
|
return v01.src
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v01 *V01) Close() {
|
||||||
|
v01.can()
|
||||||
|
v01.src.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v01 *V01) Read() []button.Button {
|
||||||
|
line := v01.src.Read()
|
||||||
|
var msg message
|
||||||
|
if err := json.Unmarshal(line, &msg); err != nil {
|
||||||
|
log.Printf("%v: %s", err, line)
|
||||||
|
}
|
||||||
|
v01.telemetry(msg)
|
||||||
|
|
||||||
|
buttons := v01.transform(msg).buttons()
|
||||||
|
if v01.cfg.Quiet {
|
||||||
|
for i := range buttons {
|
||||||
|
buttons[i].Down = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buttons
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v01 *V01) dotelemetry() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-v01.ctx.Done():
|
||||||
|
return
|
||||||
|
case msg := <-v01.telemetryc:
|
||||||
|
v01._telemetry(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v01 *V01) telemetry(msg message) {
|
||||||
|
select {
|
||||||
|
case v01.telemetryc <- msg:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v01 *V01) _telemetry(msg message) {
|
||||||
|
// TODO oof
|
||||||
|
v01.cfg.lock.Lock()
|
||||||
|
defer v01.cfg.lock.Unlock()
|
||||||
|
if v01.cfg.Users == nil {
|
||||||
|
v01.cfg.Users = map[string]configUser{}
|
||||||
|
}
|
||||||
|
u := v01.cfg.Users[msg.U]
|
||||||
|
u.Meta.LastLag = time.Now().UnixNano()/int64(time.Millisecond) - msg.T
|
||||||
|
u.Meta.LastTSMS = msg.T
|
||||||
|
if FlagDebug {
|
||||||
|
log.Printf("%s|%dms", msg.U, u.Meta.LastLag)
|
||||||
|
}
|
||||||
|
v01.cfg.Users[msg.U] = u
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v01 *V01) transform(msg message) message {
|
||||||
|
if len(v01.cfg.Players) == 0 {
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
user := v01.cfg.Users[msg.U]
|
||||||
|
if user.State.Player < 1 {
|
||||||
|
msg.Y = ""
|
||||||
|
msg.N = ""
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
player := v01.cfg.Players[user.State.Player-1]
|
||||||
|
msg.Y = player.Transformation.pipe(msg.Y)
|
||||||
|
msg.N = player.Transformation.pipe(msg.N)
|
||||||
|
return msg
|
||||||
|
}
|
||||||
192
src/device/input/parse/v01/v01_exported_test.go
Normal file
192
src/device/input/parse/v01/v01_exported_test.go
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
package v01_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"mayhem-party/src/device/input/button"
|
||||||
|
v01 "mayhem-party/src/device/input/parse/v01"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestV01(t *testing.T) {
|
||||||
|
src := constSrc(fmt.Sprintf(`{"T":%v,"U":"bel","Y":"abc","N":"cde"}`, time.Now().UnixNano()/int64(time.Millisecond)-50))
|
||||||
|
t.Logf("(%v) %s", len(src), src.Read())
|
||||||
|
v01 := v01.NewV01(context.Background(), src)
|
||||||
|
defer v01.Close()
|
||||||
|
got := v01.Read()
|
||||||
|
want := []button.Button{
|
||||||
|
{Down: true, Char: 'a'},
|
||||||
|
{Down: true, Char: 'b'},
|
||||||
|
{Down: true, Char: 'c'},
|
||||||
|
{Down: false, Char: 'c'},
|
||||||
|
{Down: false, Char: 'd'},
|
||||||
|
{Down: false, Char: 'e'},
|
||||||
|
}
|
||||||
|
if len(got) != len(want) {
|
||||||
|
t.Fatal(len(want), len(got))
|
||||||
|
}
|
||||||
|
for i := range got {
|
||||||
|
if got[i] != want[i] {
|
||||||
|
t.Errorf("[%d] want %+v got %+v", i, want[i], got[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestV01WithCfg(t *testing.T) {
|
||||||
|
d := t.TempDir()
|
||||||
|
p := path.Join(d, "cfg.yaml")
|
||||||
|
os.WriteFile(p, []byte(`
|
||||||
|
users:
|
||||||
|
bel:
|
||||||
|
state:
|
||||||
|
player: 2
|
||||||
|
players:
|
||||||
|
- transformation:
|
||||||
|
w: t
|
||||||
|
- transformation:
|
||||||
|
w: i
|
||||||
|
`), os.ModePerm)
|
||||||
|
v01.FlagParseV01Config = p
|
||||||
|
|
||||||
|
t.Run("unknown user ignored", func(t *testing.T) {
|
||||||
|
v01 := v01.NewV01(context.Background(), constSrc(`{"U":"qt","Y":"w"}`))
|
||||||
|
defer v01.Close()
|
||||||
|
got := v01.Read()
|
||||||
|
if len(got) != 0 {
|
||||||
|
t.Error(got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("player2", func(t *testing.T) {
|
||||||
|
v01 := v01.NewV01(context.Background(), constSrc(`{"U":"bel","Y":"w","N":"w"}`))
|
||||||
|
defer v01.Close()
|
||||||
|
got := v01.Read()
|
||||||
|
if len(got) != 2 {
|
||||||
|
t.Error(got)
|
||||||
|
}
|
||||||
|
if got[0] != (button.Button{Char: 'i', Down: true}) {
|
||||||
|
t.Error(got[0])
|
||||||
|
}
|
||||||
|
if got[1] != (button.Button{Char: 'i', Down: false}) {
|
||||||
|
t.Error(got[1])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestV01Feedback(t *testing.T) {
|
||||||
|
d := t.TempDir()
|
||||||
|
p := path.Join(d, "cfg.yaml")
|
||||||
|
os.WriteFile(p, []byte(`
|
||||||
|
feedback:
|
||||||
|
addr: :27071
|
||||||
|
ttsurl: http://localhost:15002
|
||||||
|
users:
|
||||||
|
bel:
|
||||||
|
state:
|
||||||
|
player: 2
|
||||||
|
message: to bel
|
||||||
|
broadcast:
|
||||||
|
message: to everyone
|
||||||
|
players:
|
||||||
|
- transformation:
|
||||||
|
w: t
|
||||||
|
- transformation:
|
||||||
|
w: i
|
||||||
|
`), os.ModePerm)
|
||||||
|
v01.FlagParseV01Config = p
|
||||||
|
ctx, can := context.WithCancel(context.Background())
|
||||||
|
defer can()
|
||||||
|
|
||||||
|
v01 := v01.NewV01(ctx, constSrc(`{"U":"qt","Y":"w"}`))
|
||||||
|
defer v01.Close()
|
||||||
|
|
||||||
|
for {
|
||||||
|
time.Sleep(time.Millisecond * 100)
|
||||||
|
resp, err := http.Get("http://localhost:27071?user=bel")
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("specific user", func(t *testing.T) {
|
||||||
|
resp, err := http.Get("http://localhost:27071?user=bel")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
b, _ := io.ReadAll(resp.Body)
|
||||||
|
if string(b) != "to bel" {
|
||||||
|
t.Error(b)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("broadcast", func(t *testing.T) {
|
||||||
|
resp, err := http.Get("http://localhost:27071")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
b, _ := io.ReadAll(resp.Body)
|
||||||
|
if string(b) != "to everyone" {
|
||||||
|
t.Error(b)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("change broadcast", func(t *testing.T) {
|
||||||
|
want := `my new broadcast`
|
||||||
|
r, _ := http.NewRequest(http.MethodPut, "http://localhost:27071/broadcast", strings.NewReader(want))
|
||||||
|
resp, err := http.DefaultClient.Do(r)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
|
||||||
|
resp, err = http.Get("http://localhost:27071")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
b, _ := io.ReadAll(resp.Body)
|
||||||
|
if string(b) != want {
|
||||||
|
t.Error(string(b))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("tts", func(t *testing.T) {
|
||||||
|
if os.Getenv("INTEGRATION_TTS") != "true" {
|
||||||
|
t.Skip("$INTEGRATION_TTS is not true")
|
||||||
|
}
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
resp, err := http.Get("http://localhost:27071/?say=hello%20world")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
}
|
||||||
|
time.Sleep(time.Millisecond * 2500)
|
||||||
|
r, _ := http.NewRequest(http.MethodGet, "http://localhost:27071", nil)
|
||||||
|
r.Header.Set("say", "No, HTTP does not define any limit. However most web servers do limit size of headers they accept. For example in Apache default limit is 8KB, in IIS it's 16K. Server will return 413 Entity Too Large error if headers size exceeds that limit.")
|
||||||
|
resp, err := http.DefaultClient.Do(r)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
time.Sleep(time.Millisecond * 8500)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type constSrc string
|
||||||
|
|
||||||
|
func (c constSrc) Close() {}
|
||||||
|
|
||||||
|
func (c constSrc) Read() []byte {
|
||||||
|
return []byte(c)
|
||||||
|
}
|
||||||
92
src/device/input/parse/v01/v01_test.go
Normal file
92
src/device/input/parse/v01/v01_test.go
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
package v01
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestV01TransformationPipe(t *testing.T) {
|
||||||
|
cases := map[string]struct {
|
||||||
|
input string
|
||||||
|
xform map[string]string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
"empty input": {
|
||||||
|
xform: map[string]string{"a": "bc"},
|
||||||
|
},
|
||||||
|
"empty xform": {
|
||||||
|
input: "aa",
|
||||||
|
want: "aa",
|
||||||
|
},
|
||||||
|
"all": {
|
||||||
|
input: "aa",
|
||||||
|
xform: map[string]string{"a": "cc"},
|
||||||
|
want: "cc",
|
||||||
|
},
|
||||||
|
"last": {
|
||||||
|
input: "ba",
|
||||||
|
xform: map[string]string{"a": "cc"},
|
||||||
|
want: "bc",
|
||||||
|
},
|
||||||
|
"first": {
|
||||||
|
input: "ab",
|
||||||
|
xform: map[string]string{"a": "cc"},
|
||||||
|
want: "cb",
|
||||||
|
},
|
||||||
|
"noop": {
|
||||||
|
input: "bb",
|
||||||
|
xform: map[string]string{"a": "bc"},
|
||||||
|
want: "bb",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, d := range cases {
|
||||||
|
c := d
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
got := transformation(c.xform).pipe(c.input)
|
||||||
|
if got != c.want {
|
||||||
|
t.Errorf("%+v(%s) want %s got %s", c.xform, c.input, c.want, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestV01Quiet(t *testing.T) {
|
||||||
|
ctx, can := context.WithCancel(context.Background())
|
||||||
|
defer can()
|
||||||
|
v01 := NewV01(ctx, constSrc(`{"Y":"a", "N":"b"}`))
|
||||||
|
|
||||||
|
v01.cfg.Quiet = false
|
||||||
|
if got := v01.Read(); len(got) != 2 {
|
||||||
|
t.Error(len(got))
|
||||||
|
} else if got[0].Char != 'a' {
|
||||||
|
t.Error(got[0].Char)
|
||||||
|
} else if got[0].Down != true {
|
||||||
|
t.Error(got[0].Down)
|
||||||
|
} else if got[1].Char != 'b' {
|
||||||
|
t.Error(got[1].Char)
|
||||||
|
} else if got[1].Down != false {
|
||||||
|
t.Error(got[1].Down)
|
||||||
|
}
|
||||||
|
|
||||||
|
v01.cfg.Quiet = true
|
||||||
|
if got := v01.Read(); len(got) != 2 {
|
||||||
|
t.Error(len(got))
|
||||||
|
} else if got[0].Char != 'a' {
|
||||||
|
t.Error(got[0].Char)
|
||||||
|
} else if got[0].Down != false {
|
||||||
|
t.Error(got[0].Down)
|
||||||
|
} else if got[1].Char != 'b' {
|
||||||
|
t.Error(got[1].Char)
|
||||||
|
} else if got[1].Down != false {
|
||||||
|
t.Error(got[1].Down)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type constSrc string
|
||||||
|
|
||||||
|
func (c constSrc) Close() {}
|
||||||
|
|
||||||
|
func (c constSrc) Read() []byte {
|
||||||
|
return []byte(c)
|
||||||
|
}
|
||||||
@@ -50,7 +50,7 @@ func (kb Keyboard) Read() []byte {
|
|||||||
if err != nil && err != io.EOF {
|
if err != nil && err != io.EOF {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
if os.Getenv("DEBUG") == "true" {
|
if FlagDebug {
|
||||||
log.Printf("raw.Keyboard.Read() %s", b[:n])
|
log.Printf("raw.Keyboard.Read() %s", b[:n])
|
||||||
}
|
}
|
||||||
return b[:n]
|
return b[:n]
|
||||||
|
|||||||
@@ -6,21 +6,27 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
FlagRawKeyboard = os.Getenv("RAW_KEYBOARD") == "true"
|
||||||
|
FlagRawUDP = os.Getenv("RAW_UDP")
|
||||||
|
FlagRawRandomWeightFile = os.Getenv("RAW_RANDOM_WEIGHT_FILE")
|
||||||
|
)
|
||||||
|
|
||||||
type Raw interface {
|
type Raw interface {
|
||||||
Read() []byte
|
Read() []byte
|
||||||
Close()
|
Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(ctx context.Context) Raw {
|
func New(ctx context.Context) Raw {
|
||||||
if os.Getenv("RAW_KEYBOARD") == "true" {
|
if FlagRawKeyboard {
|
||||||
return NewKeyboard()
|
return NewKeyboard()
|
||||||
}
|
}
|
||||||
if port, _ := strconv.Atoi(os.Getenv("RAW_UDP")); port != 0 {
|
if port, _ := strconv.Atoi(FlagRawUDP); port != 0 {
|
||||||
return NewUDP(ctx, port)
|
return NewUDP(ctx, port)
|
||||||
}
|
}
|
||||||
generator := randomCharFromRange('a', 'g')
|
generator := randomCharFromRange('a', 'g')
|
||||||
if p, ok := os.LookupEnv("RAW_RANDOM_WEIGHT_FILE"); ok && len(p) > 0 {
|
if FlagRawRandomWeightFile != "" {
|
||||||
generator = randomCharFromWeightFile(p)
|
generator = randomCharFromWeightFile(FlagRawRandomWeightFile)
|
||||||
}
|
}
|
||||||
return NewRandom(generator)
|
return NewRandom(generator)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
FlagDebug = os.Getenv("DEBUG") == "true"
|
||||||
|
)
|
||||||
|
|
||||||
type UDP struct {
|
type UDP struct {
|
||||||
conn net.PacketConn
|
conn net.PacketConn
|
||||||
c chan []byte
|
c chan []byte
|
||||||
@@ -29,14 +33,13 @@ func NewUDP(ctx context.Context, port int) UDP {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (udp UDP) listen() {
|
func (udp UDP) listen() {
|
||||||
debugging := os.Getenv("DEBUG") == "true"
|
|
||||||
for udp.ctx.Err() == nil {
|
for udp.ctx.Err() == nil {
|
||||||
buff := make([]byte, 256)
|
buff := make([]byte, 256)
|
||||||
n, _, err := udp.conn.ReadFrom(buff)
|
n, _, err := udp.conn.ReadFrom(buff)
|
||||||
if err != nil && udp.ctx.Err() == nil {
|
if err != nil && udp.ctx.Err() == nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
if debugging {
|
if FlagDebug {
|
||||||
log.Printf("raw.UDP.Read() => %s", buff[:n])
|
log.Printf("raw.UDP.Read() => %s", buff[:n])
|
||||||
}
|
}
|
||||||
select {
|
select {
|
||||||
|
|||||||
@@ -3,11 +3,16 @@ package wrap
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"mayhem-party/src/device/input/button"
|
"mayhem-party/src/device/input/button"
|
||||||
|
"mayhem-party/src/device/input/raw"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
FlagBufferedStickyDuration = os.Getenv("WRAP_BUFFERED_STICKY_DURATION")
|
||||||
|
)
|
||||||
|
|
||||||
type Buffered struct {
|
type Buffered struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
can context.CancelFunc
|
can context.CancelFunc
|
||||||
@@ -21,7 +26,7 @@ type Buffered struct {
|
|||||||
func NewBuffered(ctx context.Context, input Wrap) *Buffered {
|
func NewBuffered(ctx context.Context, input Wrap) *Buffered {
|
||||||
ctx, can := context.WithCancel(ctx)
|
ctx, can := context.WithCancel(ctx)
|
||||||
expirationInterval := time.Millisecond * 125
|
expirationInterval := time.Millisecond * 125
|
||||||
if d, err := time.ParseDuration(os.Getenv("WRAP_BUFFERED_STICKY_DURATION")); err == nil {
|
if d, err := time.ParseDuration(FlagBufferedStickyDuration); err == nil {
|
||||||
expirationInterval = d
|
expirationInterval = d
|
||||||
}
|
}
|
||||||
result := &Buffered{
|
result := &Buffered{
|
||||||
@@ -57,6 +62,11 @@ func (b *Buffered) listen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *Buffered) CloseWrap() raw.Raw {
|
||||||
|
b.can()
|
||||||
|
return b.input.CloseWrap()
|
||||||
|
}
|
||||||
|
|
||||||
func (b *Buffered) Close() {
|
func (b *Buffered) Close() {
|
||||||
b.input.Close()
|
b.input.Close()
|
||||||
b.can()
|
b.can()
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
package wrap
|
|
||||||
|
|
||||||
import (
|
|
||||||
"mayhem-party/src/device/input/button"
|
|
||||||
"mayhem-party/src/device/input/raw"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Protocol struct {
|
|
||||||
src raw.Raw
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewProtocol(src raw.Raw) Protocol {
|
|
||||||
return Protocol{
|
|
||||||
src: src,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p Protocol) Close() {
|
|
||||||
p.src.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p Protocol) Read() []button.Button {
|
|
||||||
panic(nil)
|
|
||||||
}
|
|
||||||
@@ -4,8 +4,18 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"log"
|
"log"
|
||||||
"mayhem-party/src/device/input/button"
|
"mayhem-party/src/device/input/button"
|
||||||
|
"mayhem-party/src/device/input/raw"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"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 {
|
type Refresh struct {
|
||||||
@@ -13,14 +23,12 @@ type Refresh struct {
|
|||||||
input Wrap
|
input Wrap
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRefreshCh(sig os.Signal) <-chan os.Signal {
|
func NewRefresh(ctx context.Context, newWrap func() Wrap) *Refresh {
|
||||||
c := make(chan os.Signal, 1)
|
return NewRefreshWith(ctx, newWrap, ChSigUsr1)
|
||||||
signal.Notify(c, sig)
|
|
||||||
return c
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRefresh(newWrap func() Wrap, ch <-chan os.Signal) *Refresh {
|
func NewRefreshWith(ctx context.Context, newWrap func() Wrap, ch <-chan os.Signal) *Refresh {
|
||||||
ctx, can := context.WithCancel(context.Background())
|
ctx, can := context.WithCancel(ctx)
|
||||||
result := &Refresh{
|
result := &Refresh{
|
||||||
can: can,
|
can: can,
|
||||||
input: newWrap(),
|
input: newWrap(),
|
||||||
@@ -33,6 +41,7 @@ func NewRefresh(newWrap func() Wrap, ch <-chan os.Signal) *Refresh {
|
|||||||
return
|
return
|
||||||
case sig := <-ch:
|
case sig := <-ch:
|
||||||
log.Println("refreshing for", sig)
|
log.Println("refreshing for", sig)
|
||||||
|
result.input.CloseWrap()
|
||||||
result.input = newWrap()
|
result.input = newWrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -40,6 +49,11 @@ func NewRefresh(newWrap func() Wrap, ch <-chan os.Signal) *Refresh {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Refresh) CloseWrap() raw.Raw {
|
||||||
|
r.can()
|
||||||
|
return r.input.CloseWrap()
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Refresh) Read() []button.Button {
|
func (r *Refresh) Read() []button.Button {
|
||||||
return r.input.Read()
|
return r.input.Read()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package wrap
|
package wrap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"mayhem-party/src/device/input/button"
|
||||||
|
"mayhem-party/src/device/input/raw"
|
||||||
"os"
|
"os"
|
||||||
"syscall"
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -15,7 +18,7 @@ func TestRefresh(t *testing.T) {
|
|||||||
}
|
}
|
||||||
ch := make(chan os.Signal, 1)
|
ch := make(chan os.Signal, 1)
|
||||||
defer close(ch)
|
defer close(ch)
|
||||||
refresh := NewRefresh(generator, ch)
|
refresh := NewRefreshWith(context.Background(), generator, ch)
|
||||||
defer refresh.Close()
|
defer refresh.Close()
|
||||||
|
|
||||||
assertIts := func(t *testing.T, b byte) {
|
assertIts := func(t *testing.T, b byte) {
|
||||||
@@ -42,3 +45,57 @@ func TestRefresh(t *testing.T) {
|
|||||||
assertIts(t, byte('c'))
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package wrap
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"mayhem-party/src/device/input/button"
|
"mayhem-party/src/device/input/button"
|
||||||
|
"mayhem-party/src/device/input/raw"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/go-yaml/yaml"
|
"github.com/go-yaml/yaml"
|
||||||
@@ -38,6 +39,10 @@ func NewRemap(input Wrap, m map[byte]byte) Remap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (re Remap) CloseWrap() raw.Raw {
|
||||||
|
return re.input.CloseWrap()
|
||||||
|
}
|
||||||
|
|
||||||
func (re Remap) Close() {
|
func (re Remap) Close() {
|
||||||
re.input.Close()
|
re.input.Close()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,36 +3,42 @@ package wrap
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"mayhem-party/src/device/input/button"
|
"mayhem-party/src/device/input/button"
|
||||||
|
"mayhem-party/src/device/input/raw"
|
||||||
"os"
|
"os"
|
||||||
"syscall"
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
FlagBuffered = os.Getenv("WRAP_BUFFERED") == "true"
|
||||||
|
FlagRemapFile = os.Getenv("WRAP_REMAP_FILE")
|
||||||
|
FlagRefreshOnSigUsr1 = os.Getenv("WRAP_REFRESH_ON_SIGUSR1") == "true"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Wrap interface {
|
type Wrap interface {
|
||||||
Read() []button.Button
|
Read() []button.Button
|
||||||
Close()
|
Close()
|
||||||
|
CloseWrap() raw.Raw
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(ctx context.Context, srcFunc func() button.Parser) Wrap {
|
func New(ctx context.Context, parserFunc func() Wrap) Wrap {
|
||||||
maker := func() Wrap {
|
maker := func() Wrap {
|
||||||
return srcFunc()
|
return parserFunc()
|
||||||
}
|
}
|
||||||
if os.Getenv("WRAP_BUFFERED") == "true" {
|
if FlagBuffered {
|
||||||
oldMaker := maker
|
oldMaker := maker
|
||||||
maker = func() Wrap {
|
maker = func() Wrap {
|
||||||
return NewBuffered(ctx, oldMaker())
|
return NewBuffered(ctx, oldMaker())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if p := os.Getenv("WRAP_REMAP_FILE"); p != "" {
|
if FlagRemapFile != "" {
|
||||||
oldMaker := maker
|
oldMaker := maker
|
||||||
maker = func() Wrap {
|
maker = func() Wrap {
|
||||||
return NewRemapFromFile(oldMaker(), p)
|
return NewRemapFromFile(oldMaker(), FlagRemapFile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if os.Getenv("WRAP_REFRESH_ON_SIGUSR1") != "" {
|
if FlagRefreshOnSigUsr1 {
|
||||||
oldMaker := maker
|
oldMaker := maker
|
||||||
c := NewRefreshCh(syscall.SIGUSR1)
|
|
||||||
maker = func() Wrap {
|
maker = func() Wrap {
|
||||||
return NewRefresh(oldMaker, c)
|
return NewRefresh(ctx, oldMaker)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return maker()
|
return maker()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package wrap
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"mayhem-party/src/device/input/button"
|
"mayhem-party/src/device/input/button"
|
||||||
|
"mayhem-party/src/device/input/raw"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -10,11 +11,11 @@ func TestWrap(t *testing.T) {
|
|||||||
var _ Wrap = &Refresh{}
|
var _ Wrap = &Refresh{}
|
||||||
var _ Wrap = &Buffered{}
|
var _ Wrap = &Buffered{}
|
||||||
var _ Wrap = &Remap{}
|
var _ Wrap = &Remap{}
|
||||||
var _ Wrap = Protocol{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type dummyParser button.Button
|
type dummyParser button.Button
|
||||||
|
|
||||||
|
func (d dummyParser) CloseWrap() raw.Raw { return nil }
|
||||||
func (d dummyParser) Close() {}
|
func (d dummyParser) Close() {}
|
||||||
func (d dummyParser) Read() []button.Button {
|
func (d dummyParser) Read() []button.Button {
|
||||||
return []button.Button{button.Button(d)}
|
return []button.Button{button.Button(d)}
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ func Main(ctx context.Context) error {
|
|||||||
defer reader.Close()
|
defer reader.Close()
|
||||||
|
|
||||||
interval := time.Millisecond * 50
|
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 {
|
} else if v, err := time.ParseDuration(intervalS); err != nil {
|
||||||
panic(err)
|
return err
|
||||||
} else {
|
} else {
|
||||||
interval = v
|
interval = v
|
||||||
}
|
}
|
||||||
|
|||||||
44
todo.yaml
44
todo.yaml
@@ -1,44 +0,0 @@
|
|||||||
todo:
|
|
||||||
- change from 'a','b','c' from rust to just 11,21,31,41 so playerName is known implicitly
|
|
||||||
- lag via UDP formatted inputs as space-delimited TS PID buttonIdx buttonIdx buttonIdx
|
|
||||||
- input.MayhemParty as a logical wrapper from mod10 but then gotta translate back
|
|
||||||
to char for keyboard things somewhere; space delimited?
|
|
||||||
- todo: rusty configs have "name" for each client
|
|
||||||
details: |
|
|
||||||
'if name == server_broadcasted_name { debug_print_in_gui(server_broadcasted_message) }'
|
|
||||||
- todo: rotation triggers
|
|
||||||
subtasks:
|
|
||||||
- minigame end
|
|
||||||
- random word from cur wikipedia page
|
|
||||||
- each person has their own hotword
|
|
||||||
- only spectators have hotwords and must get a player to speak it
|
|
||||||
- tribunal to vote who said it
|
|
||||||
- we have 7 players oooooof
|
|
||||||
scheduled: []
|
|
||||||
done:
|
|
||||||
- todo: sticky keyboard input mode for enable/disable explicitly
|
|
||||||
ts: Thu Mar 23 20:55:52 MDT 2023
|
|
||||||
- todo: case-sensitive
|
|
||||||
ts: Fri Mar 24 13:39:26 MDT 2023
|
|
||||||
- todo: rusty configs have "name" for each client so "if name == server_broadcasted_name
|
|
||||||
{ debug_print_in_gui(server_broadcasted_message) }
|
|
||||||
ts: Fri Mar 24 16:40:09 MDT 2023
|
|
||||||
- todo: change from 'a','b','c' from rust to just 10,11,12 so playerName is known
|
|
||||||
implicitly but then gotta translate back to char for keyboard things somewhere;
|
|
||||||
space delimited?
|
|
||||||
ts: Fri Mar 24 17:00:55 MDT 2023
|
|
||||||
- todo: '"Button" to interface or strings'
|
|
||||||
ts: Fri Mar 24 17:01:01 MDT 2023
|
|
||||||
- todo: input.UDP as a raw provider
|
|
||||||
ts: Fri Mar 24 19:58:59 MDT 2023
|
|
||||||
- todo: input.MayhemParty as a logical wrapper
|
|
||||||
ts: Fri Mar 24 19:58:59 MDT 2023
|
|
||||||
- todo: change from 'a','b','c' from rust to just 11,21,31,41 so playerName is known
|
|
||||||
implicitly from %10 but then gotta translate back to char for keyboard things
|
|
||||||
somewhere; space delimited?
|
|
||||||
ts: Fri Mar 24 19:58:59 MDT 2023
|
|
||||||
- todo: input."Button" to interface or strings
|
|
||||||
ts: Fri Mar 24 21:16:39 MDT 2023
|
|
||||||
- todo: input.MayhemParty as a logical wrapper from %10 but then gotta translate back
|
|
||||||
to char for keyboard things somewhere; space delimited?
|
|
||||||
ts: Fri Mar 24 21:16:39 MDT 2023
|
|
||||||
Reference in New Issue
Block a user