94 Commits

Author SHA1 Message Date
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
bel
0cddc33ac6 update host mp env file 2023-03-25 10:59:07 -06:00
bel
a1a12b1873 input Getenvs to FlagXYZ 2023-03-25 10:58:13 -06:00
bel
ae1e32391c refresh neither leaks wraps, allows 2 of the same at once, nor closes raws 2023-03-25 10:50:39 -06:00
bel
97cc3ae151 refresh users global ch 2023-03-25 10:25:11 -06:00
bel
2113252e2d no lookup env 2023-03-25 10:20:05 -06:00
bel
2cae3c6d28 dont do raw.New, instead add raw.Raw.Refresh explicit 2023-03-25 10:17:36 -06:00
bel
de261ae400 todo 2023-03-25 10:15:23 -06:00
bel
3dd0a557d4 add ctx to v01 2023-03-25 10:15:14 -06:00
bel
51ae1b27b4 on refresh, recreate raw too, because i dont wanna be leaking by not Closing on refresh 2023-03-25 10:13:25 -06:00
bel
50e89492cf todo 2023-03-25 09:12:44 -06:00
bel
3d9ea1296c external test on player transformation 2023-03-25 09:12:10 -06:00
bel
db69f76aa0 unit tests are good and v01cfg transforms input if players and user in players 2023-03-25 09:06:43 -06:00
bel
0ee3a8b6e8 todo 2023-03-25 00:44:30 -06:00
bel
b379f1d82c sample cfg file 2023-03-25 00:43:51 -06:00
bel
c83f9d8700 load v01 config 2023-03-25 00:30:13 -06:00
bel
6289222b69 todo 2023-03-25 00:13:22 -06:00
bel
607a65e22e if debugging then print lag to stderr 2023-03-25 00:11:12 -06:00
bel
6bbb297c59 todo 2023-03-25 00:06:32 -06:00
bel
95866f7df0 upgrade host.config.mp.env to v01 2023-03-25 00:02:08 -06:00
bel
aaa949cc2a upgrade host.configs.rusty-pipe to v01 2023-03-25 00:01:35 -06:00
bel
ed2b7b7cb9 rename v1 to v01 for git tag 2023-03-24 22:27:56 -06:00
bel
1ef3afd647 whitespace 2023-03-24 22:26:34 -06:00
bel
2746051a2a typing 2023-03-24 22:24:08 -06:00
bel
610aef4f7e rebuild parser on refresh 2023-03-24 22:15:36 -06:00
bel
a9ca58f154 v1 complete 2023-03-24 22:10:37 -06:00
bel
7182ab387f test button.plaintext parser 2023-03-24 21:38:06 -06:00
bel
0e46f6e122 todo 2023-03-24 21:17:01 -06:00
bel
01777c8c3e clean shutdown with udp 2023-03-24 21:14:27 -06:00
bel
aa16b66332 udp in bg thread 2023-03-24 21:02:47 -06:00
bel
2af373aed7 integrate button.Parser 2023-03-24 20:52:44 -06:00
bel
896f5e9c92 wrap accepts button.Parser 2023-03-24 20:50:58 -06:00
bel
b319ed7e6d split wrap protocol parsing into input.button 2023-03-24 20:25:15 -06:00
bel
9990273b19 protocol should be pkg 2023-03-24 20:05:55 -06:00
bel
ea7f2d8932 change testdata env to new RAW_ and WRAP_ 2023-03-24 19:53:55 -06:00
bel
38b00e55b0 split src/devices/input into src/devices/input/{raw,wrap} 2023-03-24 19:51:38 -06:00
bel
ab673a81f0 update host config based on mayhem-party having its own udp server now 2023-03-24 18:55:44 -06:00
Bel LaPointe
287b9c7b4e udp input except no clean shutdown 2023-03-24 18:48:54 -06:00
bel
126f5ab60a har we go 2023-03-24 18:35:16 -06:00
bel
3c19f984a9 add required field to rusty configs 2023-03-24 15:39:40 -06:00
bel
cf3b93464a update readme for compile 2023-03-24 15:29:33 -06:00
bel
edcea37148 drop .[] chars as they dont work on linux and macos for me out of the box 2023-03-24 15:28:17 -06:00
Bel LaPointe
b4e4de82ae almost 2023-03-24 15:13:17 -06:00
Bel LaPointe
cdfcfe8fd0 make rotate.sh first class citizen for receiving signal 2023-03-24 14:44:47 -06:00
Bel LaPointe
bf677856a2 script generating player offset files 2023-03-24 14:40:39 -06:00
Bel LaPointe
b9d76d5e8f create mvp files to run mayhem party without remap to stdout when hosting 2023-03-24 14:16:05 -06:00
Bel LaPointe
d292a830a1 set up players 1..5 rusty-pipe.yamls 2023-03-24 14:09:31 -06:00
Bel LaPointe
745175210c mv README#host to host.d 2023-03-24 14:01:44 -06:00
Bel LaPointe
6536daee7f support alpha, numeric, f0..10, punctuation, math keys 2023-03-24 13:49:12 -06:00
Bel LaPointe
9ce50f2622 todo 2023-03-24 13:39:29 -06:00
Bel LaPointe
ea0bb5d365 revert back to case sensitive because you cant hold A and a at the same time 2023-03-24 13:39:17 -06:00
Bel LaPointe
20488d2be8 no wait shift means sideaffecting 2023-03-24 13:37:53 -06:00
Bel LaPointe
7b7486cc93 keys support case 2023-03-24 13:30:46 -06:00
Bel LaPointe
e5a668b691 howtohost 2023-03-24 13:05:45 -06:00
Bel LaPointe
c298bb0dfd todo 2023-03-24 12:57:31 -06:00
Bel LaPointe
adabc4eb98 input.New refactor and test 2023-03-24 12:05:50 -06:00
Bel LaPointe
6e1bfc177d fix random weighted char because %sum can never get last value of sum 2023-03-24 11:51:29 -06:00
Bel LaPointe
e491cc5cbc refresher input 2023-03-24 11:46:55 -06:00
bel
37d02f0f52 debugs 2023-03-23 21:05:30 -06:00
bel
e832085fc2 todo 2023-03-23 20:55:58 -06:00
bel
8e92c9a6d6 keyboard supports !a to indicate release a 2023-03-23 20:39:57 -06:00
Bel LaPointe
1fc6d71db6 $INPUT_BUFFERED_STICKY_DURATION 2023-03-23 17:00:49 -06:00
Bel LaPointe
4f48ee805f whoops nums bitwised 2023-03-23 16:14:25 -06:00
Bel LaPointe
f9ec874491 support numbers as well 2023-03-23 16:10:31 -06:00
Bel LaPointe
32c186e1e2 input ignores newline chars 2023-03-23 15:53:01 -06:00
Bel LaPointe
17b2891f9a readme 2023-03-02 15:38:59 -07:00
61 changed files with 1924 additions and 117 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/mayhem-party
**/*.sw*

View File

@@ -9,3 +9,14 @@ Think Dug's Twitch Chat Plays
* multiplayer engine
* server; https://github.com/LizardByte/Sunshine
* client; https://moonlight-stream.org/
## DONE
* input
* random from weighted file
* buffered
* remapped from file
* output
* to keyboard
* to stderr

15
go.mod
View File

@@ -3,6 +3,17 @@ 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/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,4 +1,44 @@
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/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/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=

58
host.d/README.md Normal file
View File

@@ -0,0 +1,58 @@
# 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
# https://www.reddit.com/r/rust/comments/5k8uab/crosscompiling_from_ubuntu_to_windows_with_rustup/
(
echo '[target.x86_64-pc-windows-gnu]'
echo 'linker = "x86_64-w64-mingw32-gcc"'
echo 'ar = "x86_64-w64-mingw32-gcc-ar"'
) >> $HOME/.cargo/config
sudo apt install mingw-w64
rustup target add x86_64-pc-windows-gnu
echo windows
cargo build --release --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

@@ -0,0 +1,7 @@
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
export BUTTON_V01_CONFIG=./config.d/mayhem-party.d/v01.yaml

View File

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

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

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1,55 @@
# 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

@@ -0,0 +1,55 @@
# 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

@@ -0,0 +1,55 @@
# 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

@@ -0,0 +1,55 @@
# 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

@@ -0,0 +1,21 @@
#! /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

@@ -0,0 +1,21 @@
feedback:
addr: :17071
users:
bel:
player: 2
message: its bel
broadcast:
message: 8
players:
- {}
- transformation:
w: t
a: f
s: g
d: h
q: r
e: y
1: 5
2: 6
3: 7
4: 8

View File

@@ -0,0 +1,30 @@
streams:
input:
debug: false
engine:
name: gui
gui:
user: bel
feedback:
url: http://mayhem-party.home.blapointe.com:17071?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: false
engine:
name: udp
udp:
host: mayhem-party.home.blapointe.com
port: 17070

View File

@@ -0,0 +1,30 @@
streams:
input:
debug: false
engine:
name: gui
gui:
user: zach
feedback:
url: http://mayhem-party.home.blapointe.com:17071?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: false
engine:
name: udp
udp:
host: mayhem-party.home.blapointe.com
port: 17070

View File

@@ -0,0 +1,30 @@
streams:
input:
debug: false
engine:
name: gui
gui:
user: chase
feedback:
url: http://mayhem-party.home.blapointe.com:17071?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: false
engine:
name: udp
udp:
host: mayhem-party.home.blapointe.com
port: 17070

View File

@@ -0,0 +1,30 @@
streams:
input:
debug: false
engine:
name: gui
gui:
user: mason
feedback:
url: http://mayhem-party.home.blapointe.com:17071?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: false
engine:
name: udp
udp:
host: mayhem-party.home.blapointe.com
port: 17070

View File

@@ -0,0 +1,30 @@
streams:
input:
debug: false
engine:
name: gui
gui:
user: nat
feedback:
url: http://mayhem-party.home.blapointe.com:17071?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: false
engine:
name: udp
udp:
host: mayhem-party.home.blapointe.com
port: 17070

View File

@@ -0,0 +1,30 @@
streams:
input:
debug: false
engine:
name: gui
gui:
user: roxy
feedback:
url: http://mayhem-party.home.blapointe.com:17071?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: false
engine:
name: udp
udp:
host: mayhem-party.home.blapointe.com
port: 17070

View File

@@ -0,0 +1,30 @@
streams:
input:
debug: false
engine:
name: gui
gui:
user: bill
feedback:
url: http://mayhem-party.home.blapointe.com:17071?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: false
engine:
name: udp
udp:
host: mayhem-party.home.blapointe.com
port: 17070

View File

@@ -0,0 +1 @@
../../../../rusty-pipe.d/target/x86_64-pc-windows-gnu/release/rusty-pipe.exe

View File

@@ -0,0 +1,6 @@
package button
type Button struct {
Char byte
Down bool
}

View File

@@ -2,28 +2,20 @@ package input
import (
"context"
"os"
"mayhem-party/src/device/input/button"
"mayhem-party/src/device/input/parse"
"mayhem-party/src/device/input/raw"
"mayhem-party/src/device/input/wrap"
)
type Input interface {
Read() []Button
Read() []button.Button
Close()
}
func New(ctx context.Context) Input {
foo := randomCharFromRange('a', 'g')
if p, ok := os.LookupEnv("INPUT_RANDOM_WEIGHT_FILE"); ok && len(p) > 0 {
foo = randomCharFromWeightFile(p)
}
var result Input = NewRandom(foo)
if os.Getenv("INPUT_KEYBOARD") == "true" {
result = NewKeyboard()
}
if os.Getenv("INPUT_BUFFERED") == "true" {
result = NewBuffered(ctx, result)
}
if p, ok := os.LookupEnv("INPUT_REMAP_FILE"); ok && len(p) > 0 {
result = NewRemapFromFile(result, p)
}
return result
src := raw.New(ctx)
return wrap.New(ctx, func() wrap.Wrap {
return parse.New(ctx, src)
})
}

View File

@@ -3,6 +3,8 @@ package input_test
import (
"context"
"mayhem-party/src/device/input"
"mayhem-party/src/device/input/raw"
"mayhem-party/src/device/input/wrap"
"os"
"path"
"testing"
@@ -30,11 +32,11 @@ func TestNewRemapped(t *testing.T) {
t.Fatal(err)
}
os.Setenv("INPUT_REMAP_FILE", remap)
os.Setenv("INPUT_RANDOM_WEIGHT_FILE", rand)
wrap.FlagRemapFile = remap
raw.FlagRawRandomWeightFile = rand
t.Cleanup(func() {
os.Unsetenv("INPUT_REMAP_FILE")
os.Unsetenv("INPUT_RANDOM_WEIGHT_FILE")
wrap.FlagRemapFile = ""
raw.FlagRawRandomWeightFile = ""
})
r := input.New(context.Background())
@@ -50,9 +52,9 @@ func TestNewRemapped(t *testing.T) {
}
func TestNewBuffered(t *testing.T) {
os.Setenv("INPUT_BUFFERED", "true")
wrap.FlagBuffered = true
t.Cleanup(func() {
os.Unsetenv("INPUT_BUFFERED")
wrap.FlagBuffered = false
})
r := input.New(context.Background())
@@ -71,9 +73,9 @@ func TestNewRandomWeightFile(t *testing.T) {
t.Fatal(err)
}
os.Setenv("INPUT_RANDOM_WEIGHT_FILE", p)
raw.FlagRawRandomWeightFile = p
t.Cleanup(func() {
os.Unsetenv("INPUT_RANDOM_WEIGHT_FILE")
raw.FlagRawRandomWeightFile = ""
})
r := input.New(context.Background())

View File

@@ -1,10 +0,0 @@
package input
import "testing"
func TestInput(t *testing.T) {
var _ Input = &Random{}
var _ Input = Keyboard{}
var _ Input = &Buffered{}
var _ Input = Remap{}
}

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

@@ -0,0 +1,48 @@
package parse
import (
"mayhem-party/src/device/input/button"
"mayhem-party/src/device/input/raw"
"os"
)
var (
FlagParsePlaintextRelease = os.Getenv("PARSE_PLAINTEXT_RELEASE")
)
type Plaintext struct {
src raw.Raw
release byte
}
func NewPlaintext(src raw.Raw) Plaintext {
releaseChar := byte('!')
if FlagParsePlaintextRelease != "" {
releaseChar = byte(FlagParsePlaintextRelease[0])
}
return Plaintext{
src: src,
release: releaseChar,
}
}
func (p Plaintext) Close() { p.src.Close() }
func (p Plaintext) CloseWrap() raw.Raw { return p.src }
func (p Plaintext) Read() []button.Button {
b := p.src.Read()
buttons := make([]button.Button, 0, len(b))
down := true
for i := range b {
if b[i] == p.release {
down = false
} else {
if b[i] != '\n' {
buttons = append(buttons, button.Button{Char: b[i], Down: down})
}
down = true
}
}
return buttons
}

View File

@@ -0,0 +1,30 @@
package parse_test
import (
"mayhem-party/src/device/input/button"
"mayhem-party/src/device/input/parse"
"testing"
)
func TestPlaintext(t *testing.T) {
src := constSrc("c!b")
p := parse.NewPlaintext(src)
got := p.Read()
if len(got) != 2 {
t.Fatal(len(got))
}
if got[0] != (button.Button{Char: 'c', Down: true}) {
t.Error(got[0])
}
if got[1] != (button.Button{Char: 'b', Down: false}) {
t.Error(got[1])
}
}
type constSrc string
func (c constSrc) Close() {}
func (c constSrc) Read() []byte {
return []byte(c)
}

View File

@@ -0,0 +1,15 @@
package v01
type config struct {
Feedback struct {
Addr string
TTSURL string
}
Users map[string]struct {
Player int
Message string
}
Players []struct {
Transformation transformation
}
}

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,100 @@
package v01
import (
"io"
"log"
"mayhem-party/src/device/input/wrap"
"net/http"
"sync"
"syscall"
)
func (v01 *V01) listen() {
if v01.cfg.Feedback.Addr == "" {
return
}
v01._listen()
}
func (v01 *V01) _listen() {
mutex := &sync.RWMutex{}
s := &http.Server{
Addr: v01.cfg.Feedback.Addr,
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
mutex.RLock()
defer mutex.RUnlock()
} else {
mutex.Lock()
defer mutex.Unlock()
}
v01.ServeHTTP(w, r)
}),
}
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.globalQueries(r)
}
func (v01 *V01) serveHTTP(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/":
v01.getUserFeedback(w, r)
case "/broadcast":
v01.putBroadcast(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.Message))
}
func (v01 *V01) putBroadcast(w http.ResponseWriter, r *http.Request) {
b, _ := io.ReadAll(r.Body)
v := v01.cfg.Users["broadcast"]
v.Message = string(b)
v01.cfg.Users["broadcast"] = v
}
func (v01 *V01) globalQueries(r *http.Request) {
v01.globalQuerySay(r)
v01.globalQueryRefresh(r)
}
func (v01 *V01) globalQuerySay(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) globalQueryRefresh(r *http.Request) {
if _, ok := r.URL.Query()["refresh"]; !ok {
return
}
select {
case wrap.ChSigUsr1 <- syscall.SIGUSR1:
default:
}
}

View File

@@ -0,0 +1,19 @@
feedback:
addr: :17071
ttsurl: http://localhost:15002
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

@@ -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,73 @@
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)
time.Sleep(duration)
return nil
}

View File

@@ -0,0 +1,86 @@
package v01
import (
"context"
"encoding/json"
"io/ioutil"
"log"
"mayhem-party/src/device/input/button"
"mayhem-party/src/device/input/raw"
"os"
"time"
"gopkg.in/yaml.v2"
)
var (
FlagDebug = os.Getenv("DEBUG") == "true"
FlagParseV01Config = os.Getenv("PARSE_V01_CONFIG")
)
type (
V01 struct {
ctx context.Context
can context.CancelFunc
src raw.Raw
cfg config
}
)
func NewV01(ctx context.Context, src raw.Raw) *V01 {
var cfg config
b, _ := ioutil.ReadFile(FlagParseV01Config)
yaml.Unmarshal(b, &cfg)
ctx, can := context.WithCancel(ctx)
result := &V01{
ctx: ctx,
can: can,
src: src,
cfg: cfg,
}
go result.listen()
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)
return v01.transform(msg).buttons()
}
func (v01 *V01) telemetry(msg message) {
if FlagDebug {
log.Printf("%s|%dms", msg.U, time.Now().UnixNano()/int64(time.Millisecond)-msg.T)
}
}
func (v01 *V01) transform(msg message) message {
if len(v01.cfg.Players) == 0 {
return msg
}
user := v01.cfg.Users[msg.U]
if user.Player < 1 {
msg.Y = ""
msg.N = ""
return msg
}
player := v01.cfg.Players[user.Player-1]
msg.Y = player.Transformation.pipe(msg.Y)
msg.N = player.Transformation.pipe(msg.N)
return msg
}

View File

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

View File

@@ -0,0 +1,51 @@
package v01
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 := 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)
}
})
}
}

View File

@@ -1,7 +1,8 @@
package input
package raw
import (
"io"
"log"
"os"
"os/exec"
"runtime"
@@ -43,16 +44,14 @@ func (kb Keyboard) Close() {
}
}
func (kb Keyboard) Read() []Button {
func (kb Keyboard) Read() []byte {
b := make([]byte, 5)
n, err := os.Stdin.Read(b)
if err != nil && err != io.EOF {
panic(err)
}
result := make([]Button, n)
for i := range result {
result[i] = Button{Char: b[i], Down: true}
if FlagDebug {
log.Printf("raw.Keyboard.Read() %s", b[:n])
}
return result
return b[:n]
}

View File

@@ -1,4 +1,4 @@
package input
package raw
import (
"bytes"
@@ -6,19 +6,15 @@ import (
"io"
"math/rand"
"os"
"sort"
"time"
"github.com/go-yaml/yaml"
)
type Button struct {
Char byte
Down bool
}
type Random struct {
generator func() byte
down []Button
down []byte
}
func NewRandom(generator func() byte) *Random {
@@ -29,19 +25,8 @@ func NewRandom(generator func() byte) *Random {
func (r *Random) Close() {
}
func (r *Random) Read() []Button {
if len(r.down) > 0 && rand.Int()%2 == 0 {
was := r.down
for i := range was {
was[i].Down = false
}
r.down = r.down[:0]
return was
} else {
c := Button{Char: r.generator(), Down: true}
r.down = append(r.down, c)
return []Button{c}
}
func (r *Random) Read() []byte {
return []byte{r.generator()}
}
func randomCharFromRange(start, stop byte) func() byte {
@@ -87,11 +72,15 @@ func randomCharFromWeights(m map[byte]int) func() byte {
}
sum += v
}
sort.Slice(result, func(i, j int) bool {
return result[i].i < result[j].i
})
if sum <= 0 {
panic("weights must total nonzero")
}
return func() byte {
n := rand.Int() % sum
r := rand.Int()
n := r % (sum + 1)
for _, v := range result {
n -= v.i
if n <= 0 {

View File

@@ -1,4 +1,4 @@
package input
package raw
import (
"strings"
@@ -8,7 +8,7 @@ import (
func TestRandomCharFromWeights(t *testing.T) {
weights := map[byte]int{
'a': 1,
'b': 99,
'b': 20,
}
foo := randomCharFromWeights(weights)
for {

View File

@@ -0,0 +1,32 @@
package raw
import (
"context"
"os"
"strconv"
)
var (
FlagRawKeyboard = os.Getenv("RAW_KEYBOARD") == "true"
FlagRawUDP = os.Getenv("RAW_UDP")
FlagRawRandomWeightFile = os.Getenv("RAW_RANDOM_WEIGHT_FILE")
)
type Raw interface {
Read() []byte
Close()
}
func New(ctx context.Context) Raw {
if FlagRawKeyboard {
return NewKeyboard()
}
if port, _ := strconv.Atoi(FlagRawUDP); port != 0 {
return NewUDP(ctx, port)
}
generator := randomCharFromRange('a', 'g')
if FlagRawRandomWeightFile != "" {
generator = randomCharFromWeightFile(FlagRawRandomWeightFile)
}
return NewRandom(generator)
}

View File

@@ -0,0 +1,9 @@
package raw
import "testing"
func TestRaw(t *testing.T) {
var _ Raw = &Random{}
var _ Raw = UDP{}
var _ Raw = Keyboard{}
}

View File

@@ -0,0 +1,63 @@
package raw
import (
"context"
"log"
"net"
"os"
"strconv"
)
var (
FlagDebug = os.Getenv("DEBUG") == "true"
)
type UDP struct {
conn net.PacketConn
c chan []byte
ctx context.Context
}
func NewUDP(ctx context.Context, port int) UDP {
conn, err := net.ListenPacket("udp", ":"+strconv.Itoa(port))
if err != nil {
panic(err)
}
result := UDP{
conn: conn,
c: make(chan []byte, 8),
ctx: ctx,
}
go result.listen()
return result
}
func (udp UDP) listen() {
for udp.ctx.Err() == nil {
buff := make([]byte, 256)
n, _, err := udp.conn.ReadFrom(buff)
if err != nil && udp.ctx.Err() == nil {
panic(err)
}
if FlagDebug {
log.Printf("raw.UDP.Read() => %s", buff[:n])
}
select {
case udp.c <- buff[:n]:
case <-udp.ctx.Done():
}
}
}
func (udp UDP) Read() []byte {
select {
case v := <-udp.c:
return v
case <-udp.ctx.Done():
return []byte{}
}
}
func (udp UDP) Close() {
udp.conn.Close()
}

View File

@@ -1,31 +1,42 @@
package input
package wrap
import (
"context"
"mayhem-party/src/device/input/button"
"mayhem-party/src/device/input/raw"
"os"
"sync"
"time"
)
var (
FlagBufferedStickyDuration = os.Getenv("WRAP_BUFFERED_STICKY_DURATION")
)
type Buffered struct {
ctx context.Context
can context.CancelFunc
lock sync.Mutex
keys map[byte]int64
input Input
input Wrap
listenInterval time.Duration
expirationInterval time.Duration
}
func NewBuffered(ctx context.Context, input Input) *Buffered {
func NewBuffered(ctx context.Context, input Wrap) *Buffered {
ctx, can := context.WithCancel(ctx)
expirationInterval := time.Millisecond * 125
if d, err := time.ParseDuration(FlagBufferedStickyDuration); err == nil {
expirationInterval = d
}
result := &Buffered{
input: input,
ctx: ctx,
can: can,
lock: sync.Mutex{},
keys: map[byte]int64{},
listenInterval: time.Millisecond * 20,
expirationInterval: time.Millisecond * 100,
listenInterval: time.Millisecond * 10,
expirationInterval: expirationInterval,
}
go result.listen()
return result
@@ -39,6 +50,8 @@ func (b *Buffered) listen() {
for i := range buttons {
if buttons[i].Down {
b.keys[buttons[i].Char] = time.Now().UnixNano()
} else {
b.keys[buttons[i].Char] = 0
}
}
b.lock.Unlock()
@@ -49,12 +62,17 @@ func (b *Buffered) listen() {
}
}
func (b *Buffered) CloseWrap() raw.Raw {
b.can()
return b.input.CloseWrap()
}
func (b *Buffered) Close() {
b.input.Close()
b.can()
}
func (b *Buffered) Read() []Button {
func (b *Buffered) Read() []button.Button {
for b.ctx.Err() == nil {
result := b.read()
if len(result) > 0 {
@@ -65,19 +83,19 @@ func (b *Buffered) Read() []Button {
case <-time.After(b.listenInterval):
}
}
return []Button{}
return []button.Button{}
}
func (b *Buffered) read() []Button {
func (b *Buffered) read() []button.Button {
b.lock.Lock()
defer b.lock.Unlock()
result := make([]Button, 0, len(b.keys))
result := make([]button.Button, 0, len(b.keys))
for k, v := range b.keys {
isFresh := v > 0
isStale := v < 0 && time.Since(time.Unix(0, -1*v)) > b.expirationInterval
if isFresh || isStale {
result = append(result, Button{Char: k, Down: isFresh})
result = append(result, button.Button{Char: k, Down: isFresh})
}
if isFresh {
b.keys[k] = -1 * v

View File

@@ -0,0 +1,64 @@
package wrap
import (
"context"
"log"
"mayhem-party/src/device/input/button"
"mayhem-party/src/device/input/raw"
"os"
"os/signal"
"syscall"
)
var (
ChSigUsr1 = func() chan os.Signal {
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGUSR1)
return c
}()
)
type Refresh struct {
can context.CancelFunc
input Wrap
}
func NewRefresh(ctx context.Context, newWrap func() Wrap) *Refresh {
return NewRefreshWith(ctx, newWrap, ChSigUsr1)
}
func NewRefreshWith(ctx context.Context, newWrap func() Wrap, ch <-chan os.Signal) *Refresh {
ctx, can := context.WithCancel(ctx)
result := &Refresh{
can: can,
input: newWrap(),
}
go func() {
defer log.Println("refreshing done")
for {
select {
case <-ctx.Done():
return
case sig := <-ch:
log.Println("refreshing for", sig)
result.input.CloseWrap()
result.input = newWrap()
}
}
}()
return result
}
func (r *Refresh) CloseWrap() raw.Raw {
r.can()
return r.input.CloseWrap()
}
func (r *Refresh) Read() []button.Button {
return r.input.Read()
}
func (r *Refresh) Close() {
r.can()
r.input.Close()
}

View File

@@ -0,0 +1,101 @@
package wrap
import (
"context"
"mayhem-party/src/device/input/button"
"mayhem-party/src/device/input/raw"
"os"
"syscall"
"testing"
"time"
)
func TestRefresh(t *testing.T) {
b := byte('a')
generator := func() Wrap {
b += byte(1)
return dummyParser{Char: b}
}
ch := make(chan os.Signal, 1)
defer close(ch)
refresh := NewRefreshWith(context.Background(), generator, ch)
defer refresh.Close()
assertIts := func(t *testing.T, b byte) {
for i := 0; i < 10; i++ {
some := false
for j, button := range refresh.Read() {
some = true
if button.Char != b {
t.Error(i, j, b, button)
}
}
if !some {
t.Error("empty read")
}
}
}
t.Run("called once on init", func(t *testing.T) {
assertIts(t, byte('b'))
})
ch <- syscall.SIGUSR1
time.Sleep(time.Millisecond * 450)
t.Run("called once on signal", func(t *testing.T) {
assertIts(t, byte('c'))
})
}
func TestRefreshDoesntCloseSources(t *testing.T) {
src := &telemetrySrc{}
newParsers := 0
newParser := func() Wrap {
newParsers += 1
return src
}
ctx, can := context.WithCancel(context.Background())
defer can()
refresh := NewRefresh(ctx, newParser)
if newParsers != 1 {
t.Error(newParsers)
}
for i := 0; i < 5; i++ {
refresh.Read()
}
if want := (telemetrySrc{reads: 5}); *src != want {
t.Errorf("%+v", *src)
} else if newParsers != 1 {
t.Error(newParsers)
}
for i := 0; i < 5; i++ {
ChSigUsr1 <- syscall.SIGINT
}
time.Sleep(time.Millisecond * 250)
if want := (telemetrySrc{reads: 5, closeWraps: 5}); *src != want {
t.Errorf("want %+v, got %+v", want, *src)
} else if newParsers != 6 {
t.Error(newParsers)
}
}
type telemetrySrc struct {
closeWraps int
closes int
reads int
}
func (src *telemetrySrc) CloseWrap() raw.Raw {
src.closeWraps += 1
return nil
}
func (src *telemetrySrc) Close() {
src.closes += 1
}
func (src *telemetrySrc) Read() []button.Button {
src.reads += 1
return nil
}

View File

@@ -1,17 +1,19 @@
package input
package wrap
import (
"mayhem-party/src/device/input/button"
"mayhem-party/src/device/input/raw"
"os"
"github.com/go-yaml/yaml"
)
type Remap struct {
input Input
input Wrap
m map[byte]byte
}
func NewRemapFromFile(input Input, p string) Remap {
func NewRemapFromFile(input Wrap, p string) Remap {
b, err := os.ReadFile(p)
if err != nil {
panic(err)
@@ -30,18 +32,22 @@ func NewRemapFromFile(input Input, p string) Remap {
return NewRemap(input, remap)
}
func NewRemap(input Input, m map[byte]byte) Remap {
func NewRemap(input Wrap, m map[byte]byte) Remap {
return Remap{
input: input,
m: m,
}
}
func (re Remap) CloseWrap() raw.Raw {
return re.input.CloseWrap()
}
func (re Remap) Close() {
re.input.Close()
}
func (re Remap) Read() []Button {
func (re Remap) Read() []button.Button {
result := re.input.Read()
for i := range result {
if v, ok := re.m[result[i].Char]; ok {

View File

@@ -0,0 +1,45 @@
package wrap
import (
"context"
"mayhem-party/src/device/input/button"
"mayhem-party/src/device/input/raw"
"os"
)
var (
FlagBuffered = os.Getenv("WRAP_BUFFERED") == "true"
FlagRemapFile = os.Getenv("WRAP_REMAP_FILE")
FlagRefreshOnSigUsr1 = os.Getenv("WRAP_REFRESH_ON_SIGUSR1") == "true"
)
type Wrap interface {
Read() []button.Button
Close()
CloseWrap() raw.Raw
}
func New(ctx context.Context, parserFunc func() Wrap) Wrap {
maker := func() Wrap {
return parserFunc()
}
if FlagBuffered {
oldMaker := maker
maker = func() Wrap {
return NewBuffered(ctx, oldMaker())
}
}
if FlagRemapFile != "" {
oldMaker := maker
maker = func() Wrap {
return NewRemapFromFile(oldMaker(), FlagRemapFile)
}
}
if FlagRefreshOnSigUsr1 {
oldMaker := maker
maker = func() Wrap {
return NewRefresh(ctx, oldMaker)
}
}
return maker()
}

View File

@@ -0,0 +1,22 @@
package wrap
import (
"mayhem-party/src/device/input/button"
"mayhem-party/src/device/input/raw"
"testing"
)
func TestWrap(t *testing.T) {
var _ Wrap = dummyParser{}
var _ Wrap = &Refresh{}
var _ Wrap = &Buffered{}
var _ Wrap = &Remap{}
}
type dummyParser button.Button
func (d dummyParser) CloseWrap() raw.Raw { return nil }
func (d dummyParser) Close() {}
func (d dummyParser) Read() []button.Button {
return []button.Button{button.Button(d)}
}

View File

@@ -28,6 +28,31 @@ var (
'x': X,
'y': Y,
'z': Z,
'1': N1,
'2': N2,
'3': N3,
'4': N4,
'5': N5,
'6': N6,
'7': N7,
'8': N8,
'9': N9,
'0': N0,
'!': F1,
'@': F2,
'#': F3,
'$': F4,
'%': F5,
'^': F6,
'&': F7,
'*': F8,
'(': F9,
')': F10,
',': PComma,
'/': PFSlash,
';': PSemicolon,
'-': PMinus,
'=': PEqual,
}
keyToChar = func() map[Key]byte {
result := map[Key]byte{}
@@ -47,7 +72,7 @@ func ToChar(k Key) byte {
}
func FromChar(b byte) Key {
if b < 'a' {
if 'A' <= b && b <= 'Z' {
b += 'a' - 'A'
}
v, ok := charToKey[b]

View File

@@ -3,13 +3,16 @@ package key
import "testing"
func TestFromChar(t *testing.T) {
if got := FromChar('1'); got != N1 {
t.Error(got)
}
if got := FromChar('a'); got != A {
t.Error(got)
}
if got := FromChar('A'); got != A {
t.Error(got)
}
if got := FromChar('!'); got != Undef {
if got := FromChar(byte(0)); got != Undef {
t.Error(got)
}
if got := ToChar(A); got != 'a' {

View File

@@ -5,31 +5,56 @@ import "github.com/micmonay/keybd_event"
type Key int
const (
Undef = Key(keybd_event.VK_SP11)
A = Key(keybd_event.VK_A)
B = Key(keybd_event.VK_B)
C = Key(keybd_event.VK_C)
D = Key(keybd_event.VK_D)
E = Key(keybd_event.VK_E)
F = Key(keybd_event.VK_F)
G = Key(keybd_event.VK_G)
H = Key(keybd_event.VK_H)
I = Key(keybd_event.VK_I)
J = Key(keybd_event.VK_J)
K = Key(keybd_event.VK_K)
L = Key(keybd_event.VK_L)
M = Key(keybd_event.VK_M)
N = Key(keybd_event.VK_N)
O = Key(keybd_event.VK_O)
P = Key(keybd_event.VK_P)
Q = Key(keybd_event.VK_Q)
R = Key(keybd_event.VK_R)
S = Key(keybd_event.VK_S)
T = Key(keybd_event.VK_T)
U = Key(keybd_event.VK_U)
V = Key(keybd_event.VK_V)
W = Key(keybd_event.VK_W)
X = Key(keybd_event.VK_X)
Y = Key(keybd_event.VK_Y)
Z = Key(keybd_event.VK_Z)
Undef = Key(keybd_event.VK_SP11)
A = Key(keybd_event.VK_A)
B = Key(keybd_event.VK_B)
C = Key(keybd_event.VK_C)
D = Key(keybd_event.VK_D)
E = Key(keybd_event.VK_E)
F = Key(keybd_event.VK_F)
G = Key(keybd_event.VK_G)
H = Key(keybd_event.VK_H)
I = Key(keybd_event.VK_I)
J = Key(keybd_event.VK_J)
K = Key(keybd_event.VK_K)
L = Key(keybd_event.VK_L)
M = Key(keybd_event.VK_M)
N = Key(keybd_event.VK_N)
O = Key(keybd_event.VK_O)
P = Key(keybd_event.VK_P)
Q = Key(keybd_event.VK_Q)
R = Key(keybd_event.VK_R)
S = Key(keybd_event.VK_S)
T = Key(keybd_event.VK_T)
U = Key(keybd_event.VK_U)
V = Key(keybd_event.VK_V)
W = Key(keybd_event.VK_W)
X = Key(keybd_event.VK_X)
Y = Key(keybd_event.VK_Y)
Z = Key(keybd_event.VK_Z)
N1 = Key(keybd_event.VK_1)
N2 = Key(keybd_event.VK_2)
N3 = Key(keybd_event.VK_3)
N4 = Key(keybd_event.VK_4)
N5 = Key(keybd_event.VK_5)
N6 = Key(keybd_event.VK_6)
N7 = Key(keybd_event.VK_7)
N8 = Key(keybd_event.VK_8)
N9 = Key(keybd_event.VK_9)
N0 = Key(keybd_event.VK_0)
F1 = Key(keybd_event.VK_F1)
F2 = Key(keybd_event.VK_F2)
F3 = Key(keybd_event.VK_F3)
F4 = Key(keybd_event.VK_F4)
F5 = Key(keybd_event.VK_F5)
F6 = Key(keybd_event.VK_F6)
F7 = Key(keybd_event.VK_F7)
F8 = Key(keybd_event.VK_F8)
F9 = Key(keybd_event.VK_F9)
F10 = Key(keybd_event.VK_F10)
PComma = Key(keybd_event.VK_COMMA)
PFSlash = Key(keybd_event.VK_BACKSLASH)
PSemicolon = Key(keybd_event.VK_SEMICOLON)
PMinus = Key(keybd_event.VK_MINUS)
PEqual = Key(keybd_event.VK_EQUAL)
)

View File

@@ -2,6 +2,7 @@ package src
import (
"context"
"log"
"mayhem-party/src/device/input"
"mayhem-party/src/device/output"
"mayhem-party/src/device/output/key"
@@ -16,7 +17,7 @@ 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)
} else {
@@ -37,6 +38,9 @@ func Main(ctx context.Context) error {
state := map[key.Key]bool{}
for block() {
delta := reader.Read()
if os.Getenv("DEBUG") == "true" {
log.Printf("src.Main.reader.Read(): %+v", delta)
}
for _, button := range delta {
state[key.FromChar(button.Char)] = button.Down
}

View File

@@ -2,3 +2,9 @@ w: i
a: j
s: k
d: l
q: u
e: o
1: 0
2: 9
3: 8
4: 7

View File

@@ -1,2 +1,2 @@
export INPUT_BUFFERED=true
export INPUT_KEYBOARD=true
export WRAP_BUFFERED=true
export RAW_KEYBOARD=true

View File

@@ -1,2 +1,2 @@
export INPUT_RANDOM_WEIGHT_FILE=testdata/INPUT_RANDOM_WEIGHT_FILE.yaml
export INPUT_REMAP_FILE=testdata/INPUT_REMAP_FILE.yaml
export RAW_RANDOM_WEIGHT_FILE=testdata/INPUT_RANDOM_WEIGHT_FILE.yaml
export WRAP_REMAP_FILE=testdata/INPUT_REMAP_FILE.yaml

View File

@@ -1,3 +1,3 @@
export OUTPUT_KEYBOARD=true
export MAIN_INTERVAL_DURATION=250ms
export INPUT_RANDOM_WEIGHT_FILE=testdata/INPUT_RANDOM_WEIGHT_FILE.yaml
export RAW_RANDOM_WEIGHT_FILE=testdata/INPUT_RANDOM_WEIGHT_FILE.yaml

80
todo.yaml Executable file
View File

@@ -0,0 +1,80 @@
todo:
- https via home.blapointe and rproxy
- tts for when someone said the word via larynx docker + http.get + https://pkg.go.dev/github.com/faiface/beep@v1.1.0/wav
- 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
- 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
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
- todo: change from 'a','b','c' from rust to just 11,21,31,41 so playerName is known
implicitly
ts: Sat Mar 25 00:06:21 MDT 2023
- todo: lag via UDP formatted inputs as space-delimited TS PID buttonIdx buttonIdx
buttonIdx
ts: Sat Mar 25 00:13:19 MDT 2023
- todo: map keys triggered by user to player idx and their keys
ts: Sat Mar 25 00:44:19 MDT 2023
- todo: use button.V01Cfg; map keys triggered by user to player idx and their keys
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