81 Commits

Author SHA1 Message Date
bel
a36f07d0c1 accesslog 2023-03-27 21:39:07 -06:00
bel
09d9911293 log access 2023-03-27 21:21:03 -06:00
bel
60d391b7a4 flush config to disk every http 2023-03-27 21:05:38 -06:00
Bel LaPointe
60ed9c1269 todo 2023-03-27 17:55:42 -06:00
Bel LaPointe
13cf35bdd8 after voting clear votes 2023-03-27 17:55:19 -06:00
Bel LaPointe
79c90ac40c todo 2023-03-27 11:04:58 -06:00
Bel LaPointe
dd1b053efa telemetry in bg thread and lossy so no block keyboad though i still find it pretty sus 2023-03-27 11:03:56 -06:00
Bel LaPointe
f619fe9e1b some todo 2023-03-27 11:01:36 -06:00
Bel LaPointe
781bfb8a67 todo 2023-03-27 11:01:14 -06:00
Bel LaPointe
9ece270a13 rename State.GM THINGS to State.GM.Things 2023-03-27 11:00:40 -06:00
Bel LaPointe
f647a03467 shuffle v01.config to split out state and meta 2023-03-27 10:54:02 -06:00
Bel LaPointe
ecb719a97a test last GM RPC 2023-03-27 10:13:59 -06:00
Bel LaPointe
1738ce7d19 test a vote for someone saying themself 2023-03-27 10:11:17 -06:00
Bel LaPointe
354d07d6bf test serveGMVote 2023-03-27 09:45:57 -06:00
Bel LaPointe
39ab01525f shuffle 2023-03-27 09:35:11 -06:00
Bel LaPointe
ceeeb8fe4b test fillNonPlayerAliases 2023-03-27 09:33:25 -06:00
Bel LaPointe
d029d82366 impl,test fill non player aliases 2023-03-27 09:30:29 -06:00
Bel LaPointe
0435f7b3e8 test shuffle 2023-03-27 09:24:37 -06:00
Bel LaPointe
5ef0dde50d test no players/users and shuffle 2023-03-27 09:21:16 -06:00
Bel LaPointe
bc3f0271e7 test happy 1:1 shuffle 2023-03-27 08:26:55 -06:00
Bel LaPointe
e2d7c4a908 test unhappy swap 2023-03-27 08:23:37 -06:00
Bel LaPointe
c744704b63 test happy swap 2023-03-27 08:21:03 -06:00
Bel LaPointe
213fd555e4 test happy shuffle 2023-03-27 08:20:50 -06:00
Bel LaPointe
dd41028aab test /gm/rpc/broadcastSomeoneSaidAlias 2023-03-27 07:26:40 -06:00
Bel LaPointe
8ff1c2fab4 test nonzero player on status 2023-03-27 06:58:59 -06:00
Bel LaPointe
1f7b222b9c test nonzero status 2023-03-27 06:58:21 -06:00
Bel LaPointe
1842023224 struct 2023-03-27 06:53:46 -06:00
Bel LaPointe
45b873f462 test status 2023-03-27 06:51:15 -06:00
Bel LaPointe
88a78c489f test status 2023-03-27 06:50:31 -06:00
Bel LaPointe
8314bdc457 impl public status endpoint 2023-03-27 06:24:41 -06:00
Bel LaPointe
a6a9b177e9 whoops dont sleep use some ctx 2023-03-27 06:19:39 -06:00
Bel LaPointe
f649862dd4 todo 2023-03-27 06:18:52 -06:00
Bel LaPointe
85804d6f84 lock on every http req BUT LOCKS ON TELEMETRY SO BEWARE 2023-03-27 06:18:32 -06:00
Bel LaPointe
f14871218d stub 2023-03-27 06:14:52 -06:00
Bel LaPointe
26f052d981 workin on gm rpc 2023-03-27 06:13:26 -06:00
bel
2d4cb394de wip 2023-03-26 23:35:10 -06:00
bel
163bf2b405 serve GET /config 2023-03-26 22:39:47 -06:00
bel
e968ce17ce stash last lag and ts for each user 2023-03-26 22:19:07 -06:00
bel
f07e67b3fd export host.d to -venue repo 2023-03-26 16:24:08 -06:00
bel
fbf4849517 todo 2023-03-26 14:54:27 -06:00
bel
804ce02407 todo 2023-03-26 14:28:48 -06:00
bel
4a86d2b6ca update readme for STT setting Quiet mode 2023-03-26 14:27:42 -06:00
bel
c663b1a12c expose PATCH /config 2023-03-26 14:17:33 -06:00
bel
af42db6803 v01.config accepts and applies json patch 2023-03-26 10:20:19 -06:00
bel
f9dc4cff9f todo 2023-03-26 10:08:04 -06:00
bel
37050f3d87 test quiet mode 2023-03-26 10:06:22 -06:00
bel
74717609ec v01.yaml has .quiet=true to cause all button pushes to become releases 2023-03-26 10:00:10 -06:00
bel
24ae45896f todo 2023-03-26 09:57:03 -06:00
bel
8b29648c50 add hotwords file for stt 2023-03-26 09:55:50 -06:00
bel
1eba008efe readme order 2023-03-26 09:49:51 -06:00
bel
d48c545030 update host.README for tts and stt integrated 2023-03-26 09:47:19 -06:00
bel
323ca466ad update configs in host.d for tts 2023-03-26 09:29:02 -06:00
bel
67e504ced6 accept say in headers for more length 2023-03-26 09:25:48 -06:00
bel
ad967d5047 test a longer input and its k 2023-03-26 09:23:20 -06:00
bel
8fd0067ad1 block while tts speaking for singleton 2023-03-26 09:18:31 -06:00
bel
43566be7ae ?say=XYZ to TTS 2023-03-26 09:13:24 -06:00
bel
cb8b254cbb ?say=XYZ to TTS 2023-03-26 09:12:05 -06:00
bel
340ca1d2f5 go test ok 2023-03-26 08:48:46 -06:00
bel
02c49852c0 todo 2023-03-26 08:48:00 -06:00
bel
02c9dce1b3 split server 2023-03-26 08:46:31 -06:00
bel
a3650642ca todo 2023-03-26 08:44:30 -06:00
bel
fbded57807 rename 2023-03-26 08:42:40 -06:00
bel
44cb05487e split out message, config 2023-03-26 08:42:09 -06:00
bel
e1e2ce3eec split out xform 2023-03-26 08:39:50 -06:00
bel
4c7f444887 more splitting v01 2023-03-26 08:38:24 -06:00
bel
0311fc56a3 split v01 into its own pkg 2023-03-26 08:37:13 -06:00
bel
9902684990 todo 2023-03-26 08:34:48 -06:00
bel
967e66bdb3 todo 2023-03-25 23:16:48 -06:00
bel
ff21bfb8b3 todo 2023-03-25 23:08:59 -06:00
bel
c153636e24 locks 2023-03-25 23:04:00 -06:00
bel
efe4adf129 k no deadlock 2023-03-25 22:57:12 -06:00
bel
802266e500 remove wrapToParse dependence 2023-03-25 22:54:23 -06:00
bel
373d8be1a0 split button and parse packages 2023-03-25 22:52:09 -06:00
bel
bd5654128e accept PUT /broadcast to change the broadcast message 2023-03-25 22:37:19 -06:00
bel
9073658e12 update readme with linux build windows 2023-03-25 19:21:46 -06:00
bel
7df4d09553 update rusty-pipes for feedback 2023-03-25 14:51:19 -06:00
bel
1ad60189f4 todo 2023-03-25 11:29:54 -06:00
bel
766c77b00a todo 2023-03-25 11:28:41 -06:00
bel
bcdf545188 todo 2023-03-25 11:28:30 -06:00
bel
3264d9ad55 can send messages back to specific and ALL viewers 2023-03-25 11:27:49 -06:00
bel
3f35f7f936 manual test w rusty-pipe v0.1.3 ok 2023-03-25 11:01:38 -06:00
45 changed files with 1817 additions and 861 deletions

17
go.mod
View File

@@ -3,7 +3,18 @@ module mayhem-party
go 1.19
require (
github.com/go-yaml/yaml v2.1.0+incompatible // indirect
github.com/micmonay/keybd_event v1.1.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
github.com/faiface/beep v1.1.0
github.com/go-yaml/yaml v2.1.0+incompatible
gopkg.in/yaml.v2 v2.4.0
)
require (
github.com/evanphx/json-patch/v5 v5.6.0 // indirect
github.com/hajimehoshi/oto v0.7.1 // indirect
github.com/micmonay/keybd_event v1.1.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 // indirect
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067 // indirect
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6 // indirect
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 // indirect
)

40
go.sum
View File

@@ -1,7 +1,47 @@
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/d4l3k/messagediff v1.2.2-0.20190829033028-7e0a312ae40b/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo=
github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww=
github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
github.com/faiface/beep v1.1.0 h1:A2gWP6xf5Rh7RG/p9/VAW2jRSDEGQm5sbOb38sf5d4c=
github.com/faiface/beep v1.1.0/go.mod h1:6I8p6kK2q4opL/eWb+kAkk38ehnTunWeToJB+s51sT4=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs=
github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498=
github.com/go-audio/wav v1.0.0/go.mod h1:3yoReyQOsiARkvPl3ERCi8JFjihzG6WhjYpZCf5zAWE=
github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwnTLB6vQiq+o=
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
github.com/hajimehoshi/go-mp3 v0.3.0/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM=
github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI=
github.com/hajimehoshi/oto v0.7.1 h1:I7maFPz5MBCwiutOrz++DLdbr4rTzBsbBuV2VpgU9kk=
github.com/hajimehoshi/oto v0.7.1/go.mod h1:wovJ8WWMfFKvP587mhHgot/MBr4DnNy9m6EepeVGnos=
github.com/icza/bitio v1.0.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A=
github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6/go.mod h1:xQig96I1VNBDIWGCdTt54nHt6EeI639SmHycLYL7FkA=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jfreymuth/oggvorbis v1.0.1/go.mod h1:NqS+K+UXKje0FUYUPosyQ+XTVvjmVjps1aEZH1sumIk=
github.com/jfreymuth/vorbis v1.0.0/go.mod h1:8zy3lUAm9K/rJJk223RKy6vjCZTWC61NA2QD06bfOE0=
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mewkiz/flac v1.0.7/go.mod h1:yU74UH277dBUpqxPouHSQIar3G1X/QIclVbFahSd1pU=
github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2/go.mod h1:3E2FUC/qYUfM8+r9zAwpeHJzqRVVMIYnpzD/clwWxyA=
github.com/micmonay/keybd_event v1.1.1 h1:rv7omwXWYL9Lgf3PUq6uBgJI2k1yGkL/GD6dxc6nmSs=
github.com/micmonay/keybd_event v1.1.1/go.mod h1:CGMWMDNgsfPljzrAWoybUOSKafQPZpv+rLigt2LzNGI=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 h1:idBdZTd9UioThJp8KpM/rTSinK/ChZFBE43/WtIy8zg=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067 h1:KYGJGHOQy8oSi1fDlSpcZF0+juKwk/hEMv5SiwHogR0=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6 h1:vyLBGJPIl9ZYbcQFM2USFmJBK6KI+t+z6jL0lbwjrnc=
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 h1:9nuHUbU8dRnRRfj9KjWUVrJeoexdbeMjttk6Oh1rD10=
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

View File

@@ -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`

View File

@@ -1,6 +0,0 @@
export DEBUG=true
export RAW_UDP=17070
export BUTTON_V01=true
export WRAP_REFRESH_ON_SIGUSR1=true
export MAIN_INTERVAL_DURATION=5ms
export OUTPUT_KEYBOARD=false

View File

@@ -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:])

View File

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

View File

@@ -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'

View File

@@ -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'

View File

@@ -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'
')': ';'

View File

@@ -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'
'*': ','
'(': '-'
')': '/'

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

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

View File

@@ -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{}
}

View File

@@ -1,16 +0,0 @@
users:
bel:
player: 0
message: "hi"
players:
- buttons:
up: "w"
down: "s"
left: "a"
right: "d"
l: "q"
r: "e"
a: "1"
b: "2"
x: "3"
y: "4"

View File

@@ -1,122 +0,0 @@
package button
import (
"context"
"encoding/json"
"io/ioutil"
"log"
"mayhem-party/src/device/input/raw"
"os"
"time"
"gopkg.in/yaml.v2"
)
var (
FlagDebug = os.Getenv("DEBUG") == "true"
FlagButtonV01Config = os.Getenv("BUTTON_V01_CONFIG")
)
type (
V01 struct {
ctx context.Context
can context.CancelFunc
src raw.Raw
cfg v01Cfg
}
v01Msg struct {
T int64
U string
Y string
N string
}
v01Cfg struct {
Users map[string]struct {
Player int
Message string
}
Players []struct {
Transformation v01Transformation
}
}
v01Transformation map[string]string
)
func NewV01(ctx context.Context, src raw.Raw) V01 {
var cfg v01Cfg
b, _ := ioutil.ReadFile(FlagButtonV01Config)
yaml.Unmarshal(b, &cfg)
ctx, can := context.WithCancel(ctx)
return V01{
ctx: ctx,
can: can,
src: src,
cfg: cfg,
}
}
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 {
line := v01.src.Read()
var msg v01Msg
if err := json.Unmarshal(line, &msg); err != nil {
log.Printf("%v: %s", err, line)
}
v01.telemetry(msg)
return v01.cfg.transform(msg).buttons()
}
func (cfg v01Cfg) transform(msg v01Msg) v01Msg {
if len(cfg.Players) == 0 {
return msg
}
user := cfg.Users[msg.U]
if user.Player < 1 {
msg.Y = ""
msg.N = ""
return msg
}
player := cfg.Players[user.Player-1]
msg.Y = player.Transformation.pipe(msg.Y)
msg.N = player.Transformation.pipe(msg.N)
return msg
}
func (t v01Transformation) 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
}
func (v01 V01) telemetry(msg v01Msg) {
if FlagDebug {
log.Printf("%s|%dms", msg.U, time.Now().UnixNano()/int64(time.Millisecond)-msg.T)
}
}
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 FlagDebug {
log.Printf("%+v", msg)
}
return buttons
}

View File

@@ -1,72 +0,0 @@
package button_test
import (
"context"
"fmt"
"mayhem-party/src/device/input/button"
"os"
"path"
"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 := button.NewV01(context.Background(), 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])
}
}
}
func TestV01WithCfg(t *testing.T) {
d := t.TempDir()
p := path.Join(d, "cfg.yaml")
os.WriteFile(p, []byte(`
users:
bel:
player: 2
players:
- transformation:
w: t
- transformation:
w: i
`), os.ModePerm)
button.FlagButtonV01Config = p
t.Run("unknown user ignored", func(t *testing.T) {
v01 := button.NewV01(context.Background(), constSrc(`{"U":"qt","Y":"w"}`))
got := v01.Read()
if len(got) != 0 {
t.Error(got)
}
})
t.Run("player2", func(t *testing.T) {
v01 := button.NewV01(context.Background(), constSrc(`{"U":"bel","Y":"w","N":"w"}`))
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])
}
})
}

View File

@@ -1,51 +0,0 @@
package button
import (
"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 := v01Transformation(c.xform).pipe(c.input)
if got != c.want {
t.Errorf("%+v(%s) want %s got %s", c.xform, c.input, c.want, got)
}
})
}
}

View File

@@ -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)
})
}

View File

@@ -0,0 +1,26 @@
package parse
import (
"context"
"mayhem-party/src/device/input/button"
v01 "mayhem-party/src/device/input/parse/v01"
"mayhem-party/src/device/input/raw"
"os"
)
var (
FlagParseV01 = os.Getenv("PARSE_V01") == "true"
)
type Parser interface {
Read() []button.Button
Close()
CloseWrap() raw.Raw
}
func New(ctx context.Context, src raw.Raw) Parser {
if FlagParseV01 {
return v01.NewV01(ctx, src)
}
return NewPlaintext(src)
}

View File

@@ -0,0 +1,12 @@
package parse_test
import (
"mayhem-party/src/device/input/parse"
v01 "mayhem-party/src/device/input/parse/v01"
"testing"
)
func TestParser(t *testing.T) {
var _ parse.Parser = parse.Plaintext{}
var _ parse.Parser = &v01.V01{}
}

View File

@@ -1,12 +1,13 @@
package button
package parse
import (
"mayhem-party/src/device/input/button"
"mayhem-party/src/device/input/raw"
"os"
)
var (
FlagButtonPlaintextRelease = os.Getenv("BUTTON_PLAINTEXT_RELEASE")
FlagParsePlaintextRelease = os.Getenv("PARSE_PLAINTEXT_RELEASE")
)
type Plaintext struct {
@@ -16,8 +17,8 @@ type Plaintext struct {
func NewPlaintext(src raw.Raw) Plaintext {
releaseChar := byte('!')
if FlagButtonPlaintextRelease != "" {
releaseChar = byte(FlagButtonPlaintextRelease[0])
if FlagParsePlaintextRelease != "" {
releaseChar = byte(FlagParsePlaintextRelease[0])
}
return Plaintext{
src: src,
@@ -29,16 +30,16 @@ func (p Plaintext) Close() { p.src.Close() }
func (p Plaintext) CloseWrap() raw.Raw { return p.src }
func (p Plaintext) Read() []Button {
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
}

View File

@@ -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))

View File

@@ -0,0 +1,69 @@
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
}
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
}

View File

@@ -0,0 +1,83 @@
package v01
import (
"fmt"
"sync"
"testing"
)
func TestConfigPatch(t *testing.T) {
cases := map[string]struct {
cfg config
patch interface{}
want config
}{
"nil patch": {
cfg: config{Quiet: true},
patch: nil,
want: config{Quiet: true},
},
"[] patch": {
cfg: config{Quiet: true},
patch: []interface{}{},
want: config{Quiet: true},
},
"set fake field": {
cfg: config{Quiet: true},
patch: []interface{}{
map[string]interface{}{"op": "add", "path": "/Fake", "value": true},
},
want: config{Quiet: true},
},
"remove field": {
cfg: config{Quiet: true},
patch: []interface{}{
map[string]interface{}{"op": "remove", "path": "/Quiet"},
},
want: config{Quiet: false},
},
"replace field with valid": {
cfg: config{Quiet: true},
patch: []interface{}{
map[string]interface{}{"op": "replace", "path": "/Quiet", "value": false},
},
want: config{Quiet: false},
},
"replace field with invalid": {
cfg: config{Quiet: true},
patch: []interface{}{
map[string]interface{}{"op": "replace", "path": "/Quiet", "value": "teehee"},
},
want: config{Quiet: true},
},
"test and noop": {
cfg: config{Quiet: true},
patch: []interface{}{
map[string]interface{}{"op": "test", "path": "/Quiet", "value": false},
map[string]interface{}{"op": "replace", "path": "/Quiet", "value": false},
},
want: config{Quiet: true},
},
"test and apply": {
cfg: config{Quiet: true},
patch: []interface{}{
map[string]interface{}{"op": "test", "path": "/Quiet", "value": true},
map[string]interface{}{"op": "replace", "path": "/Quiet", "value": false},
},
want: config{Quiet: false},
},
}
for name, d := range cases {
c := d
t.Run(name, func(t *testing.T) {
c.cfg.lock = &sync.Mutex{}
got := c.cfg.WithPatch(c.patch)
got.lock = nil
c.want.lock = nil
if fmt.Sprintf("%+v", got) != fmt.Sprintf("%+v", c.want) {
t.Errorf("(%+v).Patch(%+v) want %+v, got %+v", c.cfg, c.patch, c.want, got)
}
})
}
}

View File

@@ -0,0 +1,27 @@
package v01
import (
"log"
"mayhem-party/src/device/input/button"
)
type message struct {
T int64
U string
Y string
N string
}
func (msg message) buttons() []button.Button {
buttons := make([]button.Button, len(msg.Y)+len(msg.N))
for i := range msg.Y {
buttons[i] = button.Button{Char: msg.Y[i], Down: true}
}
for i := range msg.N {
buttons[len(msg.Y)+i] = button.Button{Char: msg.N[i], Down: false}
}
if FlagDebug {
log.Printf("%+v", msg)
}
return buttons
}

View File

@@ -0,0 +1,354 @@
package v01
import (
"encoding/json"
"errors"
"fmt"
"io"
"log"
"math/rand"
"mayhem-party/src/device/input/wrap"
"net/http"
"os"
"strings"
"sync"
"syscall"
"time"
"gopkg.in/yaml.v2"
)
func (v01 *V01) listen() {
if v01.cfg.Feedback.Addr == "" {
return
}
v01._listen()
}
func (v01 *V01) _listen() {
mutex := &sync.Mutex{}
s := &http.Server{
Addr: v01.cfg.Feedback.Addr,
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
defer func() { log.Printf("%vms | %s %s", time.Since(start).Milliseconds(), r.Method, r.URL) }()
v01.cfg.lock.Lock()
defer v01.cfg.lock.Unlock()
if r.Method == http.MethodGet {
mutex.Lock()
defer mutex.Unlock()
} else {
mutex.Lock()
defer mutex.Unlock()
}
v01.ServeHTTP(w, r)
v01.stashConfig() // TODO
}),
}
go func() {
<-v01.ctx.Done()
log.Println("closing v01 server")
s.Close()
}()
log.Println("starting v01 server")
if err := s.ListenAndServe(); err != nil && v01.ctx.Err() == nil {
log.Println("err with v01 server", err)
panic(err)
}
}
func (v01 *V01) ServeHTTP(w http.ResponseWriter, r *http.Request) {
r = r.WithContext(v01.ctx)
v01.serveHTTP(w, r)
v01.serveGlobalQueries(r)
}
func (v01 *V01) serveHTTP(w http.ResponseWriter, r *http.Request) {
switch strings.Split(r.URL.Path[1:], "/")[0] {
case "":
v01.getUserFeedback(w, r)
case "broadcast":
v01.servePutBroadcast(w, r)
case "config":
v01.serveConfig(w, r)
case "gm":
v01.serveGM(w, r)
}
}
func (v01 *V01) getUserFeedback(w http.ResponseWriter, r *http.Request) {
user, ok := v01.cfg.Users[r.URL.Query().Get("user")]
if !ok {
user = v01.cfg.Users["broadcast"]
}
w.Write([]byte(user.State.Message))
}
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) {
u := v01.cfg.Users["broadcast"]
u.State.Message = v
v01.cfg.Users["broadcast"] = u
}
func (v01 *V01) serveConfig(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
v01.serveGetConfig(w, r)
} else {
v01.servePatchConfig(w, r)
}
}
func (v01 *V01) serveGetConfig(w http.ResponseWriter, r *http.Request) {
b, _ := json.Marshal(v01.cfg)
w.Write(b)
}
func (v01 *V01) servePatchConfig(w http.ResponseWriter, r *http.Request) {
b, _ := io.ReadAll(r.Body)
var v []interface{}
if err := json.Unmarshal(b, &v); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
v01.cfg = v01.cfg.WithPatch(v)
if err := v01.stashConfig(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func (v01 *V01) stashConfig() error {
if b, err := yaml.Marshal(v01.cfg); err == nil && FlagParseV01Config != "" {
if err := os.WriteFile(FlagParseV01Config, b, os.ModePerm); err != nil {
return err
}
} else if err != nil {
return err
}
return nil
}
func (v01 *V01) serveGlobalQueries(r *http.Request) {
v01.serveGlobalQuerySay(r)
v01.serveGlobalQueryRefresh(r)
}
func (v01 *V01) serveGlobalQuerySay(r *http.Request) {
text := r.URL.Query().Get("say")
if text == "" {
text = r.Header.Get("say")
}
if text == "" {
return
}
go v01.tts(text)
}
func (v01 *V01) serveGlobalQueryRefresh(r *http.Request) {
if _, ok := r.URL.Query()["refresh"]; !ok {
return
}
select {
case wrap.ChSigUsr1 <- syscall.SIGUSR1:
default:
}
}
func (v01 *V01) serveGM(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/gm/rpc/status":
v01.serveGMStatus(w, r)
case "/gm/rpc/broadcastSomeoneSaidAlias":
v01.serveGMSomeoneSaidAlias(w, r)
case "/gm/rpc/fillNonPlayerAliases":
v01.serveGMFillNonPlayerAliases(w, r)
case "/gm/rpc/vote":
v01.serveGMVote(w, r)
case "/gm/rpc/elect":
v01.serveGMElect(w, r)
case "/gm/rpc/shuffle":
v01.serveGMShuffle(r)
case "/gm/rpc/swap":
if errCode, err := v01.serveGMSwap(r.URL.Query().Get("a"), r.URL.Query().Get("b")); err != nil {
http.Error(w, err.Error(), errCode)
return
}
default:
http.NotFound(w, r)
return
}
}
func (v01 *V01) serveGMStatus(w http.ResponseWriter, r *http.Request) {
users := map[string]struct {
Lag time.Duration `yaml:"lag,omitempty"`
Player int `yaml:"player,omitempty"`
IdleFor time.Duration `yaml:"idle_for,omitempty"`
}{}
for k, v := range v01.cfg.Users {
v2 := users[k]
v2.Lag = time.Duration(v.Meta.LastLag) * time.Millisecond
v2.Player = v.State.Player
if v.Meta.LastTSMS > 0 {
v2.IdleFor = time.Since(time.Unix(0, v.Meta.LastTSMS*int64(time.Millisecond)))
}
users[k] = v2
}
yaml.NewEncoder(w).Encode(map[string]interface{}{
"Players": len(v01.cfg.Players),
"Users": users,
})
}
func (v01 *V01) serveGMSomeoneSaidAlias(w http.ResponseWriter, r *http.Request) {
v01.cfg.Quiet = true
for k, v := range v01.cfg.Users {
v.State.GM.LastAlias = v.State.GM.Alias
v.State.GM.Alias = ""
v01.cfg.Users[k] = v
}
v01.servePutBroadcastValue(fmt.Sprintf("<<SOMEONE SAID %q>>", strings.ToUpper(r.URL.Query().Get("message"))))
}
func (v01 *V01) serveGMFillNonPlayerAliases(w http.ResponseWriter, r *http.Request) {
b, _ := io.ReadAll(r.Body)
var pool []string
yaml.Unmarshal(b, &pool)
n := 0
for _, v := range v01.cfg.Users {
if v.State.Player == 0 {
n += 1
}
}
if n < 1 {
w.WriteHeader(http.StatusNoContent)
return
}
if len(pool) < n {
http.Error(w, fmt.Sprintf("request body must contain a list of %v options", n), http.StatusBadRequest)
return
}
for i := 0; i < 100; i++ {
a, b := rand.Int()%len(pool), rand.Int()%len(pool)
pool[a], pool[b] = pool[b], pool[a]
}
i := 0
for k, v := range v01.cfg.Users {
if v.State.Player == 0 {
v.State.GM.Alias = pool[i]
v01.cfg.Users[k] = v
i += 1
}
}
}
func (v01 *V01) serveGMElect(w http.ResponseWriter, r *http.Request) {
alias := r.URL.Query().Get("alias")
aliasWinner := ""
votes := map[string]int{}
for k, v := range v01.cfg.Users {
votes[v.State.GM.Vote] = votes[v.State.GM.Vote] + 1
if v.State.GM.LastAlias == alias {
aliasWinner = k
}
}
if aliasWinner == "" {
http.Error(w, "who is "+alias+"?", http.StatusBadRequest)
return
}
threshold := 0.1 + float64(len(votes))/2.0
winner := ""
for k, v := range votes {
if float64(v) > threshold {
winner = k
}
}
if winner == "" {
v01.serveGMShuffle(r)
} else if _, err := v01.serveGMSwap(winner, aliasWinner); err != nil {
v01.serveGMShuffle(r)
}
for k, v := range v01.cfg.Users {
v.State.GM.Vote = ""
v01.cfg.Users[k] = v
}
yaml.NewEncoder(w).Encode(votes)
}
func (v01 *V01) serveGMVote(w http.ResponseWriter, r *http.Request) {
switch r.URL.Query().Get("payload") {
case "":
counts := map[string]string{}
for k, v := range v01.cfg.Users {
if v.State.GM.Vote != "" {
counts[k] = "voted"
} else {
counts[k] = "voting"
}
}
yaml.NewEncoder(w).Encode(counts)
default:
voter := r.URL.Query().Get("user")
candidate := r.URL.Query().Get("payload")
v, ok := v01.cfg.Users[voter]
if _, ok2 := v01.cfg.Users[candidate]; !ok || !ok2 {
http.Error(w, "bad voter/candidate", http.StatusBadRequest)
return
}
v.State.GM.Vote = candidate
v01.cfg.Users[voter] = v
}
}
func (v01 *V01) serveGMShuffle(r *http.Request) {
poolSize := len(v01.cfg.Users)
if altSize := len(v01.cfg.Players); altSize > poolSize {
poolSize = altSize
}
pool := make([]int, poolSize)
if poolSize > 0 {
for i := range v01.cfg.Players {
pool[i] = i + 1
}
for i := 0; i < 30; i++ {
l := rand.Int() % poolSize
r := rand.Int() % poolSize
pool[l], pool[r] = pool[r], pool[l]
}
}
i := 0
msg := []string{}
for k, v := range v01.cfg.Users {
v.State.Player = pool[i]
v01.cfg.Users[k] = v
if pool[i] > 0 {
msg = append(msg, fmt.Sprintf("%s is now player %v", k, v.State.Player))
}
i += 1
}
v01.servePutBroadcastValue(strings.Join(msg, ", "))
v01.cfg.Quiet = false
}
func (v01 *V01) serveGMSwap(userA, userB string) (int, error) {
if userA == userB {
return http.StatusConflict, errors.New("/spiderman-pointing")
}
_, okA := v01.cfg.Users[userA]
_, okB := v01.cfg.Users[userB]
if !okA || !okB {
return http.StatusBadRequest, errors.New("who dat?")
}
a := v01.cfg.Users[userA]
b := v01.cfg.Users[userB]
a.State.Player, b.State.Player = b.State.Player, a.State.Player
v01.cfg.Users[userA] = a
v01.cfg.Users[userB] = b
v01.cfg.Quiet = false
v01.servePutBroadcastValue(fmt.Sprintf(`%s is now player %v and %s is now player %v`, userA, a.State.Player, userB, b.State.Player))
return http.StatusOK, nil
}

View File

@@ -0,0 +1,574 @@
package v01
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path"
"strconv"
"strings"
"sync"
"testing"
"time"
"gopkg.in/yaml.v2"
)
func TestPatchConfig(t *testing.T) {
dir := t.TempDir()
p := path.Join(dir, t.Name()+".yaml")
cases := map[string]struct {
was config
patch string
want config
}{
"replace entire doc": {
was: config{
Feedback: configFeedback{Addr: "a", TTSURL: "a"},
Users: map[string]configUser{"a": configUser{State: configUserState{Player: 1, Message: "a"}}},
Players: []configPlayer{configPlayer{Transformation: transformation{"a": "a"}}},
Quiet: true,
},
patch: `[{"op": "replace", "path": "", "value": {
"Feedback": {"Addr": "b", "TTSURL": "b"},
"Users": {"b": {"State":{"Player": 2, "Message": "b"}}},
"Players": [{"Transformation": {"b": "b"}}],
"Quiet": false
}}]`,
want: config{
Feedback: configFeedback{Addr: "b", TTSURL: "b"},
Users: map[string]configUser{"b": configUser{State: configUserState{Player: 2, Message: "b"}}},
Players: []configPlayer{configPlayer{Transformation: transformation{"b": "b"}}},
Quiet: false,
},
},
}
for name, d := range cases {
c := d
for _, usesdisk := range []bool{false, true} {
t.Run(fmt.Sprintf("%s disk=%v", name, usesdisk), func(t *testing.T) {
b, _ := yaml.Marshal(c.was)
os.WriteFile(p, b, os.ModePerm)
FlagParseV01Config = ""
if usesdisk {
FlagParseV01Config = p
}
v01 := &V01{cfg: c.was}
v01.cfg.lock = &sync.Mutex{}
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodPatch, "/config", strings.NewReader(c.patch))
v01.servePatchConfig(w, r)
if fmt.Sprintf("%+v", c.want) != fmt.Sprintf("%+v", v01.cfg) {
t.Errorf("want \n\t%+v, got \n\t%+v", c.want, v01.cfg)
}
if usesdisk {
b, _ := os.ReadFile(p)
var got config
yaml.Unmarshal(b, &got)
if fmt.Sprintf("%+v", c.want) != fmt.Sprintf("%+v", v01.cfg) {
t.Errorf("want \n\t%+v, got \n\t%+v", c.want, v01.cfg)
}
}
})
}
}
}
func TestServeGM(t *testing.T) {
ctx, can := context.WithCancel(context.Background())
defer can()
do := func(v01 *V01, path, body string, method ...string) *httptest.ResponseRecorder {
m := http.MethodPost
if len(method) > 0 {
m = method[0]
}
w := httptest.NewRecorder()
r := httptest.NewRequest(m, path, strings.NewReader(body))
v01.ServeHTTP(w, r)
return w
}
t.Run("status", func(t *testing.T) {
v01 := NewV01(ctx, nil)
var result struct {
Players int `yaml:"Players"`
Users map[string]struct {
Player int
Lag string
IdleFor string `yaml:"idle_for"`
} `yaml:"Users"`
}
t.Run("empty", func(t *testing.T) {
resp := do(v01, "/gm/rpc/status", "")
if resp.Code != http.StatusOK {
t.Error(resp.Code)
}
t.Log(string(resp.Body.Bytes()))
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
t.Fatal(err)
}
if result.Players != 0 {
t.Error(result.Players)
}
if len(result.Users) != 0 {
t.Error(result.Users)
}
})
t.Run("full", func(t *testing.T) {
v01.cfg.Players = []configPlayer{
{},
{},
{},
{},
}
v01.cfg.Users = map[string]configUser{
"bel": configUser{
State: configUserState{Player: 3},
Meta: configUserMeta{
LastTSMS: time.Now().Add(-1*time.Minute).UnixNano() / int64(time.Millisecond),
LastLag: int64(time.Second / time.Millisecond),
},
},
"zach": configUser{},
"chase": configUser{},
"mason": configUser{},
"nat": configUser{},
"roxy": configUser{},
"bill": configUser{},
}
resp := do(v01, "/gm/rpc/status", "")
if resp.Code != http.StatusOK {
t.Error(resp.Code)
}
t.Log(string(resp.Body.Bytes()))
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
t.Fatal(err)
}
if result.Players != 4 {
t.Error(result.Players)
}
if len(result.Users) != 7 {
t.Error(result.Users)
}
if d, err := time.ParseDuration(result.Users["bel"].Lag); err != nil {
t.Error(err)
} else if d != time.Second {
t.Error(d)
}
if d, err := time.ParseDuration(result.Users["bel"].IdleFor); err != nil {
t.Error(err)
} else if d < time.Minute || d > 2*time.Minute {
t.Error(d)
}
if result.Users["bel"].Player != 3 {
t.Error(result.Users["bel"].Player)
}
})
})
t.Run("broadcastSomeoneSaidAlias", func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.Quiet = false
v01.cfg.Users = map[string]configUser{
"bel": configUser{State: configUserState{
GM: configUserStateGM{
Alias: "driver",
},
Message: "if someone else says 'driver', then you get to play",
}},
"broadcast": configUser{State: configUserState{
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.Users["broadcast"]; bc.State.Message == ":)" {
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.Users["broadcast"].State.Message != `bel is now player 0 and zach is now player 1` {
t.Error(v01.cfg.Users["broadcast"].State.Message)
}
})
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.Users["broadcast"].State.Message, `is now player 1`) || strings.Contains(v01.cfg.Users["broadcast"].State.Message, ",") {
t.Error(v01.cfg.Users["broadcast"].State.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] != 3 {
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 !strings.HasSuffix(v01.cfg.Users["broadcast"].State.Message, `is now player 1`) || strings.Contains(v01.cfg.Users["broadcast"].State.Message, ",") {
t.Error(v01.cfg.Users["broadcast"].State.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("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) != 3 {
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+1 {
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)
}
})
}

View File

@@ -0,0 +1,25 @@
feedback:
addr: :17071
ttsurl: http://localhost:15002
users:
bel:
state:
player: 0
message: "hi"
alias: driver
meta:
tsms: 1
lastlag: 2
players:
- buttons:
up: "w"
down: "s"
left: "a"
right: "d"
l: "q"
r: "e"
a: "1"
b: "2"
x: "3"
y: "4"
quiet: false

View File

@@ -0,0 +1,14 @@
package v01
type (
transformation map[string]string
)
func (t transformation) pipe(s string) string {
for i := range s {
if v := t[s[i:i+1]]; v != "" {
s = s[:i] + v[:1] + s[i+1:]
}
}
return s
}

View File

@@ -0,0 +1,76 @@
package v01
import (
"bytes"
"fmt"
"io"
"log"
"net/http"
"net/url"
"sync"
"time"
"github.com/faiface/beep"
"github.com/faiface/beep/effects"
"github.com/faiface/beep/speaker"
"github.com/faiface/beep/wav"
)
var (
ttsLock = &sync.RWMutex{}
)
func (v01 *V01) tts(text string) {
if err := v01._tts(text); err != nil {
log.Printf("failed to tts: %s: %v", text, err)
}
}
func (v01 *V01) _tts(text string) error {
if v01.cfg.Feedback.TTSURL == "" {
return nil
}
url, err := url.Parse(v01.cfg.Feedback.TTSURL)
if err != nil {
return err
}
if len(url.Path) < 2 {
url.Path = "/api/tts"
}
q := url.Query()
if q.Get("voice") == "" {
q.Set("voice", "en-us/glados-glow_tts")
}
if q.Get("lengthScale") == "" {
q.Set("lengthScale", "1")
}
q.Set("text", text)
url.RawQuery = q.Encode()
resp, err := http.Get(url.String())
if err != nil {
return err
}
defer resp.Body.Close()
b, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK || resp.Header.Get("Content-Type") != "audio/wav" {
return fmt.Errorf("failed to call ttsurl: (%d) %s", resp.StatusCode, b)
}
decoder, format, err := wav.Decode(bytes.NewReader(b))
if err != nil {
return err
}
ttsLock.Lock()
defer ttsLock.Unlock()
speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/30))
speaker.Play(&effects.Volume{Streamer: beep.ResampleRatio(4, 1, &beep.Ctrl{Streamer: beep.Loop(1, decoder)})})
duration := time.Duration(decoder.Len()) * format.SampleRate.D(1)
select {
case <-v01.ctx.Done():
case <-time.After(duration):
}
return nil
}

View File

@@ -0,0 +1,125 @@
package v01
import (
"context"
"encoding/json"
"io/ioutil"
"log"
"mayhem-party/src/device/input/button"
"mayhem-party/src/device/input/raw"
"os"
"sync"
"time"
"gopkg.in/yaml.v2"
)
var (
FlagDebug = os.Getenv("DEBUG") == "true"
FlagParseV01Config = os.Getenv("V01_CONFIG")
)
type (
V01 struct {
ctx context.Context
can context.CancelFunc
src raw.Raw
cfg config
telemetryc chan message
}
)
func NewV01(ctx context.Context, src raw.Raw) *V01 {
var cfg config
cfg.lock = &sync.Mutex{}
b, _ := ioutil.ReadFile(FlagParseV01Config)
yaml.Unmarshal(b, &cfg)
ctx, can := context.WithCancel(ctx)
result := &V01{
ctx: ctx,
can: can,
src: src,
cfg: cfg,
telemetryc: make(chan message),
}
go result.listen()
go result.dotelemetry()
return result
}
func (v01 *V01) CloseWrap() raw.Raw {
v01.can()
return v01.src
}
func (v01 *V01) Close() {
v01.can()
v01.src.Close()
}
func (v01 *V01) Read() []button.Button {
line := v01.src.Read()
var msg message
if err := json.Unmarshal(line, &msg); err != nil {
log.Printf("%v: %s", err, line)
}
v01.telemetry(msg)
buttons := v01.transform(msg).buttons()
if v01.cfg.Quiet {
for i := range buttons {
buttons[i].Down = false
}
}
return buttons
}
func (v01 *V01) dotelemetry() {
for {
select {
case <-v01.ctx.Done():
return
case msg := <-v01.telemetryc:
v01._telemetry(msg)
}
}
}
func (v01 *V01) telemetry(msg message) {
select {
case v01.telemetryc <- msg:
default:
}
}
func (v01 *V01) _telemetry(msg message) {
// TODO oof
v01.cfg.lock.Lock()
defer v01.cfg.lock.Unlock()
if v01.cfg.Users == nil {
v01.cfg.Users = map[string]configUser{}
}
u := v01.cfg.Users[msg.U]
u.Meta.LastLag = time.Now().UnixNano()/int64(time.Millisecond) - msg.T
u.Meta.LastTSMS = msg.T
if FlagDebug {
log.Printf("%s|%dms", msg.U, u.Meta.LastLag)
}
v01.cfg.Users[msg.U] = u
}
func (v01 *V01) transform(msg message) message {
if len(v01.cfg.Players) == 0 {
return msg
}
user := v01.cfg.Users[msg.U]
if user.State.Player < 1 {
msg.Y = ""
msg.N = ""
return msg
}
player := v01.cfg.Players[user.State.Player-1]
msg.Y = player.Transformation.pipe(msg.Y)
msg.N = player.Transformation.pipe(msg.N)
return msg
}

View File

@@ -0,0 +1,193 @@
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:
state:
message: to everyone
players:
- transformation:
w: t
- transformation:
w: i
`), os.ModePerm)
v01.FlagParseV01Config = p
ctx, can := context.WithCancel(context.Background())
defer can()
v01 := v01.NewV01(ctx, constSrc(`{"U":"qt","Y":"w"}`))
defer v01.Close()
for {
time.Sleep(time.Millisecond * 100)
resp, err := http.Get("http://localhost:27071?user=bel")
if err != nil {
continue
}
resp.Body.Close()
break
}
t.Run("specific user", func(t *testing.T) {
resp, err := http.Get("http://localhost:27071?user=bel")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
b, _ := io.ReadAll(resp.Body)
if string(b) != "to bel" {
t.Error(b)
}
})
t.Run("broadcast", func(t *testing.T) {
resp, err := http.Get("http://localhost:27071")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
b, _ := io.ReadAll(resp.Body)
if string(b) != "to everyone" {
t.Error(b)
}
})
t.Run("change broadcast", func(t *testing.T) {
want := `my new broadcast`
r, _ := http.NewRequest(http.MethodPut, "http://localhost:27071/broadcast", strings.NewReader(want))
resp, err := http.DefaultClient.Do(r)
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
resp, err = http.Get("http://localhost:27071")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
b, _ := io.ReadAll(resp.Body)
if string(b) != want {
t.Error(string(b))
}
})
t.Run("tts", func(t *testing.T) {
if os.Getenv("INTEGRATION_TTS") != "true" {
t.Skip("$INTEGRATION_TTS is not true")
}
for i := 0; i < 2; i++ {
resp, err := http.Get("http://localhost:27071/?say=hello%20world")
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
}
time.Sleep(time.Millisecond * 2500)
r, _ := http.NewRequest(http.MethodGet, "http://localhost:27071", nil)
r.Header.Set("say", "No, HTTP does not define any limit. However most web servers do limit size of headers they accept. For example in Apache default limit is 8KB, in IIS it's 16K. Server will return 413 Entity Too Large error if headers size exceeds that limit.")
resp, err := http.DefaultClient.Do(r)
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
time.Sleep(time.Millisecond * 8500)
})
}
type constSrc string
func (c constSrc) Close() {}
func (c constSrc) Read() []byte {
return []byte(c)
}

View File

@@ -0,0 +1,92 @@
package v01
import (
"context"
"testing"
)
func TestV01TransformationPipe(t *testing.T) {
cases := map[string]struct {
input string
xform map[string]string
want string
}{
"empty input": {
xform: map[string]string{"a": "bc"},
},
"empty xform": {
input: "aa",
want: "aa",
},
"all": {
input: "aa",
xform: map[string]string{"a": "cc"},
want: "cc",
},
"last": {
input: "ba",
xform: map[string]string{"a": "cc"},
want: "bc",
},
"first": {
input: "ab",
xform: map[string]string{"a": "cc"},
want: "cb",
},
"noop": {
input: "bb",
xform: map[string]string{"a": "bc"},
want: "bb",
},
}
for name, d := range cases {
c := d
t.Run(name, func(t *testing.T) {
got := transformation(c.xform).pipe(c.input)
if got != c.want {
t.Errorf("%+v(%s) want %s got %s", c.xform, c.input, c.want, got)
}
})
}
}
func TestV01Quiet(t *testing.T) {
ctx, can := context.WithCancel(context.Background())
defer can()
v01 := NewV01(ctx, constSrc(`{"Y":"a", "N":"b"}`))
v01.cfg.Quiet = false
if got := v01.Read(); len(got) != 2 {
t.Error(len(got))
} else if got[0].Char != 'a' {
t.Error(got[0].Char)
} else if got[0].Down != true {
t.Error(got[0].Down)
} else if got[1].Char != 'b' {
t.Error(got[1].Char)
} else if got[1].Down != false {
t.Error(got[1].Down)
}
v01.cfg.Quiet = true
if got := v01.Read(); len(got) != 2 {
t.Error(len(got))
} else if got[0].Char != 'a' {
t.Error(got[0].Char)
} else if got[0].Down != false {
t.Error(got[0].Down)
} else if got[1].Char != 'b' {
t.Error(got[1].Char)
} else if got[1].Down != false {
t.Error(got[1].Down)
}
}
type constSrc string
func (c constSrc) Close() {}
func (c constSrc) Read() []byte {
return []byte(c)
}

View File

@@ -11,7 +11,7 @@ import (
)
var (
chSigUsr1 = func() chan os.Signal {
ChSigUsr1 = func() chan os.Signal {
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGUSR1)
return c
@@ -24,7 +24,7 @@ type Refresh struct {
}
func NewRefresh(ctx context.Context, newWrap func() Wrap) *Refresh {
return NewRefreshWith(ctx, newWrap, chSigUsr1)
return NewRefreshWith(ctx, newWrap, ChSigUsr1)
}
func NewRefreshWith(ctx context.Context, newWrap func() Wrap, ch <-chan os.Signal) *Refresh {

View File

@@ -3,6 +3,7 @@ package wrap
import (
"context"
"mayhem-party/src/device/input/button"
"mayhem-party/src/device/input/raw"
"os"
"syscall"
"testing"
@@ -50,7 +51,7 @@ func TestRefreshDoesntCloseSources(t *testing.T) {
newParsers := 0
newParser := func() Wrap {
newParsers += 1
return button.NewPlaintext(src)
return src
}
ctx, can := context.WithCancel(context.Background())
defer can()
@@ -69,10 +70,10 @@ func TestRefreshDoesntCloseSources(t *testing.T) {
}
for i := 0; i < 5; i++ {
chSigUsr1 <- syscall.SIGINT
ChSigUsr1 <- syscall.SIGINT
}
time.Sleep(time.Millisecond * 250)
if want := (telemetrySrc{reads: 5}); *src != want {
if want := (telemetrySrc{reads: 5, closeWraps: 5}); *src != want {
t.Errorf("want %+v, got %+v", want, *src)
} else if newParsers != 6 {
t.Error(newParsers)
@@ -80,15 +81,21 @@ func TestRefreshDoesntCloseSources(t *testing.T) {
}
type telemetrySrc struct {
closes int
reads int
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() []byte {
func (src *telemetrySrc) Read() []button.Button {
src.reads += 1
return []byte("foo")
return nil
}

View File

@@ -19,9 +19,9 @@ type Wrap interface {
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 FlagBuffered {
oldMaker := maker

View File

@@ -1,19 +1,22 @@
todo:
- v01cfg includes messages to send per client and exposes http server for it
- send clients messages to display
- 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) }'
- -venue needs to update for new env variables for GUI
- -venue needs to udpate hits hotword path for new Users.[].State.GM.Alias
- clients can vote
- single docker image to run all
- https via home.blapointe and rproxy
- trigger dolphin pause via query param mapping to a button that is a pause hotkey
- 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
- ui for election start, election votes, election end stuff
- todo: stdin
subtasks:
- minigame end
- todo: voice recognition of hotwords to vote who dun it
subtasks:
- 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
scheduled: []
done:
- todo: sticky keyboard input mode for enable/disable explicitly
@@ -54,3 +57,48 @@ done:
ts: Sat Mar 25 09:12:43 MDT 2023
- todo: v01cfg includes messages to send per client and exposes tcp server for it
ts: Sat Mar 25 10:09:06 MDT 2023
- todo: v01cfg includes messages to send per client and exposes http server for it
ts: Sat Mar 25 11:28:29 MDT 2023
- todo: send clients messages to display
ts: Sat Mar 25 11:28:29 MDT 2023
- todo: input.MayhemParty as a logical wrapper from mod10 but then gotta translate
back to char for keyboard things somewhere; space delimited?
ts: Sat Mar 25 11:28:40 MDT 2023
- todo: rusty configs have "name" for each client
details: |
'if name == server_broadcasted_name { debug_print_in_gui(server_broadcasted_message) }'
ts: Sat Mar 25 11:28:40 MDT 2023
- 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
ts: Sat Mar 25 11:29:52 MDT 2023
- todo: we have 7 players oooooof
ts: Sat Mar 25 11:29:52 MDT 2023
- todo: endpoint for v01 to start read-only mode so when hotword spoken, players are
dcd without losing players
ts: Sat Mar 25 23:16:47 MDT 2023
- todo: tts for when someone said the word via larynx docker + http.get + https://pkg.go.dev/github.com/faiface/beep@v1.1.0/wav
ts: Sun Mar 26 09:57:02 MDT 2023
- todo: endpoint for v01 to start read-only mode so when hotword spoken, players are
dcd without losing players; press a hotkey that is bound to dolphin emulator pause
ts: Sun Mar 26 14:28:46 MDT 2023
- todo: game master to coordinate config change
ts: Mon Mar 27 11:01:10 MDT 2023
- todo: rotation triggers
subtasks:
- todo: stdin
subtasks:
- minigame end
- todo: voice recognition of hotwords to vote who dun it
subtasks:
- 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
ts: Mon Mar 27 11:04:56 MDT 2023
- todo: clients can send STT via box
ts: Mon Mar 27 17:55:41 MDT 2023