Compare commits
67 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3264d9ad55 | ||
|
|
3f35f7f936 | ||
|
|
0cddc33ac6 | ||
|
|
a1a12b1873 | ||
|
|
ae1e32391c | ||
|
|
97cc3ae151 | ||
|
|
2113252e2d | ||
|
|
2cae3c6d28 | ||
|
|
de261ae400 | ||
|
|
3dd0a557d4 | ||
|
|
51ae1b27b4 | ||
|
|
50e89492cf | ||
|
|
3d9ea1296c | ||
|
|
db69f76aa0 | ||
|
|
0ee3a8b6e8 | ||
|
|
b379f1d82c | ||
|
|
c83f9d8700 | ||
|
|
6289222b69 | ||
|
|
607a65e22e | ||
|
|
6bbb297c59 | ||
|
|
95866f7df0 | ||
|
|
aaa949cc2a | ||
|
|
ed2b7b7cb9 | ||
|
|
1ef3afd647 | ||
|
|
2746051a2a | ||
|
|
610aef4f7e | ||
|
|
a9ca58f154 | ||
|
|
7182ab387f | ||
|
|
0e46f6e122 | ||
|
|
01777c8c3e | ||
|
|
aa16b66332 | ||
|
|
2af373aed7 | ||
|
|
896f5e9c92 | ||
|
|
b319ed7e6d | ||
|
|
9990273b19 | ||
|
|
ea7f2d8932 | ||
|
|
38b00e55b0 | ||
|
|
ab673a81f0 | ||
|
|
287b9c7b4e | ||
|
|
126f5ab60a | ||
|
|
3c19f984a9 | ||
|
|
cf3b93464a | ||
|
|
edcea37148 | ||
|
|
b4e4de82ae | ||
|
|
cdfcfe8fd0 | ||
|
|
bf677856a2 | ||
|
|
b9d76d5e8f | ||
|
|
d292a830a1 | ||
|
|
745175210c | ||
|
|
6536daee7f | ||
|
|
9ce50f2622 | ||
|
|
ea0bb5d365 | ||
|
|
20488d2be8 | ||
|
|
7b7486cc93 | ||
|
|
e5a668b691 | ||
|
|
c298bb0dfd | ||
|
|
adabc4eb98 | ||
|
|
6e1bfc177d | ||
|
|
e491cc5cbc | ||
|
|
37d02f0f52 | ||
|
|
e832085fc2 | ||
|
|
8e92c9a6d6 | ||
|
|
1fc6d71db6 | ||
|
|
4f48ee805f | ||
|
|
f9ec874491 | ||
|
|
32c186e1e2 | ||
|
|
17b2891f9a |
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/mayhem-party
|
||||||
|
**/*.sw*
|
||||||
11
README.md
11
README.md
@@ -9,3 +9,14 @@ Think Dug's Twitch Chat Plays
|
|||||||
* multiplayer engine
|
* multiplayer engine
|
||||||
* server; https://github.com/LizardByte/Sunshine
|
* server; https://github.com/LizardByte/Sunshine
|
||||||
* client; https://moonlight-stream.org/
|
* client; https://moonlight-stream.org/
|
||||||
|
|
||||||
|
## DONE
|
||||||
|
|
||||||
|
* input
|
||||||
|
* random from weighted file
|
||||||
|
* buffered
|
||||||
|
* remapped from file
|
||||||
|
* output
|
||||||
|
* to keyboard
|
||||||
|
* to stderr
|
||||||
|
|
||||||
|
|||||||
1
go.mod
1
go.mod
@@ -5,4 +5,5 @@ go 1.19
|
|||||||
require (
|
require (
|
||||||
github.com/go-yaml/yaml v2.1.0+incompatible // indirect
|
github.com/go-yaml/yaml v2.1.0+incompatible // indirect
|
||||||
github.com/micmonay/keybd_event v1.1.1 // indirect
|
github.com/micmonay/keybd_event v1.1.1 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
3
go.sum
3
go.sum
@@ -2,3 +2,6 @@ github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwn
|
|||||||
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
|
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
|
||||||
github.com/micmonay/keybd_event v1.1.1 h1:rv7omwXWYL9Lgf3PUq6uBgJI2k1yGkL/GD6dxc6nmSs=
|
github.com/micmonay/keybd_event v1.1.1 h1:rv7omwXWYL9Lgf3PUq6uBgJI2k1yGkL/GD6dxc6nmSs=
|
||||||
github.com/micmonay/keybd_event v1.1.1/go.mod h1:CGMWMDNgsfPljzrAWoybUOSKafQPZpv+rLigt2LzNGI=
|
github.com/micmonay/keybd_event v1.1.1/go.mod h1:CGMWMDNgsfPljzrAWoybUOSKafQPZpv+rLigt2LzNGI=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
|||||||
51
host.d/README.md
Normal file
51
host.d/README.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# Hosting a Mayhem Party
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
1. [`rusty-pipe`](https://gogs.inhome.blapointe.com/bel/rusty-pipe)
|
||||||
|
1. [`mayhem-party`](https://gogs.inhome.blapointe.com/bel/mayhem-party)
|
||||||
|
1. [`stt`](https://gogs.inhome.blapointe.com/bel/stt)
|
||||||
|
|
||||||
|
# Clients
|
||||||
|
|
||||||
|
## Distribute `rusty-pipe`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rustup target add x86_64-pc-windows-gnu
|
||||||
|
echo windows
|
||||||
|
cargo build --target x86_64-pc-windows-gnu && ls target/x86_64-pc-windows-gnu/release/rusty-pipe.exe
|
||||||
|
echo local
|
||||||
|
cargo install --path ./
|
||||||
|
```
|
||||||
|
|
||||||
|
Each client needs 1 executable and 1 env file with a unique set of buttons
|
||||||
|
|
||||||
|
> 10 buttons per player
|
||||||
|
> `go doc key Undef | grep Key | grep -v Undef | wc -l` total (51)
|
||||||
|
|
||||||
|
The server cannot be a client because math. Maybe a VM on the client as a server would work tho.
|
||||||
|
|
||||||
|
See `./config.d/rusty-pipe.d`
|
||||||
|
|
||||||
|
# Server
|
||||||
|
|
||||||
|
## `mayhem-party`
|
||||||
|
|
||||||
|
### Configs
|
||||||
|
|
||||||
|
`cd ./config.d/mayhem-party.d/remap.d; bash ./rotate.sh`
|
||||||
|
|
||||||
|
> rotate anytime via stdin or `pkill -SIGUSR1 -f rotate.sh`
|
||||||
|
|
||||||
|
### Binary
|
||||||
|
|
||||||
|
`bash -c 'true; source ./config.d/mayhem-party.d/env.env; mayhem-party'`
|
||||||
|
|
||||||
|
## Game Playing
|
||||||
|
|
||||||
|
Foreground
|
||||||
|
|
||||||
|
## `stt`
|
||||||
|
|
||||||
|
TODO pipe stt detecting relevant strings -> change the `./config.d/mayhem-party.d/remap.d/live.yaml` link -> `SIGUSR1` to `mayhem-party`
|
||||||
|
|
||||||
7
host.d/config.d/mayhem-party.d/env.env
Normal file
7
host.d/config.d/mayhem-party.d/env.env
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export DEBUG=true
|
||||||
|
export RAW_UDP=17070
|
||||||
|
export BUTTON_V01=true
|
||||||
|
export WRAP_REFRESH_ON_SIGUSR1=true
|
||||||
|
export MAIN_INTERVAL_DURATION=5ms
|
||||||
|
export OUTPUT_KEYBOARD=false
|
||||||
|
export BUTTON_V01_CONFIG=./config.d/mayhem-party.d/v01.yaml
|
||||||
31
host.d/config.d/mayhem-party.d/remap.d/.players_offset.py
Normal file
31
host.d/config.d/mayhem-party.d/remap.d/.players_offset.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import yaml
|
||||||
|
from sys import argv
|
||||||
|
from sys import stderr
|
||||||
|
|
||||||
|
def log(*args):
|
||||||
|
print(*args, file=stderr)
|
||||||
|
|
||||||
|
def main(args):
|
||||||
|
players = []
|
||||||
|
for i in range(1, 5+1):
|
||||||
|
with open(f"../../rusty-pipe.d/{i}.yaml", "r") as f:
|
||||||
|
players.append(yaml.safe_load(f)["streams"]["input"]["engine"]["gui"]["buttons"])
|
||||||
|
log(players[-1])
|
||||||
|
for arg in args:
|
||||||
|
offset = int(arg)
|
||||||
|
for i in range(len(players)):
|
||||||
|
j = (i+offset)%len(players)
|
||||||
|
if i == j:
|
||||||
|
break
|
||||||
|
keys = players[i]
|
||||||
|
values = players[j]
|
||||||
|
log(f"player {i+1} plays as player {j+1}")
|
||||||
|
print(f"# player {i+1} controls {j+1}")
|
||||||
|
for k in keys:
|
||||||
|
key = keys[k]
|
||||||
|
value = values[k]
|
||||||
|
print(f"'{key}': '{value}'")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
from sys import argv
|
||||||
|
main(argv[1:])
|
||||||
1
host.d/config.d/mayhem-party.d/remap.d/live.yaml
Symbolic link
1
host.d/config.d/mayhem-party.d/remap.d/live.yaml
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
players_offset_4.yaml
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
55
host.d/config.d/mayhem-party.d/remap.d/players_offset_1.yaml
Normal file
55
host.d/config.d/mayhem-party.d/remap.d/players_offset_1.yaml
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# player 1 controls 2
|
||||||
|
'1': 'q'
|
||||||
|
'2': 'w'
|
||||||
|
'3': 'e'
|
||||||
|
'4': 'r'
|
||||||
|
'5': 't'
|
||||||
|
'6': 'y'
|
||||||
|
'7': 'u'
|
||||||
|
'8': 'i'
|
||||||
|
'9': 'o'
|
||||||
|
'0': 'p'
|
||||||
|
# player 2 controls 3
|
||||||
|
'q': 'a'
|
||||||
|
'w': 's'
|
||||||
|
'e': 'd'
|
||||||
|
'r': 'f'
|
||||||
|
't': 'g'
|
||||||
|
'y': 'h'
|
||||||
|
'u': 'j'
|
||||||
|
'i': 'k'
|
||||||
|
'o': 'l'
|
||||||
|
'p': ';'
|
||||||
|
# player 3 controls 4
|
||||||
|
'a': 'z'
|
||||||
|
's': 'x'
|
||||||
|
'd': 'c'
|
||||||
|
'f': 'v'
|
||||||
|
'g': 'b'
|
||||||
|
'h': 'n'
|
||||||
|
'j': 'm'
|
||||||
|
'k': ','
|
||||||
|
'l': '-'
|
||||||
|
';': '/'
|
||||||
|
# player 4 controls 5
|
||||||
|
'z': '!'
|
||||||
|
'x': '@'
|
||||||
|
'c': '#'
|
||||||
|
'v': '$'
|
||||||
|
'b': '%'
|
||||||
|
'n': '^'
|
||||||
|
'm': '&'
|
||||||
|
',': '*'
|
||||||
|
'-': '('
|
||||||
|
'/': ')'
|
||||||
|
# player 5 controls 1
|
||||||
|
'!': '1'
|
||||||
|
'@': '2'
|
||||||
|
'#': '3'
|
||||||
|
'$': '4'
|
||||||
|
'%': '5'
|
||||||
|
'^': '6'
|
||||||
|
'&': '7'
|
||||||
|
'*': '8'
|
||||||
|
'(': '9'
|
||||||
|
')': '0'
|
||||||
55
host.d/config.d/mayhem-party.d/remap.d/players_offset_2.yaml
Normal file
55
host.d/config.d/mayhem-party.d/remap.d/players_offset_2.yaml
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# player 1 controls 3
|
||||||
|
'1': 'a'
|
||||||
|
'2': 's'
|
||||||
|
'3': 'd'
|
||||||
|
'4': 'f'
|
||||||
|
'5': 'g'
|
||||||
|
'6': 'h'
|
||||||
|
'7': 'j'
|
||||||
|
'8': 'k'
|
||||||
|
'9': 'l'
|
||||||
|
'0': ';'
|
||||||
|
# player 2 controls 4
|
||||||
|
'q': 'z'
|
||||||
|
'w': 'x'
|
||||||
|
'e': 'c'
|
||||||
|
'r': 'v'
|
||||||
|
't': 'b'
|
||||||
|
'y': 'n'
|
||||||
|
'u': 'm'
|
||||||
|
'i': ','
|
||||||
|
'o': '-'
|
||||||
|
'p': '/'
|
||||||
|
# player 3 controls 5
|
||||||
|
'a': '!'
|
||||||
|
's': '@'
|
||||||
|
'd': '#'
|
||||||
|
'f': '$'
|
||||||
|
'g': '%'
|
||||||
|
'h': '^'
|
||||||
|
'j': '&'
|
||||||
|
'k': '*'
|
||||||
|
'l': '('
|
||||||
|
';': ')'
|
||||||
|
# player 4 controls 1
|
||||||
|
'z': '1'
|
||||||
|
'x': '2'
|
||||||
|
'c': '3'
|
||||||
|
'v': '4'
|
||||||
|
'b': '5'
|
||||||
|
'n': '6'
|
||||||
|
'm': '7'
|
||||||
|
',': '8'
|
||||||
|
'-': '9'
|
||||||
|
'/': '0'
|
||||||
|
# player 5 controls 2
|
||||||
|
'!': 'q'
|
||||||
|
'@': 'w'
|
||||||
|
'#': 'e'
|
||||||
|
'$': 'r'
|
||||||
|
'%': 't'
|
||||||
|
'^': 'y'
|
||||||
|
'&': 'u'
|
||||||
|
'*': 'i'
|
||||||
|
'(': 'o'
|
||||||
|
')': 'p'
|
||||||
55
host.d/config.d/mayhem-party.d/remap.d/players_offset_3.yaml
Normal file
55
host.d/config.d/mayhem-party.d/remap.d/players_offset_3.yaml
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# player 1 controls 4
|
||||||
|
'1': 'z'
|
||||||
|
'2': 'x'
|
||||||
|
'3': 'c'
|
||||||
|
'4': 'v'
|
||||||
|
'5': 'b'
|
||||||
|
'6': 'n'
|
||||||
|
'7': 'm'
|
||||||
|
'8': ','
|
||||||
|
'9': '-'
|
||||||
|
'0': '/'
|
||||||
|
# player 2 controls 5
|
||||||
|
'q': '!'
|
||||||
|
'w': '@'
|
||||||
|
'e': '#'
|
||||||
|
'r': '$'
|
||||||
|
't': '%'
|
||||||
|
'y': '^'
|
||||||
|
'u': '&'
|
||||||
|
'i': '*'
|
||||||
|
'o': '('
|
||||||
|
'p': ')'
|
||||||
|
# player 3 controls 1
|
||||||
|
'a': '1'
|
||||||
|
's': '2'
|
||||||
|
'd': '3'
|
||||||
|
'f': '4'
|
||||||
|
'g': '5'
|
||||||
|
'h': '6'
|
||||||
|
'j': '7'
|
||||||
|
'k': '8'
|
||||||
|
'l': '9'
|
||||||
|
';': '0'
|
||||||
|
# player 4 controls 2
|
||||||
|
'z': 'q'
|
||||||
|
'x': 'w'
|
||||||
|
'c': 'e'
|
||||||
|
'v': 'r'
|
||||||
|
'b': 't'
|
||||||
|
'n': 'y'
|
||||||
|
'm': 'u'
|
||||||
|
',': 'i'
|
||||||
|
'-': 'o'
|
||||||
|
'/': 'p'
|
||||||
|
# player 5 controls 3
|
||||||
|
'!': 'a'
|
||||||
|
'@': 's'
|
||||||
|
'#': 'd'
|
||||||
|
'$': 'f'
|
||||||
|
'%': 'g'
|
||||||
|
'^': 'h'
|
||||||
|
'&': 'j'
|
||||||
|
'*': 'k'
|
||||||
|
'(': 'l'
|
||||||
|
')': ';'
|
||||||
55
host.d/config.d/mayhem-party.d/remap.d/players_offset_4.yaml
Normal file
55
host.d/config.d/mayhem-party.d/remap.d/players_offset_4.yaml
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# player 1 controls 5
|
||||||
|
'1': '!'
|
||||||
|
'2': '@'
|
||||||
|
'3': '#'
|
||||||
|
'4': '$'
|
||||||
|
'5': '%'
|
||||||
|
'6': '^'
|
||||||
|
'7': '&'
|
||||||
|
'8': '*'
|
||||||
|
'9': '('
|
||||||
|
'0': ')'
|
||||||
|
# player 2 controls 1
|
||||||
|
'q': '1'
|
||||||
|
'w': '2'
|
||||||
|
'e': '3'
|
||||||
|
'r': '4'
|
||||||
|
't': '5'
|
||||||
|
'y': '6'
|
||||||
|
'u': '7'
|
||||||
|
'i': '8'
|
||||||
|
'o': '9'
|
||||||
|
'p': '0'
|
||||||
|
# player 3 controls 2
|
||||||
|
'a': 'q'
|
||||||
|
's': 'w'
|
||||||
|
'd': 'e'
|
||||||
|
'f': 'r'
|
||||||
|
'g': 't'
|
||||||
|
'h': 'y'
|
||||||
|
'j': 'u'
|
||||||
|
'k': 'i'
|
||||||
|
'l': 'o'
|
||||||
|
';': 'p'
|
||||||
|
# player 4 controls 3
|
||||||
|
'z': 'a'
|
||||||
|
'x': 's'
|
||||||
|
'c': 'd'
|
||||||
|
'v': 'f'
|
||||||
|
'b': 'g'
|
||||||
|
'n': 'h'
|
||||||
|
'm': 'j'
|
||||||
|
',': 'k'
|
||||||
|
'-': 'l'
|
||||||
|
'/': ';'
|
||||||
|
# player 5 controls 4
|
||||||
|
'!': 'z'
|
||||||
|
'@': 'x'
|
||||||
|
'#': 'c'
|
||||||
|
'$': 'v'
|
||||||
|
'%': 'b'
|
||||||
|
'^': 'n'
|
||||||
|
'&': 'm'
|
||||||
|
'*': ','
|
||||||
|
'(': '-'
|
||||||
|
')': '/'
|
||||||
21
host.d/config.d/mayhem-party.d/remap.d/rotate.sh
Normal file
21
host.d/config.d/mayhem-party.d/remap.d/rotate.sh
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
#! /bin/bash
|
||||||
|
|
||||||
|
mayhem_party_rotate() {
|
||||||
|
local currently=$(realpath live.yaml | grep -o '[0-9].yaml$' | grep -o '^[0-9]')
|
||||||
|
local next=${NEXT:-$((RANDOM%5))}
|
||||||
|
while [ -z "$NEXT" ] && [ "$next" == "$currently" ]; do
|
||||||
|
next=$((RANDOM%5))
|
||||||
|
done
|
||||||
|
rm live.yaml
|
||||||
|
ln -s players_offset_$next.yaml live.yaml
|
||||||
|
pkill -SIGUSR1 -f mayhem-party
|
||||||
|
}
|
||||||
|
|
||||||
|
trap mayhem_party_rotate SIGUSR1
|
||||||
|
|
||||||
|
if [ "$0" == "$BASH_SOURCE" ]; then
|
||||||
|
NEXT=0 mayhem_party_rotate
|
||||||
|
while read -p "$(date) > [press enter to rotate]"; do
|
||||||
|
mayhem_party_rotate
|
||||||
|
done
|
||||||
|
fi
|
||||||
21
host.d/config.d/mayhem-party.d/v01.yaml
Normal file
21
host.d/config.d/mayhem-party.d/v01.yaml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
feedback:
|
||||||
|
addr: :17071
|
||||||
|
users:
|
||||||
|
bel:
|
||||||
|
player: 2
|
||||||
|
message: its bel
|
||||||
|
broadcast:
|
||||||
|
message: 8
|
||||||
|
players:
|
||||||
|
- {}
|
||||||
|
- transformation:
|
||||||
|
w: t
|
||||||
|
a: f
|
||||||
|
s: g
|
||||||
|
d: h
|
||||||
|
q: r
|
||||||
|
e: y
|
||||||
|
1: 5
|
||||||
|
2: 6
|
||||||
|
3: 7
|
||||||
|
4: 8
|
||||||
28
host.d/config.d/rusty-pipe.d/1.yaml
Normal file
28
host.d/config.d/rusty-pipe.d/1.yaml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
streams:
|
||||||
|
input:
|
||||||
|
debug: false
|
||||||
|
engine:
|
||||||
|
name: gui
|
||||||
|
gui:
|
||||||
|
user: bel
|
||||||
|
press: {prefix: "", suffix: ""}
|
||||||
|
release: {prefix: "", suffix: ""}
|
||||||
|
format: '{"T":{{ms}},"U":"{{user}}","Y":"{{pressed}}","N":"{{released}}"}'
|
||||||
|
buttons:
|
||||||
|
up: 'w'
|
||||||
|
down: 's'
|
||||||
|
left: 'a'
|
||||||
|
right: 'd'
|
||||||
|
l: 'e'
|
||||||
|
r: 'q'
|
||||||
|
a: '1'
|
||||||
|
b: '2'
|
||||||
|
x: '3'
|
||||||
|
y: '4'
|
||||||
|
output:
|
||||||
|
debug: true
|
||||||
|
engine:
|
||||||
|
name: udp
|
||||||
|
udp:
|
||||||
|
host: mayhem-party.home.blapointe.com
|
||||||
|
port: 17070
|
||||||
28
host.d/config.d/rusty-pipe.d/2.yaml
Normal file
28
host.d/config.d/rusty-pipe.d/2.yaml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
streams:
|
||||||
|
input:
|
||||||
|
debug: false
|
||||||
|
engine:
|
||||||
|
name: gui
|
||||||
|
gui:
|
||||||
|
user: zach
|
||||||
|
press: {prefix: "", suffix: ""}
|
||||||
|
release: {prefix: "", suffix: ""}
|
||||||
|
format: '{"T":{{ms}},"U":"{{user}}","Y":"{{pressed}}","N":"{{released}}"}'
|
||||||
|
buttons:
|
||||||
|
up: 'w'
|
||||||
|
down: 's'
|
||||||
|
left: 'a'
|
||||||
|
right: 'd'
|
||||||
|
l: 'e'
|
||||||
|
r: 'q'
|
||||||
|
a: '1'
|
||||||
|
b: '2'
|
||||||
|
x: '3'
|
||||||
|
y: '4'
|
||||||
|
output:
|
||||||
|
debug: true
|
||||||
|
engine:
|
||||||
|
name: udp
|
||||||
|
udp:
|
||||||
|
host: mayhem-party.home.blapointe.com
|
||||||
|
port: 17070
|
||||||
28
host.d/config.d/rusty-pipe.d/3.yaml
Normal file
28
host.d/config.d/rusty-pipe.d/3.yaml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
streams:
|
||||||
|
input:
|
||||||
|
debug: false
|
||||||
|
engine:
|
||||||
|
name: gui
|
||||||
|
gui:
|
||||||
|
user: mason
|
||||||
|
press: {prefix: "", suffix: ""}
|
||||||
|
release: {prefix: "", suffix: ""}
|
||||||
|
format: '{"T":{{ms}},"U":"{{user}}","Y":"{{pressed}}","N":"{{released}}"}'
|
||||||
|
buttons:
|
||||||
|
up: 'w'
|
||||||
|
down: 's'
|
||||||
|
left: 'a'
|
||||||
|
right: 'd'
|
||||||
|
l: 'e'
|
||||||
|
r: 'q'
|
||||||
|
a: '1'
|
||||||
|
b: '2'
|
||||||
|
x: '3'
|
||||||
|
y: '4'
|
||||||
|
output:
|
||||||
|
debug: true
|
||||||
|
engine:
|
||||||
|
name: udp
|
||||||
|
udp:
|
||||||
|
host: mayhem-party.home.blapointe.com
|
||||||
|
port: 17070
|
||||||
28
host.d/config.d/rusty-pipe.d/4.yaml
Normal file
28
host.d/config.d/rusty-pipe.d/4.yaml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
streams:
|
||||||
|
input:
|
||||||
|
debug: false
|
||||||
|
engine:
|
||||||
|
name: gui
|
||||||
|
gui:
|
||||||
|
user: nat
|
||||||
|
press: {prefix: "", suffix: ""}
|
||||||
|
release: {prefix: "", suffix: ""}
|
||||||
|
format: '{"T":{{ms}},"U":"{{user}}","Y":"{{pressed}}","N":"{{released}}"}'
|
||||||
|
buttons:
|
||||||
|
up: 'w'
|
||||||
|
down: 's'
|
||||||
|
left: 'a'
|
||||||
|
right: 'd'
|
||||||
|
l: 'e'
|
||||||
|
r: 'q'
|
||||||
|
a: '1'
|
||||||
|
b: '2'
|
||||||
|
x: '3'
|
||||||
|
y: '4'
|
||||||
|
output:
|
||||||
|
debug: true
|
||||||
|
engine:
|
||||||
|
name: udp
|
||||||
|
udp:
|
||||||
|
host: mayhem-party.home.blapointe.com
|
||||||
|
port: 17070
|
||||||
28
host.d/config.d/rusty-pipe.d/5.yaml
Normal file
28
host.d/config.d/rusty-pipe.d/5.yaml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
streams:
|
||||||
|
input:
|
||||||
|
debug: false
|
||||||
|
engine:
|
||||||
|
name: gui
|
||||||
|
gui:
|
||||||
|
user: roxy
|
||||||
|
press: {prefix: "", suffix: ""}
|
||||||
|
release: {prefix: "", suffix: ""}
|
||||||
|
format: '{"T":{{ms}},"U":"{{user}}","Y":"{{pressed}}","N":"{{released}}"}'
|
||||||
|
buttons:
|
||||||
|
up: 'w'
|
||||||
|
down: 's'
|
||||||
|
left: 'a'
|
||||||
|
right: 'd'
|
||||||
|
l: 'e'
|
||||||
|
r: 'q'
|
||||||
|
a: '1'
|
||||||
|
b: '2'
|
||||||
|
x: '3'
|
||||||
|
y: '4'
|
||||||
|
output:
|
||||||
|
debug: true
|
||||||
|
engine:
|
||||||
|
name: udp
|
||||||
|
udp:
|
||||||
|
host: mayhem-party.home.blapointe.com
|
||||||
|
port: 17070
|
||||||
28
host.d/config.d/rusty-pipe.d/6.yaml
Normal file
28
host.d/config.d/rusty-pipe.d/6.yaml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
streams:
|
||||||
|
input:
|
||||||
|
debug: false
|
||||||
|
engine:
|
||||||
|
name: gui
|
||||||
|
gui:
|
||||||
|
user: chase
|
||||||
|
press: {prefix: "", suffix: ""}
|
||||||
|
release: {prefix: "", suffix: ""}
|
||||||
|
format: '{"T":{{ms}},"U":"{{user}}","Y":"{{pressed}}","N":"{{released}}"}'
|
||||||
|
buttons:
|
||||||
|
up: 'w'
|
||||||
|
down: 's'
|
||||||
|
left: 'a'
|
||||||
|
right: 'd'
|
||||||
|
l: 'e'
|
||||||
|
r: 'q'
|
||||||
|
a: '1'
|
||||||
|
b: '2'
|
||||||
|
x: '3'
|
||||||
|
y: '4'
|
||||||
|
output:
|
||||||
|
debug: true
|
||||||
|
engine:
|
||||||
|
name: udp
|
||||||
|
udp:
|
||||||
|
host: mayhem-party.home.blapointe.com
|
||||||
|
port: 17070
|
||||||
28
host.d/config.d/rusty-pipe.d/7.yaml
Normal file
28
host.d/config.d/rusty-pipe.d/7.yaml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
streams:
|
||||||
|
input:
|
||||||
|
debug: false
|
||||||
|
engine:
|
||||||
|
name: gui
|
||||||
|
gui:
|
||||||
|
user: bill
|
||||||
|
press: {prefix: "", suffix: ""}
|
||||||
|
release: {prefix: "", suffix: ""}
|
||||||
|
format: '{"T":{{ms}},"U":"{{user}}","Y":"{{pressed}}","N":"{{released}}"}'
|
||||||
|
buttons:
|
||||||
|
up: 'w'
|
||||||
|
down: 's'
|
||||||
|
left: 'a'
|
||||||
|
right: 'd'
|
||||||
|
l: 'e'
|
||||||
|
r: 'q'
|
||||||
|
a: '1'
|
||||||
|
b: '2'
|
||||||
|
x: '3'
|
||||||
|
y: '4'
|
||||||
|
output:
|
||||||
|
debug: true
|
||||||
|
engine:
|
||||||
|
name: udp
|
||||||
|
udp:
|
||||||
|
host: mayhem-party.home.blapointe.com
|
||||||
|
port: 17070
|
||||||
6
src/device/input/button/button.go
Normal file
6
src/device/input/button/button.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package button
|
||||||
|
|
||||||
|
type Button struct {
|
||||||
|
Char byte
|
||||||
|
Down bool
|
||||||
|
}
|
||||||
24
src/device/input/button/parser.go
Normal file
24
src/device/input/button/parser.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package button
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"mayhem-party/src/device/input/raw"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
FlagButtonV01 = os.Getenv("BUTTON_V01") == "true"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Parser interface {
|
||||||
|
Read() []Button
|
||||||
|
Close()
|
||||||
|
CloseWrap() raw.Raw
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(ctx context.Context, src raw.Raw) Parser {
|
||||||
|
if FlagButtonV01 {
|
||||||
|
return NewV01(ctx, src)
|
||||||
|
}
|
||||||
|
return NewPlaintext(src)
|
||||||
|
}
|
||||||
11
src/device/input/button/parser_test.go
Normal file
11
src/device/input/button/parser_test.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package button_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"mayhem-party/src/device/input/button"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParser(t *testing.T) {
|
||||||
|
var _ button.Parser = button.Plaintext{}
|
||||||
|
var _ button.Parser = button.V01{}
|
||||||
|
}
|
||||||
47
src/device/input/button/plaintext.go
Normal file
47
src/device/input/button/plaintext.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package button
|
||||||
|
|
||||||
|
import (
|
||||||
|
"mayhem-party/src/device/input/raw"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
FlagButtonPlaintextRelease = os.Getenv("BUTTON_PLAINTEXT_RELEASE")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Plaintext struct {
|
||||||
|
src raw.Raw
|
||||||
|
release byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPlaintext(src raw.Raw) Plaintext {
|
||||||
|
releaseChar := byte('!')
|
||||||
|
if FlagButtonPlaintextRelease != "" {
|
||||||
|
releaseChar = byte(FlagButtonPlaintextRelease[0])
|
||||||
|
}
|
||||||
|
return Plaintext{
|
||||||
|
src: src,
|
||||||
|
release: releaseChar,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Plaintext) Close() { p.src.Close() }
|
||||||
|
|
||||||
|
func (p Plaintext) CloseWrap() raw.Raw { return p.src }
|
||||||
|
|
||||||
|
func (p Plaintext) Read() []Button {
|
||||||
|
b := p.src.Read()
|
||||||
|
buttons := make([]Button, 0, len(b))
|
||||||
|
down := true
|
||||||
|
for i := range b {
|
||||||
|
if b[i] == p.release {
|
||||||
|
down = false
|
||||||
|
} else {
|
||||||
|
if b[i] != '\n' {
|
||||||
|
buttons = append(buttons, Button{Char: b[i], Down: down})
|
||||||
|
}
|
||||||
|
down = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buttons
|
||||||
|
}
|
||||||
29
src/device/input/button/plaintext_test.go
Normal file
29
src/device/input/button/plaintext_test.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package button_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"mayhem-party/src/device/input/button"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPlaintext(t *testing.T) {
|
||||||
|
src := constSrc("c!b")
|
||||||
|
p := button.NewPlaintext(src)
|
||||||
|
got := p.Read()
|
||||||
|
if len(got) != 2 {
|
||||||
|
t.Fatal(len(got))
|
||||||
|
}
|
||||||
|
if got[0] != (button.Button{Char: 'c', Down: true}) {
|
||||||
|
t.Error(got[0])
|
||||||
|
}
|
||||||
|
if got[1] != (button.Button{Char: 'b', Down: false}) {
|
||||||
|
t.Error(got[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type constSrc string
|
||||||
|
|
||||||
|
func (c constSrc) Close() {}
|
||||||
|
|
||||||
|
func (c constSrc) Read() []byte {
|
||||||
|
return []byte(c)
|
||||||
|
}
|
||||||
16
src/device/input/button/testdata/v01.yaml
vendored
Normal file
16
src/device/input/button/testdata/v01.yaml
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
users:
|
||||||
|
bel:
|
||||||
|
player: 0
|
||||||
|
message: "hi"
|
||||||
|
players:
|
||||||
|
- buttons:
|
||||||
|
up: "w"
|
||||||
|
down: "s"
|
||||||
|
left: "a"
|
||||||
|
right: "d"
|
||||||
|
l: "q"
|
||||||
|
r: "e"
|
||||||
|
a: "1"
|
||||||
|
b: "2"
|
||||||
|
x: "3"
|
||||||
|
y: "4"
|
||||||
155
src/device/input/button/v01.go
Normal file
155
src/device/input/button/v01.go
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
package button
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"mayhem-party/src/device/input/raw"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
FlagDebug = os.Getenv("DEBUG") == "true"
|
||||||
|
FlagButtonV01Config = os.Getenv("BUTTON_V01_CONFIG")
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
V01 struct {
|
||||||
|
ctx context.Context
|
||||||
|
can context.CancelFunc
|
||||||
|
src raw.Raw
|
||||||
|
cfg v01Cfg
|
||||||
|
}
|
||||||
|
v01Msg struct {
|
||||||
|
T int64
|
||||||
|
U string
|
||||||
|
Y string
|
||||||
|
N string
|
||||||
|
}
|
||||||
|
v01Cfg struct {
|
||||||
|
Feedback struct {
|
||||||
|
Addr string
|
||||||
|
}
|
||||||
|
Users map[string]struct {
|
||||||
|
Player int
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
Players []struct {
|
||||||
|
Transformation v01Transformation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v01Transformation map[string]string
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewV01(ctx context.Context, src raw.Raw) V01 {
|
||||||
|
var cfg v01Cfg
|
||||||
|
b, _ := ioutil.ReadFile(FlagButtonV01Config)
|
||||||
|
yaml.Unmarshal(b, &cfg)
|
||||||
|
ctx, can := context.WithCancel(ctx)
|
||||||
|
result := V01{
|
||||||
|
ctx: ctx,
|
||||||
|
can: can,
|
||||||
|
src: src,
|
||||||
|
cfg: cfg,
|
||||||
|
}
|
||||||
|
go result.listen()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v01 V01) listen() {
|
||||||
|
if v01.cfg.Feedback.Addr == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s := &http.Server{
|
||||||
|
Addr: v01.cfg.Feedback.Addr,
|
||||||
|
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r = r.WithContext(v01.ctx)
|
||||||
|
user, ok := v01.cfg.Users[r.URL.Query().Get("user")]
|
||||||
|
if !ok {
|
||||||
|
user = v01.cfg.Users["broadcast"]
|
||||||
|
}
|
||||||
|
w.Write([]byte(user.Message))
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
<-v01.ctx.Done()
|
||||||
|
log.Println("closing v01 server")
|
||||||
|
s.Close()
|
||||||
|
}()
|
||||||
|
log.Println("starting v01 server")
|
||||||
|
if err := s.ListenAndServe(); err != nil && v01.ctx.Err() == nil {
|
||||||
|
log.Println("err with v01 server", err)
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v01 V01) CloseWrap() raw.Raw {
|
||||||
|
v01.can()
|
||||||
|
return v01.src
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v01 V01) Close() {
|
||||||
|
v01.can()
|
||||||
|
v01.src.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v01 V01) Read() []Button {
|
||||||
|
line := v01.src.Read()
|
||||||
|
var msg v01Msg
|
||||||
|
if err := json.Unmarshal(line, &msg); err != nil {
|
||||||
|
log.Printf("%v: %s", err, line)
|
||||||
|
}
|
||||||
|
v01.telemetry(msg)
|
||||||
|
|
||||||
|
return v01.cfg.transform(msg).buttons()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg v01Cfg) transform(msg v01Msg) v01Msg {
|
||||||
|
if len(cfg.Players) == 0 {
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
user := cfg.Users[msg.U]
|
||||||
|
if user.Player < 1 {
|
||||||
|
msg.Y = ""
|
||||||
|
msg.N = ""
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
player := cfg.Players[user.Player-1]
|
||||||
|
msg.Y = player.Transformation.pipe(msg.Y)
|
||||||
|
msg.N = player.Transformation.pipe(msg.N)
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t v01Transformation) pipe(s string) string {
|
||||||
|
for i := range s {
|
||||||
|
if v := t[s[i:i+1]]; v != "" {
|
||||||
|
s = s[:i] + v[:1] + s[i+1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v01 V01) telemetry(msg v01Msg) {
|
||||||
|
if FlagDebug {
|
||||||
|
log.Printf("%s|%dms", msg.U, time.Now().UnixNano()/int64(time.Millisecond)-msg.T)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msg v01Msg) buttons() []Button {
|
||||||
|
buttons := make([]Button, len(msg.Y)+len(msg.N))
|
||||||
|
for i := range msg.Y {
|
||||||
|
buttons[i] = Button{Char: msg.Y[i], Down: true}
|
||||||
|
}
|
||||||
|
for i := range msg.N {
|
||||||
|
buttons[len(msg.Y)+i] = Button{Char: msg.N[i], Down: false}
|
||||||
|
}
|
||||||
|
if FlagDebug {
|
||||||
|
log.Printf("%+v", msg)
|
||||||
|
}
|
||||||
|
return buttons
|
||||||
|
}
|
||||||
137
src/device/input/button/v01_exported_test.go
Normal file
137
src/device/input/button/v01_exported_test.go
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
package button_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"mayhem-party/src/device/input/button"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestV01(t *testing.T) {
|
||||||
|
src := constSrc(fmt.Sprintf(`{"T":%v,"U":"bel","Y":"abc","N":"cde"}`, time.Now().UnixNano()/int64(time.Millisecond)-50))
|
||||||
|
t.Logf("(%v) %s", len(src), src.Read())
|
||||||
|
v01 := button.NewV01(context.Background(), src)
|
||||||
|
defer v01.Close()
|
||||||
|
got := v01.Read()
|
||||||
|
want := []button.Button{
|
||||||
|
{Down: true, Char: 'a'},
|
||||||
|
{Down: true, Char: 'b'},
|
||||||
|
{Down: true, Char: 'c'},
|
||||||
|
{Down: false, Char: 'c'},
|
||||||
|
{Down: false, Char: 'd'},
|
||||||
|
{Down: false, Char: 'e'},
|
||||||
|
}
|
||||||
|
if len(got) != len(want) {
|
||||||
|
t.Fatal(len(want), len(got))
|
||||||
|
}
|
||||||
|
for i := range got {
|
||||||
|
if got[i] != want[i] {
|
||||||
|
t.Errorf("[%d] want %+v got %+v", i, want[i], got[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestV01WithCfg(t *testing.T) {
|
||||||
|
d := t.TempDir()
|
||||||
|
p := path.Join(d, "cfg.yaml")
|
||||||
|
os.WriteFile(p, []byte(`
|
||||||
|
users:
|
||||||
|
bel:
|
||||||
|
player: 2
|
||||||
|
players:
|
||||||
|
- transformation:
|
||||||
|
w: t
|
||||||
|
- transformation:
|
||||||
|
w: i
|
||||||
|
`), os.ModePerm)
|
||||||
|
button.FlagButtonV01Config = p
|
||||||
|
|
||||||
|
t.Run("unknown user ignored", func(t *testing.T) {
|
||||||
|
v01 := button.NewV01(context.Background(), constSrc(`{"U":"qt","Y":"w"}`))
|
||||||
|
defer v01.Close()
|
||||||
|
got := v01.Read()
|
||||||
|
if len(got) != 0 {
|
||||||
|
t.Error(got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("player2", func(t *testing.T) {
|
||||||
|
v01 := button.NewV01(context.Background(), constSrc(`{"U":"bel","Y":"w","N":"w"}`))
|
||||||
|
defer v01.Close()
|
||||||
|
got := v01.Read()
|
||||||
|
if len(got) != 2 {
|
||||||
|
t.Error(got)
|
||||||
|
}
|
||||||
|
if got[0] != (button.Button{Char: 'i', Down: true}) {
|
||||||
|
t.Error(got[0])
|
||||||
|
}
|
||||||
|
if got[1] != (button.Button{Char: 'i', Down: false}) {
|
||||||
|
t.Error(got[1])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestV01Feedback(t *testing.T) {
|
||||||
|
d := t.TempDir()
|
||||||
|
p := path.Join(d, "cfg.yaml")
|
||||||
|
os.WriteFile(p, []byte(`
|
||||||
|
feedback:
|
||||||
|
addr: :27071
|
||||||
|
users:
|
||||||
|
bel:
|
||||||
|
player: 2
|
||||||
|
message: to bel
|
||||||
|
broadcast:
|
||||||
|
message: to everyone
|
||||||
|
players:
|
||||||
|
- transformation:
|
||||||
|
w: t
|
||||||
|
- transformation:
|
||||||
|
w: i
|
||||||
|
`), os.ModePerm)
|
||||||
|
button.FlagButtonV01Config = p
|
||||||
|
ctx, can := context.WithCancel(context.Background())
|
||||||
|
defer can()
|
||||||
|
|
||||||
|
v01 := button.NewV01(ctx, constSrc(`{"U":"qt","Y":"w"}`))
|
||||||
|
defer v01.Close()
|
||||||
|
|
||||||
|
for {
|
||||||
|
time.Sleep(time.Millisecond * 100)
|
||||||
|
resp, err := http.Get("http://localhost:27071?user=bel")
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("specific user", func(t *testing.T) {
|
||||||
|
resp, err := http.Get("http://localhost:27071?user=bel")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
b, _ := io.ReadAll(resp.Body)
|
||||||
|
if string(b) != "to bel" {
|
||||||
|
t.Error(b)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("broadcast", func(t *testing.T) {
|
||||||
|
resp, err := http.Get("http://localhost:27071")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
b, _ := io.ReadAll(resp.Body)
|
||||||
|
if string(b) != "to everyone" {
|
||||||
|
t.Error(b)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
51
src/device/input/button/v01_test.go
Normal file
51
src/device/input/button/v01_test.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package button
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestV01TransformationPipe(t *testing.T) {
|
||||||
|
cases := map[string]struct {
|
||||||
|
input string
|
||||||
|
xform map[string]string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
"empty input": {
|
||||||
|
xform: map[string]string{"a": "bc"},
|
||||||
|
},
|
||||||
|
"empty xform": {
|
||||||
|
input: "aa",
|
||||||
|
want: "aa",
|
||||||
|
},
|
||||||
|
"all": {
|
||||||
|
input: "aa",
|
||||||
|
xform: map[string]string{"a": "cc"},
|
||||||
|
want: "cc",
|
||||||
|
},
|
||||||
|
"last": {
|
||||||
|
input: "ba",
|
||||||
|
xform: map[string]string{"a": "cc"},
|
||||||
|
want: "bc",
|
||||||
|
},
|
||||||
|
"first": {
|
||||||
|
input: "ab",
|
||||||
|
xform: map[string]string{"a": "cc"},
|
||||||
|
want: "cb",
|
||||||
|
},
|
||||||
|
"noop": {
|
||||||
|
input: "bb",
|
||||||
|
xform: map[string]string{"a": "bc"},
|
||||||
|
want: "bb",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, d := range cases {
|
||||||
|
c := d
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
got := v01Transformation(c.xform).pipe(c.input)
|
||||||
|
if got != c.want {
|
||||||
|
t.Errorf("%+v(%s) want %s got %s", c.xform, c.input, c.want, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,28 +2,19 @@ package input
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"os"
|
"mayhem-party/src/device/input/button"
|
||||||
|
"mayhem-party/src/device/input/raw"
|
||||||
|
"mayhem-party/src/device/input/wrap"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Input interface {
|
type Input interface {
|
||||||
Read() []Button
|
Read() []button.Button
|
||||||
Close()
|
Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(ctx context.Context) Input {
|
func New(ctx context.Context) Input {
|
||||||
foo := randomCharFromRange('a', 'g')
|
src := raw.New(ctx)
|
||||||
if p, ok := os.LookupEnv("INPUT_RANDOM_WEIGHT_FILE"); ok && len(p) > 0 {
|
return wrap.New(ctx, func() button.Parser {
|
||||||
foo = randomCharFromWeightFile(p)
|
return button.New(ctx, src)
|
||||||
}
|
})
|
||||||
var result Input = NewRandom(foo)
|
|
||||||
if os.Getenv("INPUT_KEYBOARD") == "true" {
|
|
||||||
result = NewKeyboard()
|
|
||||||
}
|
|
||||||
if os.Getenv("INPUT_BUFFERED") == "true" {
|
|
||||||
result = NewBuffered(ctx, result)
|
|
||||||
}
|
|
||||||
if p, ok := os.LookupEnv("INPUT_REMAP_FILE"); ok && len(p) > 0 {
|
|
||||||
result = NewRemapFromFile(result, p)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package input_test
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"mayhem-party/src/device/input"
|
"mayhem-party/src/device/input"
|
||||||
|
"mayhem-party/src/device/input/raw"
|
||||||
|
"mayhem-party/src/device/input/wrap"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -30,11 +32,11 @@ func TestNewRemapped(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
os.Setenv("INPUT_REMAP_FILE", remap)
|
wrap.FlagRemapFile = remap
|
||||||
os.Setenv("INPUT_RANDOM_WEIGHT_FILE", rand)
|
raw.FlagRawRandomWeightFile = rand
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
os.Unsetenv("INPUT_REMAP_FILE")
|
wrap.FlagRemapFile = ""
|
||||||
os.Unsetenv("INPUT_RANDOM_WEIGHT_FILE")
|
raw.FlagRawRandomWeightFile = ""
|
||||||
})
|
})
|
||||||
|
|
||||||
r := input.New(context.Background())
|
r := input.New(context.Background())
|
||||||
@@ -50,9 +52,9 @@ func TestNewRemapped(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestNewBuffered(t *testing.T) {
|
func TestNewBuffered(t *testing.T) {
|
||||||
os.Setenv("INPUT_BUFFERED", "true")
|
wrap.FlagBuffered = true
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
os.Unsetenv("INPUT_BUFFERED")
|
wrap.FlagBuffered = false
|
||||||
})
|
})
|
||||||
|
|
||||||
r := input.New(context.Background())
|
r := input.New(context.Background())
|
||||||
@@ -71,9 +73,9 @@ func TestNewRandomWeightFile(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
os.Setenv("INPUT_RANDOM_WEIGHT_FILE", p)
|
raw.FlagRawRandomWeightFile = p
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
os.Unsetenv("INPUT_RANDOM_WEIGHT_FILE")
|
raw.FlagRawRandomWeightFile = ""
|
||||||
})
|
})
|
||||||
|
|
||||||
r := input.New(context.Background())
|
r := input.New(context.Background())
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
package input
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func TestInput(t *testing.T) {
|
|
||||||
var _ Input = &Random{}
|
|
||||||
var _ Input = Keyboard{}
|
|
||||||
var _ Input = &Buffered{}
|
|
||||||
var _ Input = Remap{}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
package input
|
package raw
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"runtime"
|
"runtime"
|
||||||
@@ -43,16 +44,14 @@ func (kb Keyboard) Close() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (kb Keyboard) Read() []Button {
|
func (kb Keyboard) Read() []byte {
|
||||||
b := make([]byte, 5)
|
b := make([]byte, 5)
|
||||||
n, err := os.Stdin.Read(b)
|
n, err := os.Stdin.Read(b)
|
||||||
if err != nil && err != io.EOF {
|
if err != nil && err != io.EOF {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
if FlagDebug {
|
||||||
result := make([]Button, n)
|
log.Printf("raw.Keyboard.Read() %s", b[:n])
|
||||||
for i := range result {
|
|
||||||
result[i] = Button{Char: b[i], Down: true}
|
|
||||||
}
|
}
|
||||||
return result
|
return b[:n]
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package input
|
package raw
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -6,19 +6,15 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-yaml/yaml"
|
"github.com/go-yaml/yaml"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Button struct {
|
|
||||||
Char byte
|
|
||||||
Down bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type Random struct {
|
type Random struct {
|
||||||
generator func() byte
|
generator func() byte
|
||||||
down []Button
|
down []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRandom(generator func() byte) *Random {
|
func NewRandom(generator func() byte) *Random {
|
||||||
@@ -29,19 +25,8 @@ func NewRandom(generator func() byte) *Random {
|
|||||||
func (r *Random) Close() {
|
func (r *Random) Close() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Random) Read() []Button {
|
func (r *Random) Read() []byte {
|
||||||
if len(r.down) > 0 && rand.Int()%2 == 0 {
|
return []byte{r.generator()}
|
||||||
was := r.down
|
|
||||||
for i := range was {
|
|
||||||
was[i].Down = false
|
|
||||||
}
|
|
||||||
r.down = r.down[:0]
|
|
||||||
return was
|
|
||||||
} else {
|
|
||||||
c := Button{Char: r.generator(), Down: true}
|
|
||||||
r.down = append(r.down, c)
|
|
||||||
return []Button{c}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func randomCharFromRange(start, stop byte) func() byte {
|
func randomCharFromRange(start, stop byte) func() byte {
|
||||||
@@ -87,11 +72,15 @@ func randomCharFromWeights(m map[byte]int) func() byte {
|
|||||||
}
|
}
|
||||||
sum += v
|
sum += v
|
||||||
}
|
}
|
||||||
|
sort.Slice(result, func(i, j int) bool {
|
||||||
|
return result[i].i < result[j].i
|
||||||
|
})
|
||||||
if sum <= 0 {
|
if sum <= 0 {
|
||||||
panic("weights must total nonzero")
|
panic("weights must total nonzero")
|
||||||
}
|
}
|
||||||
return func() byte {
|
return func() byte {
|
||||||
n := rand.Int() % sum
|
r := rand.Int()
|
||||||
|
n := r % (sum + 1)
|
||||||
for _, v := range result {
|
for _, v := range result {
|
||||||
n -= v.i
|
n -= v.i
|
||||||
if n <= 0 {
|
if n <= 0 {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package input
|
package raw
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
func TestRandomCharFromWeights(t *testing.T) {
|
func TestRandomCharFromWeights(t *testing.T) {
|
||||||
weights := map[byte]int{
|
weights := map[byte]int{
|
||||||
'a': 1,
|
'a': 1,
|
||||||
'b': 99,
|
'b': 20,
|
||||||
}
|
}
|
||||||
foo := randomCharFromWeights(weights)
|
foo := randomCharFromWeights(weights)
|
||||||
for {
|
for {
|
||||||
32
src/device/input/raw/raw.go
Normal file
32
src/device/input/raw/raw.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package raw
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
FlagRawKeyboard = os.Getenv("RAW_KEYBOARD") == "true"
|
||||||
|
FlagRawUDP = os.Getenv("RAW_UDP")
|
||||||
|
FlagRawRandomWeightFile = os.Getenv("RAW_RANDOM_WEIGHT_FILE")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Raw interface {
|
||||||
|
Read() []byte
|
||||||
|
Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(ctx context.Context) Raw {
|
||||||
|
if FlagRawKeyboard {
|
||||||
|
return NewKeyboard()
|
||||||
|
}
|
||||||
|
if port, _ := strconv.Atoi(FlagRawUDP); port != 0 {
|
||||||
|
return NewUDP(ctx, port)
|
||||||
|
}
|
||||||
|
generator := randomCharFromRange('a', 'g')
|
||||||
|
if FlagRawRandomWeightFile != "" {
|
||||||
|
generator = randomCharFromWeightFile(FlagRawRandomWeightFile)
|
||||||
|
}
|
||||||
|
return NewRandom(generator)
|
||||||
|
}
|
||||||
9
src/device/input/raw/raw_test.go
Normal file
9
src/device/input/raw/raw_test.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package raw
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestRaw(t *testing.T) {
|
||||||
|
var _ Raw = &Random{}
|
||||||
|
var _ Raw = UDP{}
|
||||||
|
var _ Raw = Keyboard{}
|
||||||
|
}
|
||||||
63
src/device/input/raw/udp.go
Normal file
63
src/device/input/raw/udp.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package raw
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
FlagDebug = os.Getenv("DEBUG") == "true"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UDP struct {
|
||||||
|
conn net.PacketConn
|
||||||
|
c chan []byte
|
||||||
|
ctx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUDP(ctx context.Context, port int) UDP {
|
||||||
|
conn, err := net.ListenPacket("udp", ":"+strconv.Itoa(port))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
result := UDP{
|
||||||
|
conn: conn,
|
||||||
|
c: make(chan []byte, 8),
|
||||||
|
ctx: ctx,
|
||||||
|
}
|
||||||
|
go result.listen()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (udp UDP) listen() {
|
||||||
|
for udp.ctx.Err() == nil {
|
||||||
|
buff := make([]byte, 256)
|
||||||
|
n, _, err := udp.conn.ReadFrom(buff)
|
||||||
|
if err != nil && udp.ctx.Err() == nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if FlagDebug {
|
||||||
|
log.Printf("raw.UDP.Read() => %s", buff[:n])
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case udp.c <- buff[:n]:
|
||||||
|
case <-udp.ctx.Done():
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (udp UDP) Read() []byte {
|
||||||
|
select {
|
||||||
|
case v := <-udp.c:
|
||||||
|
return v
|
||||||
|
case <-udp.ctx.Done():
|
||||||
|
return []byte{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (udp UDP) Close() {
|
||||||
|
udp.conn.Close()
|
||||||
|
}
|
||||||
@@ -1,31 +1,42 @@
|
|||||||
package input
|
package wrap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"mayhem-party/src/device/input/button"
|
||||||
|
"mayhem-party/src/device/input/raw"
|
||||||
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
FlagBufferedStickyDuration = os.Getenv("WRAP_BUFFERED_STICKY_DURATION")
|
||||||
|
)
|
||||||
|
|
||||||
type Buffered struct {
|
type Buffered struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
can context.CancelFunc
|
can context.CancelFunc
|
||||||
lock sync.Mutex
|
lock sync.Mutex
|
||||||
keys map[byte]int64
|
keys map[byte]int64
|
||||||
input Input
|
input Wrap
|
||||||
listenInterval time.Duration
|
listenInterval time.Duration
|
||||||
expirationInterval time.Duration
|
expirationInterval time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBuffered(ctx context.Context, input Input) *Buffered {
|
func NewBuffered(ctx context.Context, input Wrap) *Buffered {
|
||||||
ctx, can := context.WithCancel(ctx)
|
ctx, can := context.WithCancel(ctx)
|
||||||
|
expirationInterval := time.Millisecond * 125
|
||||||
|
if d, err := time.ParseDuration(FlagBufferedStickyDuration); err == nil {
|
||||||
|
expirationInterval = d
|
||||||
|
}
|
||||||
result := &Buffered{
|
result := &Buffered{
|
||||||
input: input,
|
input: input,
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
can: can,
|
can: can,
|
||||||
lock: sync.Mutex{},
|
lock: sync.Mutex{},
|
||||||
keys: map[byte]int64{},
|
keys: map[byte]int64{},
|
||||||
listenInterval: time.Millisecond * 20,
|
listenInterval: time.Millisecond * 10,
|
||||||
expirationInterval: time.Millisecond * 100,
|
expirationInterval: expirationInterval,
|
||||||
}
|
}
|
||||||
go result.listen()
|
go result.listen()
|
||||||
return result
|
return result
|
||||||
@@ -39,6 +50,8 @@ func (b *Buffered) listen() {
|
|||||||
for i := range buttons {
|
for i := range buttons {
|
||||||
if buttons[i].Down {
|
if buttons[i].Down {
|
||||||
b.keys[buttons[i].Char] = time.Now().UnixNano()
|
b.keys[buttons[i].Char] = time.Now().UnixNano()
|
||||||
|
} else {
|
||||||
|
b.keys[buttons[i].Char] = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
b.lock.Unlock()
|
b.lock.Unlock()
|
||||||
@@ -49,12 +62,17 @@ func (b *Buffered) listen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *Buffered) CloseWrap() raw.Raw {
|
||||||
|
b.can()
|
||||||
|
return b.input.CloseWrap()
|
||||||
|
}
|
||||||
|
|
||||||
func (b *Buffered) Close() {
|
func (b *Buffered) Close() {
|
||||||
b.input.Close()
|
b.input.Close()
|
||||||
b.can()
|
b.can()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Buffered) Read() []Button {
|
func (b *Buffered) Read() []button.Button {
|
||||||
for b.ctx.Err() == nil {
|
for b.ctx.Err() == nil {
|
||||||
result := b.read()
|
result := b.read()
|
||||||
if len(result) > 0 {
|
if len(result) > 0 {
|
||||||
@@ -65,19 +83,19 @@ func (b *Buffered) Read() []Button {
|
|||||||
case <-time.After(b.listenInterval):
|
case <-time.After(b.listenInterval):
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return []Button{}
|
return []button.Button{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Buffered) read() []Button {
|
func (b *Buffered) read() []button.Button {
|
||||||
b.lock.Lock()
|
b.lock.Lock()
|
||||||
defer b.lock.Unlock()
|
defer b.lock.Unlock()
|
||||||
|
|
||||||
result := make([]Button, 0, len(b.keys))
|
result := make([]button.Button, 0, len(b.keys))
|
||||||
for k, v := range b.keys {
|
for k, v := range b.keys {
|
||||||
isFresh := v > 0
|
isFresh := v > 0
|
||||||
isStale := v < 0 && time.Since(time.Unix(0, -1*v)) > b.expirationInterval
|
isStale := v < 0 && time.Since(time.Unix(0, -1*v)) > b.expirationInterval
|
||||||
if isFresh || isStale {
|
if isFresh || isStale {
|
||||||
result = append(result, Button{Char: k, Down: isFresh})
|
result = append(result, button.Button{Char: k, Down: isFresh})
|
||||||
}
|
}
|
||||||
if isFresh {
|
if isFresh {
|
||||||
b.keys[k] = -1 * v
|
b.keys[k] = -1 * v
|
||||||
64
src/device/input/wrap/refresh.go
Normal file
64
src/device/input/wrap/refresh.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package wrap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"mayhem-party/src/device/input/button"
|
||||||
|
"mayhem-party/src/device/input/raw"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
chSigUsr1 = func() chan os.Signal {
|
||||||
|
c := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(c, syscall.SIGUSR1)
|
||||||
|
return c
|
||||||
|
}()
|
||||||
|
)
|
||||||
|
|
||||||
|
type Refresh struct {
|
||||||
|
can context.CancelFunc
|
||||||
|
input Wrap
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRefresh(ctx context.Context, newWrap func() Wrap) *Refresh {
|
||||||
|
return NewRefreshWith(ctx, newWrap, chSigUsr1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRefreshWith(ctx context.Context, newWrap func() Wrap, ch <-chan os.Signal) *Refresh {
|
||||||
|
ctx, can := context.WithCancel(ctx)
|
||||||
|
result := &Refresh{
|
||||||
|
can: can,
|
||||||
|
input: newWrap(),
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
defer log.Println("refreshing done")
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case sig := <-ch:
|
||||||
|
log.Println("refreshing for", sig)
|
||||||
|
result.input.CloseWrap()
|
||||||
|
result.input = newWrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Refresh) CloseWrap() raw.Raw {
|
||||||
|
r.can()
|
||||||
|
return r.input.CloseWrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Refresh) Read() []button.Button {
|
||||||
|
return r.input.Read()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Refresh) Close() {
|
||||||
|
r.can()
|
||||||
|
r.input.Close()
|
||||||
|
}
|
||||||
94
src/device/input/wrap/refresh_test.go
Normal file
94
src/device/input/wrap/refresh_test.go
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
package wrap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"mayhem-party/src/device/input/button"
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRefresh(t *testing.T) {
|
||||||
|
b := byte('a')
|
||||||
|
generator := func() Wrap {
|
||||||
|
b += byte(1)
|
||||||
|
return dummyParser{Char: b}
|
||||||
|
}
|
||||||
|
ch := make(chan os.Signal, 1)
|
||||||
|
defer close(ch)
|
||||||
|
refresh := NewRefreshWith(context.Background(), generator, ch)
|
||||||
|
defer refresh.Close()
|
||||||
|
|
||||||
|
assertIts := func(t *testing.T, b byte) {
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
some := false
|
||||||
|
for j, button := range refresh.Read() {
|
||||||
|
some = true
|
||||||
|
if button.Char != b {
|
||||||
|
t.Error(i, j, b, button)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !some {
|
||||||
|
t.Error("empty read")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("called once on init", func(t *testing.T) {
|
||||||
|
assertIts(t, byte('b'))
|
||||||
|
})
|
||||||
|
ch <- syscall.SIGUSR1
|
||||||
|
time.Sleep(time.Millisecond * 450)
|
||||||
|
t.Run("called once on signal", func(t *testing.T) {
|
||||||
|
assertIts(t, byte('c'))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRefreshDoesntCloseSources(t *testing.T) {
|
||||||
|
src := &telemetrySrc{}
|
||||||
|
newParsers := 0
|
||||||
|
newParser := func() Wrap {
|
||||||
|
newParsers += 1
|
||||||
|
return button.NewPlaintext(src)
|
||||||
|
}
|
||||||
|
ctx, can := context.WithCancel(context.Background())
|
||||||
|
defer can()
|
||||||
|
refresh := NewRefresh(ctx, newParser)
|
||||||
|
if newParsers != 1 {
|
||||||
|
t.Error(newParsers)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
refresh.Read()
|
||||||
|
}
|
||||||
|
if want := (telemetrySrc{reads: 5}); *src != want {
|
||||||
|
t.Errorf("%+v", *src)
|
||||||
|
} else if newParsers != 1 {
|
||||||
|
t.Error(newParsers)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
chSigUsr1 <- syscall.SIGINT
|
||||||
|
}
|
||||||
|
time.Sleep(time.Millisecond * 250)
|
||||||
|
if want := (telemetrySrc{reads: 5}); *src != want {
|
||||||
|
t.Errorf("want %+v, got %+v", want, *src)
|
||||||
|
} else if newParsers != 6 {
|
||||||
|
t.Error(newParsers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type telemetrySrc struct {
|
||||||
|
closes int
|
||||||
|
reads int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (src *telemetrySrc) Close() {
|
||||||
|
src.closes += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (src *telemetrySrc) Read() []byte {
|
||||||
|
src.reads += 1
|
||||||
|
return []byte("foo")
|
||||||
|
}
|
||||||
@@ -1,17 +1,19 @@
|
|||||||
package input
|
package wrap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"mayhem-party/src/device/input/button"
|
||||||
|
"mayhem-party/src/device/input/raw"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/go-yaml/yaml"
|
"github.com/go-yaml/yaml"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Remap struct {
|
type Remap struct {
|
||||||
input Input
|
input Wrap
|
||||||
m map[byte]byte
|
m map[byte]byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRemapFromFile(input Input, p string) Remap {
|
func NewRemapFromFile(input Wrap, p string) Remap {
|
||||||
b, err := os.ReadFile(p)
|
b, err := os.ReadFile(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@@ -30,18 +32,22 @@ func NewRemapFromFile(input Input, p string) Remap {
|
|||||||
return NewRemap(input, remap)
|
return NewRemap(input, remap)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRemap(input Input, m map[byte]byte) Remap {
|
func NewRemap(input Wrap, m map[byte]byte) Remap {
|
||||||
return Remap{
|
return Remap{
|
||||||
input: input,
|
input: input,
|
||||||
m: m,
|
m: m,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (re Remap) CloseWrap() raw.Raw {
|
||||||
|
return re.input.CloseWrap()
|
||||||
|
}
|
||||||
|
|
||||||
func (re Remap) Close() {
|
func (re Remap) Close() {
|
||||||
re.input.Close()
|
re.input.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (re Remap) Read() []Button {
|
func (re Remap) Read() []button.Button {
|
||||||
result := re.input.Read()
|
result := re.input.Read()
|
||||||
for i := range result {
|
for i := range result {
|
||||||
if v, ok := re.m[result[i].Char]; ok {
|
if v, ok := re.m[result[i].Char]; ok {
|
||||||
45
src/device/input/wrap/wrap.go
Normal file
45
src/device/input/wrap/wrap.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package wrap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"mayhem-party/src/device/input/button"
|
||||||
|
"mayhem-party/src/device/input/raw"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
FlagBuffered = os.Getenv("WRAP_BUFFERED") == "true"
|
||||||
|
FlagRemapFile = os.Getenv("WRAP_REMAP_FILE")
|
||||||
|
FlagRefreshOnSigUsr1 = os.Getenv("WRAP_REFRESH_ON_SIGUSR1") == "true"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Wrap interface {
|
||||||
|
Read() []button.Button
|
||||||
|
Close()
|
||||||
|
CloseWrap() raw.Raw
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(ctx context.Context, srcFunc func() button.Parser) Wrap {
|
||||||
|
maker := func() Wrap {
|
||||||
|
return srcFunc()
|
||||||
|
}
|
||||||
|
if FlagBuffered {
|
||||||
|
oldMaker := maker
|
||||||
|
maker = func() Wrap {
|
||||||
|
return NewBuffered(ctx, oldMaker())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if FlagRemapFile != "" {
|
||||||
|
oldMaker := maker
|
||||||
|
maker = func() Wrap {
|
||||||
|
return NewRemapFromFile(oldMaker(), FlagRemapFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if FlagRefreshOnSigUsr1 {
|
||||||
|
oldMaker := maker
|
||||||
|
maker = func() Wrap {
|
||||||
|
return NewRefresh(ctx, oldMaker)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return maker()
|
||||||
|
}
|
||||||
22
src/device/input/wrap/wrap_test.go
Normal file
22
src/device/input/wrap/wrap_test.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package wrap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"mayhem-party/src/device/input/button"
|
||||||
|
"mayhem-party/src/device/input/raw"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWrap(t *testing.T) {
|
||||||
|
var _ Wrap = dummyParser{}
|
||||||
|
var _ Wrap = &Refresh{}
|
||||||
|
var _ Wrap = &Buffered{}
|
||||||
|
var _ Wrap = &Remap{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type dummyParser button.Button
|
||||||
|
|
||||||
|
func (d dummyParser) CloseWrap() raw.Raw { return nil }
|
||||||
|
func (d dummyParser) Close() {}
|
||||||
|
func (d dummyParser) Read() []button.Button {
|
||||||
|
return []button.Button{button.Button(d)}
|
||||||
|
}
|
||||||
@@ -28,6 +28,31 @@ var (
|
|||||||
'x': X,
|
'x': X,
|
||||||
'y': Y,
|
'y': Y,
|
||||||
'z': Z,
|
'z': Z,
|
||||||
|
'1': N1,
|
||||||
|
'2': N2,
|
||||||
|
'3': N3,
|
||||||
|
'4': N4,
|
||||||
|
'5': N5,
|
||||||
|
'6': N6,
|
||||||
|
'7': N7,
|
||||||
|
'8': N8,
|
||||||
|
'9': N9,
|
||||||
|
'0': N0,
|
||||||
|
'!': F1,
|
||||||
|
'@': F2,
|
||||||
|
'#': F3,
|
||||||
|
'$': F4,
|
||||||
|
'%': F5,
|
||||||
|
'^': F6,
|
||||||
|
'&': F7,
|
||||||
|
'*': F8,
|
||||||
|
'(': F9,
|
||||||
|
')': F10,
|
||||||
|
',': PComma,
|
||||||
|
'/': PFSlash,
|
||||||
|
';': PSemicolon,
|
||||||
|
'-': PMinus,
|
||||||
|
'=': PEqual,
|
||||||
}
|
}
|
||||||
keyToChar = func() map[Key]byte {
|
keyToChar = func() map[Key]byte {
|
||||||
result := map[Key]byte{}
|
result := map[Key]byte{}
|
||||||
@@ -47,7 +72,7 @@ func ToChar(k Key) byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func FromChar(b byte) Key {
|
func FromChar(b byte) Key {
|
||||||
if b < 'a' {
|
if 'A' <= b && b <= 'Z' {
|
||||||
b += 'a' - 'A'
|
b += 'a' - 'A'
|
||||||
}
|
}
|
||||||
v, ok := charToKey[b]
|
v, ok := charToKey[b]
|
||||||
|
|||||||
@@ -3,13 +3,16 @@ package key
|
|||||||
import "testing"
|
import "testing"
|
||||||
|
|
||||||
func TestFromChar(t *testing.T) {
|
func TestFromChar(t *testing.T) {
|
||||||
|
if got := FromChar('1'); got != N1 {
|
||||||
|
t.Error(got)
|
||||||
|
}
|
||||||
if got := FromChar('a'); got != A {
|
if got := FromChar('a'); got != A {
|
||||||
t.Error(got)
|
t.Error(got)
|
||||||
}
|
}
|
||||||
if got := FromChar('A'); got != A {
|
if got := FromChar('A'); got != A {
|
||||||
t.Error(got)
|
t.Error(got)
|
||||||
}
|
}
|
||||||
if got := FromChar('!'); got != Undef {
|
if got := FromChar(byte(0)); got != Undef {
|
||||||
t.Error(got)
|
t.Error(got)
|
||||||
}
|
}
|
||||||
if got := ToChar(A); got != 'a' {
|
if got := ToChar(A); got != 'a' {
|
||||||
|
|||||||
@@ -32,4 +32,29 @@ const (
|
|||||||
X = Key(keybd_event.VK_X)
|
X = Key(keybd_event.VK_X)
|
||||||
Y = Key(keybd_event.VK_Y)
|
Y = Key(keybd_event.VK_Y)
|
||||||
Z = Key(keybd_event.VK_Z)
|
Z = Key(keybd_event.VK_Z)
|
||||||
|
N1 = Key(keybd_event.VK_1)
|
||||||
|
N2 = Key(keybd_event.VK_2)
|
||||||
|
N3 = Key(keybd_event.VK_3)
|
||||||
|
N4 = Key(keybd_event.VK_4)
|
||||||
|
N5 = Key(keybd_event.VK_5)
|
||||||
|
N6 = Key(keybd_event.VK_6)
|
||||||
|
N7 = Key(keybd_event.VK_7)
|
||||||
|
N8 = Key(keybd_event.VK_8)
|
||||||
|
N9 = Key(keybd_event.VK_9)
|
||||||
|
N0 = Key(keybd_event.VK_0)
|
||||||
|
F1 = Key(keybd_event.VK_F1)
|
||||||
|
F2 = Key(keybd_event.VK_F2)
|
||||||
|
F3 = Key(keybd_event.VK_F3)
|
||||||
|
F4 = Key(keybd_event.VK_F4)
|
||||||
|
F5 = Key(keybd_event.VK_F5)
|
||||||
|
F6 = Key(keybd_event.VK_F6)
|
||||||
|
F7 = Key(keybd_event.VK_F7)
|
||||||
|
F8 = Key(keybd_event.VK_F8)
|
||||||
|
F9 = Key(keybd_event.VK_F9)
|
||||||
|
F10 = Key(keybd_event.VK_F10)
|
||||||
|
PComma = Key(keybd_event.VK_COMMA)
|
||||||
|
PFSlash = Key(keybd_event.VK_BACKSLASH)
|
||||||
|
PSemicolon = Key(keybd_event.VK_SEMICOLON)
|
||||||
|
PMinus = Key(keybd_event.VK_MINUS)
|
||||||
|
PEqual = Key(keybd_event.VK_EQUAL)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package src
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"log"
|
||||||
"mayhem-party/src/device/input"
|
"mayhem-party/src/device/input"
|
||||||
"mayhem-party/src/device/output"
|
"mayhem-party/src/device/output"
|
||||||
"mayhem-party/src/device/output/key"
|
"mayhem-party/src/device/output/key"
|
||||||
@@ -16,7 +17,7 @@ func Main(ctx context.Context) error {
|
|||||||
defer reader.Close()
|
defer reader.Close()
|
||||||
|
|
||||||
interval := time.Millisecond * 50
|
interval := time.Millisecond * 50
|
||||||
if intervalS, ok := os.LookupEnv("MAIN_INTERVAL_DURATION"); !ok {
|
if intervalS := os.Getenv("MAIN_INTERVAL_DURATION"); intervalS != "" {
|
||||||
} else if v, err := time.ParseDuration(intervalS); err != nil {
|
} else if v, err := time.ParseDuration(intervalS); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
} else {
|
} else {
|
||||||
@@ -37,6 +38,9 @@ func Main(ctx context.Context) error {
|
|||||||
state := map[key.Key]bool{}
|
state := map[key.Key]bool{}
|
||||||
for block() {
|
for block() {
|
||||||
delta := reader.Read()
|
delta := reader.Read()
|
||||||
|
if os.Getenv("DEBUG") == "true" {
|
||||||
|
log.Printf("src.Main.reader.Read(): %+v", delta)
|
||||||
|
}
|
||||||
for _, button := range delta {
|
for _, button := range delta {
|
||||||
state[key.FromChar(button.Char)] = button.Down
|
state[key.FromChar(button.Char)] = button.Down
|
||||||
}
|
}
|
||||||
|
|||||||
6
testdata/INPUT_REMAP_FILE.yaml
vendored
6
testdata/INPUT_REMAP_FILE.yaml
vendored
@@ -2,3 +2,9 @@ w: i
|
|||||||
a: j
|
a: j
|
||||||
s: k
|
s: k
|
||||||
d: l
|
d: l
|
||||||
|
q: u
|
||||||
|
e: o
|
||||||
|
1: 0
|
||||||
|
2: 9
|
||||||
|
3: 8
|
||||||
|
4: 7
|
||||||
|
|||||||
4
testdata/keyboard_input.env
vendored
4
testdata/keyboard_input.env
vendored
@@ -1,2 +1,2 @@
|
|||||||
export INPUT_BUFFERED=true
|
export WRAP_BUFFERED=true
|
||||||
export INPUT_KEYBOARD=true
|
export RAW_KEYBOARD=true
|
||||||
|
|||||||
4
testdata/remapped_wasd.env
vendored
4
testdata/remapped_wasd.env
vendored
@@ -1,2 +1,2 @@
|
|||||||
export INPUT_RANDOM_WEIGHT_FILE=testdata/INPUT_RANDOM_WEIGHT_FILE.yaml
|
export RAW_RANDOM_WEIGHT_FILE=testdata/INPUT_RANDOM_WEIGHT_FILE.yaml
|
||||||
export INPUT_REMAP_FILE=testdata/INPUT_REMAP_FILE.yaml
|
export WRAP_REMAP_FILE=testdata/INPUT_REMAP_FILE.yaml
|
||||||
|
|||||||
2
testdata/supermarioplay.com.env
vendored
2
testdata/supermarioplay.com.env
vendored
@@ -1,3 +1,3 @@
|
|||||||
export OUTPUT_KEYBOARD=true
|
export OUTPUT_KEYBOARD=true
|
||||||
export MAIN_INTERVAL_DURATION=250ms
|
export MAIN_INTERVAL_DURATION=250ms
|
||||||
export INPUT_RANDOM_WEIGHT_FILE=testdata/INPUT_RANDOM_WEIGHT_FILE.yaml
|
export RAW_RANDOM_WEIGHT_FILE=testdata/INPUT_RANDOM_WEIGHT_FILE.yaml
|
||||||
|
|||||||
56
todo.yaml
Executable file
56
todo.yaml
Executable file
@@ -0,0 +1,56 @@
|
|||||||
|
todo:
|
||||||
|
- v01cfg includes messages to send per client and exposes http server for it
|
||||||
|
- send clients messages to display
|
||||||
|
- input.MayhemParty as a logical wrapper from mod10 but then gotta translate back
|
||||||
|
to char for keyboard things somewhere; space delimited?
|
||||||
|
- todo: rusty configs have "name" for each client
|
||||||
|
details: |
|
||||||
|
'if name == server_broadcasted_name { debug_print_in_gui(server_broadcasted_message) }'
|
||||||
|
- todo: rotation triggers
|
||||||
|
subtasks:
|
||||||
|
- minigame end
|
||||||
|
- random word from cur wikipedia page
|
||||||
|
- each person has their own hotword
|
||||||
|
- only spectators have hotwords and must get a player to speak it
|
||||||
|
- tribunal to vote who said it
|
||||||
|
- we have 7 players oooooof
|
||||||
|
scheduled: []
|
||||||
|
done:
|
||||||
|
- todo: sticky keyboard input mode for enable/disable explicitly
|
||||||
|
ts: Thu Mar 23 20:55:52 MDT 2023
|
||||||
|
- todo: case-sensitive
|
||||||
|
ts: Fri Mar 24 13:39:26 MDT 2023
|
||||||
|
- todo: rusty configs have "name" for each client so "if name == server_broadcasted_name
|
||||||
|
{ debug_print_in_gui(server_broadcasted_message) }
|
||||||
|
ts: Fri Mar 24 16:40:09 MDT 2023
|
||||||
|
- todo: change from 'a','b','c' from rust to just 10,11,12 so playerName is known
|
||||||
|
implicitly but then gotta translate back to char for keyboard things somewhere;
|
||||||
|
space delimited?
|
||||||
|
ts: Fri Mar 24 17:00:55 MDT 2023
|
||||||
|
- todo: '"Button" to interface or strings'
|
||||||
|
ts: Fri Mar 24 17:01:01 MDT 2023
|
||||||
|
- todo: input.UDP as a raw provider
|
||||||
|
ts: Fri Mar 24 19:58:59 MDT 2023
|
||||||
|
- todo: input.MayhemParty as a logical wrapper
|
||||||
|
ts: Fri Mar 24 19:58:59 MDT 2023
|
||||||
|
- todo: change from 'a','b','c' from rust to just 11,21,31,41 so playerName is known
|
||||||
|
implicitly from %10 but then gotta translate back to char for keyboard things
|
||||||
|
somewhere; space delimited?
|
||||||
|
ts: Fri Mar 24 19:58:59 MDT 2023
|
||||||
|
- todo: input."Button" to interface or strings
|
||||||
|
ts: Fri Mar 24 21:16:39 MDT 2023
|
||||||
|
- todo: input.MayhemParty as a logical wrapper from %10 but then gotta translate back
|
||||||
|
to char for keyboard things somewhere; space delimited?
|
||||||
|
ts: Fri Mar 24 21:16:39 MDT 2023
|
||||||
|
- 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
|
||||||
Reference in New Issue
Block a user