Compare commits
87 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
b89ed62036 | |
|
|
67c93a9048 | |
|
|
8eae7ae9a6 | |
|
|
ce32620940 | |
|
|
24f4b6b8f5 | |
|
|
440191de0f | |
|
|
d5adc596ac | |
|
|
41a39c40d0 | |
|
|
9a38033b65 | |
|
|
6a4ad5ec36 | |
|
|
c2b8ab67f2 | |
|
|
9418cecdf5 | |
|
|
fb5da88774 | |
|
|
39f6bc8ed9 | |
|
|
f3cbfa1c48 | |
|
|
444245c0f5 | |
|
|
52ee1e5083 | |
|
|
934158b7a3 | |
|
|
87e63c27df | |
|
|
f98e417ba6 | |
|
|
d6a7ee3db0 | |
|
|
b814dabfd3 | |
|
|
0a91fc656d | |
|
|
5c3341e260 | |
|
|
0903c01b9a | |
|
|
342e2eef93 | |
|
|
b8b076450e | |
|
|
3bb7cad554 | |
|
|
44ec540db3 | |
|
|
e864f2a9f5 | |
|
|
3c70e42819 | |
|
|
9de8c91544 | |
|
|
7f2e25458e | |
|
|
95810d3735 | |
|
|
df65b1ed07 | |
|
|
a36f07d0c1 | |
|
|
09d9911293 | |
|
|
60d391b7a4 | |
|
|
60ed9c1269 | |
|
|
13cf35bdd8 | |
|
|
79c90ac40c | |
|
|
dd1b053efa | |
|
|
f619fe9e1b | |
|
|
781bfb8a67 | |
|
|
9ece270a13 | |
|
|
f647a03467 | |
|
|
ecb719a97a | |
|
|
1738ce7d19 | |
|
|
354d07d6bf | |
|
|
39ab01525f | |
|
|
ceeeb8fe4b | |
|
|
d029d82366 | |
|
|
0435f7b3e8 | |
|
|
5ef0dde50d | |
|
|
bc3f0271e7 | |
|
|
e2d7c4a908 | |
|
|
c744704b63 | |
|
|
213fd555e4 | |
|
|
dd41028aab | |
|
|
8ff1c2fab4 | |
|
|
1f7b222b9c | |
|
|
1842023224 | |
|
|
45b873f462 | |
|
|
88a78c489f | |
|
|
8314bdc457 | |
|
|
a6a9b177e9 | |
|
|
f649862dd4 | |
|
|
85804d6f84 | |
|
|
f14871218d | |
|
|
26f052d981 | |
|
|
2d4cb394de | |
|
|
163bf2b405 | |
|
|
e968ce17ce | |
|
|
f07e67b3fd | |
|
|
fbf4849517 | |
|
|
804ce02407 | |
|
|
4a86d2b6ca | |
|
|
c663b1a12c | |
|
|
af42db6803 | |
|
|
f9dc4cff9f | |
|
|
37050f3d87 | |
|
|
74717609ec | |
|
|
24ae45896f | |
|
|
8b29648c50 | |
|
|
1eba008efe | |
|
|
d48c545030 | |
|
|
323ca466ad |
2
go.mod
2
go.mod
|
|
@ -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
5
go.sum
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -1,58 +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
|
||||
|
||||
## `mayhem-party`
|
||||
|
||||
### Configs
|
||||
|
||||
`cd ./config.d/mayhem-party.d/remap.d; bash ./rotate.sh`
|
||||
|
||||
> rotate anytime via stdin or `pkill -SIGUSR1 -f rotate.sh`
|
||||
|
||||
### Binary
|
||||
|
||||
`bash -c 'true; source ./config.d/mayhem-party.d/env.env; mayhem-party'`
|
||||
|
||||
## Game Playing
|
||||
|
||||
Foreground
|
||||
|
||||
## `stt`
|
||||
|
||||
TODO pipe stt detecting relevant strings -> change the `./config.d/mayhem-party.d/remap.d/live.yaml` link -> `SIGUSR1` to `mayhem-party`
|
||||
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
export DEBUG=true
|
||||
export RAW_UDP=17070
|
||||
export BUTTON_V01=true
|
||||
export WRAP_REFRESH_ON_SIGUSR1=true
|
||||
export MAIN_INTERVAL_DURATION=5ms
|
||||
export OUTPUT_KEYBOARD=false
|
||||
export BUTTON_V01_CONFIG=./config.d/mayhem-party.d/v01.yaml
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
import yaml
|
||||
from sys import argv
|
||||
from sys import stderr
|
||||
|
||||
def log(*args):
|
||||
print(*args, file=stderr)
|
||||
|
||||
def main(args):
|
||||
players = []
|
||||
for i in range(1, 5+1):
|
||||
with open(f"../../rusty-pipe.d/{i}.yaml", "r") as f:
|
||||
players.append(yaml.safe_load(f)["streams"]["input"]["engine"]["gui"]["buttons"])
|
||||
log(players[-1])
|
||||
for arg in args:
|
||||
offset = int(arg)
|
||||
for i in range(len(players)):
|
||||
j = (i+offset)%len(players)
|
||||
if i == j:
|
||||
break
|
||||
keys = players[i]
|
||||
values = players[j]
|
||||
log(f"player {i+1} plays as player {j+1}")
|
||||
print(f"# player {i+1} controls {j+1}")
|
||||
for k in keys:
|
||||
key = keys[k]
|
||||
value = values[k]
|
||||
print(f"'{key}': '{value}'")
|
||||
|
||||
if __name__ == "__main__":
|
||||
from sys import argv
|
||||
main(argv[1:])
|
||||
|
|
@ -1 +0,0 @@
|
|||
players_offset_4.yaml
|
||||
|
|
@ -1 +0,0 @@
|
|||
{}
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
# player 1 controls 2
|
||||
'1': 'q'
|
||||
'2': 'w'
|
||||
'3': 'e'
|
||||
'4': 'r'
|
||||
'5': 't'
|
||||
'6': 'y'
|
||||
'7': 'u'
|
||||
'8': 'i'
|
||||
'9': 'o'
|
||||
'0': 'p'
|
||||
# player 2 controls 3
|
||||
'q': 'a'
|
||||
'w': 's'
|
||||
'e': 'd'
|
||||
'r': 'f'
|
||||
't': 'g'
|
||||
'y': 'h'
|
||||
'u': 'j'
|
||||
'i': 'k'
|
||||
'o': 'l'
|
||||
'p': ';'
|
||||
# player 3 controls 4
|
||||
'a': 'z'
|
||||
's': 'x'
|
||||
'd': 'c'
|
||||
'f': 'v'
|
||||
'g': 'b'
|
||||
'h': 'n'
|
||||
'j': 'm'
|
||||
'k': ','
|
||||
'l': '-'
|
||||
';': '/'
|
||||
# player 4 controls 5
|
||||
'z': '!'
|
||||
'x': '@'
|
||||
'c': '#'
|
||||
'v': '$'
|
||||
'b': '%'
|
||||
'n': '^'
|
||||
'm': '&'
|
||||
',': '*'
|
||||
'-': '('
|
||||
'/': ')'
|
||||
# player 5 controls 1
|
||||
'!': '1'
|
||||
'@': '2'
|
||||
'#': '3'
|
||||
'$': '4'
|
||||
'%': '5'
|
||||
'^': '6'
|
||||
'&': '7'
|
||||
'*': '8'
|
||||
'(': '9'
|
||||
')': '0'
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
# player 1 controls 3
|
||||
'1': 'a'
|
||||
'2': 's'
|
||||
'3': 'd'
|
||||
'4': 'f'
|
||||
'5': 'g'
|
||||
'6': 'h'
|
||||
'7': 'j'
|
||||
'8': 'k'
|
||||
'9': 'l'
|
||||
'0': ';'
|
||||
# player 2 controls 4
|
||||
'q': 'z'
|
||||
'w': 'x'
|
||||
'e': 'c'
|
||||
'r': 'v'
|
||||
't': 'b'
|
||||
'y': 'n'
|
||||
'u': 'm'
|
||||
'i': ','
|
||||
'o': '-'
|
||||
'p': '/'
|
||||
# player 3 controls 5
|
||||
'a': '!'
|
||||
's': '@'
|
||||
'd': '#'
|
||||
'f': '$'
|
||||
'g': '%'
|
||||
'h': '^'
|
||||
'j': '&'
|
||||
'k': '*'
|
||||
'l': '('
|
||||
';': ')'
|
||||
# player 4 controls 1
|
||||
'z': '1'
|
||||
'x': '2'
|
||||
'c': '3'
|
||||
'v': '4'
|
||||
'b': '5'
|
||||
'n': '6'
|
||||
'm': '7'
|
||||
',': '8'
|
||||
'-': '9'
|
||||
'/': '0'
|
||||
# player 5 controls 2
|
||||
'!': 'q'
|
||||
'@': 'w'
|
||||
'#': 'e'
|
||||
'$': 'r'
|
||||
'%': 't'
|
||||
'^': 'y'
|
||||
'&': 'u'
|
||||
'*': 'i'
|
||||
'(': 'o'
|
||||
')': 'p'
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
# player 1 controls 4
|
||||
'1': 'z'
|
||||
'2': 'x'
|
||||
'3': 'c'
|
||||
'4': 'v'
|
||||
'5': 'b'
|
||||
'6': 'n'
|
||||
'7': 'm'
|
||||
'8': ','
|
||||
'9': '-'
|
||||
'0': '/'
|
||||
# player 2 controls 5
|
||||
'q': '!'
|
||||
'w': '@'
|
||||
'e': '#'
|
||||
'r': '$'
|
||||
't': '%'
|
||||
'y': '^'
|
||||
'u': '&'
|
||||
'i': '*'
|
||||
'o': '('
|
||||
'p': ')'
|
||||
# player 3 controls 1
|
||||
'a': '1'
|
||||
's': '2'
|
||||
'd': '3'
|
||||
'f': '4'
|
||||
'g': '5'
|
||||
'h': '6'
|
||||
'j': '7'
|
||||
'k': '8'
|
||||
'l': '9'
|
||||
';': '0'
|
||||
# player 4 controls 2
|
||||
'z': 'q'
|
||||
'x': 'w'
|
||||
'c': 'e'
|
||||
'v': 'r'
|
||||
'b': 't'
|
||||
'n': 'y'
|
||||
'm': 'u'
|
||||
',': 'i'
|
||||
'-': 'o'
|
||||
'/': 'p'
|
||||
# player 5 controls 3
|
||||
'!': 'a'
|
||||
'@': 's'
|
||||
'#': 'd'
|
||||
'$': 'f'
|
||||
'%': 'g'
|
||||
'^': 'h'
|
||||
'&': 'j'
|
||||
'*': 'k'
|
||||
'(': 'l'
|
||||
')': ';'
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
# player 1 controls 5
|
||||
'1': '!'
|
||||
'2': '@'
|
||||
'3': '#'
|
||||
'4': '$'
|
||||
'5': '%'
|
||||
'6': '^'
|
||||
'7': '&'
|
||||
'8': '*'
|
||||
'9': '('
|
||||
'0': ')'
|
||||
# player 2 controls 1
|
||||
'q': '1'
|
||||
'w': '2'
|
||||
'e': '3'
|
||||
'r': '4'
|
||||
't': '5'
|
||||
'y': '6'
|
||||
'u': '7'
|
||||
'i': '8'
|
||||
'o': '9'
|
||||
'p': '0'
|
||||
# player 3 controls 2
|
||||
'a': 'q'
|
||||
's': 'w'
|
||||
'd': 'e'
|
||||
'f': 'r'
|
||||
'g': 't'
|
||||
'h': 'y'
|
||||
'j': 'u'
|
||||
'k': 'i'
|
||||
'l': 'o'
|
||||
';': 'p'
|
||||
# player 4 controls 3
|
||||
'z': 'a'
|
||||
'x': 's'
|
||||
'c': 'd'
|
||||
'v': 'f'
|
||||
'b': 'g'
|
||||
'n': 'h'
|
||||
'm': 'j'
|
||||
',': 'k'
|
||||
'-': 'l'
|
||||
'/': ';'
|
||||
# player 5 controls 4
|
||||
'!': 'z'
|
||||
'@': 'x'
|
||||
'#': 'c'
|
||||
'$': 'v'
|
||||
'%': 'b'
|
||||
'^': 'n'
|
||||
'&': 'm'
|
||||
'*': ','
|
||||
'(': '-'
|
||||
')': '/'
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
#! /bin/bash
|
||||
|
||||
mayhem_party_rotate() {
|
||||
local currently=$(realpath live.yaml | grep -o '[0-9].yaml$' | grep -o '^[0-9]')
|
||||
local next=${NEXT:-$((RANDOM%5))}
|
||||
while [ -z "$NEXT" ] && [ "$next" == "$currently" ]; do
|
||||
next=$((RANDOM%5))
|
||||
done
|
||||
rm live.yaml
|
||||
ln -s players_offset_$next.yaml live.yaml
|
||||
pkill -SIGUSR1 -f mayhem-party
|
||||
}
|
||||
|
||||
trap mayhem_party_rotate SIGUSR1
|
||||
|
||||
if [ "$0" == "$BASH_SOURCE" ]; then
|
||||
NEXT=0 mayhem_party_rotate
|
||||
while read -p "$(date) > [press enter to rotate]"; do
|
||||
mayhem_party_rotate
|
||||
done
|
||||
fi
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -1 +0,0 @@
|
|||
../../../../rusty-pipe.d/target/x86_64-pc-windows-gnu/release/rusty-pipe.exe
|
||||
8
main.go
8
main.go
|
|
@ -11,6 +11,14 @@ import (
|
|||
func main() {
|
||||
ctx, can := signal.NotifyContext(context.Background(), syscall.SIGINT)
|
||||
defer can()
|
||||
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Println("panic:", err)
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := src.Main(ctx); err != nil && ctx.Err() == nil {
|
||||
panic(err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
package button
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Button struct {
|
||||
Char byte
|
||||
Down bool
|
||||
}
|
||||
|
||||
func (button Button) String() string {
|
||||
return fmt.Sprintf("%c:%v", button.Char, button.Down)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +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
|
||||
}
|
||||
)
|
||||
|
||||
func (cfg config) WithPatch(v interface{}) config {
|
||||
cfg.lock.Lock()
|
||||
defer cfg.lock.Unlock()
|
||||
originalData, _ := json.Marshal(cfg)
|
||||
patchData, _ := json.Marshal(v)
|
||||
patcher, err := patch.DecodePatch(patchData)
|
||||
if err != nil {
|
||||
return cfg
|
||||
}
|
||||
patchedData, err := patcher.Apply(originalData)
|
||||
if err != nil {
|
||||
return cfg
|
||||
}
|
||||
var patched config
|
||||
if err := json.Unmarshal(patchedData, &patched); err != nil {
|
||||
return cfg
|
||||
}
|
||||
return patched
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
package v01
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConfigPatch(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
cfg config
|
||||
patch interface{}
|
||||
want config
|
||||
}{
|
||||
"nil patch": {
|
||||
cfg: config{Quiet: true},
|
||||
patch: nil,
|
||||
want: config{Quiet: true},
|
||||
},
|
||||
"[] patch": {
|
||||
cfg: config{Quiet: true},
|
||||
patch: []interface{}{},
|
||||
want: config{Quiet: true},
|
||||
},
|
||||
"set fake field": {
|
||||
cfg: config{Quiet: true},
|
||||
patch: []interface{}{
|
||||
map[string]interface{}{"op": "add", "path": "/Fake", "value": true},
|
||||
},
|
||||
want: config{Quiet: true},
|
||||
},
|
||||
"remove field": {
|
||||
cfg: config{Quiet: true},
|
||||
patch: []interface{}{
|
||||
map[string]interface{}{"op": "remove", "path": "/Quiet"},
|
||||
},
|
||||
want: config{Quiet: false},
|
||||
},
|
||||
"replace field with valid": {
|
||||
cfg: config{Quiet: true},
|
||||
patch: []interface{}{
|
||||
map[string]interface{}{"op": "replace", "path": "/Quiet", "value": false},
|
||||
},
|
||||
want: config{Quiet: false},
|
||||
},
|
||||
"replace field with invalid": {
|
||||
cfg: config{Quiet: true},
|
||||
patch: []interface{}{
|
||||
map[string]interface{}{"op": "replace", "path": "/Quiet", "value": "teehee"},
|
||||
},
|
||||
want: config{Quiet: true},
|
||||
},
|
||||
"test and noop": {
|
||||
cfg: config{Quiet: true},
|
||||
patch: []interface{}{
|
||||
map[string]interface{}{"op": "test", "path": "/Quiet", "value": false},
|
||||
map[string]interface{}{"op": "replace", "path": "/Quiet", "value": false},
|
||||
},
|
||||
want: config{Quiet: true},
|
||||
},
|
||||
"test and apply": {
|
||||
cfg: config{Quiet: true},
|
||||
patch: []interface{}{
|
||||
map[string]interface{}{"op": "test", "path": "/Quiet", "value": true},
|
||||
map[string]interface{}{"op": "replace", "path": "/Quiet", "value": false},
|
||||
},
|
||||
want: config{Quiet: false},
|
||||
},
|
||||
}
|
||||
|
||||
for name, d := range cases {
|
||||
c := d
|
||||
t.Run(name, func(t *testing.T) {
|
||||
c.cfg.lock = &sync.Mutex{}
|
||||
got := c.cfg.WithPatch(c.patch)
|
||||
got.lock = nil
|
||||
c.want.lock = nil
|
||||
if fmt.Sprintf("%+v", got) != fmt.Sprintf("%+v", c.want) {
|
||||
t.Errorf("(%+v).Patch(%+v) want %+v, got %+v", c.cfg, c.patch, c.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -3,17 +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: ['?']
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"mayhem-party/src/device/input/button"
|
||||
"mayhem-party/src/device/input/raw"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
|
|
@ -15,30 +16,36 @@ import (
|
|||
|
||||
var (
|
||||
FlagDebug = os.Getenv("DEBUG") == "true"
|
||||
FlagParseV01Config = os.Getenv("PARSE_V01_CONFIG")
|
||||
FlagParseV01Config = os.Getenv("V01_CONFIG")
|
||||
)
|
||||
|
||||
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 {
|
||||
|
|
@ -60,26 +72,60 @@ func (v01 *V01) Read() []button.Button {
|
|||
}
|
||||
v01.telemetry(msg)
|
||||
|
||||
return v01.transform(msg).buttons()
|
||||
buttons := v01.transform(msg).buttons()
|
||||
if v01.cfg.Quiet {
|
||||
for i := range buttons {
|
||||
buttons[i].Down = false
|
||||
}
|
||||
}
|
||||
return buttons
|
||||
}
|
||||
|
||||
func (v01 *V01) dotelemetry() {
|
||||
for {
|
||||
select {
|
||||
case <-v01.ctx.Done():
|
||||
return
|
||||
case msg := <-v01.telemetryc:
|
||||
v01._telemetry(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (v01 *V01) telemetry(msg message) {
|
||||
if FlagDebug {
|
||||
log.Printf("%s|%dms", msg.U, time.Now().UnixNano()/int64(time.Millisecond)-msg.T)
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package v01
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
|
|
@ -49,3 +50,43 @@ func TestV01TransformationPipe(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestV01Quiet(t *testing.T) {
|
||||
ctx, can := context.WithCancel(context.Background())
|
||||
defer can()
|
||||
v01 := NewV01(ctx, constSrc(`{"Y":"a", "N":"b"}`))
|
||||
|
||||
v01.cfg.Quiet = false
|
||||
if got := v01.Read(); len(got) != 2 {
|
||||
t.Error(len(got))
|
||||
} else if got[0].Char != 'a' {
|
||||
t.Error(got[0].Char)
|
||||
} else if got[0].Down != true {
|
||||
t.Error(got[0].Down)
|
||||
} else if got[1].Char != 'b' {
|
||||
t.Error(got[1].Char)
|
||||
} else if got[1].Down != false {
|
||||
t.Error(got[1].Down)
|
||||
}
|
||||
|
||||
v01.cfg.Quiet = true
|
||||
if got := v01.Read(); len(got) != 2 {
|
||||
t.Error(len(got))
|
||||
} else if got[0].Char != 'a' {
|
||||
t.Error(got[0].Char)
|
||||
} else if got[0].Down != false {
|
||||
t.Error(got[0].Down)
|
||||
} else if got[1].Char != 'b' {
|
||||
t.Error(got[1].Char)
|
||||
} else if got[1].Down != false {
|
||||
t.Error(got[1].Down)
|
||||
}
|
||||
}
|
||||
|
||||
type constSrc string
|
||||
|
||||
func (c constSrc) Close() {}
|
||||
|
||||
func (c constSrc) Read() []byte {
|
||||
return []byte(c)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
// a key map of allowed keys
|
||||
var allowedKeys = {
|
||||
37: 'left',
|
||||
38: 'up',
|
||||
39: 'right',
|
||||
40: 'down',
|
||||
65: 'a',
|
||||
66: 'b'
|
||||
};
|
||||
|
||||
// the 'official' Konami Code sequence
|
||||
var konamiCode = ['up', 'up', 'down', 'down', 'left', 'right', 'left', 'right', 'b', 'a'];
|
||||
|
||||
// a variable to remember the 'position' the user has reached so far.
|
||||
var konamiCodePosition = 0;
|
||||
|
||||
// add keydown event listener
|
||||
document.addEventListener('keydown', function(e) {
|
||||
// get the value of the key code from the key map
|
||||
var key = allowedKeys[e.keyCode];
|
||||
// get the value of the required key from the konami code
|
||||
var requiredKey = konamiCode[konamiCodePosition];
|
||||
|
||||
// compare the key with the required key
|
||||
if (key == requiredKey) {
|
||||
|
||||
// move to the next key in the konami code sequence
|
||||
konamiCodePosition++;
|
||||
|
||||
// if the last key is reached, activate cheats
|
||||
if (konamiCodePosition == konamiCode.length) {
|
||||
showSecrets();
|
||||
konamiCodePosition = 0;
|
||||
}
|
||||
} else {
|
||||
konamiCodePosition = 0;
|
||||
}
|
||||
});
|
||||
|
||||
function showSecrets() {
|
||||
var element = document.getElementById("konami")
|
||||
element.style = "display:block"
|
||||
var e = new Event("onKonami")
|
||||
element.dispatchEvent(new Event("onKonami"))
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
function onYouTubeIframeAPIReady() {
|
||||
|
||||
var player;
|
||||
|
||||
player = new YT.Player('konami', {
|
||||
videoId: 'V4oJ62xrFZo', // 👈 video id.
|
||||
width: 560,
|
||||
height: 316,
|
||||
playerVars: {
|
||||
'autoplay': 1,
|
||||
'controls': 1,
|
||||
'showinfo': 0,
|
||||
'modestbranding': 0,
|
||||
'loop': 1,
|
||||
'fs': 0,
|
||||
'cc_load_policty': 0,
|
||||
'iv_load_policy': 3
|
||||
},
|
||||
events: {
|
||||
'onReady': function (e) {
|
||||
e.target.setVolume(33); // For max value, set value to 100.
|
||||
document.getElementById("konami").addEventListener("onKonami", () => {e.target.playVideo()})
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<header>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/water.css@2/out/dark.css">
|
||||
<script src="konami.js"></script>
|
||||
<script src="lowerVolume.js"></script>
|
||||
<script async src="https://www.youtube.com/iframe_api"></script>
|
||||
<script>
|
||||
function formsay(message) {
|
||||
console.log(`say '${message}'`)
|
||||
http("GET", `/proxy?user=${document.getElementById("user").value}&say=${message}`, noopcallback, null)
|
||||
}
|
||||
function formsend(message) {
|
||||
console.log(`send '${message}'`)
|
||||
http("GET", `/proxy/gm/rpc/vote?user=${document.getElementById("user").value}&payload=${message}`, noopcallback, null)
|
||||
}
|
||||
function http(method, remote, callback, body) {
|
||||
var xmlhttp = new XMLHttpRequest();
|
||||
xmlhttp.onreadystatechange = function() {
|
||||
if (xmlhttp.readyState == XMLHttpRequest.DONE) {
|
||||
callback(xmlhttp.responseText, xmlhttp.status)
|
||||
}
|
||||
};
|
||||
xmlhttp.open(method, remote, true);
|
||||
if (typeof body == "undefined") {
|
||||
body = null
|
||||
}
|
||||
xmlhttp.send(body);
|
||||
}
|
||||
function noopcallback(responseBody, responseStatus) {
|
||||
}
|
||||
setInterval(() => {
|
||||
http("GET", `/proxy?user=${document.getElementById("user").value}`, (b, s) => {
|
||||
if (s != 200)
|
||||
return
|
||||
document.getElementById("ntfy").innerHTML = `<pre>${b}</pre>`
|
||||
}, null)
|
||||
}, 1500)
|
||||
|
||||
|
||||
</script>
|
||||
</header>
|
||||
<body>
|
||||
<div>
|
||||
<form>
|
||||
<h3>WHO AM I</h3>
|
||||
<select id="user">
|
||||
<option>bel</option>
|
||||
<option>zach</option>
|
||||
<option>chase</option>
|
||||
<option>mason</option>
|
||||
<option>nat</option>
|
||||
<option>roxy</option>
|
||||
<option>bill</option>
|
||||
</select>
|
||||
</form>
|
||||
<div>
|
||||
<form action="" onsubmit="formsay(this.children.say.value); return false;" style="display: inline-block;">
|
||||
<h3>SAY</h3>
|
||||
<input type="text" name="say">
|
||||
<input type="submit" value="say">
|
||||
</form>
|
||||
<form action="" onsubmit="formsend(this.children.send.value); return false;" style="display: inline-block;">
|
||||
<h3>SEND</h3>
|
||||
<select name="send">
|
||||
<option>bel</option>
|
||||
<option>zach</option>
|
||||
<option>chase</option>
|
||||
<option>mason</option>
|
||||
<option>nat</option>
|
||||
<option>roxy</option>
|
||||
<option>bill</option>
|
||||
</select>
|
||||
<input type="submit" value="send">
|
||||
</form>
|
||||
</div>
|
||||
<details>
|
||||
<summary>CONTROLS</summary>
|
||||
<form id="controls">
|
||||
<div style="display: flex; flex-wrap: wrap;">
|
||||
<div>
|
||||
<label for="input-up">Up</label>
|
||||
<input id="input-up" type="text" maxLength=1 value="w" name="w" placeholder="up" onchange="recontrol()">
|
||||
<label for="input-down">Down</label>
|
||||
<input id="input-down" type="text" maxLength=1 value="s" name="s" placeholder="down" onchange="recontrol()">
|
||||
<label for="input-left">Left</label>
|
||||
<input id="input-left" type="text" maxLength=1 value="a" name="a" placeholder="left" onchange="recontrol()">
|
||||
<label for="input-right">Right</label>
|
||||
<input id="input-right" type="text" maxLength=1 value="d" name="d" placeholder="right" onchange="recontrol()">
|
||||
</div>
|
||||
<div>
|
||||
<label for="input-start">Start</label>
|
||||
<input id="input-start" type="text" maxLength=1 value="5" name="5" placeholder="start" onchange="recontrol()">
|
||||
<label for="input-left-bumper">Left Bumper</label>
|
||||
<input id="input-left-bumper" type="text" maxLength=1 value="q" name="q" placeholder="l" onchange="recontrol()">
|
||||
<label for="input-right-bumper">Right Bumper</label>
|
||||
<input id="input-right-bumper" type="text" maxLength=1 value="e" name="e" placeholder="r" onchange="recontrol()">
|
||||
</div>
|
||||
<div>
|
||||
<label for="input-a">A</label>
|
||||
<input id="input-a" type="text" maxLength=1 value="1" name="1" placeholder="a" onchange="recontrol()">
|
||||
<label for="input-b">B</label>
|
||||
<input id="input-b" type="text" maxLength=1 value="2" name="2" placeholder="b" onchange="recontrol()">
|
||||
<label for="input-x">X</label>
|
||||
<input id="input-x" type="text" maxLength=1 value="3" name="3" placeholder="x" onchange="recontrol()">
|
||||
<label for="input-y">Y</label>
|
||||
<input id="input-y" type="text" maxLength=1 value="4" name="4" placeholder="y" onchange="recontrol()">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</details>
|
||||
</div>
|
||||
<div id="ntfy"></div>
|
||||
<div id="ws"></div>
|
||||
<div id="konami" style="display:none"></div>
|
||||
</body>
|
||||
<footer>
|
||||
<script>
|
||||
var socket = new WebSocket("ws://"+window.location.host+"/api/ws")
|
||||
|
||||
function nosend(data) {
|
||||
}
|
||||
function dosend(data) {
|
||||
console.log(JSON.stringify(data))
|
||||
socket.send(JSON.stringify(data))
|
||||
}
|
||||
send = nosend
|
||||
|
||||
socket.addEventListener("open", (_) => {
|
||||
console.log("ws open")
|
||||
send = dosend
|
||||
})
|
||||
socket.addEventListener("message", (event) => console.log("ws recv:", event.data))
|
||||
socket.addEventListener("close", (event) => console.log("ws closed"))
|
||||
|
||||
keys = {}
|
||||
document.addEventListener('keydown', (event) => {
|
||||
var name = controls[event.key]
|
||||
if (!name)
|
||||
return
|
||||
if (keys[name])
|
||||
return
|
||||
keys[name] = true
|
||||
sendKeys(name, "")
|
||||
})
|
||||
document.addEventListener('keyup', (event) => {
|
||||
var name = controls[event.key]
|
||||
if (!name)
|
||||
return
|
||||
keys[name] = false
|
||||
sendKeys("", name)
|
||||
})
|
||||
function sendKeys(y, n) {
|
||||
send({
|
||||
T: new Date().getTime(),
|
||||
U: document.getElementById("user").value,
|
||||
Y: y,
|
||||
N: n,
|
||||
})
|
||||
}
|
||||
|
||||
var controls = {}
|
||||
function recontrol() {
|
||||
for (var k in controls)
|
||||
controls[k] = false
|
||||
for (var e of document.getElementById("controls").getElementsByTagName("input"))
|
||||
controls[e.value] = e.name
|
||||
}
|
||||
recontrol()
|
||||
</script>
|
||||
</footer>
|
||||
</html>
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,4 +6,5 @@ func TestRaw(t *testing.T) {
|
|||
var _ Raw = &Random{}
|
||||
var _ Raw = UDP{}
|
||||
var _ Raw = Keyboard{}
|
||||
var _ Raw = WS{}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -53,6 +53,8 @@ var (
|
|||
';': PSemicolon,
|
||||
'-': PMinus,
|
||||
'=': PEqual,
|
||||
'<': PageDown,
|
||||
'>': PageUp,
|
||||
}
|
||||
keyToChar = func() map[Key]byte {
|
||||
result := map[Key]byte{}
|
||||
|
|
|
|||
|
|
@ -57,4 +57,6 @@ const (
|
|||
PSemicolon = Key(keybd_event.VK_SEMICOLON)
|
||||
PMinus = Key(keybd_event.VK_MINUS)
|
||||
PEqual = Key(keybd_event.VK_EQUAL)
|
||||
PageUp = Key(keybd_event.VK_PAGEUP)
|
||||
PageDown = Key(keybd_event.VK_PAGEDOWN)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,9 +17,9 @@ func Main(ctx context.Context) error {
|
|||
defer reader.Close()
|
||||
|
||||
interval := time.Millisecond * 50
|
||||
if intervalS := 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...)
|
||||
}
|
||||
|
||||
|
|
|
|||
80
todo.yaml
80
todo.yaml
|
|
@ -1,80 +0,0 @@
|
|||
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
|
||||
Loading…
Reference in New Issue