Compare commits

...

80 Commits

Author SHA1 Message Date
bel b89ed62036 dont ?say if hotword is a gm command 2023-04-19 18:49:34 -06:00
bel 67c93a9048 pageup, pagedown instead 2023-04-19 18:37:27 -06:00
bel 8eae7ae9a6 Merge branch 'master' of https://gogs.inhome.blapointe.com/bel/mayhem-party 2023-04-19 18:29:02 -06:00
bel ce32620940 add <=F24, >=F23 2023-04-19 18:28:39 -06:00
Bel LaPointe 24f4b6b8f5 v01 config has gm.hotwords.[].{call,args} and impl tap to link /gm/rpc/someoneSaidAlias to button pushes 2023-04-19 18:04:10 -06:00
zach-m 440191de0f Merge branch 'master' of https://gogs.inhome.blapointe.com/bel/mayhem-party 2023-04-10 18:55:35 -06:00
zach-m d5adc596ac adding "labels" 2023-04-10 18:55:26 -06:00
bel 41a39c40d0 oops debug logs 2023-04-10 18:40:43 -06:00
bel 9a38033b65 RAW_WS serves all static files 2023-04-10 18:38:38 -06:00
zach-m 6a4ad5ec36 adding labels 2023-04-10 18:26:47 -06:00
zach-m c2b8ab67f2 adding labels 2023-04-10 18:09:59 -06:00
Bel LaPointe 9418cecdf5 pre 2023-04-09 12:00:19 -06:00
Bel LaPointe fb5da88774 explicit ws debug 2023-04-09 11:57:45 -06:00
Bel LaPointe 39f6bc8ed9 RAW_WS_PROXY_URL=http://localhost:17071, RAW_WS=8080 to use web 2023-04-09 11:53:21 -06:00
Bel LaPointe f3cbfa1c48 html needs /proxy 2023-04-09 11:46:06 -06:00
Bel LaPointe 444245c0f5 sh 2023-04-09 11:37:46 -06:00
Bel LaPointe 52ee1e5083 mvp control via browser 2023-04-09 11:25:51 -06:00
bel 934158b7a3 y no ui 2023-04-02 11:15:51 -06:00
bel 87e63c27df go build 2023-04-02 11:13:28 -06:00
bel f98e417ba6 gr 2023-04-02 11:13:13 -06:00
bel d6a7ee3db0 grrr 2023-04-02 11:11:26 -06:00
bel b814dabfd3 gr 2023-04-02 11:10:03 -06:00
bel 0a91fc656d sh 2023-04-02 11:08:16 -06:00
bel 5c3341e260 condense status 2023-04-02 11:07:21 -06:00
bel 0903c01b9a verbose user feedback 2023-04-02 10:56:35 -06:00
bel 342e2eef93 alias formatting 2023-04-02 10:44:14 -06:00
bel b8b076450e debug 2023-04-02 10:33:20 -06:00
bel 3bb7cad554 debug 2023-04-02 10:28:12 -06:00
bel 44ec540db3 msg 2023-04-02 09:57:43 -06:00
bel e864f2a9f5 default /get includes broadcast message if no personal message and MySecretWord 2023-04-01 11:38:13 -06:00
bel 3c70e42819 broadcast isnt a user 2023-04-01 11:31:03 -06:00
bel 9de8c91544 export todo 2023-03-28 20:33:31 -06:00
Bel LaPointe 7f2e25458e backwards bool in main 2023-03-28 11:19:53 -06:00
bel 95810d3735 todo 2023-03-27 21:45:46 -06:00
bel df65b1ed07 mtodo 2023-03-27 21:43:44 -06:00
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
43 changed files with 1593 additions and 737 deletions

2
go.mod
View File

@ -9,6 +9,8 @@ require (
)
require (
github.com/evanphx/json-patch/v5 v5.6.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/hajimehoshi/oto v0.7.1 // indirect
github.com/micmonay/keybd_event v1.1.1 // indirect
github.com/pkg/errors v0.9.1 // indirect

5
go.sum
View File

@ -1,5 +1,7 @@
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=
@ -9,12 +11,15 @@ github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38r
github.com/go-audio/wav v1.0.0/go.mod h1:3yoReyQOsiARkvPl3ERCi8JFjihzG6WhjYpZCf5zAWE=
github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwnTLB6vQiq+o=
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hajimehoshi/go-mp3 v0.3.0/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM=
github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI=
github.com/hajimehoshi/oto v0.7.1 h1:I7maFPz5MBCwiutOrz++DLdbr4rTzBsbBuV2VpgU9kk=
github.com/hajimehoshi/oto v0.7.1/go.mod h1:wovJ8WWMfFKvP587mhHgot/MBr4DnNy9m6EepeVGnos=
github.com/icza/bitio v1.0.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A=
github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6/go.mod h1:xQig96I1VNBDIWGCdTt54nHt6EeI639SmHycLYL7FkA=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jfreymuth/oggvorbis v1.0.1/go.mod h1:NqS+K+UXKje0FUYUPosyQ+XTVvjmVjps1aEZH1sumIk=
github.com/jfreymuth/vorbis v1.0.0/go.mod h1:8zy3lUAm9K/rJJk223RKy6vjCZTWC61NA2QD06bfOE0=
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=

View File

@ -1,62 +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
# 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
## TTS
`cd /home/breel/Go/src/gogs.inhome.blapointe.com/tts/larynx.d; bash run.sh`
## STT
`cd /home/breel/Go/src/gogs.inhome.blapointe.com/stt.d/whisper-2023; HOTWORDS=/home/breel/Go/src/gogs.inhome.blapointe.com/mayhem-party.d/host.d/config.d/stt.d/hotwords.txt MIC_TIMEOUT=2 URL=http://localhost:17071/ HEADERS=say='what fool said, {{hotword}}, when they said, {{context}}?' python3 ./hotwords.py'
## `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

View File

@ -1,11 +0,0 @@
export DEBUG=true
export MAIN_INTERVAL_DURATION=5ms
export RAW_UDP=17070
export PARSE_V01=true
export V01_CONFIG=./config.d/mayhem-party.d/v01.yaml
export WRAP_REFRESH_ON_SIGUSR1=true
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_2.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,23 +0,0 @@
feedback:
addr: :17071
ttsurl: http://localhost:15002
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
quiet: false

View File

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

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

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

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

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

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

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

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

View File

@ -1,5 +0,0 @@
mario
party
yo
win
die

View File

@ -11,6 +11,14 @@ import (
func main() {
ctx, can := signal.NotifyContext(context.Background(), syscall.SIGINT)
defer can()
defer func() {
if err := recover(); err != nil {
log.Println("panic:", err)
panic(err)
}
}()
if err := src.Main(ctx); err != nil && ctx.Err() == nil {
panic(err)
}

View File

@ -1,6 +1,12 @@
package button
import "fmt"
type Button struct {
Char byte
Down bool
}
func (button Button) String() string {
return fmt.Sprintf("%c:%v", button.Char, button.Down)
}

View File

@ -1,16 +1,84 @@
package v01
type config struct {
Feedback struct {
import (
"encoding/json"
"sync"
patch "github.com/evanphx/json-patch/v5"
)
type (
config struct {
lock *sync.Mutex
Feedback configFeedback
Users map[string]configUser
Players []configPlayer
Quiet bool
Broadcast configBroadcast
GM configGM
}
configGM struct {
Hotwords map[string]configGMHotword
}
configGMHotword struct {
Call string
Args []string
}
configBroadcast struct {
Message string
}
configFeedback struct {
Addr string
TTSURL string
}
Users map[string]struct {
configUser struct {
Meta configUserMeta
State configUserState
}
configUserMeta struct {
LastTSMS int64
LastLag int64
}
configUserState struct {
Player int
Message string
GM configUserStateGM
}
Players []struct {
configUserStateGM struct {
Alias string
LastAlias string
Vote string
}
configPlayer struct {
Transformation transformation
}
Quiet bool
)
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

@ -1,12 +1,22 @@
package v01
import (
"encoding/json"
"errors"
"fmt"
"io"
"log"
"math/rand"
"mayhem-party/src/device/input/button"
"mayhem-party/src/device/input/wrap"
"net/http"
"os"
"strings"
"sync"
"syscall"
"time"
"gopkg.in/yaml.v2"
)
func (v01 *V01) listen() {
@ -17,18 +27,23 @@ func (v01 *V01) listen() {
}
func (v01 *V01) _listen() {
mutex := &sync.RWMutex{}
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.RLock()
defer mutex.RUnlock()
mutex.Lock()
defer mutex.Unlock()
} else {
mutex.Lock()
defer mutex.Unlock()
}
v01.ServeHTTP(w, r)
v01.stashConfig() // TODO
}),
}
go func() {
@ -46,39 +61,99 @@ func (v01 *V01) _listen() {
func (v01 *V01) ServeHTTP(w http.ResponseWriter, r *http.Request) {
r = r.WithContext(v01.ctx)
v01.serveHTTP(w, r)
v01.globalQueries(r)
v01.serveGlobalQueries(r)
}
func (v01 *V01) serveHTTP(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/":
switch strings.Split(r.URL.Path[1:], "/")[0] {
case "":
v01.getUserFeedback(w, r)
case "/broadcast":
v01.putBroadcast(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"]
user := v01.cfg.Users[r.URL.Query().Get("user")]
msg := user.State.Message
if msg == "" {
msg = v01.cfg.Broadcast.Message
}
alias := user.State.GM.Alias
if alias == "" {
alias = user.State.GM.LastAlias
}
if alias != "" {
msg = fmt.Sprintf("%s (Your secret word is '%s'. Make **someone else** say it!)", msg, alias)
}
w.Write([]byte(msg + "\n\n"))
v01.serveGMStatus(w)
if v01.cfg.Quiet {
w.Write([]byte("\n\n"))
v01.serveGMVoteRead(w)
}
w.Write([]byte(user.Message))
}
func (v01 *V01) putBroadcast(w http.ResponseWriter, r *http.Request) {
func (v01 *V01) servePutBroadcast(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
v01.servePutBroadcastValue(string(b))
}
func (v01 *V01) globalQueries(r *http.Request) {
v01.globalQuerySay(r)
v01.globalQueryRefresh(r)
func (v01 *V01) servePutBroadcastValue(v string) {
v01.cfg.Broadcast.Message = v
}
func (v01 *V01) globalQuerySay(r *http.Request) {
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")
@ -89,7 +164,7 @@ func (v01 *V01) globalQuerySay(r *http.Request) {
go v01.tts(text)
}
func (v01 *V01) globalQueryRefresh(r *http.Request) {
func (v01 *V01) serveGlobalQueryRefresh(r *http.Request) {
if _, ok := r.URL.Query()["refresh"]; !ok {
return
}
@ -98,3 +173,235 @@ func (v01 *V01) globalQueryRefresh(r *http.Request) {
default:
}
}
func (v01 *V01) serveGM(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/gm/rpc/status":
v01.serveGMStatus(w)
case "/gm/rpc/broadcastSomeoneSaidAlias":
v01.serveGMSomeoneSaid(w, r)
case "/gm/rpc/fillNonPlayerAliases":
v01.serveGMFillNonPlayerAliases(w, r)
case "/gm/rpc/vote":
v01.serveGMVote(w, r)
case "/gm/rpc/elect":
v01.serveGMElect(w, r)
case "/gm/rpc/shuffle":
v01.serveGMShuffle(r)
case "/gm/rpc/swap":
if errCode, err := v01.serveGMSwap(r.URL.Query().Get("a"), r.URL.Query().Get("b")); err != nil {
http.Error(w, err.Error(), errCode)
return
}
default:
http.NotFound(w, r)
return
}
}
func (v01 *V01) serveGMStatus(w io.Writer) {
users := map[string]string{}
for k, v := range v01.cfg.Users {
result := ""
if v.State.Player > 0 {
result += fmt.Sprintf("Player %v ", v.State.Player)
}
if ms := time.Duration(v.Meta.LastLag) * time.Millisecond; v.Meta.LastLag > 0 && ms < time.Minute {
result += fmt.Sprintf("%s ", ms.String())
}
if result == "" {
result = "..."
}
users[k] = result
}
b, _ := yaml.Marshal(map[string]interface{}{
"Players": len(v01.cfg.Players),
"Users": users,
})
w.Write(b)
}
func (v01 *V01) serveGMSomeoneSaid(w http.ResponseWriter, r *http.Request) {
if gmHotword, ok := v01.cfg.GM.Hotwords[r.URL.Query().Get("message")]; ok {
v01.serveGMSomeoneSaidGMHotword(w, r, gmHotword)
}
v01.serveGMSomeoneSaidAlias(w, r)
}
func (v01 *V01) serveGMSomeoneSaidGMHotword(w http.ResponseWriter, r *http.Request, gmHotword configGMHotword) {
switch gmHotword.Call {
case "tap":
args := append([]string{}, gmHotword.Args...)
if len(args) < 1 || len(args[0]) < 1 {
return
}
btn := args[0][0]
go func() {
v01.alt <- []button.Button{button.Button{Down: true, Char: btn}}
v01.alt <- []button.Button{button.Button{Down: false, Char: btn}}
}()
r.URL.RawQuery = ""
default:
http.NotFound(w, r)
}
}
func (v01 *V01) serveGMSomeoneSaidAlias(w http.ResponseWriter, r *http.Request) {
v01.cfg.Quiet = true
for k, v := range v01.cfg.Users {
v.State.GM.LastAlias = v.State.GM.Alias
v.State.GM.Alias = ""
v01.cfg.Users[k] = v
}
v01.servePutBroadcastValue(fmt.Sprintf("<<SOMEONE SAID %q>>", strings.ToUpper(r.URL.Query().Get("message"))))
}
func (v01 *V01) serveGMFillNonPlayerAliases(w http.ResponseWriter, r *http.Request) {
b, _ := io.ReadAll(r.Body)
var pool []string
yaml.Unmarshal(b, &pool)
n := 0
for _, v := range v01.cfg.Users {
if v.State.Player == 0 {
n += 1
}
}
if n < 1 {
w.WriteHeader(http.StatusNoContent)
return
}
if len(pool) < n {
http.Error(w, fmt.Sprintf("request body must contain a list of %v options", n), http.StatusBadRequest)
return
}
for i := 0; i < 100; i++ {
a, b := rand.Int()%len(pool), rand.Int()%len(pool)
pool[a], pool[b] = pool[b], pool[a]
}
i := 0
for k, v := range v01.cfg.Users {
if v.State.Player == 0 {
v.State.GM.Alias = pool[i]
v01.cfg.Users[k] = v
i += 1
}
}
}
func (v01 *V01) serveGMElect(w http.ResponseWriter, r *http.Request) {
alias := r.URL.Query().Get("alias")
aliasWinner := ""
votes := map[string]int{}
for k, v := range v01.cfg.Users {
votes[v.State.GM.Vote] = votes[v.State.GM.Vote] + 1
if v.State.GM.LastAlias == alias {
aliasWinner = k
}
}
if aliasWinner == "" {
http.Error(w, "who is "+alias+"?", http.StatusBadRequest)
return
}
threshold := 0.1 + float64(len(votes))/2.0
winner := ""
for k, v := range votes {
if float64(v) > threshold {
winner = k
}
}
if winner == "" {
v01.serveGMShuffle(r)
} else if _, err := v01.serveGMSwap(winner, aliasWinner); err != nil {
v01.serveGMShuffle(r)
}
for k, v := range v01.cfg.Users {
v.State.GM.Vote = ""
v01.cfg.Users[k] = v
}
yaml.NewEncoder(w).Encode(votes)
}
func (v01 *V01) serveGMVote(w http.ResponseWriter, r *http.Request) {
switch r.URL.Query().Get("payload") {
case "":
v01.serveGMVoteRead(w)
default:
v01.serveGMVoteWrite(w, r)
}
}
func (v01 *V01) serveGMVoteRead(w io.Writer) {
counts := map[string]string{}
for k, v := range v01.cfg.Users {
if v.State.GM.Vote != "" {
counts[k] = "voted"
} else {
counts[k] = "voting"
}
}
yaml.NewEncoder(w).Encode(counts)
}
func (v01 *V01) serveGMVoteWrite(w http.ResponseWriter, r *http.Request) {
voter := r.URL.Query().Get("user")
candidate := r.URL.Query().Get("payload")
v, ok := v01.cfg.Users[voter]
if _, ok2 := v01.cfg.Users[candidate]; !ok || !ok2 {
http.Error(w, "bad voter/candidate", http.StatusBadRequest)
return
}
v.State.GM.Vote = candidate
v01.cfg.Users[voter] = v
}
func (v01 *V01) serveGMShuffle(r *http.Request) {
poolSize := len(v01.cfg.Users)
if altSize := len(v01.cfg.Players); altSize > poolSize {
poolSize = altSize
}
pool := make([]int, poolSize)
if poolSize > 0 {
for i := range v01.cfg.Players {
pool[i] = i + 1
}
for i := 0; i < 30; i++ {
l := rand.Int() % poolSize
r := rand.Int() % poolSize
pool[l], pool[r] = pool[r], pool[l]
}
}
i := 0
msg := []string{}
for k, v := range v01.cfg.Users {
v.State.Player = pool[i]
v01.cfg.Users[k] = v
if pool[i] > 0 {
msg = append(msg, fmt.Sprintf("%s is now player %v", k, v.State.Player))
}
i += 1
}
v01.servePutBroadcastValue(strings.Join(msg, ", "))
v01.cfg.Quiet = false
}
func (v01 *V01) serveGMSwap(userA, userB string) (int, error) {
if userA == userB {
return http.StatusConflict, errors.New("/spiderman-pointing")
}
_, okA := v01.cfg.Users[userA]
_, okB := v01.cfg.Users[userB]
if !okA || !okB {
return http.StatusBadRequest, errors.New("who dat?")
}
a := v01.cfg.Users[userA]
b := v01.cfg.Users[userB]
a.State.Player, b.State.Player = b.State.Player, a.State.Player
v01.cfg.Users[userA] = a
v01.cfg.Users[userB] = b
v01.cfg.Quiet = false
v01.servePutBroadcastValue(fmt.Sprintf(`%s is now player %v and %s is now player %v`, userA, a.State.Player, userB, b.State.Player))
return http.StatusOK, nil
}

View File

@ -0,0 +1,592 @@
package v01
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path"
"strconv"
"strings"
"sync"
"testing"
"time"
"gopkg.in/yaml.v2"
)
func TestPatchConfig(t *testing.T) {
dir := t.TempDir()
p := path.Join(dir, t.Name()+".yaml")
cases := map[string]struct {
was config
patch string
want config
}{
"replace entire doc": {
was: config{
Feedback: configFeedback{Addr: "a", TTSURL: "a"},
Users: map[string]configUser{"a": configUser{State: configUserState{Player: 1, Message: "a"}}},
Players: []configPlayer{configPlayer{Transformation: transformation{"a": "a"}}},
Quiet: true,
},
patch: `[{"op": "replace", "path": "", "value": {
"Feedback": {"Addr": "b", "TTSURL": "b"},
"Users": {"b": {"State":{"Player": 2, "Message": "b"}}},
"Players": [{"Transformation": {"b": "b"}}],
"Quiet": false
}}]`,
want: config{
Feedback: configFeedback{Addr: "b", TTSURL: "b"},
Users: map[string]configUser{"b": configUser{State: configUserState{Player: 2, Message: "b"}}},
Players: []configPlayer{configPlayer{Transformation: transformation{"b": "b"}}},
Quiet: false,
},
},
}
for name, d := range cases {
c := d
for _, usesdisk := range []bool{false, true} {
t.Run(fmt.Sprintf("%s disk=%v", name, usesdisk), func(t *testing.T) {
b, _ := yaml.Marshal(c.was)
os.WriteFile(p, b, os.ModePerm)
FlagParseV01Config = ""
if usesdisk {
FlagParseV01Config = p
}
v01 := &V01{cfg: c.was}
v01.cfg.lock = &sync.Mutex{}
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodPatch, "/config", strings.NewReader(c.patch))
v01.servePatchConfig(w, r)
if fmt.Sprintf("%+v", c.want) != fmt.Sprintf("%+v", v01.cfg) {
t.Errorf("want \n\t%+v, got \n\t%+v", c.want, v01.cfg)
}
if usesdisk {
b, _ := os.ReadFile(p)
var got config
yaml.Unmarshal(b, &got)
if fmt.Sprintf("%+v", c.want) != fmt.Sprintf("%+v", v01.cfg) {
t.Errorf("want \n\t%+v, got \n\t%+v", c.want, v01.cfg)
}
}
})
}
}
}
func TestServeGM(t *testing.T) {
ctx, can := context.WithCancel(context.Background())
defer can()
do := func(v01 *V01, path, body string, method ...string) *httptest.ResponseRecorder {
m := http.MethodPost
if len(method) > 0 {
m = method[0]
}
w := httptest.NewRecorder()
r := httptest.NewRequest(m, path, strings.NewReader(body))
v01.ServeHTTP(w, r)
return w
}
t.Run("status", func(t *testing.T) {
v01 := NewV01(ctx, nil)
var result struct {
Players int `yaml:"Players"`
Users map[string]string `yaml:"Users"`
}
t.Run("empty", func(t *testing.T) {
resp := do(v01, "/gm/rpc/status", "")
if resp.Code != http.StatusOK {
t.Error(resp.Code)
}
t.Log(string(resp.Body.Bytes()))
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
t.Fatal(err)
}
if result.Players != 0 {
t.Error(result.Players)
}
if len(result.Users) != 0 {
t.Error(result.Users)
}
})
t.Run("full", func(t *testing.T) {
v01.cfg.Players = []configPlayer{
{},
{},
{},
{},
}
v01.cfg.Users = map[string]configUser{
"bel": configUser{
State: configUserState{Player: 3},
Meta: configUserMeta{
LastTSMS: time.Now().Add(-1*time.Minute).UnixNano() / int64(time.Millisecond),
LastLag: int64(time.Second / time.Millisecond),
},
},
"zach": configUser{},
"chase": configUser{},
"mason": configUser{},
"nat": configUser{},
"roxy": configUser{},
"bill": configUser{},
}
resp := do(v01, "/gm/rpc/status", "")
if resp.Code != http.StatusOK {
t.Error(resp.Code)
}
t.Log(string(resp.Body.Bytes()))
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
t.Fatal(err)
}
if result.Players != 4 {
t.Error(result.Players)
}
if len(result.Users) != 7 {
t.Error(result.Users)
}
if result.Users["bel"] == "" || result.Users["bel"] == "..." {
t.Error(result.Users["bel"])
}
})
})
t.Run("broadcastSomeoneSaidAlias to hotword tap", func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.GM = configGM{
Hotwords: map[string]configGMHotword{
"hotword": configGMHotword{
Call: "tap",
Args: []string{"a"},
},
},
}
do(v01, "/gm/rpc/broadcastSomeoneSaidAlias?message=hotword", "")
for i := 0; i < 2; i++ {
select {
case btn := <-v01.alt:
if len(btn) != 1 {
t.Error(btn)
} else if btn[0].Down != (i == 0) {
t.Error(btn[0])
} else if btn[0].Char != 'a' {
t.Error(btn[0].Char)
}
case <-time.After(time.Second):
t.Fatal("nothing in alt")
}
}
do(v01, "/gm/rpc/broadcastSomeoneSaidAlias?message=hotword", "")
time.Sleep(time.Millisecond * 150)
if got := v01.Read(); len(got) != 1 || !got[0].Down || got[0].Char != 'a' {
t.Error(got)
} else if got := v01.Read(); len(got) != 1 || got[0].Down || got[0].Char != 'a' {
t.Error(got)
}
})
t.Run("broadcastSomeoneSaidAlias", func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.Quiet = false
v01.cfg.Users = map[string]configUser{
"bel": configUser{State: configUserState{
GM: configUserStateGM{
Alias: "driver",
},
Message: "if someone else says 'driver', then you get to play",
}},
}
v01.cfg.Broadcast.Message = ":)"
do(v01, "/gm/rpc/broadcastSomeoneSaidAlias", "")
if !v01.cfg.Quiet {
t.Error(v01.cfg.Quiet)
}
if v := v01.cfg.Users["bel"]; v.State.GM.Alias != "" {
t.Error(v.State.GM.Alias)
} else if v.State.GM.LastAlias != "driver" {
t.Error(v.State.GM.LastAlias)
}
if bc := v01.cfg.Broadcast.Message; bc == ":)" {
t.Error(bc)
}
})
t.Run("fillNonPlayerAliases", func(t *testing.T) {
t.Run("empty", func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.Users = nil
resp := do(v01, "/gm/rpc/fillNonPlayerAliases", "[qt]")
if resp.Code != http.StatusNoContent {
t.Error(resp.Code)
}
})
t.Run("not enough", func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.Users = map[string]configUser{
"zach": configUser{State: configUserState{Player: 0}},
}
resp := do(v01, "/gm/rpc/fillNonPlayerAliases", "[]")
if resp.Code != http.StatusBadRequest {
t.Error(resp.Code)
}
})
t.Run("happy", func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.Users = map[string]configUser{
"bel": configUser{State: configUserState{Player: 1}},
"zach": configUser{State: configUserState{Player: 0}},
}
do(v01, "/gm/rpc/fillNonPlayerAliases", "[qt]")
if v := v01.cfg.Users["bel"]; v.State.GM.Alias != "" {
t.Error(v.State.GM.Alias)
} else if v.State.Player != 1 {
t.Error(v.State.Player)
}
if v := v01.cfg.Users["zach"]; v.State.GM.Alias != "qt" {
t.Error(v.State.GM.Alias)
} else if v.State.Player != 0 {
t.Error(v.State.Player)
}
})
})
t.Run("vote", func(t *testing.T) {
type result map[string]string
t.Run("cast bad vote", func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.Users = map[string]configUser{"bel": {}}
resp := do(v01, "/gm/rpc/vote?user=bel&payload=?", "")
if resp.Code != http.StatusBadRequest {
t.Error(resp)
}
if v01.cfg.Users["bel"].State.Message != "" {
t.Error(v01.cfg.Users["bel"].State.Message)
}
})
t.Run("cast vote", func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.Users = map[string]configUser{"bel": {}, "zach": {}}
do(v01, "/gm/rpc/vote?user=bel&payload=zach", "")
if v01.cfg.Users["bel"].State.GM.Vote != "zach" {
t.Error(v01.cfg.Users["bel"].State.GM.Vote)
}
})
t.Run("get non vote", func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.Users = map[string]configUser{"bel": {}}
resp := do(v01, "/gm/rpc/vote", "", "GET")
var result result
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
t.Error(err)
}
if len(result) != 1 {
t.Error(result)
}
if result["bel"] != "voting" {
t.Error(result)
}
t.Logf("%+v", result)
})
t.Run("get mid vote", func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.Users = map[string]configUser{"bel": {State: configUserState{GM: configUserStateGM{Vote: "zach"}, Message: "driver"}}}
resp := do(v01, "/gm/rpc/vote", "", "GET")
var result result
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
t.Error(err)
}
if len(result) != 1 {
t.Error(result)
}
if result["bel"] != "voted" {
t.Error(result)
}
t.Logf("%+v", result)
})
t.Run("get empty", func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.Users = nil
resp := do(v01, "/gm/rpc/vote", "", "GET")
var result result
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
t.Error(err)
}
if len(result) != 0 {
t.Error(result)
}
t.Logf("%+v", result)
})
})
t.Run("elect", func(t *testing.T) {
type result map[string]int
t.Run("happy", func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.Users = map[string]configUser{
"bel": configUser{State: configUserState{GM: configUserStateGM{Vote: "zach", LastAlias: "driver"}, Player: 1}},
"zach": configUser{State: configUserState{GM: configUserStateGM{Vote: "bel", LastAlias: "pizza"}}},
"bill": configUser{State: configUserState{GM: configUserStateGM{Vote: "bel"}, Player: 2}},
}
resp := do(v01, "/gm/rpc/elect?alias=pizza", "")
var result result
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
t.Errorf("%s => %v", resp.Body.Bytes(), err)
}
if len(result) != 2 {
t.Error(result)
} else if result["bel"] != 2 {
t.Error(result)
} else if result["zach"] != 1 {
t.Error(result)
}
if v01.cfg.Users["bel"].State.Player != 0 {
t.Error(v01.cfg.Users["bel"].State.Player)
} else if v01.cfg.Users["zach"].State.Player != 1 {
t.Error(v01.cfg.Users["zach"].State.Player)
}
if v01.cfg.Broadcast.Message != `bel is now player 0 and zach is now player 1` {
t.Error(v01.cfg.Broadcast)
}
})
t.Run("self", func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.Players = []configPlayer{{}}
v01.cfg.Users = map[string]configUser{
"bel": configUser{State: configUserState{GM: configUserStateGM{Vote: "zach", LastAlias: "driver"}, Player: 1}},
"zach": configUser{State: configUserState{GM: configUserStateGM{Vote: "bel"}}},
"bill": configUser{State: configUserState{GM: configUserStateGM{Vote: "bel"}}},
}
resp := do(v01, "/gm/rpc/elect?alias=driver", "")
var result result
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
t.Error(err)
}
if len(result) != 2 {
t.Error(result)
} else if result["bel"] != 2 {
t.Error(result)
} else if result["zach"] != 1 {
t.Error(result)
}
if !strings.HasSuffix(v01.cfg.Broadcast.Message, `is now player 1`) || strings.Contains(v01.cfg.Broadcast.Message, ",") {
t.Error(v01.cfg.Broadcast.Message)
}
assignments := map[int]int{}
for _, v := range v01.cfg.Users {
assignments[v.State.Player] = assignments[v.State.Player] + 1
}
if len(assignments) != 2 {
t.Error(assignments)
} else if assignments[0] != 2 {
t.Error(assignments[0])
} else if assignments[1] != 1 {
t.Error(assignments[1])
}
})
t.Run("tie", func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.Players = []configPlayer{{}}
v01.cfg.Users = map[string]configUser{
"bel": configUser{State: configUserState{GM: configUserStateGM{Vote: "zach", LastAlias: "driver"}, Player: 1}},
"zach": configUser{State: configUserState{GM: configUserStateGM{Vote: "bel", LastAlias: "pizza"}}},
}
resp := do(v01, "/gm/rpc/elect?alias=pizza", "")
var result result
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
t.Error(err)
}
if len(result) != 2 {
t.Error(result)
} else if result["bel"] != 1 {
t.Error(result)
} else if result["zach"] != 1 {
t.Error(result)
}
if bc := v01.cfg.Broadcast.Message; !strings.HasSuffix(bc, `is now player 1`) || strings.Contains(bc, ",") {
t.Error(bc)
}
assignments := map[int]int{}
for _, v := range v01.cfg.Users {
assignments[v.State.Player] = assignments[v.State.Player] + 1
}
if len(assignments) != 2 {
t.Error(assignments)
} else if assignments[0] != 1 {
t.Error(assignments[0])
} else if assignments[1] != 1 {
t.Error(assignments[1])
}
})
})
t.Run("shuffle", func(t *testing.T) {
t.Run("many 2u 2p", func(t *testing.T) {
v01 := NewV01(ctx, nil)
for i := 0; i < 100; i++ {
v01.cfg.Quiet = true
v01.cfg.Users = map[string]configUser{
"bel": configUser{State: configUserState{Player: 1}},
"zach": configUser{State: configUserState{Player: 2}},
}
v01.cfg.Players = []configPlayer{{}, {}}
do(v01, "/gm/rpc/shuffle", "")
if v01.cfg.Quiet {
t.Error(v01.cfg.Quiet)
}
if len(v01.cfg.Users) != 2 {
t.Error(v01.cfg.Users)
} else if len(v01.cfg.Players) != 2 {
t.Error(v01.cfg.Users)
} else if bp := v01.cfg.Users["bel"].State.Player; bp != 1 && bp != 2 {
t.Error(bp)
} else if zp := v01.cfg.Users["zach"].State.Player; zp != 1 && zp != 2 {
t.Error(zp)
} else if bp == zp {
t.Error(bp, zp)
}
}
})
cases := map[string]struct {
users int
usersAssigned int
players int
}{
"empty": {},
"just users": {users: 2},
"just players": {players: 2},
"2 unassigned users and 2 players": {users: 2, players: 2},
"2 users and 2 players": {users: 2, usersAssigned: 2, players: 2},
"1 users and 2 players": {users: 1, usersAssigned: 1, players: 2},
"1 unassigned users and 2 players": {users: 1, players: 2},
"4 players for 7 users 0 assigned": {users: 7, players: 4},
"4 players for 7 users 4 assigned": {users: 7, players: 4, usersAssigned: 4},
}
for name, d := range cases {
c := d
t.Run(name, func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.Quiet = true
v01.cfg.Users = map[string]configUser{}
for i := 0; i < c.users; i++ {
v01.cfg.Users[strconv.Itoa(i)] = configUser{}
if i < c.usersAssigned {
v01.cfg.Users[strconv.Itoa(i)] = configUser{State: configUserState{Player: i}}
}
}
v01.cfg.Players = make([]configPlayer, c.players)
do(v01, "/gm/rpc/shuffle", "")
if v01.cfg.Quiet {
t.Error(v01.cfg.Quiet)
}
if len(v01.cfg.Users) != c.users {
t.Error(v01.cfg.Users)
} else if len(v01.cfg.Players) != c.players {
t.Error(v01.cfg.Users)
}
for i := 0; i < c.users; i++ {
if _, ok := v01.cfg.Users[strconv.Itoa(i)]; !ok {
t.Error(i)
}
}
assignments := map[int]int{}
for _, v := range v01.cfg.Users {
if v.State.Player > 0 {
assignments[v.State.Player] = assignments[v.State.Player] + 1
}
}
lesser := c.users
if c.players < lesser {
lesser = c.players
}
if len(assignments) != lesser {
t.Error(assignments)
}
for _, v := range assignments {
if v != 1 {
t.Error(v)
}
}
})
}
})
t.Run("swap", func(t *testing.T) {
t.Run("self", func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.Quiet = true
v01.cfg.Users = map[string]configUser{
"bel": configUser{State: configUserState{Player: 1}},
}
resp := do(v01, "/gm/rpc/swap?a=bel&b=bel", "")
if resp.Code != http.StatusConflict {
t.Error(resp.Code)
}
if !v01.cfg.Quiet {
t.Error(v01.cfg.Quiet)
}
})
t.Run("who", func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.Quiet = true
resp := do(v01, "/gm/rpc/swap?a=bel", "")
if resp.Code != http.StatusBadRequest {
t.Error(resp.Code)
}
if !v01.cfg.Quiet {
t.Error(v01.cfg.Quiet)
}
})
t.Run("happy", func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.Quiet = true
v01.cfg.Users = map[string]configUser{
"bel": configUser{State: configUserState{Player: 1}},
"zach": configUser{State: configUserState{Player: 2}},
}
resp := do(v01, "/gm/rpc/swap?a=bel&b=zach", "")
if resp.Code != http.StatusOK {
t.Error(resp.Code)
}
if v01.cfg.Quiet {
t.Error(v01.cfg.Quiet)
}
if v01.cfg.Users["bel"].State.Player != 2 {
t.Error(v01.cfg.Users["bel"])
} else if v01.cfg.Users["zach"].State.Player != 1 {
t.Error(v01.cfg.Users["zach"])
}
})
})
t.Run("404", func(t *testing.T) {
v01 := NewV01(ctx, nil)
resp := do(v01, "/gm/teehee", "")
if resp.Code != http.StatusNotFound {
t.Error(resp.Code)
}
})
}

View File

@ -3,18 +3,26 @@ feedback:
ttsurl: http://localhost:15002
users:
bel:
player: 0
message: "hi"
meta:
lasttsms: 1681062770999
lastlag: 12
state:
player: 0
message: hi
gm:
alias: ""
lastalias: ""
vote: ""
players:
- buttons:
up: "w"
down: "s"
left: "a"
right: "d"
l: "q"
r: "e"
a: "1"
b: "2"
x: "3"
y: "4"
- transformation: {}
quiet: false
broadcast:
message: hi
gm:
hotwords:
coin:
call: tap
args: ['!']
star:
call: tap
args: ['?']

View File

@ -68,6 +68,9 @@ func (v01 *V01) _tts(text string) error {
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)
select {
case <-v01.ctx.Done():
case <-time.After(duration):
}
return nil
}

View File

@ -8,6 +8,7 @@ import (
"mayhem-party/src/device/input/button"
"mayhem-party/src/device/input/raw"
"os"
"sync"
"time"
"gopkg.in/yaml.v2"
@ -20,25 +21,31 @@ var (
type (
V01 struct {
ctx context.Context
can context.CancelFunc
src raw.Raw
cfg config
ctx context.Context
can context.CancelFunc
src raw.Raw
cfg config
telemetryc chan message
alt chan []button.Button
}
)
func NewV01(ctx context.Context, src raw.Raw) *V01 {
var cfg config
cfg.lock = &sync.Mutex{}
b, _ := ioutil.ReadFile(FlagParseV01Config)
yaml.Unmarshal(b, &cfg)
ctx, can := context.WithCancel(ctx)
result := &V01{
ctx: ctx,
can: can,
src: src,
cfg: cfg,
ctx: ctx,
can: can,
src: src,
cfg: cfg,
telemetryc: make(chan message),
alt: make(chan []button.Button, 2),
}
go result.listen()
go result.dotelemetry()
return result
}
@ -53,6 +60,11 @@ func (v01 *V01) Close() {
}
func (v01 *V01) Read() []button.Button {
select {
case alt := <-v01.alt:
return alt
default:
}
line := v01.src.Read()
var msg message
if err := json.Unmarshal(line, &msg); err != nil {
@ -69,23 +81,51 @@ func (v01 *V01) Read() []button.Button {
return 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) 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.Player < 1 {
if user.State.Player < 1 {
msg.Y = ""
msg.N = ""
return msg
}
player := v01.cfg.Players[user.Player-1]
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

@ -44,7 +44,8 @@ func TestV01WithCfg(t *testing.T) {
os.WriteFile(p, []byte(`
users:
bel:
player: 2
state:
player: 2
players:
- transformation:
w: t
@ -87,10 +88,11 @@ func TestV01Feedback(t *testing.T) {
ttsurl: http://localhost:15002
users:
bel:
player: 2
message: to bel
broadcast:
message: to everyone
state:
player: 2
message: to bel
broadcast:
message: to everyone
players:
- transformation:
w: t
@ -121,8 +123,8 @@ func TestV01Feedback(t *testing.T) {
}
defer resp.Body.Close()
b, _ := io.ReadAll(resp.Body)
if string(b) != "to bel" {
t.Error(b)
if !strings.HasPrefix(string(b), "to bel") {
t.Error(string(b))
}
})
@ -133,8 +135,8 @@ func TestV01Feedback(t *testing.T) {
}
defer resp.Body.Close()
b, _ := io.ReadAll(resp.Body)
if string(b) != "to everyone" {
t.Error(b)
if !strings.HasPrefix(string(b), "to everyone") {
t.Error(string(b))
}
})
@ -153,7 +155,7 @@ func TestV01Feedback(t *testing.T) {
}
defer resp.Body.Close()
b, _ := io.ReadAll(resp.Body)
if string(b) != want {
if !strings.HasPrefix(string(b), want) {
t.Error(string(b))
}
})

View File

@ -0,0 +1,45 @@
// a key map of allowed keys
var allowedKeys = {
37: 'left',
38: 'up',
39: 'right',
40: 'down',
65: 'a',
66: 'b'
};
// the 'official' Konami Code sequence
var konamiCode = ['up', 'up', 'down', 'down', 'left', 'right', 'left', 'right', 'b', 'a'];
// a variable to remember the 'position' the user has reached so far.
var konamiCodePosition = 0;
// add keydown event listener
document.addEventListener('keydown', function(e) {
// get the value of the key code from the key map
var key = allowedKeys[e.keyCode];
// get the value of the required key from the konami code
var requiredKey = konamiCode[konamiCodePosition];
// compare the key with the required key
if (key == requiredKey) {
// move to the next key in the konami code sequence
konamiCodePosition++;
// if the last key is reached, activate cheats
if (konamiCodePosition == konamiCode.length) {
showSecrets();
konamiCodePosition = 0;
}
} else {
konamiCodePosition = 0;
}
});
function showSecrets() {
var element = document.getElementById("konami")
element.style = "display:block"
var e = new Event("onKonami")
element.dispatchEvent(new Event("onKonami"))
}

View File

@ -0,0 +1,26 @@
function onYouTubeIframeAPIReady() {
var player;
player = new YT.Player('konami', {
videoId: 'V4oJ62xrFZo', // 👈 video id.
width: 560,
height: 316,
playerVars: {
'autoplay': 1,
'controls': 1,
'showinfo': 0,
'modestbranding': 0,
'loop': 1,
'fs': 0,
'cc_load_policty': 0,
'iv_load_policy': 3
},
events: {
'onReady': function (e) {
e.target.setVolume(33); // For max value, set value to 100.
document.getElementById("konami").addEventListener("onKonami", () => {e.target.playVideo()})
}
}
});
}

View File

@ -0,0 +1,172 @@
<!DOCTYPE html>
<html>
<header>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/water.css@2/out/dark.css">
<script src="konami.js"></script>
<script src="lowerVolume.js"></script>
<script async src="https://www.youtube.com/iframe_api"></script>
<script>
function formsay(message) {
console.log(`say '${message}'`)
http("GET", `/proxy?user=${document.getElementById("user").value}&say=${message}`, noopcallback, null)
}
function formsend(message) {
console.log(`send '${message}'`)
http("GET", `/proxy/gm/rpc/vote?user=${document.getElementById("user").value}&payload=${message}`, noopcallback, null)
}
function http(method, remote, callback, body) {
var xmlhttp = new XMLHttpRequest();
xmlhttp.onreadystatechange = function() {
if (xmlhttp.readyState == XMLHttpRequest.DONE) {
callback(xmlhttp.responseText, xmlhttp.status)
}
};
xmlhttp.open(method, remote, true);
if (typeof body == "undefined") {
body = null
}
xmlhttp.send(body);
}
function noopcallback(responseBody, responseStatus) {
}
setInterval(() => {
http("GET", `/proxy?user=${document.getElementById("user").value}`, (b, s) => {
if (s != 200)
return
document.getElementById("ntfy").innerHTML = `<pre>${b}</pre>`
}, null)
}, 1500)
</script>
</header>
<body>
<div>
<form>
<h3>WHO AM I</h3>
<select id="user">
<option>bel</option>
<option>zach</option>
<option>chase</option>
<option>mason</option>
<option>nat</option>
<option>roxy</option>
<option>bill</option>
</select>
</form>
<div>
<form action="" onsubmit="formsay(this.children.say.value); return false;" style="display: inline-block;">
<h3>SAY</h3>
<input type="text" name="say">
<input type="submit" value="say">
</form>
<form action="" onsubmit="formsend(this.children.send.value); return false;" style="display: inline-block;">
<h3>SEND</h3>
<select name="send">
<option>bel</option>
<option>zach</option>
<option>chase</option>
<option>mason</option>
<option>nat</option>
<option>roxy</option>
<option>bill</option>
</select>
<input type="submit" value="send">
</form>
</div>
<details>
<summary>CONTROLS</summary>
<form id="controls">
<div style="display: flex; flex-wrap: wrap;">
<div>
<label for="input-up">Up</label>
<input id="input-up" type="text" maxLength=1 value="w" name="w" placeholder="up" onchange="recontrol()">
<label for="input-down">Down</label>
<input id="input-down" type="text" maxLength=1 value="s" name="s" placeholder="down" onchange="recontrol()">
<label for="input-left">Left</label>
<input id="input-left" type="text" maxLength=1 value="a" name="a" placeholder="left" onchange="recontrol()">
<label for="input-right">Right</label>
<input id="input-right" type="text" maxLength=1 value="d" name="d" placeholder="right" onchange="recontrol()">
</div>
<div>
<label for="input-start">Start</label>
<input id="input-start" type="text" maxLength=1 value="5" name="5" placeholder="start" onchange="recontrol()">
<label for="input-left-bumper">Left Bumper</label>
<input id="input-left-bumper" type="text" maxLength=1 value="q" name="q" placeholder="l" onchange="recontrol()">
<label for="input-right-bumper">Right Bumper</label>
<input id="input-right-bumper" type="text" maxLength=1 value="e" name="e" placeholder="r" onchange="recontrol()">
</div>
<div>
<label for="input-a">A</label>
<input id="input-a" type="text" maxLength=1 value="1" name="1" placeholder="a" onchange="recontrol()">
<label for="input-b">B</label>
<input id="input-b" type="text" maxLength=1 value="2" name="2" placeholder="b" onchange="recontrol()">
<label for="input-x">X</label>
<input id="input-x" type="text" maxLength=1 value="3" name="3" placeholder="x" onchange="recontrol()">
<label for="input-y">Y</label>
<input id="input-y" type="text" maxLength=1 value="4" name="4" placeholder="y" onchange="recontrol()">
</div>
</div>
</form>
</details>
</div>
<div id="ntfy"></div>
<div id="ws"></div>
<div id="konami" style="display:none"></div>
</body>
<footer>
<script>
var socket = new WebSocket("ws://"+window.location.host+"/api/ws")
function nosend(data) {
}
function dosend(data) {
console.log(JSON.stringify(data))
socket.send(JSON.stringify(data))
}
send = nosend
socket.addEventListener("open", (_) => {
console.log("ws open")
send = dosend
})
socket.addEventListener("message", (event) => console.log("ws recv:", event.data))
socket.addEventListener("close", (event) => console.log("ws closed"))
keys = {}
document.addEventListener('keydown', (event) => {
var name = controls[event.key]
if (!name)
return
if (keys[name])
return
keys[name] = true
sendKeys(name, "")
})
document.addEventListener('keyup', (event) => {
var name = controls[event.key]
if (!name)
return
keys[name] = false
sendKeys("", name)
})
function sendKeys(y, n) {
send({
T: new Date().getTime(),
U: document.getElementById("user").value,
Y: y,
N: n,
})
}
var controls = {}
function recontrol() {
for (var k in controls)
controls[k] = false
for (var e of document.getElementById("controls").getElementsByTagName("input"))
controls[e.value] = e.name
}
recontrol()
</script>
</footer>
</html>

View File

@ -9,6 +9,8 @@ import (
var (
FlagRawKeyboard = os.Getenv("RAW_KEYBOARD") == "true"
FlagRawUDP = os.Getenv("RAW_UDP")
FlagRawWS = os.Getenv("RAW_WS")
FlagDebug = os.Getenv("DEBUG") != ""
FlagRawRandomWeightFile = os.Getenv("RAW_RANDOM_WEIGHT_FILE")
)
@ -21,6 +23,9 @@ func New(ctx context.Context) Raw {
if FlagRawKeyboard {
return NewKeyboard()
}
if port, _ := strconv.Atoi(FlagRawWS); port != 0 {
return NewWS(ctx, port)
}
if port, _ := strconv.Atoi(FlagRawUDP); port != 0 {
return NewUDP(ctx, port)
}

View File

@ -6,4 +6,5 @@ func TestRaw(t *testing.T) {
var _ Raw = &Random{}
var _ Raw = UDP{}
var _ Raw = Keyboard{}
var _ Raw = WS{}
}

View File

@ -4,14 +4,9 @@ import (
"context"
"log"
"net"
"os"
"strconv"
)
var (
FlagDebug = os.Getenv("DEBUG") == "true"
)
type UDP struct {
conn net.PacketConn
c chan []byte

148
src/device/input/raw/ws.go Normal file
View File

@ -0,0 +1,148 @@
package raw
import (
"context"
"embed"
_ "embed"
"fmt"
"log"
"net/http"
"net/http/httputil"
"net/url"
"os"
"path"
"strings"
"github.com/gorilla/websocket"
)
var (
FlagWSProxy = os.Getenv("RAW_WS_PROXY_URL")
FlagWSDebug = os.Getenv("RAW_WS_DEBUG") != ""
)
type WS struct {
ctx context.Context
can context.CancelFunc
ch chan []byte
}
func NewWS(ctx context.Context, port int) WS {
ctx, can := context.WithCancel(ctx)
ws := WS{ctx: ctx, can: can, ch: make(chan []byte, 256)}
go ws.listen(port)
return ws
}
func (ws WS) Read() []byte {
select {
case v := <-ws.ch:
return v
case <-ws.ctx.Done():
return nil
}
}
func (ws WS) Close() {
ws.can()
}
func (ws WS) listen(port int) {
server := &http.Server{
Addr: fmt.Sprintf(":%d", port),
Handler: ws,
}
go func() {
if err := server.ListenAndServe(); err != nil && ws.ctx.Err() == nil {
panic(err)
}
}()
log.Println("WS on", port)
<-ws.ctx.Done()
server.Close()
}
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
func (ws WS) ServeHTTP(w http.ResponseWriter, r *http.Request) {
r = r.WithContext(ws.ctx)
if err := ws.serveHTTP(w, r); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func (ws WS) serveHTTP(w http.ResponseWriter, r *http.Request) error {
switch r.URL.Path {
case "/api/ws":
return ws.serveWS(w, r)
}
if strings.HasPrefix(r.URL.Path, "/proxy") {
return ws.serveProxy(w, r)
}
return ws.serveStaticFile(w, r)
}
func (ws WS) serveProxy(w http.ResponseWriter, r *http.Request) error {
u, err := url.Parse(FlagWSProxy)
if err != nil {
return err
}
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/proxy")
if r.URL.Path == "" {
r.URL.Path = "/"
}
proxy := httputil.NewSingleHostReverseProxy(u)
proxy.ServeHTTP(w, r)
return nil
}
//go:embed public/*
var staticFiles embed.FS
func (ws WS) serveStaticFile(w http.ResponseWriter, r *http.Request) error {
if FlagWSDebug {
b, _ := os.ReadFile("src/device/input/raw/public/root.html")
w.Write(b)
return nil
}
if r.URL.Path == "/" {
r.URL.Path = "root.html"
}
r.URL.Path = path.Join("public", r.URL.Path)
http.FileServer(http.FS(staticFiles)).ServeHTTP(w, r)
return nil
}
func (ws WS) serveWS(w http.ResponseWriter, r *http.Request) error {
if err := ws._serveWS(w, r); err != nil {
log.Println("_serveWS:", err)
}
return nil
}
func (ws WS) _serveWS(w http.ResponseWriter, r *http.Request) error {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return err
}
defer conn.Close()
for ws.ctx.Err() == nil {
msgType, p, err := conn.ReadMessage()
if err != nil {
if websocket.IsCloseError(err) || websocket.IsUnexpectedCloseError(err) {
return nil
}
return err
}
if msgType == websocket.TextMessage {
log.Println(string(p))
ws.ch <- p
}
}
return nil
}

View File

@ -53,6 +53,8 @@ var (
';': PSemicolon,
'-': PMinus,
'=': PEqual,
'<': PageDown,
'>': PageUp,
}
keyToChar = func() map[Key]byte {
result := map[Key]byte{}

View File

@ -57,4 +57,6 @@ const (
PSemicolon = Key(keybd_event.VK_SEMICOLON)
PMinus = Key(keybd_event.VK_MINUS)
PEqual = Key(keybd_event.VK_EQUAL)
PageUp = Key(keybd_event.VK_PAGEUP)
PageDown = Key(keybd_event.VK_PAGEDOWN)
)

View File

@ -17,9 +17,9 @@ func Main(ctx context.Context) error {
defer reader.Close()
interval := time.Millisecond * 50
if intervalS := os.Getenv("MAIN_INTERVAL_DURATION"); intervalS != "" {
if intervalS := os.Getenv("MAIN_INTERVAL_DURATION"); intervalS == "" {
} else if v, err := time.ParseDuration(intervalS); err != nil {
panic(err)
return err
} else {
interval = v
}
@ -50,6 +50,9 @@ func Main(ctx context.Context) error {
keys = append(keys, k)
}
}
if os.Getenv("DEBUG") == "true" {
log.Printf("src.Main.writer.Press(%+v) (from %+v)", keys, delta)
}
writer.Press(keys...)
}

View File

@ -1,81 +0,0 @@
todo:
- https via home.blapointe and rproxy
- 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
- 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