Compare commits
134 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
b89ed62036 | |
|
|
67c93a9048 | |
|
|
8eae7ae9a6 | |
|
|
ce32620940 | |
|
|
24f4b6b8f5 | |
|
|
440191de0f | |
|
|
d5adc596ac | |
|
|
41a39c40d0 | |
|
|
9a38033b65 | |
|
|
6a4ad5ec36 | |
|
|
c2b8ab67f2 | |
|
|
9418cecdf5 | |
|
|
fb5da88774 | |
|
|
39f6bc8ed9 | |
|
|
f3cbfa1c48 | |
|
|
444245c0f5 | |
|
|
52ee1e5083 | |
|
|
934158b7a3 | |
|
|
87e63c27df | |
|
|
f98e417ba6 | |
|
|
d6a7ee3db0 | |
|
|
b814dabfd3 | |
|
|
0a91fc656d | |
|
|
5c3341e260 | |
|
|
0903c01b9a | |
|
|
342e2eef93 | |
|
|
b8b076450e | |
|
|
3bb7cad554 | |
|
|
44ec540db3 | |
|
|
e864f2a9f5 | |
|
|
3c70e42819 | |
|
|
9de8c91544 | |
|
|
7f2e25458e | |
|
|
95810d3735 | |
|
|
df65b1ed07 | |
|
|
a36f07d0c1 | |
|
|
09d9911293 | |
|
|
60d391b7a4 | |
|
|
60ed9c1269 | |
|
|
13cf35bdd8 | |
|
|
79c90ac40c | |
|
|
dd1b053efa | |
|
|
f619fe9e1b | |
|
|
781bfb8a67 | |
|
|
9ece270a13 | |
|
|
f647a03467 | |
|
|
ecb719a97a | |
|
|
1738ce7d19 | |
|
|
354d07d6bf | |
|
|
39ab01525f | |
|
|
ceeeb8fe4b | |
|
|
d029d82366 | |
|
|
0435f7b3e8 | |
|
|
5ef0dde50d | |
|
|
bc3f0271e7 | |
|
|
e2d7c4a908 | |
|
|
c744704b63 | |
|
|
213fd555e4 | |
|
|
dd41028aab | |
|
|
8ff1c2fab4 | |
|
|
1f7b222b9c | |
|
|
1842023224 | |
|
|
45b873f462 | |
|
|
88a78c489f | |
|
|
8314bdc457 | |
|
|
a6a9b177e9 | |
|
|
f649862dd4 | |
|
|
85804d6f84 | |
|
|
f14871218d | |
|
|
26f052d981 | |
|
|
2d4cb394de | |
|
|
163bf2b405 | |
|
|
e968ce17ce | |
|
|
f07e67b3fd | |
|
|
fbf4849517 | |
|
|
804ce02407 | |
|
|
4a86d2b6ca | |
|
|
c663b1a12c | |
|
|
af42db6803 | |
|
|
f9dc4cff9f | |
|
|
37050f3d87 | |
|
|
74717609ec | |
|
|
24ae45896f | |
|
|
8b29648c50 | |
|
|
1eba008efe | |
|
|
d48c545030 | |
|
|
323ca466ad | |
|
|
67e504ced6 | |
|
|
ad967d5047 | |
|
|
8fd0067ad1 | |
|
|
43566be7ae | |
|
|
cb8b254cbb | |
|
|
340ca1d2f5 | |
|
|
02c49852c0 | |
|
|
02c9dce1b3 | |
|
|
a3650642ca | |
|
|
fbded57807 | |
|
|
44cb05487e | |
|
|
e1e2ce3eec | |
|
|
4c7f444887 | |
|
|
0311fc56a3 | |
|
|
9902684990 | |
|
|
967e66bdb3 | |
|
|
ff21bfb8b3 | |
|
|
c153636e24 | |
|
|
efe4adf129 | |
|
|
802266e500 | |
|
|
373d8be1a0 | |
|
|
bd5654128e | |
|
|
9073658e12 | |
|
|
7df4d09553 | |
|
|
1ad60189f4 | |
|
|
766c77b00a | |
|
|
bcdf545188 | |
|
|
3264d9ad55 | |
|
|
3f35f7f936 | |
|
|
0cddc33ac6 | |
|
|
a1a12b1873 | |
|
|
ae1e32391c | |
|
|
97cc3ae151 | |
|
|
2113252e2d | |
|
|
2cae3c6d28 | |
|
|
de261ae400 | |
|
|
3dd0a557d4 | |
|
|
51ae1b27b4 | |
|
|
50e89492cf | |
|
|
3d9ea1296c | |
|
|
db69f76aa0 | |
|
|
0ee3a8b6e8 | |
|
|
b379f1d82c | |
|
|
c83f9d8700 | |
|
|
6289222b69 | |
|
|
607a65e22e | |
|
|
6bbb297c59 |
17
go.mod
17
go.mod
|
|
@ -3,6 +3,19 @@ module mayhem-party
|
|||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/go-yaml/yaml v2.1.0+incompatible // indirect
|
||||
github.com/micmonay/keybd_event v1.1.1 // indirect
|
||||
github.com/faiface/beep v1.1.0
|
||||
github.com/go-yaml/yaml v2.1.0+incompatible
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/evanphx/json-patch/v5 v5.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/hajimehoshi/oto v0.7.1 // indirect
|
||||
github.com/micmonay/keybd_event v1.1.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 // indirect
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067 // indirect
|
||||
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6 // indirect
|
||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 // indirect
|
||||
)
|
||||
|
|
|
|||
45
go.sum
45
go.sum
|
|
@ -1,4 +1,49 @@
|
|||
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||
github.com/d4l3k/messagediff v1.2.2-0.20190829033028-7e0a312ae40b/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo=
|
||||
github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww=
|
||||
github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
|
||||
github.com/faiface/beep v1.1.0 h1:A2gWP6xf5Rh7RG/p9/VAW2jRSDEGQm5sbOb38sf5d4c=
|
||||
github.com/faiface/beep v1.1.0/go.mod h1:6I8p6kK2q4opL/eWb+kAkk38ehnTunWeToJB+s51sT4=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
|
||||
github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs=
|
||||
github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498=
|
||||
github.com/go-audio/wav v1.0.0/go.mod h1:3yoReyQOsiARkvPl3ERCi8JFjihzG6WhjYpZCf5zAWE=
|
||||
github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwnTLB6vQiq+o=
|
||||
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hajimehoshi/go-mp3 v0.3.0/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM=
|
||||
github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI=
|
||||
github.com/hajimehoshi/oto v0.7.1 h1:I7maFPz5MBCwiutOrz++DLdbr4rTzBsbBuV2VpgU9kk=
|
||||
github.com/hajimehoshi/oto v0.7.1/go.mod h1:wovJ8WWMfFKvP587mhHgot/MBr4DnNy9m6EepeVGnos=
|
||||
github.com/icza/bitio v1.0.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A=
|
||||
github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6/go.mod h1:xQig96I1VNBDIWGCdTt54nHt6EeI639SmHycLYL7FkA=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jfreymuth/oggvorbis v1.0.1/go.mod h1:NqS+K+UXKje0FUYUPosyQ+XTVvjmVjps1aEZH1sumIk=
|
||||
github.com/jfreymuth/vorbis v1.0.0/go.mod h1:8zy3lUAm9K/rJJk223RKy6vjCZTWC61NA2QD06bfOE0=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
|
||||
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mewkiz/flac v1.0.7/go.mod h1:yU74UH277dBUpqxPouHSQIar3G1X/QIclVbFahSd1pU=
|
||||
github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2/go.mod h1:3E2FUC/qYUfM8+r9zAwpeHJzqRVVMIYnpzD/clwWxyA=
|
||||
github.com/micmonay/keybd_event v1.1.1 h1:rv7omwXWYL9Lgf3PUq6uBgJI2k1yGkL/GD6dxc6nmSs=
|
||||
github.com/micmonay/keybd_event v1.1.1/go.mod h1:CGMWMDNgsfPljzrAWoybUOSKafQPZpv+rLigt2LzNGI=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 h1:idBdZTd9UioThJp8KpM/rTSinK/ChZFBE43/WtIy8zg=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067 h1:KYGJGHOQy8oSi1fDlSpcZF0+juKwk/hEMv5SiwHogR0=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6 h1:vyLBGJPIl9ZYbcQFM2USFmJBK6KI+t+z6jL0lbwjrnc=
|
||||
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 h1:9nuHUbU8dRnRRfj9KjWUVrJeoexdbeMjttk6Oh1rD10=
|
||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
|
|
|
|||
|
|
@ -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=true
|
||||
export RAW_UDP=17070
|
||||
export BUTTON_PARSER_V01=true
|
||||
export WRAP_REFRESH_ON_SIGUSR1=true
|
||||
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,28 +0,0 @@
|
|||
streams:
|
||||
input:
|
||||
debug: false
|
||||
engine:
|
||||
name: gui
|
||||
gui:
|
||||
user: bel
|
||||
press: {prefix: "", suffix: ""}
|
||||
release: {prefix: "", suffix: ""}
|
||||
format: '{"T":{{ms}},"U":"{{user}}","Y":"{{pressed}}","N":"{{released}}"}'
|
||||
buttons:
|
||||
up: 'w'
|
||||
down: 's'
|
||||
left: 'a'
|
||||
right: 'd'
|
||||
l: 'e'
|
||||
r: 'q'
|
||||
a: '1'
|
||||
b: '2'
|
||||
x: '3'
|
||||
y: '4'
|
||||
output:
|
||||
debug: true
|
||||
engine:
|
||||
name: udp
|
||||
udp:
|
||||
host: mayhem-party.home.blapointe.com
|
||||
port: 17070
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
streams:
|
||||
input:
|
||||
debug: false
|
||||
engine:
|
||||
name: gui
|
||||
gui:
|
||||
user: zach
|
||||
press: {prefix: "", suffix: ""}
|
||||
release: {prefix: "", suffix: ""}
|
||||
format: '{"T":{{ms}},"U":"{{user}}","Y":"{{pressed}}","N":"{{released}}"}'
|
||||
buttons:
|
||||
up: 'w'
|
||||
down: 's'
|
||||
left: 'a'
|
||||
right: 'd'
|
||||
l: 'e'
|
||||
r: 'q'
|
||||
a: '1'
|
||||
b: '2'
|
||||
x: '3'
|
||||
y: '4'
|
||||
output:
|
||||
debug: true
|
||||
engine:
|
||||
name: udp
|
||||
udp:
|
||||
host: mayhem-party.home.blapointe.com
|
||||
port: 17070
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
streams:
|
||||
input:
|
||||
debug: false
|
||||
engine:
|
||||
name: gui
|
||||
gui:
|
||||
user: mason
|
||||
press: {prefix: "", suffix: ""}
|
||||
release: {prefix: "", suffix: ""}
|
||||
format: '{"T":{{ms}},"U":"{{user}}","Y":"{{pressed}}","N":"{{released}}"}'
|
||||
buttons:
|
||||
up: 'w'
|
||||
down: 's'
|
||||
left: 'a'
|
||||
right: 'd'
|
||||
l: 'e'
|
||||
r: 'q'
|
||||
a: '1'
|
||||
b: '2'
|
||||
x: '3'
|
||||
y: '4'
|
||||
output:
|
||||
debug: true
|
||||
engine:
|
||||
name: udp
|
||||
udp:
|
||||
host: mayhem-party.home.blapointe.com
|
||||
port: 17070
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
streams:
|
||||
input:
|
||||
debug: false
|
||||
engine:
|
||||
name: gui
|
||||
gui:
|
||||
user: nat
|
||||
press: {prefix: "", suffix: ""}
|
||||
release: {prefix: "", suffix: ""}
|
||||
format: '{"T":{{ms}},"U":"{{user}}","Y":"{{pressed}}","N":"{{released}}"}'
|
||||
buttons:
|
||||
up: 'w'
|
||||
down: 's'
|
||||
left: 'a'
|
||||
right: 'd'
|
||||
l: 'e'
|
||||
r: 'q'
|
||||
a: '1'
|
||||
b: '2'
|
||||
x: '3'
|
||||
y: '4'
|
||||
output:
|
||||
debug: true
|
||||
engine:
|
||||
name: udp
|
||||
udp:
|
||||
host: mayhem-party.home.blapointe.com
|
||||
port: 17070
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
streams:
|
||||
input:
|
||||
debug: false
|
||||
engine:
|
||||
name: gui
|
||||
gui:
|
||||
user: roxy
|
||||
press: {prefix: "", suffix: ""}
|
||||
release: {prefix: "", suffix: ""}
|
||||
format: '{"T":{{ms}},"U":"{{user}}","Y":"{{pressed}}","N":"{{released}}"}'
|
||||
buttons:
|
||||
up: 'w'
|
||||
down: 's'
|
||||
left: 'a'
|
||||
right: 'd'
|
||||
l: 'e'
|
||||
r: 'q'
|
||||
a: '1'
|
||||
b: '2'
|
||||
x: '3'
|
||||
y: '4'
|
||||
output:
|
||||
debug: true
|
||||
engine:
|
||||
name: udp
|
||||
udp:
|
||||
host: mayhem-party.home.blapointe.com
|
||||
port: 17070
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
streams:
|
||||
input:
|
||||
debug: false
|
||||
engine:
|
||||
name: gui
|
||||
gui:
|
||||
user: chase
|
||||
press: {prefix: "", suffix: ""}
|
||||
release: {prefix: "", suffix: ""}
|
||||
format: '{"T":{{ms}},"U":"{{user}}","Y":"{{pressed}}","N":"{{released}}"}'
|
||||
buttons:
|
||||
up: 'w'
|
||||
down: 's'
|
||||
left: 'a'
|
||||
right: 'd'
|
||||
l: 'e'
|
||||
r: 'q'
|
||||
a: '1'
|
||||
b: '2'
|
||||
x: '3'
|
||||
y: '4'
|
||||
output:
|
||||
debug: true
|
||||
engine:
|
||||
name: udp
|
||||
udp:
|
||||
host: mayhem-party.home.blapointe.com
|
||||
port: 17070
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
streams:
|
||||
input:
|
||||
debug: false
|
||||
engine:
|
||||
name: gui
|
||||
gui:
|
||||
user: bill
|
||||
press: {prefix: "", suffix: ""}
|
||||
release: {prefix: "", suffix: ""}
|
||||
format: '{"T":{{ms}},"U":"{{user}}","Y":"{{pressed}}","N":"{{released}}"}'
|
||||
buttons:
|
||||
up: 'w'
|
||||
down: 's'
|
||||
left: 'a'
|
||||
right: 'd'
|
||||
l: 'e'
|
||||
r: 'q'
|
||||
a: '1'
|
||||
b: '2'
|
||||
x: '3'
|
||||
y: '4'
|
||||
output:
|
||||
debug: true
|
||||
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() {
|
||||
ctx, can := signal.NotifyContext(context.Background(), syscall.SIGINT)
|
||||
defer can()
|
||||
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Println("panic:", err)
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := src.Main(ctx); err != nil && ctx.Err() == nil {
|
||||
panic(err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
package button
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Button struct {
|
||||
Char byte
|
||||
Down bool
|
||||
}
|
||||
|
||||
func (button Button) String() string {
|
||||
return fmt.Sprintf("%c:%v", button.Char, button.Down)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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_V01") == "true" {
|
||||
return NewV01(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.V01{}
|
||||
}
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
package button
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"mayhem-party/src/device/input/raw"
|
||||
"os"
|
||||
)
|
||||
|
||||
var debugging = os.Getenv("DEBUG") == "true"
|
||||
|
||||
type (
|
||||
V01 struct {
|
||||
src raw.Raw
|
||||
}
|
||||
v01Msg struct {
|
||||
T int64
|
||||
U string
|
||||
Y string
|
||||
N string
|
||||
}
|
||||
)
|
||||
|
||||
func NewV01(src raw.Raw) V01 {
|
||||
return V01{
|
||||
src: src,
|
||||
}
|
||||
}
|
||||
|
||||
func (v01 V01) Close() {
|
||||
v01.src.Close()
|
||||
}
|
||||
|
||||
func (v01 V01) Read() []Button {
|
||||
line := v01.src.Read()
|
||||
var msg v01Msg
|
||||
if err := json.Unmarshal(line, &msg); err != nil {
|
||||
log.Printf("%v: %s", err, line)
|
||||
}
|
||||
return msg.buttons()
|
||||
}
|
||||
|
||||
func (msg v01Msg) 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 TestV01(t *testing.T) {
|
||||
src := constSrc(`{"T":1,"U":"bel","Y":"abc","N":"cde"}`)
|
||||
t.Logf("(%v) %s", len(src), src.Read())
|
||||
v01 := button.NewV01(src)
|
||||
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])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package input
|
|||
import (
|
||||
"context"
|
||||
"mayhem-party/src/device/input/button"
|
||||
"mayhem-party/src/device/input/parse"
|
||||
"mayhem-party/src/device/input/raw"
|
||||
"mayhem-party/src/device/input/wrap"
|
||||
)
|
||||
|
|
@ -14,7 +15,7 @@ type Input interface {
|
|||
|
||||
func New(ctx context.Context) Input {
|
||||
src := raw.New(ctx)
|
||||
return wrap.New(ctx, func() button.Parser {
|
||||
return button.New(ctx, src)
|
||||
return wrap.New(ctx, func() wrap.Wrap {
|
||||
return parse.New(ctx, src)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ package input_test
|
|||
import (
|
||||
"context"
|
||||
"mayhem-party/src/device/input"
|
||||
"mayhem-party/src/device/input/raw"
|
||||
"mayhem-party/src/device/input/wrap"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
|
@ -30,11 +32,11 @@ func TestNewRemapped(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
os.Setenv("WRAP_REMAP_FILE", remap)
|
||||
os.Setenv("RAW_RANDOM_WEIGHT_FILE", rand)
|
||||
wrap.FlagRemapFile = remap
|
||||
raw.FlagRawRandomWeightFile = rand
|
||||
t.Cleanup(func() {
|
||||
os.Unsetenv("WRAP_REMAP_FILE")
|
||||
os.Unsetenv("RAW_RANDOM_WEIGHT_FILE")
|
||||
wrap.FlagRemapFile = ""
|
||||
raw.FlagRawRandomWeightFile = ""
|
||||
})
|
||||
|
||||
r := input.New(context.Background())
|
||||
|
|
@ -50,9 +52,9 @@ func TestNewRemapped(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestNewBuffered(t *testing.T) {
|
||||
os.Setenv("WRAP_BUFFERED", "true")
|
||||
wrap.FlagBuffered = true
|
||||
t.Cleanup(func() {
|
||||
os.Unsetenv("WRAP_BUFFERED")
|
||||
wrap.FlagBuffered = false
|
||||
})
|
||||
|
||||
r := input.New(context.Background())
|
||||
|
|
@ -71,9 +73,9 @@ func TestNewRandomWeightFile(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
os.Setenv("RAW_RANDOM_WEIGHT_FILE", p)
|
||||
raw.FlagRawRandomWeightFile = p
|
||||
t.Cleanup(func() {
|
||||
os.Unsetenv("RAW_RANDOM_WEIGHT_FILE")
|
||||
raw.FlagRawRandomWeightFile = ""
|
||||
})
|
||||
|
||||
r := input.New(context.Background())
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
package parse
|
||||
|
||||
import (
|
||||
"context"
|
||||
"mayhem-party/src/device/input/button"
|
||||
v01 "mayhem-party/src/device/input/parse/v01"
|
||||
"mayhem-party/src/device/input/raw"
|
||||
"os"
|
||||
)
|
||||
|
||||
var (
|
||||
FlagParseV01 = os.Getenv("PARSE_V01") == "true"
|
||||
)
|
||||
|
||||
type Parser interface {
|
||||
Read() []button.Button
|
||||
Close()
|
||||
CloseWrap() raw.Raw
|
||||
}
|
||||
|
||||
func New(ctx context.Context, src raw.Raw) Parser {
|
||||
if FlagParseV01 {
|
||||
return v01.NewV01(ctx, src)
|
||||
}
|
||||
return NewPlaintext(src)
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package parse_test
|
||||
|
||||
import (
|
||||
"mayhem-party/src/device/input/parse"
|
||||
v01 "mayhem-party/src/device/input/parse/v01"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParser(t *testing.T) {
|
||||
var _ parse.Parser = parse.Plaintext{}
|
||||
var _ parse.Parser = &v01.V01{}
|
||||
}
|
||||
|
|
@ -1,10 +1,15 @@
|
|||
package button
|
||||
package parse
|
||||
|
||||
import (
|
||||
"mayhem-party/src/device/input/button"
|
||||
"mayhem-party/src/device/input/raw"
|
||||
"os"
|
||||
)
|
||||
|
||||
var (
|
||||
FlagParsePlaintextRelease = os.Getenv("PARSE_PLAINTEXT_RELEASE")
|
||||
)
|
||||
|
||||
type Plaintext struct {
|
||||
src raw.Raw
|
||||
release byte
|
||||
|
|
@ -12,8 +17,8 @@ type Plaintext struct {
|
|||
|
||||
func NewPlaintext(src raw.Raw) Plaintext {
|
||||
releaseChar := byte('!')
|
||||
if v := os.Getenv("BUTTON_PLAINTEXT_RELEASE"); v != "" {
|
||||
releaseChar = byte(v[0])
|
||||
if FlagParsePlaintextRelease != "" {
|
||||
releaseChar = byte(FlagParsePlaintextRelease[0])
|
||||
}
|
||||
return Plaintext{
|
||||
src: src,
|
||||
|
|
@ -23,16 +28,18 @@ func NewPlaintext(src raw.Raw) Plaintext {
|
|||
|
||||
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()
|
||||
buttons := make([]Button, 0, len(b))
|
||||
buttons := make([]button.Button, 0, len(b))
|
||||
down := true
|
||||
for i := range b {
|
||||
if b[i] == p.release {
|
||||
down = false
|
||||
} else {
|
||||
if b[i] != '\n' {
|
||||
buttons = append(buttons, Button{Char: b[i], Down: down})
|
||||
buttons = append(buttons, button.Button{Char: b[i], Down: down})
|
||||
}
|
||||
down = true
|
||||
}
|
||||
|
|
@ -1,13 +1,14 @@
|
|||
package button_test
|
||||
package parse_test
|
||||
|
||||
import (
|
||||
"mayhem-party/src/device/input/button"
|
||||
"mayhem-party/src/device/input/parse"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPlaintext(t *testing.T) {
|
||||
src := constSrc("c!b")
|
||||
p := button.NewPlaintext(src)
|
||||
p := parse.NewPlaintext(src)
|
||||
got := p.Read()
|
||||
if len(got) != 2 {
|
||||
t.Fatal(len(got))
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
package v01
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sync"
|
||||
|
||||
patch "github.com/evanphx/json-patch/v5"
|
||||
)
|
||||
|
||||
type (
|
||||
config struct {
|
||||
lock *sync.Mutex
|
||||
Feedback configFeedback
|
||||
Users map[string]configUser
|
||||
Players []configPlayer
|
||||
Quiet bool
|
||||
Broadcast configBroadcast
|
||||
GM configGM
|
||||
}
|
||||
|
||||
configGM struct {
|
||||
Hotwords map[string]configGMHotword
|
||||
}
|
||||
|
||||
configGMHotword struct {
|
||||
Call string
|
||||
Args []string
|
||||
}
|
||||
|
||||
configBroadcast struct {
|
||||
Message string
|
||||
}
|
||||
|
||||
configFeedback struct {
|
||||
Addr string
|
||||
TTSURL string
|
||||
}
|
||||
|
||||
configUser struct {
|
||||
Meta configUserMeta
|
||||
State configUserState
|
||||
}
|
||||
|
||||
configUserMeta struct {
|
||||
LastTSMS int64
|
||||
LastLag int64
|
||||
}
|
||||
|
||||
configUserState struct {
|
||||
Player int
|
||||
Message string
|
||||
GM configUserStateGM
|
||||
}
|
||||
|
||||
configUserStateGM struct {
|
||||
Alias string
|
||||
LastAlias string
|
||||
Vote string
|
||||
}
|
||||
|
||||
configPlayer struct {
|
||||
Transformation transformation
|
||||
}
|
||||
)
|
||||
|
||||
func (cfg config) WithPatch(v interface{}) config {
|
||||
cfg.lock.Lock()
|
||||
defer cfg.lock.Unlock()
|
||||
originalData, _ := json.Marshal(cfg)
|
||||
patchData, _ := json.Marshal(v)
|
||||
patcher, err := patch.DecodePatch(patchData)
|
||||
if err != nil {
|
||||
return cfg
|
||||
}
|
||||
patchedData, err := patcher.Apply(originalData)
|
||||
if err != nil {
|
||||
return cfg
|
||||
}
|
||||
var patched config
|
||||
if err := json.Unmarshal(patchedData, &patched); err != nil {
|
||||
return cfg
|
||||
}
|
||||
return patched
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
package v01
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConfigPatch(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
cfg config
|
||||
patch interface{}
|
||||
want config
|
||||
}{
|
||||
"nil patch": {
|
||||
cfg: config{Quiet: true},
|
||||
patch: nil,
|
||||
want: config{Quiet: true},
|
||||
},
|
||||
"[] patch": {
|
||||
cfg: config{Quiet: true},
|
||||
patch: []interface{}{},
|
||||
want: config{Quiet: true},
|
||||
},
|
||||
"set fake field": {
|
||||
cfg: config{Quiet: true},
|
||||
patch: []interface{}{
|
||||
map[string]interface{}{"op": "add", "path": "/Fake", "value": true},
|
||||
},
|
||||
want: config{Quiet: true},
|
||||
},
|
||||
"remove field": {
|
||||
cfg: config{Quiet: true},
|
||||
patch: []interface{}{
|
||||
map[string]interface{}{"op": "remove", "path": "/Quiet"},
|
||||
},
|
||||
want: config{Quiet: false},
|
||||
},
|
||||
"replace field with valid": {
|
||||
cfg: config{Quiet: true},
|
||||
patch: []interface{}{
|
||||
map[string]interface{}{"op": "replace", "path": "/Quiet", "value": false},
|
||||
},
|
||||
want: config{Quiet: false},
|
||||
},
|
||||
"replace field with invalid": {
|
||||
cfg: config{Quiet: true},
|
||||
patch: []interface{}{
|
||||
map[string]interface{}{"op": "replace", "path": "/Quiet", "value": "teehee"},
|
||||
},
|
||||
want: config{Quiet: true},
|
||||
},
|
||||
"test and noop": {
|
||||
cfg: config{Quiet: true},
|
||||
patch: []interface{}{
|
||||
map[string]interface{}{"op": "test", "path": "/Quiet", "value": false},
|
||||
map[string]interface{}{"op": "replace", "path": "/Quiet", "value": false},
|
||||
},
|
||||
want: config{Quiet: true},
|
||||
},
|
||||
"test and apply": {
|
||||
cfg: config{Quiet: true},
|
||||
patch: []interface{}{
|
||||
map[string]interface{}{"op": "test", "path": "/Quiet", "value": true},
|
||||
map[string]interface{}{"op": "replace", "path": "/Quiet", "value": false},
|
||||
},
|
||||
want: config{Quiet: false},
|
||||
},
|
||||
}
|
||||
|
||||
for name, d := range cases {
|
||||
c := d
|
||||
t.Run(name, func(t *testing.T) {
|
||||
c.cfg.lock = &sync.Mutex{}
|
||||
got := c.cfg.WithPatch(c.patch)
|
||||
got.lock = nil
|
||||
c.want.lock = nil
|
||||
if fmt.Sprintf("%+v", got) != fmt.Sprintf("%+v", c.want) {
|
||||
t.Errorf("(%+v).Patch(%+v) want %+v, got %+v", c.cfg, c.patch, c.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package v01
|
||||
|
||||
import (
|
||||
"log"
|
||||
"mayhem-party/src/device/input/button"
|
||||
)
|
||||
|
||||
type message struct {
|
||||
T int64
|
||||
U string
|
||||
Y string
|
||||
N string
|
||||
}
|
||||
|
||||
func (msg message) buttons() []button.Button {
|
||||
buttons := make([]button.Button, len(msg.Y)+len(msg.N))
|
||||
for i := range msg.Y {
|
||||
buttons[i] = button.Button{Char: msg.Y[i], Down: true}
|
||||
}
|
||||
for i := range msg.N {
|
||||
buttons[len(msg.Y)+i] = button.Button{Char: msg.N[i], Down: false}
|
||||
}
|
||||
if FlagDebug {
|
||||
log.Printf("%+v", msg)
|
||||
}
|
||||
return buttons
|
||||
}
|
||||
|
|
@ -0,0 +1,407 @@
|
|||
package v01
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math/rand"
|
||||
"mayhem-party/src/device/input/button"
|
||||
"mayhem-party/src/device/input/wrap"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func (v01 *V01) listen() {
|
||||
if v01.cfg.Feedback.Addr == "" {
|
||||
return
|
||||
}
|
||||
v01._listen()
|
||||
}
|
||||
|
||||
func (v01 *V01) _listen() {
|
||||
mutex := &sync.Mutex{}
|
||||
s := &http.Server{
|
||||
Addr: v01.cfg.Feedback.Addr,
|
||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
defer func() { log.Printf("%vms | %s %s", time.Since(start).Milliseconds(), r.Method, r.URL) }()
|
||||
v01.cfg.lock.Lock()
|
||||
defer v01.cfg.lock.Unlock()
|
||||
if r.Method == http.MethodGet {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
} else {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
}
|
||||
v01.ServeHTTP(w, r)
|
||||
v01.stashConfig() // TODO
|
||||
}),
|
||||
}
|
||||
go func() {
|
||||
<-v01.ctx.Done()
|
||||
log.Println("closing v01 server")
|
||||
s.Close()
|
||||
}()
|
||||
log.Println("starting v01 server")
|
||||
if err := s.ListenAndServe(); err != nil && v01.ctx.Err() == nil {
|
||||
log.Println("err with v01 server", err)
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (v01 *V01) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
r = r.WithContext(v01.ctx)
|
||||
v01.serveHTTP(w, r)
|
||||
v01.serveGlobalQueries(r)
|
||||
}
|
||||
|
||||
func (v01 *V01) serveHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
switch strings.Split(r.URL.Path[1:], "/")[0] {
|
||||
case "":
|
||||
v01.getUserFeedback(w, r)
|
||||
case "broadcast":
|
||||
v01.servePutBroadcast(w, r)
|
||||
case "config":
|
||||
v01.serveConfig(w, r)
|
||||
case "gm":
|
||||
v01.serveGM(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (v01 *V01) getUserFeedback(w http.ResponseWriter, r *http.Request) {
|
||||
user := v01.cfg.Users[r.URL.Query().Get("user")]
|
||||
|
||||
msg := user.State.Message
|
||||
if msg == "" {
|
||||
msg = v01.cfg.Broadcast.Message
|
||||
}
|
||||
|
||||
alias := user.State.GM.Alias
|
||||
if alias == "" {
|
||||
alias = user.State.GM.LastAlias
|
||||
}
|
||||
if alias != "" {
|
||||
msg = fmt.Sprintf("%s (Your secret word is '%s'. Make **someone else** say it!)", msg, alias)
|
||||
}
|
||||
|
||||
w.Write([]byte(msg + "\n\n"))
|
||||
v01.serveGMStatus(w)
|
||||
|
||||
if v01.cfg.Quiet {
|
||||
w.Write([]byte("\n\n"))
|
||||
v01.serveGMVoteRead(w)
|
||||
}
|
||||
}
|
||||
|
||||
func (v01 *V01) servePutBroadcast(w http.ResponseWriter, r *http.Request) {
|
||||
b, _ := io.ReadAll(r.Body)
|
||||
v01.servePutBroadcastValue(string(b))
|
||||
}
|
||||
|
||||
func (v01 *V01) servePutBroadcastValue(v string) {
|
||||
v01.cfg.Broadcast.Message = v
|
||||
}
|
||||
|
||||
func (v01 *V01) serveConfig(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodGet {
|
||||
v01.serveGetConfig(w, r)
|
||||
} else {
|
||||
v01.servePatchConfig(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (v01 *V01) serveGetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
b, _ := json.Marshal(v01.cfg)
|
||||
w.Write(b)
|
||||
}
|
||||
|
||||
func (v01 *V01) servePatchConfig(w http.ResponseWriter, r *http.Request) {
|
||||
b, _ := io.ReadAll(r.Body)
|
||||
var v []interface{}
|
||||
if err := json.Unmarshal(b, &v); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
v01.cfg = v01.cfg.WithPatch(v)
|
||||
if err := v01.stashConfig(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func (v01 *V01) stashConfig() error {
|
||||
if b, err := yaml.Marshal(v01.cfg); err == nil && FlagParseV01Config != "" {
|
||||
if err := os.WriteFile(FlagParseV01Config, b, os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v01 *V01) serveGlobalQueries(r *http.Request) {
|
||||
v01.serveGlobalQuerySay(r)
|
||||
v01.serveGlobalQueryRefresh(r)
|
||||
}
|
||||
|
||||
func (v01 *V01) serveGlobalQuerySay(r *http.Request) {
|
||||
text := r.URL.Query().Get("say")
|
||||
if text == "" {
|
||||
text = r.Header.Get("say")
|
||||
}
|
||||
if text == "" {
|
||||
return
|
||||
}
|
||||
go v01.tts(text)
|
||||
}
|
||||
|
||||
func (v01 *V01) serveGlobalQueryRefresh(r *http.Request) {
|
||||
if _, ok := r.URL.Query()["refresh"]; !ok {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case wrap.ChSigUsr1 <- syscall.SIGUSR1:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (v01 *V01) serveGM(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/gm/rpc/status":
|
||||
v01.serveGMStatus(w)
|
||||
case "/gm/rpc/broadcastSomeoneSaidAlias":
|
||||
v01.serveGMSomeoneSaid(w, r)
|
||||
case "/gm/rpc/fillNonPlayerAliases":
|
||||
v01.serveGMFillNonPlayerAliases(w, r)
|
||||
case "/gm/rpc/vote":
|
||||
v01.serveGMVote(w, r)
|
||||
case "/gm/rpc/elect":
|
||||
v01.serveGMElect(w, r)
|
||||
case "/gm/rpc/shuffle":
|
||||
v01.serveGMShuffle(r)
|
||||
case "/gm/rpc/swap":
|
||||
if errCode, err := v01.serveGMSwap(r.URL.Query().Get("a"), r.URL.Query().Get("b")); err != nil {
|
||||
http.Error(w, err.Error(), errCode)
|
||||
return
|
||||
}
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (v01 *V01) serveGMStatus(w io.Writer) {
|
||||
users := map[string]string{}
|
||||
for k, v := range v01.cfg.Users {
|
||||
result := ""
|
||||
|
||||
if v.State.Player > 0 {
|
||||
result += fmt.Sprintf("Player %v ", v.State.Player)
|
||||
}
|
||||
|
||||
if ms := time.Duration(v.Meta.LastLag) * time.Millisecond; v.Meta.LastLag > 0 && ms < time.Minute {
|
||||
result += fmt.Sprintf("%s ", ms.String())
|
||||
}
|
||||
|
||||
if result == "" {
|
||||
result = "..."
|
||||
}
|
||||
users[k] = result
|
||||
}
|
||||
b, _ := yaml.Marshal(map[string]interface{}{
|
||||
"Players": len(v01.cfg.Players),
|
||||
"Users": users,
|
||||
})
|
||||
w.Write(b)
|
||||
}
|
||||
|
||||
func (v01 *V01) serveGMSomeoneSaid(w http.ResponseWriter, r *http.Request) {
|
||||
if gmHotword, ok := v01.cfg.GM.Hotwords[r.URL.Query().Get("message")]; ok {
|
||||
v01.serveGMSomeoneSaidGMHotword(w, r, gmHotword)
|
||||
}
|
||||
v01.serveGMSomeoneSaidAlias(w, r)
|
||||
}
|
||||
|
||||
func (v01 *V01) serveGMSomeoneSaidGMHotword(w http.ResponseWriter, r *http.Request, gmHotword configGMHotword) {
|
||||
switch gmHotword.Call {
|
||||
case "tap":
|
||||
args := append([]string{}, gmHotword.Args...)
|
||||
if len(args) < 1 || len(args[0]) < 1 {
|
||||
return
|
||||
}
|
||||
btn := args[0][0]
|
||||
go func() {
|
||||
v01.alt <- []button.Button{button.Button{Down: true, Char: btn}}
|
||||
v01.alt <- []button.Button{button.Button{Down: false, Char: btn}}
|
||||
}()
|
||||
r.URL.RawQuery = ""
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (v01 *V01) serveGMSomeoneSaidAlias(w http.ResponseWriter, r *http.Request) {
|
||||
v01.cfg.Quiet = true
|
||||
for k, v := range v01.cfg.Users {
|
||||
v.State.GM.LastAlias = v.State.GM.Alias
|
||||
v.State.GM.Alias = ""
|
||||
v01.cfg.Users[k] = v
|
||||
}
|
||||
v01.servePutBroadcastValue(fmt.Sprintf("<<SOMEONE SAID %q>>", strings.ToUpper(r.URL.Query().Get("message"))))
|
||||
}
|
||||
|
||||
func (v01 *V01) serveGMFillNonPlayerAliases(w http.ResponseWriter, r *http.Request) {
|
||||
b, _ := io.ReadAll(r.Body)
|
||||
var pool []string
|
||||
yaml.Unmarshal(b, &pool)
|
||||
n := 0
|
||||
for _, v := range v01.cfg.Users {
|
||||
if v.State.Player == 0 {
|
||||
n += 1
|
||||
}
|
||||
}
|
||||
if n < 1 {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
if len(pool) < n {
|
||||
http.Error(w, fmt.Sprintf("request body must contain a list of %v options", n), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
for i := 0; i < 100; i++ {
|
||||
a, b := rand.Int()%len(pool), rand.Int()%len(pool)
|
||||
pool[a], pool[b] = pool[b], pool[a]
|
||||
}
|
||||
i := 0
|
||||
for k, v := range v01.cfg.Users {
|
||||
if v.State.Player == 0 {
|
||||
v.State.GM.Alias = pool[i]
|
||||
v01.cfg.Users[k] = v
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (v01 *V01) serveGMElect(w http.ResponseWriter, r *http.Request) {
|
||||
alias := r.URL.Query().Get("alias")
|
||||
aliasWinner := ""
|
||||
votes := map[string]int{}
|
||||
for k, v := range v01.cfg.Users {
|
||||
votes[v.State.GM.Vote] = votes[v.State.GM.Vote] + 1
|
||||
if v.State.GM.LastAlias == alias {
|
||||
aliasWinner = k
|
||||
}
|
||||
}
|
||||
if aliasWinner == "" {
|
||||
http.Error(w, "who is "+alias+"?", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
threshold := 0.1 + float64(len(votes))/2.0
|
||||
winner := ""
|
||||
for k, v := range votes {
|
||||
if float64(v) > threshold {
|
||||
winner = k
|
||||
}
|
||||
}
|
||||
if winner == "" {
|
||||
v01.serveGMShuffle(r)
|
||||
} else if _, err := v01.serveGMSwap(winner, aliasWinner); err != nil {
|
||||
v01.serveGMShuffle(r)
|
||||
}
|
||||
for k, v := range v01.cfg.Users {
|
||||
v.State.GM.Vote = ""
|
||||
v01.cfg.Users[k] = v
|
||||
}
|
||||
yaml.NewEncoder(w).Encode(votes)
|
||||
}
|
||||
|
||||
func (v01 *V01) serveGMVote(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Query().Get("payload") {
|
||||
case "":
|
||||
v01.serveGMVoteRead(w)
|
||||
default:
|
||||
v01.serveGMVoteWrite(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (v01 *V01) serveGMVoteRead(w io.Writer) {
|
||||
counts := map[string]string{}
|
||||
for k, v := range v01.cfg.Users {
|
||||
if v.State.GM.Vote != "" {
|
||||
counts[k] = "voted"
|
||||
} else {
|
||||
counts[k] = "voting"
|
||||
}
|
||||
}
|
||||
yaml.NewEncoder(w).Encode(counts)
|
||||
}
|
||||
|
||||
func (v01 *V01) serveGMVoteWrite(w http.ResponseWriter, r *http.Request) {
|
||||
voter := r.URL.Query().Get("user")
|
||||
candidate := r.URL.Query().Get("payload")
|
||||
v, ok := v01.cfg.Users[voter]
|
||||
if _, ok2 := v01.cfg.Users[candidate]; !ok || !ok2 {
|
||||
http.Error(w, "bad voter/candidate", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
v.State.GM.Vote = candidate
|
||||
v01.cfg.Users[voter] = v
|
||||
}
|
||||
|
||||
func (v01 *V01) serveGMShuffle(r *http.Request) {
|
||||
poolSize := len(v01.cfg.Users)
|
||||
if altSize := len(v01.cfg.Players); altSize > poolSize {
|
||||
poolSize = altSize
|
||||
}
|
||||
pool := make([]int, poolSize)
|
||||
if poolSize > 0 {
|
||||
for i := range v01.cfg.Players {
|
||||
pool[i] = i + 1
|
||||
}
|
||||
for i := 0; i < 30; i++ {
|
||||
l := rand.Int() % poolSize
|
||||
r := rand.Int() % poolSize
|
||||
pool[l], pool[r] = pool[r], pool[l]
|
||||
}
|
||||
}
|
||||
i := 0
|
||||
msg := []string{}
|
||||
for k, v := range v01.cfg.Users {
|
||||
v.State.Player = pool[i]
|
||||
v01.cfg.Users[k] = v
|
||||
if pool[i] > 0 {
|
||||
msg = append(msg, fmt.Sprintf("%s is now player %v", k, v.State.Player))
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
v01.servePutBroadcastValue(strings.Join(msg, ", "))
|
||||
v01.cfg.Quiet = false
|
||||
}
|
||||
|
||||
func (v01 *V01) serveGMSwap(userA, userB string) (int, error) {
|
||||
if userA == userB {
|
||||
return http.StatusConflict, errors.New("/spiderman-pointing")
|
||||
}
|
||||
_, okA := v01.cfg.Users[userA]
|
||||
_, okB := v01.cfg.Users[userB]
|
||||
if !okA || !okB {
|
||||
return http.StatusBadRequest, errors.New("who dat?")
|
||||
}
|
||||
a := v01.cfg.Users[userA]
|
||||
b := v01.cfg.Users[userB]
|
||||
a.State.Player, b.State.Player = b.State.Player, a.State.Player
|
||||
v01.cfg.Users[userA] = a
|
||||
v01.cfg.Users[userB] = b
|
||||
v01.cfg.Quiet = false
|
||||
v01.servePutBroadcastValue(fmt.Sprintf(`%s is now player %v and %s is now player %v`, userA, a.State.Player, userB, b.State.Player))
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,592 @@
|
|||
package v01
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func TestPatchConfig(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
p := path.Join(dir, t.Name()+".yaml")
|
||||
cases := map[string]struct {
|
||||
was config
|
||||
patch string
|
||||
want config
|
||||
}{
|
||||
"replace entire doc": {
|
||||
was: config{
|
||||
Feedback: configFeedback{Addr: "a", TTSURL: "a"},
|
||||
Users: map[string]configUser{"a": configUser{State: configUserState{Player: 1, Message: "a"}}},
|
||||
Players: []configPlayer{configPlayer{Transformation: transformation{"a": "a"}}},
|
||||
Quiet: true,
|
||||
},
|
||||
patch: `[{"op": "replace", "path": "", "value": {
|
||||
"Feedback": {"Addr": "b", "TTSURL": "b"},
|
||||
"Users": {"b": {"State":{"Player": 2, "Message": "b"}}},
|
||||
"Players": [{"Transformation": {"b": "b"}}],
|
||||
"Quiet": false
|
||||
}}]`,
|
||||
want: config{
|
||||
Feedback: configFeedback{Addr: "b", TTSURL: "b"},
|
||||
Users: map[string]configUser{"b": configUser{State: configUserState{Player: 2, Message: "b"}}},
|
||||
Players: []configPlayer{configPlayer{Transformation: transformation{"b": "b"}}},
|
||||
Quiet: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, d := range cases {
|
||||
c := d
|
||||
for _, usesdisk := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("%s disk=%v", name, usesdisk), func(t *testing.T) {
|
||||
b, _ := yaml.Marshal(c.was)
|
||||
os.WriteFile(p, b, os.ModePerm)
|
||||
FlagParseV01Config = ""
|
||||
if usesdisk {
|
||||
FlagParseV01Config = p
|
||||
}
|
||||
v01 := &V01{cfg: c.was}
|
||||
v01.cfg.lock = &sync.Mutex{}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest(http.MethodPatch, "/config", strings.NewReader(c.patch))
|
||||
v01.servePatchConfig(w, r)
|
||||
if fmt.Sprintf("%+v", c.want) != fmt.Sprintf("%+v", v01.cfg) {
|
||||
t.Errorf("want \n\t%+v, got \n\t%+v", c.want, v01.cfg)
|
||||
}
|
||||
if usesdisk {
|
||||
b, _ := os.ReadFile(p)
|
||||
var got config
|
||||
yaml.Unmarshal(b, &got)
|
||||
if fmt.Sprintf("%+v", c.want) != fmt.Sprintf("%+v", v01.cfg) {
|
||||
t.Errorf("want \n\t%+v, got \n\t%+v", c.want, v01.cfg)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeGM(t *testing.T) {
|
||||
ctx, can := context.WithCancel(context.Background())
|
||||
defer can()
|
||||
|
||||
do := func(v01 *V01, path, body string, method ...string) *httptest.ResponseRecorder {
|
||||
m := http.MethodPost
|
||||
if len(method) > 0 {
|
||||
m = method[0]
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest(m, path, strings.NewReader(body))
|
||||
v01.ServeHTTP(w, r)
|
||||
return w
|
||||
}
|
||||
|
||||
t.Run("status", func(t *testing.T) {
|
||||
v01 := NewV01(ctx, nil)
|
||||
var result struct {
|
||||
Players int `yaml:"Players"`
|
||||
Users map[string]string `yaml:"Users"`
|
||||
}
|
||||
|
||||
t.Run("empty", func(t *testing.T) {
|
||||
resp := do(v01, "/gm/rpc/status", "")
|
||||
if resp.Code != http.StatusOK {
|
||||
t.Error(resp.Code)
|
||||
}
|
||||
t.Log(string(resp.Body.Bytes()))
|
||||
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.Players != 0 {
|
||||
t.Error(result.Players)
|
||||
}
|
||||
if len(result.Users) != 0 {
|
||||
t.Error(result.Users)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("full", func(t *testing.T) {
|
||||
v01.cfg.Players = []configPlayer{
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
}
|
||||
v01.cfg.Users = map[string]configUser{
|
||||
"bel": configUser{
|
||||
State: configUserState{Player: 3},
|
||||
Meta: configUserMeta{
|
||||
LastTSMS: time.Now().Add(-1*time.Minute).UnixNano() / int64(time.Millisecond),
|
||||
LastLag: int64(time.Second / time.Millisecond),
|
||||
},
|
||||
},
|
||||
"zach": configUser{},
|
||||
"chase": configUser{},
|
||||
"mason": configUser{},
|
||||
"nat": configUser{},
|
||||
"roxy": configUser{},
|
||||
"bill": configUser{},
|
||||
}
|
||||
resp := do(v01, "/gm/rpc/status", "")
|
||||
if resp.Code != http.StatusOK {
|
||||
t.Error(resp.Code)
|
||||
}
|
||||
t.Log(string(resp.Body.Bytes()))
|
||||
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.Players != 4 {
|
||||
t.Error(result.Players)
|
||||
}
|
||||
if len(result.Users) != 7 {
|
||||
t.Error(result.Users)
|
||||
}
|
||||
if result.Users["bel"] == "" || result.Users["bel"] == "..." {
|
||||
t.Error(result.Users["bel"])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("broadcastSomeoneSaidAlias to hotword tap", func(t *testing.T) {
|
||||
v01 := NewV01(ctx, nil)
|
||||
v01.cfg.GM = configGM{
|
||||
Hotwords: map[string]configGMHotword{
|
||||
"hotword": configGMHotword{
|
||||
Call: "tap",
|
||||
Args: []string{"a"},
|
||||
},
|
||||
},
|
||||
}
|
||||
do(v01, "/gm/rpc/broadcastSomeoneSaidAlias?message=hotword", "")
|
||||
for i := 0; i < 2; i++ {
|
||||
select {
|
||||
case btn := <-v01.alt:
|
||||
if len(btn) != 1 {
|
||||
t.Error(btn)
|
||||
} else if btn[0].Down != (i == 0) {
|
||||
t.Error(btn[0])
|
||||
} else if btn[0].Char != 'a' {
|
||||
t.Error(btn[0].Char)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("nothing in alt")
|
||||
}
|
||||
}
|
||||
do(v01, "/gm/rpc/broadcastSomeoneSaidAlias?message=hotword", "")
|
||||
time.Sleep(time.Millisecond * 150)
|
||||
if got := v01.Read(); len(got) != 1 || !got[0].Down || got[0].Char != 'a' {
|
||||
t.Error(got)
|
||||
} else if got := v01.Read(); len(got) != 1 || got[0].Down || got[0].Char != 'a' {
|
||||
t.Error(got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("broadcastSomeoneSaidAlias", func(t *testing.T) {
|
||||
v01 := NewV01(ctx, nil)
|
||||
v01.cfg.Quiet = false
|
||||
v01.cfg.Users = map[string]configUser{
|
||||
"bel": configUser{State: configUserState{
|
||||
GM: configUserStateGM{
|
||||
Alias: "driver",
|
||||
},
|
||||
Message: "if someone else says 'driver', then you get to play",
|
||||
}},
|
||||
}
|
||||
v01.cfg.Broadcast.Message = ":)"
|
||||
do(v01, "/gm/rpc/broadcastSomeoneSaidAlias", "")
|
||||
if !v01.cfg.Quiet {
|
||||
t.Error(v01.cfg.Quiet)
|
||||
}
|
||||
if v := v01.cfg.Users["bel"]; v.State.GM.Alias != "" {
|
||||
t.Error(v.State.GM.Alias)
|
||||
} else if v.State.GM.LastAlias != "driver" {
|
||||
t.Error(v.State.GM.LastAlias)
|
||||
}
|
||||
if bc := v01.cfg.Broadcast.Message; bc == ":)" {
|
||||
t.Error(bc)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fillNonPlayerAliases", func(t *testing.T) {
|
||||
t.Run("empty", func(t *testing.T) {
|
||||
v01 := NewV01(ctx, nil)
|
||||
v01.cfg.Users = nil
|
||||
resp := do(v01, "/gm/rpc/fillNonPlayerAliases", "[qt]")
|
||||
if resp.Code != http.StatusNoContent {
|
||||
t.Error(resp.Code)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("not enough", func(t *testing.T) {
|
||||
v01 := NewV01(ctx, nil)
|
||||
v01.cfg.Users = map[string]configUser{
|
||||
"zach": configUser{State: configUserState{Player: 0}},
|
||||
}
|
||||
resp := do(v01, "/gm/rpc/fillNonPlayerAliases", "[]")
|
||||
if resp.Code != http.StatusBadRequest {
|
||||
t.Error(resp.Code)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("happy", func(t *testing.T) {
|
||||
v01 := NewV01(ctx, nil)
|
||||
v01.cfg.Users = map[string]configUser{
|
||||
"bel": configUser{State: configUserState{Player: 1}},
|
||||
"zach": configUser{State: configUserState{Player: 0}},
|
||||
}
|
||||
do(v01, "/gm/rpc/fillNonPlayerAliases", "[qt]")
|
||||
if v := v01.cfg.Users["bel"]; v.State.GM.Alias != "" {
|
||||
t.Error(v.State.GM.Alias)
|
||||
} else if v.State.Player != 1 {
|
||||
t.Error(v.State.Player)
|
||||
}
|
||||
if v := v01.cfg.Users["zach"]; v.State.GM.Alias != "qt" {
|
||||
t.Error(v.State.GM.Alias)
|
||||
} else if v.State.Player != 0 {
|
||||
t.Error(v.State.Player)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("vote", func(t *testing.T) {
|
||||
type result map[string]string
|
||||
|
||||
t.Run("cast bad vote", func(t *testing.T) {
|
||||
v01 := NewV01(ctx, nil)
|
||||
v01.cfg.Users = map[string]configUser{"bel": {}}
|
||||
resp := do(v01, "/gm/rpc/vote?user=bel&payload=?", "")
|
||||
if resp.Code != http.StatusBadRequest {
|
||||
t.Error(resp)
|
||||
}
|
||||
if v01.cfg.Users["bel"].State.Message != "" {
|
||||
t.Error(v01.cfg.Users["bel"].State.Message)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("cast vote", func(t *testing.T) {
|
||||
v01 := NewV01(ctx, nil)
|
||||
v01.cfg.Users = map[string]configUser{"bel": {}, "zach": {}}
|
||||
do(v01, "/gm/rpc/vote?user=bel&payload=zach", "")
|
||||
if v01.cfg.Users["bel"].State.GM.Vote != "zach" {
|
||||
t.Error(v01.cfg.Users["bel"].State.GM.Vote)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("get non vote", func(t *testing.T) {
|
||||
v01 := NewV01(ctx, nil)
|
||||
v01.cfg.Users = map[string]configUser{"bel": {}}
|
||||
resp := do(v01, "/gm/rpc/vote", "", "GET")
|
||||
var result result
|
||||
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if len(result) != 1 {
|
||||
t.Error(result)
|
||||
}
|
||||
if result["bel"] != "voting" {
|
||||
t.Error(result)
|
||||
}
|
||||
t.Logf("%+v", result)
|
||||
})
|
||||
|
||||
t.Run("get mid vote", func(t *testing.T) {
|
||||
v01 := NewV01(ctx, nil)
|
||||
v01.cfg.Users = map[string]configUser{"bel": {State: configUserState{GM: configUserStateGM{Vote: "zach"}, Message: "driver"}}}
|
||||
resp := do(v01, "/gm/rpc/vote", "", "GET")
|
||||
var result result
|
||||
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if len(result) != 1 {
|
||||
t.Error(result)
|
||||
}
|
||||
if result["bel"] != "voted" {
|
||||
t.Error(result)
|
||||
}
|
||||
t.Logf("%+v", result)
|
||||
})
|
||||
|
||||
t.Run("get empty", func(t *testing.T) {
|
||||
v01 := NewV01(ctx, nil)
|
||||
v01.cfg.Users = nil
|
||||
resp := do(v01, "/gm/rpc/vote", "", "GET")
|
||||
var result result
|
||||
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if len(result) != 0 {
|
||||
t.Error(result)
|
||||
}
|
||||
t.Logf("%+v", result)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("elect", func(t *testing.T) {
|
||||
type result map[string]int
|
||||
|
||||
t.Run("happy", func(t *testing.T) {
|
||||
v01 := NewV01(ctx, nil)
|
||||
v01.cfg.Users = map[string]configUser{
|
||||
"bel": configUser{State: configUserState{GM: configUserStateGM{Vote: "zach", LastAlias: "driver"}, Player: 1}},
|
||||
"zach": configUser{State: configUserState{GM: configUserStateGM{Vote: "bel", LastAlias: "pizza"}}},
|
||||
"bill": configUser{State: configUserState{GM: configUserStateGM{Vote: "bel"}, Player: 2}},
|
||||
}
|
||||
resp := do(v01, "/gm/rpc/elect?alias=pizza", "")
|
||||
var result result
|
||||
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
|
||||
t.Errorf("%s => %v", resp.Body.Bytes(), err)
|
||||
}
|
||||
if len(result) != 2 {
|
||||
t.Error(result)
|
||||
} else if result["bel"] != 2 {
|
||||
t.Error(result)
|
||||
} else if result["zach"] != 1 {
|
||||
t.Error(result)
|
||||
}
|
||||
if v01.cfg.Users["bel"].State.Player != 0 {
|
||||
t.Error(v01.cfg.Users["bel"].State.Player)
|
||||
} else if v01.cfg.Users["zach"].State.Player != 1 {
|
||||
t.Error(v01.cfg.Users["zach"].State.Player)
|
||||
}
|
||||
if v01.cfg.Broadcast.Message != `bel is now player 0 and zach is now player 1` {
|
||||
t.Error(v01.cfg.Broadcast)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("self", func(t *testing.T) {
|
||||
v01 := NewV01(ctx, nil)
|
||||
v01.cfg.Players = []configPlayer{{}}
|
||||
v01.cfg.Users = map[string]configUser{
|
||||
"bel": configUser{State: configUserState{GM: configUserStateGM{Vote: "zach", LastAlias: "driver"}, Player: 1}},
|
||||
"zach": configUser{State: configUserState{GM: configUserStateGM{Vote: "bel"}}},
|
||||
"bill": configUser{State: configUserState{GM: configUserStateGM{Vote: "bel"}}},
|
||||
}
|
||||
resp := do(v01, "/gm/rpc/elect?alias=driver", "")
|
||||
var result result
|
||||
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if len(result) != 2 {
|
||||
t.Error(result)
|
||||
} else if result["bel"] != 2 {
|
||||
t.Error(result)
|
||||
} else if result["zach"] != 1 {
|
||||
t.Error(result)
|
||||
}
|
||||
if !strings.HasSuffix(v01.cfg.Broadcast.Message, `is now player 1`) || strings.Contains(v01.cfg.Broadcast.Message, ",") {
|
||||
t.Error(v01.cfg.Broadcast.Message)
|
||||
}
|
||||
assignments := map[int]int{}
|
||||
for _, v := range v01.cfg.Users {
|
||||
assignments[v.State.Player] = assignments[v.State.Player] + 1
|
||||
}
|
||||
if len(assignments) != 2 {
|
||||
t.Error(assignments)
|
||||
} else if assignments[0] != 2 {
|
||||
t.Error(assignments[0])
|
||||
} else if assignments[1] != 1 {
|
||||
t.Error(assignments[1])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("tie", func(t *testing.T) {
|
||||
v01 := NewV01(ctx, nil)
|
||||
v01.cfg.Players = []configPlayer{{}}
|
||||
v01.cfg.Users = map[string]configUser{
|
||||
"bel": configUser{State: configUserState{GM: configUserStateGM{Vote: "zach", LastAlias: "driver"}, Player: 1}},
|
||||
"zach": configUser{State: configUserState{GM: configUserStateGM{Vote: "bel", LastAlias: "pizza"}}},
|
||||
}
|
||||
resp := do(v01, "/gm/rpc/elect?alias=pizza", "")
|
||||
var result result
|
||||
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if len(result) != 2 {
|
||||
t.Error(result)
|
||||
} else if result["bel"] != 1 {
|
||||
t.Error(result)
|
||||
} else if result["zach"] != 1 {
|
||||
t.Error(result)
|
||||
}
|
||||
if bc := v01.cfg.Broadcast.Message; !strings.HasSuffix(bc, `is now player 1`) || strings.Contains(bc, ",") {
|
||||
t.Error(bc)
|
||||
}
|
||||
assignments := map[int]int{}
|
||||
for _, v := range v01.cfg.Users {
|
||||
assignments[v.State.Player] = assignments[v.State.Player] + 1
|
||||
}
|
||||
if len(assignments) != 2 {
|
||||
t.Error(assignments)
|
||||
} else if assignments[0] != 1 {
|
||||
t.Error(assignments[0])
|
||||
} else if assignments[1] != 1 {
|
||||
t.Error(assignments[1])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("shuffle", func(t *testing.T) {
|
||||
t.Run("many 2u 2p", func(t *testing.T) {
|
||||
v01 := NewV01(ctx, nil)
|
||||
for i := 0; i < 100; i++ {
|
||||
v01.cfg.Quiet = true
|
||||
v01.cfg.Users = map[string]configUser{
|
||||
"bel": configUser{State: configUserState{Player: 1}},
|
||||
"zach": configUser{State: configUserState{Player: 2}},
|
||||
}
|
||||
v01.cfg.Players = []configPlayer{{}, {}}
|
||||
do(v01, "/gm/rpc/shuffle", "")
|
||||
if v01.cfg.Quiet {
|
||||
t.Error(v01.cfg.Quiet)
|
||||
}
|
||||
if len(v01.cfg.Users) != 2 {
|
||||
t.Error(v01.cfg.Users)
|
||||
} else if len(v01.cfg.Players) != 2 {
|
||||
t.Error(v01.cfg.Users)
|
||||
} else if bp := v01.cfg.Users["bel"].State.Player; bp != 1 && bp != 2 {
|
||||
t.Error(bp)
|
||||
} else if zp := v01.cfg.Users["zach"].State.Player; zp != 1 && zp != 2 {
|
||||
t.Error(zp)
|
||||
} else if bp == zp {
|
||||
t.Error(bp, zp)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
cases := map[string]struct {
|
||||
users int
|
||||
usersAssigned int
|
||||
players int
|
||||
}{
|
||||
"empty": {},
|
||||
"just users": {users: 2},
|
||||
"just players": {players: 2},
|
||||
"2 unassigned users and 2 players": {users: 2, players: 2},
|
||||
"2 users and 2 players": {users: 2, usersAssigned: 2, players: 2},
|
||||
"1 users and 2 players": {users: 1, usersAssigned: 1, players: 2},
|
||||
"1 unassigned users and 2 players": {users: 1, players: 2},
|
||||
"4 players for 7 users 0 assigned": {users: 7, players: 4},
|
||||
"4 players for 7 users 4 assigned": {users: 7, players: 4, usersAssigned: 4},
|
||||
}
|
||||
|
||||
for name, d := range cases {
|
||||
c := d
|
||||
t.Run(name, func(t *testing.T) {
|
||||
v01 := NewV01(ctx, nil)
|
||||
v01.cfg.Quiet = true
|
||||
v01.cfg.Users = map[string]configUser{}
|
||||
for i := 0; i < c.users; i++ {
|
||||
v01.cfg.Users[strconv.Itoa(i)] = configUser{}
|
||||
if i < c.usersAssigned {
|
||||
v01.cfg.Users[strconv.Itoa(i)] = configUser{State: configUserState{Player: i}}
|
||||
}
|
||||
}
|
||||
v01.cfg.Players = make([]configPlayer, c.players)
|
||||
|
||||
do(v01, "/gm/rpc/shuffle", "")
|
||||
if v01.cfg.Quiet {
|
||||
t.Error(v01.cfg.Quiet)
|
||||
}
|
||||
|
||||
if len(v01.cfg.Users) != c.users {
|
||||
t.Error(v01.cfg.Users)
|
||||
} else if len(v01.cfg.Players) != c.players {
|
||||
t.Error(v01.cfg.Users)
|
||||
}
|
||||
for i := 0; i < c.users; i++ {
|
||||
if _, ok := v01.cfg.Users[strconv.Itoa(i)]; !ok {
|
||||
t.Error(i)
|
||||
}
|
||||
}
|
||||
assignments := map[int]int{}
|
||||
for _, v := range v01.cfg.Users {
|
||||
if v.State.Player > 0 {
|
||||
assignments[v.State.Player] = assignments[v.State.Player] + 1
|
||||
}
|
||||
}
|
||||
lesser := c.users
|
||||
if c.players < lesser {
|
||||
lesser = c.players
|
||||
}
|
||||
if len(assignments) != lesser {
|
||||
t.Error(assignments)
|
||||
}
|
||||
for _, v := range assignments {
|
||||
if v != 1 {
|
||||
t.Error(v)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("swap", func(t *testing.T) {
|
||||
t.Run("self", func(t *testing.T) {
|
||||
v01 := NewV01(ctx, nil)
|
||||
v01.cfg.Quiet = true
|
||||
v01.cfg.Users = map[string]configUser{
|
||||
"bel": configUser{State: configUserState{Player: 1}},
|
||||
}
|
||||
resp := do(v01, "/gm/rpc/swap?a=bel&b=bel", "")
|
||||
if resp.Code != http.StatusConflict {
|
||||
t.Error(resp.Code)
|
||||
}
|
||||
if !v01.cfg.Quiet {
|
||||
t.Error(v01.cfg.Quiet)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("who", func(t *testing.T) {
|
||||
v01 := NewV01(ctx, nil)
|
||||
v01.cfg.Quiet = true
|
||||
resp := do(v01, "/gm/rpc/swap?a=bel", "")
|
||||
if resp.Code != http.StatusBadRequest {
|
||||
t.Error(resp.Code)
|
||||
}
|
||||
if !v01.cfg.Quiet {
|
||||
t.Error(v01.cfg.Quiet)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("happy", func(t *testing.T) {
|
||||
v01 := NewV01(ctx, nil)
|
||||
v01.cfg.Quiet = true
|
||||
v01.cfg.Users = map[string]configUser{
|
||||
"bel": configUser{State: configUserState{Player: 1}},
|
||||
"zach": configUser{State: configUserState{Player: 2}},
|
||||
}
|
||||
resp := do(v01, "/gm/rpc/swap?a=bel&b=zach", "")
|
||||
if resp.Code != http.StatusOK {
|
||||
t.Error(resp.Code)
|
||||
}
|
||||
if v01.cfg.Quiet {
|
||||
t.Error(v01.cfg.Quiet)
|
||||
}
|
||||
if v01.cfg.Users["bel"].State.Player != 2 {
|
||||
t.Error(v01.cfg.Users["bel"])
|
||||
} else if v01.cfg.Users["zach"].State.Player != 1 {
|
||||
t.Error(v01.cfg.Users["zach"])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("404", func(t *testing.T) {
|
||||
v01 := NewV01(ctx, nil)
|
||||
resp := do(v01, "/gm/teehee", "")
|
||||
if resp.Code != http.StatusNotFound {
|
||||
t.Error(resp.Code)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
feedback:
|
||||
addr: :17071
|
||||
ttsurl: http://localhost:15002
|
||||
users:
|
||||
bel:
|
||||
meta:
|
||||
lasttsms: 1681062770999
|
||||
lastlag: 12
|
||||
state:
|
||||
player: 0
|
||||
message: hi
|
||||
gm:
|
||||
alias: ""
|
||||
lastalias: ""
|
||||
vote: ""
|
||||
players:
|
||||
- transformation: {}
|
||||
quiet: false
|
||||
broadcast:
|
||||
message: hi
|
||||
gm:
|
||||
hotwords:
|
||||
coin:
|
||||
call: tap
|
||||
args: ['!']
|
||||
star:
|
||||
call: tap
|
||||
args: ['?']
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package v01
|
||||
|
||||
type (
|
||||
transformation map[string]string
|
||||
)
|
||||
|
||||
func (t transformation) pipe(s string) string {
|
||||
for i := range s {
|
||||
if v := t[s[i:i+1]]; v != "" {
|
||||
s = s[:i] + v[:1] + s[i+1:]
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
package v01
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/faiface/beep"
|
||||
"github.com/faiface/beep/effects"
|
||||
"github.com/faiface/beep/speaker"
|
||||
"github.com/faiface/beep/wav"
|
||||
)
|
||||
|
||||
var (
|
||||
ttsLock = &sync.RWMutex{}
|
||||
)
|
||||
|
||||
func (v01 *V01) tts(text string) {
|
||||
if err := v01._tts(text); err != nil {
|
||||
log.Printf("failed to tts: %s: %v", text, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (v01 *V01) _tts(text string) error {
|
||||
if v01.cfg.Feedback.TTSURL == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
url, err := url.Parse(v01.cfg.Feedback.TTSURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(url.Path) < 2 {
|
||||
url.Path = "/api/tts"
|
||||
}
|
||||
q := url.Query()
|
||||
if q.Get("voice") == "" {
|
||||
q.Set("voice", "en-us/glados-glow_tts")
|
||||
}
|
||||
if q.Get("lengthScale") == "" {
|
||||
q.Set("lengthScale", "1")
|
||||
}
|
||||
q.Set("text", text)
|
||||
url.RawQuery = q.Encode()
|
||||
|
||||
resp, err := http.Get(url.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
b, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode != http.StatusOK || resp.Header.Get("Content-Type") != "audio/wav" {
|
||||
return fmt.Errorf("failed to call ttsurl: (%d) %s", resp.StatusCode, b)
|
||||
}
|
||||
|
||||
decoder, format, err := wav.Decode(bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ttsLock.Lock()
|
||||
defer ttsLock.Unlock()
|
||||
speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/30))
|
||||
speaker.Play(&effects.Volume{Streamer: beep.ResampleRatio(4, 1, &beep.Ctrl{Streamer: beep.Loop(1, decoder)})})
|
||||
duration := time.Duration(decoder.Len()) * format.SampleRate.D(1)
|
||||
select {
|
||||
case <-v01.ctx.Done():
|
||||
case <-time.After(duration):
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
package v01
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"mayhem-party/src/device/input/button"
|
||||
"mayhem-party/src/device/input/raw"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
var (
|
||||
FlagDebug = os.Getenv("DEBUG") == "true"
|
||||
FlagParseV01Config = os.Getenv("V01_CONFIG")
|
||||
)
|
||||
|
||||
type (
|
||||
V01 struct {
|
||||
ctx context.Context
|
||||
can context.CancelFunc
|
||||
src raw.Raw
|
||||
cfg config
|
||||
telemetryc chan message
|
||||
alt chan []button.Button
|
||||
}
|
||||
)
|
||||
|
||||
func NewV01(ctx context.Context, src raw.Raw) *V01 {
|
||||
var cfg config
|
||||
cfg.lock = &sync.Mutex{}
|
||||
b, _ := ioutil.ReadFile(FlagParseV01Config)
|
||||
yaml.Unmarshal(b, &cfg)
|
||||
ctx, can := context.WithCancel(ctx)
|
||||
result := &V01{
|
||||
ctx: ctx,
|
||||
can: can,
|
||||
src: src,
|
||||
cfg: cfg,
|
||||
telemetryc: make(chan message),
|
||||
alt: make(chan []button.Button, 2),
|
||||
}
|
||||
go result.listen()
|
||||
go result.dotelemetry()
|
||||
return result
|
||||
}
|
||||
|
||||
func (v01 *V01) CloseWrap() raw.Raw {
|
||||
v01.can()
|
||||
return v01.src
|
||||
}
|
||||
|
||||
func (v01 *V01) Close() {
|
||||
v01.can()
|
||||
v01.src.Close()
|
||||
}
|
||||
|
||||
func (v01 *V01) Read() []button.Button {
|
||||
select {
|
||||
case alt := <-v01.alt:
|
||||
return alt
|
||||
default:
|
||||
}
|
||||
line := v01.src.Read()
|
||||
var msg message
|
||||
if err := json.Unmarshal(line, &msg); err != nil {
|
||||
log.Printf("%v: %s", err, line)
|
||||
}
|
||||
v01.telemetry(msg)
|
||||
|
||||
buttons := v01.transform(msg).buttons()
|
||||
if v01.cfg.Quiet {
|
||||
for i := range buttons {
|
||||
buttons[i].Down = false
|
||||
}
|
||||
}
|
||||
return buttons
|
||||
}
|
||||
|
||||
func (v01 *V01) dotelemetry() {
|
||||
for {
|
||||
select {
|
||||
case <-v01.ctx.Done():
|
||||
return
|
||||
case msg := <-v01.telemetryc:
|
||||
v01._telemetry(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (v01 *V01) telemetry(msg message) {
|
||||
select {
|
||||
case v01.telemetryc <- msg:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (v01 *V01) _telemetry(msg message) {
|
||||
// TODO oof
|
||||
v01.cfg.lock.Lock()
|
||||
defer v01.cfg.lock.Unlock()
|
||||
if v01.cfg.Users == nil {
|
||||
v01.cfg.Users = map[string]configUser{}
|
||||
}
|
||||
u := v01.cfg.Users[msg.U]
|
||||
u.Meta.LastLag = time.Now().UnixNano()/int64(time.Millisecond) - msg.T
|
||||
u.Meta.LastTSMS = msg.T
|
||||
if FlagDebug {
|
||||
log.Printf("%s|%dms", msg.U, u.Meta.LastLag)
|
||||
}
|
||||
v01.cfg.Users[msg.U] = u
|
||||
}
|
||||
|
||||
func (v01 *V01) transform(msg message) message {
|
||||
if len(v01.cfg.Players) == 0 {
|
||||
return msg
|
||||
}
|
||||
user := v01.cfg.Users[msg.U]
|
||||
if user.State.Player < 1 {
|
||||
msg.Y = ""
|
||||
msg.N = ""
|
||||
return msg
|
||||
}
|
||||
player := v01.cfg.Players[user.State.Player-1]
|
||||
msg.Y = player.Transformation.pipe(msg.Y)
|
||||
msg.N = player.Transformation.pipe(msg.N)
|
||||
return msg
|
||||
}
|
||||
|
|
@ -0,0 +1,192 @@
|
|||
package v01_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"mayhem-party/src/device/input/button"
|
||||
v01 "mayhem-party/src/device/input/parse/v01"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestV01(t *testing.T) {
|
||||
src := constSrc(fmt.Sprintf(`{"T":%v,"U":"bel","Y":"abc","N":"cde"}`, time.Now().UnixNano()/int64(time.Millisecond)-50))
|
||||
t.Logf("(%v) %s", len(src), src.Read())
|
||||
v01 := v01.NewV01(context.Background(), src)
|
||||
defer v01.Close()
|
||||
got := v01.Read()
|
||||
want := []button.Button{
|
||||
{Down: true, Char: 'a'},
|
||||
{Down: true, Char: 'b'},
|
||||
{Down: true, Char: 'c'},
|
||||
{Down: false, Char: 'c'},
|
||||
{Down: false, Char: 'd'},
|
||||
{Down: false, Char: 'e'},
|
||||
}
|
||||
if len(got) != len(want) {
|
||||
t.Fatal(len(want), len(got))
|
||||
}
|
||||
for i := range got {
|
||||
if got[i] != want[i] {
|
||||
t.Errorf("[%d] want %+v got %+v", i, want[i], got[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestV01WithCfg(t *testing.T) {
|
||||
d := t.TempDir()
|
||||
p := path.Join(d, "cfg.yaml")
|
||||
os.WriteFile(p, []byte(`
|
||||
users:
|
||||
bel:
|
||||
state:
|
||||
player: 2
|
||||
players:
|
||||
- transformation:
|
||||
w: t
|
||||
- transformation:
|
||||
w: i
|
||||
`), os.ModePerm)
|
||||
v01.FlagParseV01Config = p
|
||||
|
||||
t.Run("unknown user ignored", func(t *testing.T) {
|
||||
v01 := v01.NewV01(context.Background(), constSrc(`{"U":"qt","Y":"w"}`))
|
||||
defer v01.Close()
|
||||
got := v01.Read()
|
||||
if len(got) != 0 {
|
||||
t.Error(got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("player2", func(t *testing.T) {
|
||||
v01 := v01.NewV01(context.Background(), constSrc(`{"U":"bel","Y":"w","N":"w"}`))
|
||||
defer v01.Close()
|
||||
got := v01.Read()
|
||||
if len(got) != 2 {
|
||||
t.Error(got)
|
||||
}
|
||||
if got[0] != (button.Button{Char: 'i', Down: true}) {
|
||||
t.Error(got[0])
|
||||
}
|
||||
if got[1] != (button.Button{Char: 'i', Down: false}) {
|
||||
t.Error(got[1])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestV01Feedback(t *testing.T) {
|
||||
d := t.TempDir()
|
||||
p := path.Join(d, "cfg.yaml")
|
||||
os.WriteFile(p, []byte(`
|
||||
feedback:
|
||||
addr: :27071
|
||||
ttsurl: http://localhost:15002
|
||||
users:
|
||||
bel:
|
||||
state:
|
||||
player: 2
|
||||
message: to bel
|
||||
broadcast:
|
||||
message: to everyone
|
||||
players:
|
||||
- transformation:
|
||||
w: t
|
||||
- transformation:
|
||||
w: i
|
||||
`), os.ModePerm)
|
||||
v01.FlagParseV01Config = p
|
||||
ctx, can := context.WithCancel(context.Background())
|
||||
defer can()
|
||||
|
||||
v01 := v01.NewV01(ctx, constSrc(`{"U":"qt","Y":"w"}`))
|
||||
defer v01.Close()
|
||||
|
||||
for {
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
resp, err := http.Get("http://localhost:27071?user=bel")
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
resp.Body.Close()
|
||||
break
|
||||
}
|
||||
|
||||
t.Run("specific user", func(t *testing.T) {
|
||||
resp, err := http.Get("http://localhost:27071?user=bel")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
b, _ := io.ReadAll(resp.Body)
|
||||
if !strings.HasPrefix(string(b), "to bel") {
|
||||
t.Error(string(b))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("broadcast", func(t *testing.T) {
|
||||
resp, err := http.Get("http://localhost:27071")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
b, _ := io.ReadAll(resp.Body)
|
||||
if !strings.HasPrefix(string(b), "to everyone") {
|
||||
t.Error(string(b))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("change broadcast", func(t *testing.T) {
|
||||
want := `my new broadcast`
|
||||
r, _ := http.NewRequest(http.MethodPut, "http://localhost:27071/broadcast", strings.NewReader(want))
|
||||
resp, err := http.DefaultClient.Do(r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
resp, err = http.Get("http://localhost:27071")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
b, _ := io.ReadAll(resp.Body)
|
||||
if !strings.HasPrefix(string(b), want) {
|
||||
t.Error(string(b))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("tts", func(t *testing.T) {
|
||||
if os.Getenv("INTEGRATION_TTS") != "true" {
|
||||
t.Skip("$INTEGRATION_TTS is not true")
|
||||
}
|
||||
for i := 0; i < 2; i++ {
|
||||
resp, err := http.Get("http://localhost:27071/?say=hello%20world")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
}
|
||||
time.Sleep(time.Millisecond * 2500)
|
||||
r, _ := http.NewRequest(http.MethodGet, "http://localhost:27071", nil)
|
||||
r.Header.Set("say", "No, HTTP does not define any limit. However most web servers do limit size of headers they accept. For example in Apache default limit is 8KB, in IIS it's 16K. Server will return 413 Entity Too Large error if headers size exceeds that limit.")
|
||||
resp, err := http.DefaultClient.Do(r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
time.Sleep(time.Millisecond * 8500)
|
||||
})
|
||||
}
|
||||
|
||||
type constSrc string
|
||||
|
||||
func (c constSrc) Close() {}
|
||||
|
||||
func (c constSrc) Read() []byte {
|
||||
return []byte(c)
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
package v01
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestV01TransformationPipe(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
input string
|
||||
xform map[string]string
|
||||
want string
|
||||
}{
|
||||
"empty input": {
|
||||
xform: map[string]string{"a": "bc"},
|
||||
},
|
||||
"empty xform": {
|
||||
input: "aa",
|
||||
want: "aa",
|
||||
},
|
||||
"all": {
|
||||
input: "aa",
|
||||
xform: map[string]string{"a": "cc"},
|
||||
want: "cc",
|
||||
},
|
||||
"last": {
|
||||
input: "ba",
|
||||
xform: map[string]string{"a": "cc"},
|
||||
want: "bc",
|
||||
},
|
||||
"first": {
|
||||
input: "ab",
|
||||
xform: map[string]string{"a": "cc"},
|
||||
want: "cb",
|
||||
},
|
||||
"noop": {
|
||||
input: "bb",
|
||||
xform: map[string]string{"a": "bc"},
|
||||
want: "bb",
|
||||
},
|
||||
}
|
||||
|
||||
for name, d := range cases {
|
||||
c := d
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got := transformation(c.xform).pipe(c.input)
|
||||
if got != c.want {
|
||||
t.Errorf("%+v(%s) want %s got %s", c.xform, c.input, c.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestV01Quiet(t *testing.T) {
|
||||
ctx, can := context.WithCancel(context.Background())
|
||||
defer can()
|
||||
v01 := NewV01(ctx, constSrc(`{"Y":"a", "N":"b"}`))
|
||||
|
||||
v01.cfg.Quiet = false
|
||||
if got := v01.Read(); len(got) != 2 {
|
||||
t.Error(len(got))
|
||||
} else if got[0].Char != 'a' {
|
||||
t.Error(got[0].Char)
|
||||
} else if got[0].Down != true {
|
||||
t.Error(got[0].Down)
|
||||
} else if got[1].Char != 'b' {
|
||||
t.Error(got[1].Char)
|
||||
} else if got[1].Down != false {
|
||||
t.Error(got[1].Down)
|
||||
}
|
||||
|
||||
v01.cfg.Quiet = true
|
||||
if got := v01.Read(); len(got) != 2 {
|
||||
t.Error(len(got))
|
||||
} else if got[0].Char != 'a' {
|
||||
t.Error(got[0].Char)
|
||||
} else if got[0].Down != false {
|
||||
t.Error(got[0].Down)
|
||||
} else if got[1].Char != 'b' {
|
||||
t.Error(got[1].Char)
|
||||
} else if got[1].Down != false {
|
||||
t.Error(got[1].Down)
|
||||
}
|
||||
}
|
||||
|
||||
type constSrc string
|
||||
|
||||
func (c constSrc) Close() {}
|
||||
|
||||
func (c constSrc) Read() []byte {
|
||||
return []byte(c)
|
||||
}
|
||||
|
|
@ -50,7 +50,7 @@ func (kb Keyboard) Read() []byte {
|
|||
if err != nil && err != io.EOF {
|
||||
panic(err)
|
||||
}
|
||||
if os.Getenv("DEBUG") == "true" {
|
||||
if FlagDebug {
|
||||
log.Printf("raw.Keyboard.Read() %s", b[:n])
|
||||
}
|
||||
return b[:n]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
// a key map of allowed keys
|
||||
var allowedKeys = {
|
||||
37: 'left',
|
||||
38: 'up',
|
||||
39: 'right',
|
||||
40: 'down',
|
||||
65: 'a',
|
||||
66: 'b'
|
||||
};
|
||||
|
||||
// the 'official' Konami Code sequence
|
||||
var konamiCode = ['up', 'up', 'down', 'down', 'left', 'right', 'left', 'right', 'b', 'a'];
|
||||
|
||||
// a variable to remember the 'position' the user has reached so far.
|
||||
var konamiCodePosition = 0;
|
||||
|
||||
// add keydown event listener
|
||||
document.addEventListener('keydown', function(e) {
|
||||
// get the value of the key code from the key map
|
||||
var key = allowedKeys[e.keyCode];
|
||||
// get the value of the required key from the konami code
|
||||
var requiredKey = konamiCode[konamiCodePosition];
|
||||
|
||||
// compare the key with the required key
|
||||
if (key == requiredKey) {
|
||||
|
||||
// move to the next key in the konami code sequence
|
||||
konamiCodePosition++;
|
||||
|
||||
// if the last key is reached, activate cheats
|
||||
if (konamiCodePosition == konamiCode.length) {
|
||||
showSecrets();
|
||||
konamiCodePosition = 0;
|
||||
}
|
||||
} else {
|
||||
konamiCodePosition = 0;
|
||||
}
|
||||
});
|
||||
|
||||
function showSecrets() {
|
||||
var element = document.getElementById("konami")
|
||||
element.style = "display:block"
|
||||
var e = new Event("onKonami")
|
||||
element.dispatchEvent(new Event("onKonami"))
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
function onYouTubeIframeAPIReady() {
|
||||
|
||||
var player;
|
||||
|
||||
player = new YT.Player('konami', {
|
||||
videoId: 'V4oJ62xrFZo', // 👈 video id.
|
||||
width: 560,
|
||||
height: 316,
|
||||
playerVars: {
|
||||
'autoplay': 1,
|
||||
'controls': 1,
|
||||
'showinfo': 0,
|
||||
'modestbranding': 0,
|
||||
'loop': 1,
|
||||
'fs': 0,
|
||||
'cc_load_policty': 0,
|
||||
'iv_load_policy': 3
|
||||
},
|
||||
events: {
|
||||
'onReady': function (e) {
|
||||
e.target.setVolume(33); // For max value, set value to 100.
|
||||
document.getElementById("konami").addEventListener("onKonami", () => {e.target.playVideo()})
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<header>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/water.css@2/out/dark.css">
|
||||
<script src="konami.js"></script>
|
||||
<script src="lowerVolume.js"></script>
|
||||
<script async src="https://www.youtube.com/iframe_api"></script>
|
||||
<script>
|
||||
function formsay(message) {
|
||||
console.log(`say '${message}'`)
|
||||
http("GET", `/proxy?user=${document.getElementById("user").value}&say=${message}`, noopcallback, null)
|
||||
}
|
||||
function formsend(message) {
|
||||
console.log(`send '${message}'`)
|
||||
http("GET", `/proxy/gm/rpc/vote?user=${document.getElementById("user").value}&payload=${message}`, noopcallback, null)
|
||||
}
|
||||
function http(method, remote, callback, body) {
|
||||
var xmlhttp = new XMLHttpRequest();
|
||||
xmlhttp.onreadystatechange = function() {
|
||||
if (xmlhttp.readyState == XMLHttpRequest.DONE) {
|
||||
callback(xmlhttp.responseText, xmlhttp.status)
|
||||
}
|
||||
};
|
||||
xmlhttp.open(method, remote, true);
|
||||
if (typeof body == "undefined") {
|
||||
body = null
|
||||
}
|
||||
xmlhttp.send(body);
|
||||
}
|
||||
function noopcallback(responseBody, responseStatus) {
|
||||
}
|
||||
setInterval(() => {
|
||||
http("GET", `/proxy?user=${document.getElementById("user").value}`, (b, s) => {
|
||||
if (s != 200)
|
||||
return
|
||||
document.getElementById("ntfy").innerHTML = `<pre>${b}</pre>`
|
||||
}, null)
|
||||
}, 1500)
|
||||
|
||||
|
||||
</script>
|
||||
</header>
|
||||
<body>
|
||||
<div>
|
||||
<form>
|
||||
<h3>WHO AM I</h3>
|
||||
<select id="user">
|
||||
<option>bel</option>
|
||||
<option>zach</option>
|
||||
<option>chase</option>
|
||||
<option>mason</option>
|
||||
<option>nat</option>
|
||||
<option>roxy</option>
|
||||
<option>bill</option>
|
||||
</select>
|
||||
</form>
|
||||
<div>
|
||||
<form action="" onsubmit="formsay(this.children.say.value); return false;" style="display: inline-block;">
|
||||
<h3>SAY</h3>
|
||||
<input type="text" name="say">
|
||||
<input type="submit" value="say">
|
||||
</form>
|
||||
<form action="" onsubmit="formsend(this.children.send.value); return false;" style="display: inline-block;">
|
||||
<h3>SEND</h3>
|
||||
<select name="send">
|
||||
<option>bel</option>
|
||||
<option>zach</option>
|
||||
<option>chase</option>
|
||||
<option>mason</option>
|
||||
<option>nat</option>
|
||||
<option>roxy</option>
|
||||
<option>bill</option>
|
||||
</select>
|
||||
<input type="submit" value="send">
|
||||
</form>
|
||||
</div>
|
||||
<details>
|
||||
<summary>CONTROLS</summary>
|
||||
<form id="controls">
|
||||
<div style="display: flex; flex-wrap: wrap;">
|
||||
<div>
|
||||
<label for="input-up">Up</label>
|
||||
<input id="input-up" type="text" maxLength=1 value="w" name="w" placeholder="up" onchange="recontrol()">
|
||||
<label for="input-down">Down</label>
|
||||
<input id="input-down" type="text" maxLength=1 value="s" name="s" placeholder="down" onchange="recontrol()">
|
||||
<label for="input-left">Left</label>
|
||||
<input id="input-left" type="text" maxLength=1 value="a" name="a" placeholder="left" onchange="recontrol()">
|
||||
<label for="input-right">Right</label>
|
||||
<input id="input-right" type="text" maxLength=1 value="d" name="d" placeholder="right" onchange="recontrol()">
|
||||
</div>
|
||||
<div>
|
||||
<label for="input-start">Start</label>
|
||||
<input id="input-start" type="text" maxLength=1 value="5" name="5" placeholder="start" onchange="recontrol()">
|
||||
<label for="input-left-bumper">Left Bumper</label>
|
||||
<input id="input-left-bumper" type="text" maxLength=1 value="q" name="q" placeholder="l" onchange="recontrol()">
|
||||
<label for="input-right-bumper">Right Bumper</label>
|
||||
<input id="input-right-bumper" type="text" maxLength=1 value="e" name="e" placeholder="r" onchange="recontrol()">
|
||||
</div>
|
||||
<div>
|
||||
<label for="input-a">A</label>
|
||||
<input id="input-a" type="text" maxLength=1 value="1" name="1" placeholder="a" onchange="recontrol()">
|
||||
<label for="input-b">B</label>
|
||||
<input id="input-b" type="text" maxLength=1 value="2" name="2" placeholder="b" onchange="recontrol()">
|
||||
<label for="input-x">X</label>
|
||||
<input id="input-x" type="text" maxLength=1 value="3" name="3" placeholder="x" onchange="recontrol()">
|
||||
<label for="input-y">Y</label>
|
||||
<input id="input-y" type="text" maxLength=1 value="4" name="4" placeholder="y" onchange="recontrol()">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</details>
|
||||
</div>
|
||||
<div id="ntfy"></div>
|
||||
<div id="ws"></div>
|
||||
<div id="konami" style="display:none"></div>
|
||||
</body>
|
||||
<footer>
|
||||
<script>
|
||||
var socket = new WebSocket("ws://"+window.location.host+"/api/ws")
|
||||
|
||||
function nosend(data) {
|
||||
}
|
||||
function dosend(data) {
|
||||
console.log(JSON.stringify(data))
|
||||
socket.send(JSON.stringify(data))
|
||||
}
|
||||
send = nosend
|
||||
|
||||
socket.addEventListener("open", (_) => {
|
||||
console.log("ws open")
|
||||
send = dosend
|
||||
})
|
||||
socket.addEventListener("message", (event) => console.log("ws recv:", event.data))
|
||||
socket.addEventListener("close", (event) => console.log("ws closed"))
|
||||
|
||||
keys = {}
|
||||
document.addEventListener('keydown', (event) => {
|
||||
var name = controls[event.key]
|
||||
if (!name)
|
||||
return
|
||||
if (keys[name])
|
||||
return
|
||||
keys[name] = true
|
||||
sendKeys(name, "")
|
||||
})
|
||||
document.addEventListener('keyup', (event) => {
|
||||
var name = controls[event.key]
|
||||
if (!name)
|
||||
return
|
||||
keys[name] = false
|
||||
sendKeys("", name)
|
||||
})
|
||||
function sendKeys(y, n) {
|
||||
send({
|
||||
T: new Date().getTime(),
|
||||
U: document.getElementById("user").value,
|
||||
Y: y,
|
||||
N: n,
|
||||
})
|
||||
}
|
||||
|
||||
var controls = {}
|
||||
function recontrol() {
|
||||
for (var k in controls)
|
||||
controls[k] = false
|
||||
for (var e of document.getElementById("controls").getElementsByTagName("input"))
|
||||
controls[e.value] = e.name
|
||||
}
|
||||
recontrol()
|
||||
</script>
|
||||
</footer>
|
||||
</html>
|
||||
|
|
@ -6,21 +6,32 @@ import (
|
|||
"strconv"
|
||||
)
|
||||
|
||||
var (
|
||||
FlagRawKeyboard = os.Getenv("RAW_KEYBOARD") == "true"
|
||||
FlagRawUDP = os.Getenv("RAW_UDP")
|
||||
FlagRawWS = os.Getenv("RAW_WS")
|
||||
FlagDebug = os.Getenv("DEBUG") != ""
|
||||
FlagRawRandomWeightFile = os.Getenv("RAW_RANDOM_WEIGHT_FILE")
|
||||
)
|
||||
|
||||
type Raw interface {
|
||||
Read() []byte
|
||||
Close()
|
||||
}
|
||||
|
||||
func New(ctx context.Context) Raw {
|
||||
if os.Getenv("RAW_KEYBOARD") == "true" {
|
||||
if FlagRawKeyboard {
|
||||
return NewKeyboard()
|
||||
}
|
||||
if port, _ := strconv.Atoi(os.Getenv("RAW_UDP")); port != 0 {
|
||||
if port, _ := strconv.Atoi(FlagRawWS); port != 0 {
|
||||
return NewWS(ctx, port)
|
||||
}
|
||||
if port, _ := strconv.Atoi(FlagRawUDP); port != 0 {
|
||||
return NewUDP(ctx, port)
|
||||
}
|
||||
generator := randomCharFromRange('a', 'g')
|
||||
if p, ok := os.LookupEnv("RAW_RANDOM_WEIGHT_FILE"); ok && len(p) > 0 {
|
||||
generator = randomCharFromWeightFile(p)
|
||||
if FlagRawRandomWeightFile != "" {
|
||||
generator = randomCharFromWeightFile(FlagRawRandomWeightFile)
|
||||
}
|
||||
return NewRandom(generator)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,4 +6,5 @@ func TestRaw(t *testing.T) {
|
|||
var _ Raw = &Random{}
|
||||
var _ Raw = UDP{}
|
||||
var _ Raw = Keyboard{}
|
||||
var _ Raw = WS{}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import (
|
|||
"context"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
|
|
@ -29,14 +28,13 @@ func NewUDP(ctx context.Context, port int) UDP {
|
|||
}
|
||||
|
||||
func (udp UDP) listen() {
|
||||
debugging := os.Getenv("DEBUG") == "true"
|
||||
for udp.ctx.Err() == nil {
|
||||
buff := make([]byte, 256)
|
||||
n, _, err := udp.conn.ReadFrom(buff)
|
||||
if err != nil && udp.ctx.Err() == nil {
|
||||
panic(err)
|
||||
}
|
||||
if debugging {
|
||||
if FlagDebug {
|
||||
log.Printf("raw.UDP.Read() => %s", buff[:n])
|
||||
}
|
||||
select {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,148 @@
|
|||
package raw
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
var (
|
||||
FlagWSProxy = os.Getenv("RAW_WS_PROXY_URL")
|
||||
FlagWSDebug = os.Getenv("RAW_WS_DEBUG") != ""
|
||||
)
|
||||
|
||||
type WS struct {
|
||||
ctx context.Context
|
||||
can context.CancelFunc
|
||||
ch chan []byte
|
||||
}
|
||||
|
||||
func NewWS(ctx context.Context, port int) WS {
|
||||
ctx, can := context.WithCancel(ctx)
|
||||
ws := WS{ctx: ctx, can: can, ch: make(chan []byte, 256)}
|
||||
go ws.listen(port)
|
||||
return ws
|
||||
}
|
||||
|
||||
func (ws WS) Read() []byte {
|
||||
select {
|
||||
case v := <-ws.ch:
|
||||
return v
|
||||
case <-ws.ctx.Done():
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (ws WS) Close() {
|
||||
ws.can()
|
||||
}
|
||||
|
||||
func (ws WS) listen(port int) {
|
||||
server := &http.Server{
|
||||
Addr: fmt.Sprintf(":%d", port),
|
||||
Handler: ws,
|
||||
}
|
||||
go func() {
|
||||
if err := server.ListenAndServe(); err != nil && ws.ctx.Err() == nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
log.Println("WS on", port)
|
||||
<-ws.ctx.Done()
|
||||
server.Close()
|
||||
}
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
}
|
||||
|
||||
func (ws WS) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
r = r.WithContext(ws.ctx)
|
||||
if err := ws.serveHTTP(w, r); err != nil {
|
||||
log.Println(err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func (ws WS) serveHTTP(w http.ResponseWriter, r *http.Request) error {
|
||||
switch r.URL.Path {
|
||||
case "/api/ws":
|
||||
return ws.serveWS(w, r)
|
||||
}
|
||||
if strings.HasPrefix(r.URL.Path, "/proxy") {
|
||||
return ws.serveProxy(w, r)
|
||||
}
|
||||
return ws.serveStaticFile(w, r)
|
||||
}
|
||||
|
||||
func (ws WS) serveProxy(w http.ResponseWriter, r *http.Request) error {
|
||||
u, err := url.Parse(FlagWSProxy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/proxy")
|
||||
if r.URL.Path == "" {
|
||||
r.URL.Path = "/"
|
||||
}
|
||||
proxy := httputil.NewSingleHostReverseProxy(u)
|
||||
proxy.ServeHTTP(w, r)
|
||||
return nil
|
||||
}
|
||||
|
||||
//go:embed public/*
|
||||
var staticFiles embed.FS
|
||||
|
||||
func (ws WS) serveStaticFile(w http.ResponseWriter, r *http.Request) error {
|
||||
if FlagWSDebug {
|
||||
b, _ := os.ReadFile("src/device/input/raw/public/root.html")
|
||||
w.Write(b)
|
||||
return nil
|
||||
}
|
||||
if r.URL.Path == "/" {
|
||||
r.URL.Path = "root.html"
|
||||
}
|
||||
r.URL.Path = path.Join("public", r.URL.Path)
|
||||
http.FileServer(http.FS(staticFiles)).ServeHTTP(w, r)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ws WS) serveWS(w http.ResponseWriter, r *http.Request) error {
|
||||
if err := ws._serveWS(w, r); err != nil {
|
||||
log.Println("_serveWS:", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ws WS) _serveWS(w http.ResponseWriter, r *http.Request) error {
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
for ws.ctx.Err() == nil {
|
||||
msgType, p, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
if websocket.IsCloseError(err) || websocket.IsUnexpectedCloseError(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if msgType == websocket.TextMessage {
|
||||
log.Println(string(p))
|
||||
ws.ch <- p
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -3,11 +3,16 @@ package wrap
|
|||
import (
|
||||
"context"
|
||||
"mayhem-party/src/device/input/button"
|
||||
"mayhem-party/src/device/input/raw"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
FlagBufferedStickyDuration = os.Getenv("WRAP_BUFFERED_STICKY_DURATION")
|
||||
)
|
||||
|
||||
type Buffered struct {
|
||||
ctx context.Context
|
||||
can context.CancelFunc
|
||||
|
|
@ -21,7 +26,7 @@ type Buffered struct {
|
|||
func NewBuffered(ctx context.Context, input Wrap) *Buffered {
|
||||
ctx, can := context.WithCancel(ctx)
|
||||
expirationInterval := time.Millisecond * 125
|
||||
if d, err := time.ParseDuration(os.Getenv("WRAP_BUFFERED_STICKY_DURATION")); err == nil {
|
||||
if d, err := time.ParseDuration(FlagBufferedStickyDuration); err == nil {
|
||||
expirationInterval = d
|
||||
}
|
||||
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() {
|
||||
b.input.Close()
|
||||
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"
|
||||
"log"
|
||||
"mayhem-party/src/device/input/button"
|
||||
"mayhem-party/src/device/input/raw"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
var (
|
||||
ChSigUsr1 = func() chan os.Signal {
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, syscall.SIGUSR1)
|
||||
return c
|
||||
}()
|
||||
)
|
||||
|
||||
type Refresh struct {
|
||||
|
|
@ -13,14 +23,12 @@ type Refresh struct {
|
|||
input Wrap
|
||||
}
|
||||
|
||||
func NewRefreshCh(sig os.Signal) <-chan os.Signal {
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, sig)
|
||||
return c
|
||||
func NewRefresh(ctx context.Context, newWrap func() Wrap) *Refresh {
|
||||
return NewRefreshWith(ctx, newWrap, ChSigUsr1)
|
||||
}
|
||||
|
||||
func NewRefresh(newWrap func() Wrap, ch <-chan os.Signal) *Refresh {
|
||||
ctx, can := context.WithCancel(context.Background())
|
||||
func NewRefreshWith(ctx context.Context, newWrap func() Wrap, ch <-chan os.Signal) *Refresh {
|
||||
ctx, can := context.WithCancel(ctx)
|
||||
result := &Refresh{
|
||||
can: can,
|
||||
input: newWrap(),
|
||||
|
|
@ -33,6 +41,7 @@ func NewRefresh(newWrap func() Wrap, ch <-chan os.Signal) *Refresh {
|
|||
return
|
||||
case sig := <-ch:
|
||||
log.Println("refreshing for", sig)
|
||||
result.input.CloseWrap()
|
||||
result.input = newWrap()
|
||||
}
|
||||
}
|
||||
|
|
@ -40,6 +49,11 @@ func NewRefresh(newWrap func() Wrap, ch <-chan os.Signal) *Refresh {
|
|||
return result
|
||||
}
|
||||
|
||||
func (r *Refresh) CloseWrap() raw.Raw {
|
||||
r.can()
|
||||
return r.input.CloseWrap()
|
||||
}
|
||||
|
||||
func (r *Refresh) Read() []button.Button {
|
||||
return r.input.Read()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
package wrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"mayhem-party/src/device/input/button"
|
||||
"mayhem-party/src/device/input/raw"
|
||||
"os"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
|
@ -15,7 +18,7 @@ func TestRefresh(t *testing.T) {
|
|||
}
|
||||
ch := make(chan os.Signal, 1)
|
||||
defer close(ch)
|
||||
refresh := NewRefresh(generator, ch)
|
||||
refresh := NewRefreshWith(context.Background(), generator, ch)
|
||||
defer refresh.Close()
|
||||
|
||||
assertIts := func(t *testing.T, b byte) {
|
||||
|
|
@ -42,3 +45,57 @@ func TestRefresh(t *testing.T) {
|
|||
assertIts(t, byte('c'))
|
||||
})
|
||||
}
|
||||
|
||||
func TestRefreshDoesntCloseSources(t *testing.T) {
|
||||
src := &telemetrySrc{}
|
||||
newParsers := 0
|
||||
newParser := func() Wrap {
|
||||
newParsers += 1
|
||||
return src
|
||||
}
|
||||
ctx, can := context.WithCancel(context.Background())
|
||||
defer can()
|
||||
refresh := NewRefresh(ctx, newParser)
|
||||
if newParsers != 1 {
|
||||
t.Error(newParsers)
|
||||
}
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
refresh.Read()
|
||||
}
|
||||
if want := (telemetrySrc{reads: 5}); *src != want {
|
||||
t.Errorf("%+v", *src)
|
||||
} else if newParsers != 1 {
|
||||
t.Error(newParsers)
|
||||
}
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
ChSigUsr1 <- syscall.SIGINT
|
||||
}
|
||||
time.Sleep(time.Millisecond * 250)
|
||||
if want := (telemetrySrc{reads: 5, closeWraps: 5}); *src != want {
|
||||
t.Errorf("want %+v, got %+v", want, *src)
|
||||
} else if newParsers != 6 {
|
||||
t.Error(newParsers)
|
||||
}
|
||||
}
|
||||
|
||||
type telemetrySrc struct {
|
||||
closeWraps int
|
||||
closes int
|
||||
reads int
|
||||
}
|
||||
|
||||
func (src *telemetrySrc) CloseWrap() raw.Raw {
|
||||
src.closeWraps += 1
|
||||
return nil
|
||||
}
|
||||
|
||||
func (src *telemetrySrc) Close() {
|
||||
src.closes += 1
|
||||
}
|
||||
|
||||
func (src *telemetrySrc) Read() []button.Button {
|
||||
src.reads += 1
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package wrap
|
|||
|
||||
import (
|
||||
"mayhem-party/src/device/input/button"
|
||||
"mayhem-party/src/device/input/raw"
|
||||
"os"
|
||||
|
||||
"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() {
|
||||
re.input.Close()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,36 +3,42 @@ package wrap
|
|||
import (
|
||||
"context"
|
||||
"mayhem-party/src/device/input/button"
|
||||
"mayhem-party/src/device/input/raw"
|
||||
"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 {
|
||||
Read() []button.Button
|
||||
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 {
|
||||
return srcFunc()
|
||||
return parserFunc()
|
||||
}
|
||||
if os.Getenv("WRAP_BUFFERED") == "true" {
|
||||
if FlagBuffered {
|
||||
oldMaker := maker
|
||||
maker = func() Wrap {
|
||||
return NewBuffered(ctx, oldMaker())
|
||||
}
|
||||
}
|
||||
if p := os.Getenv("WRAP_REMAP_FILE"); p != "" {
|
||||
if FlagRemapFile != "" {
|
||||
oldMaker := maker
|
||||
maker = func() Wrap {
|
||||
return NewRemapFromFile(oldMaker(), p)
|
||||
return NewRemapFromFile(oldMaker(), FlagRemapFile)
|
||||
}
|
||||
}
|
||||
if os.Getenv("WRAP_REFRESH_ON_SIGUSR1") != "" {
|
||||
if FlagRefreshOnSigUsr1 {
|
||||
oldMaker := maker
|
||||
c := NewRefreshCh(syscall.SIGUSR1)
|
||||
maker = func() Wrap {
|
||||
return NewRefresh(oldMaker, c)
|
||||
return NewRefresh(ctx, oldMaker)
|
||||
}
|
||||
}
|
||||
return maker()
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package wrap
|
|||
|
||||
import (
|
||||
"mayhem-party/src/device/input/button"
|
||||
"mayhem-party/src/device/input/raw"
|
||||
"testing"
|
||||
)
|
||||
|
||||
|
|
@ -10,12 +11,12 @@ func TestWrap(t *testing.T) {
|
|||
var _ Wrap = &Refresh{}
|
||||
var _ Wrap = &Buffered{}
|
||||
var _ Wrap = &Remap{}
|
||||
var _ Wrap = Protocol{}
|
||||
}
|
||||
|
||||
type dummyParser button.Button
|
||||
|
||||
func (d dummyParser) Close() {}
|
||||
func (d dummyParser) CloseWrap() raw.Raw { return nil }
|
||||
func (d dummyParser) Close() {}
|
||||
func (d dummyParser) Read() []button.Button {
|
||||
return []button.Button{button.Button(d)}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,6 +53,8 @@ var (
|
|||
';': PSemicolon,
|
||||
'-': PMinus,
|
||||
'=': PEqual,
|
||||
'<': PageDown,
|
||||
'>': PageUp,
|
||||
}
|
||||
keyToChar = func() map[Key]byte {
|
||||
result := map[Key]byte{}
|
||||
|
|
|
|||
|
|
@ -57,4 +57,6 @@ const (
|
|||
PSemicolon = Key(keybd_event.VK_SEMICOLON)
|
||||
PMinus = Key(keybd_event.VK_MINUS)
|
||||
PEqual = Key(keybd_event.VK_EQUAL)
|
||||
PageUp = Key(keybd_event.VK_PAGEUP)
|
||||
PageDown = Key(keybd_event.VK_PAGEDOWN)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,9 +17,9 @@ func Main(ctx context.Context) error {
|
|||
defer reader.Close()
|
||||
|
||||
interval := time.Millisecond * 50
|
||||
if intervalS, ok := os.LookupEnv("MAIN_INTERVAL_DURATION"); !ok {
|
||||
if intervalS := os.Getenv("MAIN_INTERVAL_DURATION"); intervalS == "" {
|
||||
} else if v, err := time.ParseDuration(intervalS); err != nil {
|
||||
panic(err)
|
||||
return err
|
||||
} else {
|
||||
interval = v
|
||||
}
|
||||
|
|
@ -50,6 +50,9 @@ func Main(ctx context.Context) error {
|
|||
keys = append(keys, k)
|
||||
}
|
||||
}
|
||||
if os.Getenv("DEBUG") == "true" {
|
||||
log.Printf("src.Main.writer.Press(%+v) (from %+v)", keys, delta)
|
||||
}
|
||||
writer.Press(keys...)
|
||||
}
|
||||
|
||||
|
|
|
|||
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
|
||||
Loading…
Reference in New Issue