44 Commits

Author SHA1 Message Date
Bel LaPointe
7f2e25458e backwards bool in main 2023-03-28 11:19:53 -06:00
bel
95810d3735 todo 2023-03-27 21:45:46 -06:00
bel
df65b1ed07 mtodo 2023-03-27 21:43:44 -06:00
bel
a36f07d0c1 accesslog 2023-03-27 21:39:07 -06:00
bel
09d9911293 log access 2023-03-27 21:21:03 -06:00
bel
60d391b7a4 flush config to disk every http 2023-03-27 21:05:38 -06:00
Bel LaPointe
60ed9c1269 todo 2023-03-27 17:55:42 -06:00
Bel LaPointe
13cf35bdd8 after voting clear votes 2023-03-27 17:55:19 -06:00
Bel LaPointe
79c90ac40c todo 2023-03-27 11:04:58 -06:00
Bel LaPointe
dd1b053efa telemetry in bg thread and lossy so no block keyboad though i still find it pretty sus 2023-03-27 11:03:56 -06:00
Bel LaPointe
f619fe9e1b some todo 2023-03-27 11:01:36 -06:00
Bel LaPointe
781bfb8a67 todo 2023-03-27 11:01:14 -06:00
Bel LaPointe
9ece270a13 rename State.GM THINGS to State.GM.Things 2023-03-27 11:00:40 -06:00
Bel LaPointe
f647a03467 shuffle v01.config to split out state and meta 2023-03-27 10:54:02 -06:00
Bel LaPointe
ecb719a97a test last GM RPC 2023-03-27 10:13:59 -06:00
Bel LaPointe
1738ce7d19 test a vote for someone saying themself 2023-03-27 10:11:17 -06:00
Bel LaPointe
354d07d6bf test serveGMVote 2023-03-27 09:45:57 -06:00
Bel LaPointe
39ab01525f shuffle 2023-03-27 09:35:11 -06:00
Bel LaPointe
ceeeb8fe4b test fillNonPlayerAliases 2023-03-27 09:33:25 -06:00
Bel LaPointe
d029d82366 impl,test fill non player aliases 2023-03-27 09:30:29 -06:00
Bel LaPointe
0435f7b3e8 test shuffle 2023-03-27 09:24:37 -06:00
Bel LaPointe
5ef0dde50d test no players/users and shuffle 2023-03-27 09:21:16 -06:00
Bel LaPointe
bc3f0271e7 test happy 1:1 shuffle 2023-03-27 08:26:55 -06:00
Bel LaPointe
e2d7c4a908 test unhappy swap 2023-03-27 08:23:37 -06:00
Bel LaPointe
c744704b63 test happy swap 2023-03-27 08:21:03 -06:00
Bel LaPointe
213fd555e4 test happy shuffle 2023-03-27 08:20:50 -06:00
Bel LaPointe
dd41028aab test /gm/rpc/broadcastSomeoneSaidAlias 2023-03-27 07:26:40 -06:00
Bel LaPointe
8ff1c2fab4 test nonzero player on status 2023-03-27 06:58:59 -06:00
Bel LaPointe
1f7b222b9c test nonzero status 2023-03-27 06:58:21 -06:00
Bel LaPointe
1842023224 struct 2023-03-27 06:53:46 -06:00
Bel LaPointe
45b873f462 test status 2023-03-27 06:51:15 -06:00
Bel LaPointe
88a78c489f test status 2023-03-27 06:50:31 -06:00
Bel LaPointe
8314bdc457 impl public status endpoint 2023-03-27 06:24:41 -06:00
Bel LaPointe
a6a9b177e9 whoops dont sleep use some ctx 2023-03-27 06:19:39 -06:00
Bel LaPointe
f649862dd4 todo 2023-03-27 06:18:52 -06:00
Bel LaPointe
85804d6f84 lock on every http req BUT LOCKS ON TELEMETRY SO BEWARE 2023-03-27 06:18:32 -06:00
Bel LaPointe
f14871218d stub 2023-03-27 06:14:52 -06:00
Bel LaPointe
26f052d981 workin on gm rpc 2023-03-27 06:13:26 -06:00
bel
2d4cb394de wip 2023-03-26 23:35:10 -06:00
bel
163bf2b405 serve GET /config 2023-03-26 22:39:47 -06:00
bel
e968ce17ce stash last lag and ts for each user 2023-03-26 22:19:07 -06:00
bel
f07e67b3fd export host.d to -venue repo 2023-03-26 16:24:08 -06:00
bel
fbf4849517 todo 2023-03-26 14:54:27 -06:00
bel
804ce02407 todo 2023-03-26 14:28:48 -06:00
31 changed files with 909 additions and 642 deletions

View File

@@ -1,62 +0,0 @@
# Hosting a Mayhem Party
## Requirements
1. [`rusty-pipe`](https://gogs.inhome.blapointe.com/bel/rusty-pipe)
1. [`mayhem-party`](https://gogs.inhome.blapointe.com/bel/mayhem-party)
1. [`stt`](https://gogs.inhome.blapointe.com/bel/stt)
# Clients
## Distribute `rusty-pipe`
```bash
# https://www.reddit.com/r/rust/comments/5k8uab/crosscompiling_from_ubuntu_to_windows_with_rustup/
(
echo '[target.x86_64-pc-windows-gnu]'
echo 'linker = "x86_64-w64-mingw32-gcc"'
echo 'ar = "x86_64-w64-mingw32-gcc-ar"'
) >> $HOME/.cargo/config
sudo apt install mingw-w64
rustup target add x86_64-pc-windows-gnu
echo windows
cargo build --release --target x86_64-pc-windows-gnu && ls target/x86_64-pc-windows-gnu/release/rusty-pipe.exe
echo local
cargo install --path ./
```
Each client needs 1 executable and 1 env file with a unique set of buttons
> 10 buttons per player
> `go doc key Undef | grep Key | grep -v Undef | wc -l` total (51)
The server cannot be a client because math. Maybe a VM on the client as a server would work tho.
See `./config.d/rusty-pipe.d`
# Server
## TTS
`cd /home/breel/Go/src/gogs.inhome.blapointe.com/tts/larynx.d; bash run.sh`
## STT
`cd /home/breel/Go/src/gogs.inhome.blapointe.com/stt.d/whisper-2023; HOTWORDS=/home/breel/Go/src/gogs.inhome.blapointe.com/mayhem-party.d/host.d/config.d/stt.d/hotwords.txt MIC_TIMEOUT=2 URL=http://localhost:17071/config HEADERS=say='Eye herd {{hotword}}' BODY='[{"op":"replace", "path":"/Quiet", "value":true}]' python3 ./hotwords.py'
## `mayhem-party`
### Configs
`cd ./config.d/mayhem-party.d/remap.d; bash ./rotate.sh`
> rotate anytime via stdin or `pkill -SIGUSR1 -f rotate.sh`
### Binary
`bash -c 'true; source ./config.d/mayhem-party.d/env.env; mayhem-party'`
## Game Playing
Foreground

View File

@@ -1,11 +0,0 @@
export DEBUG=true
export MAIN_INTERVAL_DURATION=5ms
export RAW_UDP=17070
export PARSE_V01=true
export V01_CONFIG=./config.d/mayhem-party.d/v01.yaml
export WRAP_REFRESH_ON_SIGUSR1=true
export OUTPUT_KEYBOARD=false

View File

@@ -1,31 +0,0 @@
import yaml
from sys import argv
from sys import stderr
def log(*args):
print(*args, file=stderr)
def main(args):
players = []
for i in range(1, 5+1):
with open(f"../../rusty-pipe.d/{i}.yaml", "r") as f:
players.append(yaml.safe_load(f)["streams"]["input"]["engine"]["gui"]["buttons"])
log(players[-1])
for arg in args:
offset = int(arg)
for i in range(len(players)):
j = (i+offset)%len(players)
if i == j:
break
keys = players[i]
values = players[j]
log(f"player {i+1} plays as player {j+1}")
print(f"# player {i+1} controls {j+1}")
for k in keys:
key = keys[k]
value = values[k]
print(f"'{key}': '{value}'")
if __name__ == "__main__":
from sys import argv
main(argv[1:])

View File

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

View File

@@ -1,55 +0,0 @@
# player 1 controls 2
'1': 'q'
'2': 'w'
'3': 'e'
'4': 'r'
'5': 't'
'6': 'y'
'7': 'u'
'8': 'i'
'9': 'o'
'0': 'p'
# player 2 controls 3
'q': 'a'
'w': 's'
'e': 'd'
'r': 'f'
't': 'g'
'y': 'h'
'u': 'j'
'i': 'k'
'o': 'l'
'p': ';'
# player 3 controls 4
'a': 'z'
's': 'x'
'd': 'c'
'f': 'v'
'g': 'b'
'h': 'n'
'j': 'm'
'k': ','
'l': '-'
';': '/'
# player 4 controls 5
'z': '!'
'x': '@'
'c': '#'
'v': '$'
'b': '%'
'n': '^'
'm': '&'
',': '*'
'-': '('
'/': ')'
# player 5 controls 1
'!': '1'
'@': '2'
'#': '3'
'$': '4'
'%': '5'
'^': '6'
'&': '7'
'*': '8'
'(': '9'
')': '0'

View File

@@ -1,55 +0,0 @@
# player 1 controls 3
'1': 'a'
'2': 's'
'3': 'd'
'4': 'f'
'5': 'g'
'6': 'h'
'7': 'j'
'8': 'k'
'9': 'l'
'0': ';'
# player 2 controls 4
'q': 'z'
'w': 'x'
'e': 'c'
'r': 'v'
't': 'b'
'y': 'n'
'u': 'm'
'i': ','
'o': '-'
'p': '/'
# player 3 controls 5
'a': '!'
's': '@'
'd': '#'
'f': '$'
'g': '%'
'h': '^'
'j': '&'
'k': '*'
'l': '('
';': ')'
# player 4 controls 1
'z': '1'
'x': '2'
'c': '3'
'v': '4'
'b': '5'
'n': '6'
'm': '7'
',': '8'
'-': '9'
'/': '0'
# player 5 controls 2
'!': 'q'
'@': 'w'
'#': 'e'
'$': 'r'
'%': 't'
'^': 'y'
'&': 'u'
'*': 'i'
'(': 'o'
')': 'p'

View File

@@ -1,55 +0,0 @@
# player 1 controls 4
'1': 'z'
'2': 'x'
'3': 'c'
'4': 'v'
'5': 'b'
'6': 'n'
'7': 'm'
'8': ','
'9': '-'
'0': '/'
# player 2 controls 5
'q': '!'
'w': '@'
'e': '#'
'r': '$'
't': '%'
'y': '^'
'u': '&'
'i': '*'
'o': '('
'p': ')'
# player 3 controls 1
'a': '1'
's': '2'
'd': '3'
'f': '4'
'g': '5'
'h': '6'
'j': '7'
'k': '8'
'l': '9'
';': '0'
# player 4 controls 2
'z': 'q'
'x': 'w'
'c': 'e'
'v': 'r'
'b': 't'
'n': 'y'
'm': 'u'
',': 'i'
'-': 'o'
'/': 'p'
# player 5 controls 3
'!': 'a'
'@': 's'
'#': 'd'
'$': 'f'
'%': 'g'
'^': 'h'
'&': 'j'
'*': 'k'
'(': 'l'
')': ';'

View File

@@ -1,55 +0,0 @@
# player 1 controls 5
'1': '!'
'2': '@'
'3': '#'
'4': '$'
'5': '%'
'6': '^'
'7': '&'
'8': '*'
'9': '('
'0': ')'
# player 2 controls 1
'q': '1'
'w': '2'
'e': '3'
'r': '4'
't': '5'
'y': '6'
'u': '7'
'i': '8'
'o': '9'
'p': '0'
# player 3 controls 2
'a': 'q'
's': 'w'
'd': 'e'
'f': 'r'
'g': 't'
'h': 'y'
'j': 'u'
'k': 'i'
'l': 'o'
';': 'p'
# player 4 controls 3
'z': 'a'
'x': 's'
'c': 'd'
'v': 'f'
'b': 'g'
'n': 'h'
'm': 'j'
',': 'k'
'-': 'l'
'/': ';'
# player 5 controls 4
'!': 'z'
'@': 'x'
'#': 'c'
'$': 'v'
'%': 'b'
'^': 'n'
'&': 'm'
'*': ','
'(': '-'
')': '/'

View File

@@ -1,21 +0,0 @@
#! /bin/bash
mayhem_party_rotate() {
local currently=$(realpath live.yaml | grep -o '[0-9].yaml$' | grep -o '^[0-9]')
local next=${NEXT:-$((RANDOM%5))}
while [ -z "$NEXT" ] && [ "$next" == "$currently" ]; do
next=$((RANDOM%5))
done
rm live.yaml
ln -s players_offset_$next.yaml live.yaml
pkill -SIGUSR1 -f mayhem-party
}
trap mayhem_party_rotate SIGUSR1
if [ "$0" == "$BASH_SOURCE" ]; then
NEXT=0 mayhem_party_rotate
while read -p "$(date) > [press enter to rotate]"; do
mayhem_party_rotate
done
fi

View File

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

View File

@@ -1,30 +0,0 @@
streams:
input:
debug: false
engine:
name: gui
gui:
user: bel
feedback:
url: http://mayhem-party.home.blapointe.com:17071?user=bel
press: {prefix: "", suffix: ""}
release: {prefix: "", suffix: ""}
format: '{"T":{{ms}},"U":"{{user}}","Y":"{{pressed}}","N":"{{released}}"}'
buttons:
up: 'w'
down: 's'
left: 'a'
right: 'd'
l: 'e'
r: 'q'
a: '1'
b: '2'
x: '3'
y: '4'
output:
debug: false
engine:
name: udp
udp:
host: mayhem-party.home.blapointe.com
port: 17070

View File

@@ -1,30 +0,0 @@
streams:
input:
debug: false
engine:
name: gui
gui:
user: zach
feedback:
url: http://mayhem-party.home.blapointe.com:17071?user=zach
press: {prefix: "", suffix: ""}
release: {prefix: "", suffix: ""}
format: '{"T":{{ms}},"U":"{{user}}","Y":"{{pressed}}","N":"{{released}}"}'
buttons:
up: 'w'
down: 's'
left: 'a'
right: 'd'
l: 'e'
r: 'q'
a: '1'
b: '2'
x: '3'
y: '4'
output:
debug: false
engine:
name: udp
udp:
host: mayhem-party.home.blapointe.com
port: 17070

View File

@@ -1,30 +0,0 @@
streams:
input:
debug: false
engine:
name: gui
gui:
user: chase
feedback:
url: http://mayhem-party.home.blapointe.com:17071?user=chase
press: {prefix: "", suffix: ""}
release: {prefix: "", suffix: ""}
format: '{"T":{{ms}},"U":"{{user}}","Y":"{{pressed}}","N":"{{released}}"}'
buttons:
up: 'w'
down: 's'
left: 'a'
right: 'd'
l: 'e'
r: 'q'
a: '1'
b: '2'
x: '3'
y: '4'
output:
debug: false
engine:
name: udp
udp:
host: mayhem-party.home.blapointe.com
port: 17070

View File

@@ -1,30 +0,0 @@
streams:
input:
debug: false
engine:
name: gui
gui:
user: mason
feedback:
url: http://mayhem-party.home.blapointe.com:17071?user=mason
press: {prefix: "", suffix: ""}
release: {prefix: "", suffix: ""}
format: '{"T":{{ms}},"U":"{{user}}","Y":"{{pressed}}","N":"{{released}}"}'
buttons:
up: 'w'
down: 's'
left: 'a'
right: 'd'
l: 'e'
r: 'q'
a: '1'
b: '2'
x: '3'
y: '4'
output:
debug: false
engine:
name: udp
udp:
host: mayhem-party.home.blapointe.com
port: 17070

View File

@@ -1,30 +0,0 @@
streams:
input:
debug: false
engine:
name: gui
gui:
user: nat
feedback:
url: http://mayhem-party.home.blapointe.com:17071?user=nat
press: {prefix: "", suffix: ""}
release: {prefix: "", suffix: ""}
format: '{"T":{{ms}},"U":"{{user}}","Y":"{{pressed}}","N":"{{released}}"}'
buttons:
up: 'w'
down: 's'
left: 'a'
right: 'd'
l: 'e'
r: 'q'
a: '1'
b: '2'
x: '3'
y: '4'
output:
debug: false
engine:
name: udp
udp:
host: mayhem-party.home.blapointe.com
port: 17070

View File

@@ -1,30 +0,0 @@
streams:
input:
debug: false
engine:
name: gui
gui:
user: roxy
feedback:
url: http://mayhem-party.home.blapointe.com:17071?user=roxy
press: {prefix: "", suffix: ""}
release: {prefix: "", suffix: ""}
format: '{"T":{{ms}},"U":"{{user}}","Y":"{{pressed}}","N":"{{released}}"}'
buttons:
up: 'w'
down: 's'
left: 'a'
right: 'd'
l: 'e'
r: 'q'
a: '1'
b: '2'
x: '3'
y: '4'
output:
debug: false
engine:
name: udp
udp:
host: mayhem-party.home.blapointe.com
port: 17070

View File

@@ -1,30 +0,0 @@
streams:
input:
debug: false
engine:
name: gui
gui:
user: bill
feedback:
url: http://mayhem-party.home.blapointe.com:17071?user=bill
press: {prefix: "", suffix: ""}
release: {prefix: "", suffix: ""}
format: '{"T":{{ms}},"U":"{{user}}","Y":"{{pressed}}","N":"{{released}}"}'
buttons:
up: 'w'
down: 's'
left: 'a'
right: 'd'
l: 'e'
r: 'q'
a: '1'
b: '2'
x: '3'
y: '4'
output:
debug: false
engine:
name: udp
udp:
host: mayhem-party.home.blapointe.com
port: 17070

View File

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

View File

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

View File

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

View File

@@ -2,12 +2,14 @@ package v01
import (
"encoding/json"
"sync"
patch "github.com/evanphx/json-patch/v5"
)
type (
config struct {
lock *sync.Mutex
Feedback configFeedback
Users map[string]configUser
Players []configPlayer
@@ -20,8 +22,25 @@ type (
}
configUser struct {
Meta configUserMeta
State configUserState
}
configUserMeta struct {
LastTSMS int64
LastLag int64
}
configUserState struct {
Player int
Message string
GM configUserStateGM
}
configUserStateGM struct {
Alias string
LastAlias string
Vote string
}
configPlayer struct {
@@ -30,6 +49,8 @@ type (
)
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)

View File

@@ -2,6 +2,7 @@ package v01
import (
"fmt"
"sync"
"testing"
)
@@ -70,7 +71,10 @@ func TestConfigPatch(t *testing.T) {
for name, d := range cases {
c := d
t.Run(name, func(t *testing.T) {
c.cfg.lock = &sync.Mutex{}
got := c.cfg.WithPatch(c.patch)
got.lock = nil
c.want.lock = nil
if fmt.Sprintf("%+v", got) != fmt.Sprintf("%+v", c.want) {
t.Errorf("(%+v).Patch(%+v) want %+v, got %+v", c.cfg, c.patch, c.want, got)
}

View File

@@ -2,13 +2,18 @@ package v01
import (
"encoding/json"
"errors"
"fmt"
"io"
"log"
"math/rand"
"mayhem-party/src/device/input/wrap"
"net/http"
"os"
"strings"
"sync"
"syscall"
"time"
"gopkg.in/yaml.v2"
)
@@ -21,18 +26,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() {
@@ -50,17 +60,19 @@ 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 "/config":
v01.patchConfig(w, r)
case "broadcast":
v01.servePutBroadcast(w, r)
case "config":
v01.serveConfig(w, r)
case "gm":
v01.serveGM(w, r)
}
}
@@ -69,17 +81,34 @@ func (v01 *V01) getUserFeedback(w http.ResponseWriter, r *http.Request) {
if !ok {
user = v01.cfg.Users["broadcast"]
}
w.Write([]byte(user.Message))
w.Write([]byte(user.State.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) patchConfig(w http.ResponseWriter, r *http.Request) {
func (v01 *V01) servePutBroadcastValue(v string) {
u := v01.cfg.Users["broadcast"]
u.State.Message = v
v01.cfg.Users["broadcast"] = u
}
func (v01 *V01) serveConfig(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
v01.serveGetConfig(w, r)
} else {
v01.servePatchConfig(w, r)
}
}
func (v01 *V01) serveGetConfig(w http.ResponseWriter, r *http.Request) {
b, _ := json.Marshal(v01.cfg)
w.Write(b)
}
func (v01 *V01) servePatchConfig(w http.ResponseWriter, r *http.Request) {
b, _ := io.ReadAll(r.Body)
var v []interface{}
if err := json.Unmarshal(b, &v); err != nil {
@@ -87,19 +116,28 @@ func (v01 *V01) patchConfig(w http.ResponseWriter, r *http.Request) {
return
}
v01.cfg = v01.cfg.WithPatch(v)
if b, err := yaml.Marshal(v01.cfg); err == nil && FlagParseV01Config != "" {
if err := os.WriteFile(FlagParseV01Config, b, os.ModePerm); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
if err := v01.stashConfig(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func (v01 *V01) globalQueries(r *http.Request) {
v01.globalQuerySay(r)
v01.globalQueryRefresh(r)
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) globalQuerySay(r *http.Request) {
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")
@@ -110,7 +148,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
}
@@ -119,3 +157,198 @@ 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, r)
case "/gm/rpc/broadcastSomeoneSaidAlias":
v01.serveGMSomeoneSaidAlias(w, r)
case "/gm/rpc/fillNonPlayerAliases":
v01.serveGMFillNonPlayerAliases(w, r)
case "/gm/rpc/vote":
v01.serveGMVote(w, r)
case "/gm/rpc/elect":
v01.serveGMElect(w, r)
case "/gm/rpc/shuffle":
v01.serveGMShuffle(r)
case "/gm/rpc/swap":
if errCode, err := v01.serveGMSwap(r.URL.Query().Get("a"), r.URL.Query().Get("b")); err != nil {
http.Error(w, err.Error(), errCode)
return
}
default:
http.NotFound(w, r)
return
}
}
func (v01 *V01) serveGMStatus(w http.ResponseWriter, r *http.Request) {
users := map[string]struct {
Lag time.Duration `yaml:"lag,omitempty"`
Player int `yaml:"player,omitempty"`
IdleFor time.Duration `yaml:"idle_for,omitempty"`
}{}
for k, v := range v01.cfg.Users {
v2 := users[k]
v2.Lag = time.Duration(v.Meta.LastLag) * time.Millisecond
v2.Player = v.State.Player
if v.Meta.LastTSMS > 0 {
v2.IdleFor = time.Since(time.Unix(0, v.Meta.LastTSMS*int64(time.Millisecond)))
}
users[k] = v2
}
yaml.NewEncoder(w).Encode(map[string]interface{}{
"Players": len(v01.cfg.Players),
"Users": users,
})
}
func (v01 *V01) serveGMSomeoneSaidAlias(w http.ResponseWriter, r *http.Request) {
v01.cfg.Quiet = true
for k, v := range v01.cfg.Users {
v.State.GM.LastAlias = v.State.GM.Alias
v.State.GM.Alias = ""
v01.cfg.Users[k] = v
}
v01.servePutBroadcastValue(fmt.Sprintf("<<SOMEONE SAID %q>>", strings.ToUpper(r.URL.Query().Get("message"))))
}
func (v01 *V01) serveGMFillNonPlayerAliases(w http.ResponseWriter, r *http.Request) {
b, _ := io.ReadAll(r.Body)
var pool []string
yaml.Unmarshal(b, &pool)
n := 0
for _, v := range v01.cfg.Users {
if v.State.Player == 0 {
n += 1
}
}
if n < 1 {
w.WriteHeader(http.StatusNoContent)
return
}
if len(pool) < n {
http.Error(w, fmt.Sprintf("request body must contain a list of %v options", n), http.StatusBadRequest)
return
}
for i := 0; i < 100; i++ {
a, b := rand.Int()%len(pool), rand.Int()%len(pool)
pool[a], pool[b] = pool[b], pool[a]
}
i := 0
for k, v := range v01.cfg.Users {
if v.State.Player == 0 {
v.State.GM.Alias = pool[i]
v01.cfg.Users[k] = v
i += 1
}
}
}
func (v01 *V01) serveGMElect(w http.ResponseWriter, r *http.Request) {
alias := r.URL.Query().Get("alias")
aliasWinner := ""
votes := map[string]int{}
for k, v := range v01.cfg.Users {
votes[v.State.GM.Vote] = votes[v.State.GM.Vote] + 1
if v.State.GM.LastAlias == alias {
aliasWinner = k
}
}
if aliasWinner == "" {
http.Error(w, "who is "+alias+"?", http.StatusBadRequest)
return
}
threshold := 0.1 + float64(len(votes))/2.0
winner := ""
for k, v := range votes {
if float64(v) > threshold {
winner = k
}
}
if winner == "" {
v01.serveGMShuffle(r)
} else if _, err := v01.serveGMSwap(winner, aliasWinner); err != nil {
v01.serveGMShuffle(r)
}
for k, v := range v01.cfg.Users {
v.State.GM.Vote = ""
v01.cfg.Users[k] = v
}
yaml.NewEncoder(w).Encode(votes)
}
func (v01 *V01) serveGMVote(w http.ResponseWriter, r *http.Request) {
switch r.URL.Query().Get("payload") {
case "":
counts := map[string]string{}
for k, v := range v01.cfg.Users {
if v.State.GM.Vote != "" {
counts[k] = "voted"
} else {
counts[k] = "voting"
}
}
yaml.NewEncoder(w).Encode(counts)
default:
voter := r.URL.Query().Get("user")
candidate := r.URL.Query().Get("payload")
v, ok := v01.cfg.Users[voter]
if _, ok2 := v01.cfg.Users[candidate]; !ok || !ok2 {
http.Error(w, "bad voter/candidate", http.StatusBadRequest)
return
}
v.State.GM.Vote = candidate
v01.cfg.Users[voter] = v
}
}
func (v01 *V01) serveGMShuffle(r *http.Request) {
poolSize := len(v01.cfg.Users)
if altSize := len(v01.cfg.Players); altSize > poolSize {
poolSize = altSize
}
pool := make([]int, poolSize)
if poolSize > 0 {
for i := range v01.cfg.Players {
pool[i] = i + 1
}
for i := 0; i < 30; i++ {
l := rand.Int() % poolSize
r := rand.Int() % poolSize
pool[l], pool[r] = pool[r], pool[l]
}
}
i := 0
msg := []string{}
for k, v := range v01.cfg.Users {
v.State.Player = pool[i]
v01.cfg.Users[k] = v
if pool[i] > 0 {
msg = append(msg, fmt.Sprintf("%s is now player %v", k, v.State.Player))
}
i += 1
}
v01.servePutBroadcastValue(strings.Join(msg, ", "))
v01.cfg.Quiet = false
}
func (v01 *V01) serveGMSwap(userA, userB string) (int, error) {
if userA == userB {
return http.StatusConflict, errors.New("/spiderman-pointing")
}
_, okA := v01.cfg.Users[userA]
_, okB := v01.cfg.Users[userB]
if !okA || !okB {
return http.StatusBadRequest, errors.New("who dat?")
}
a := v01.cfg.Users[userA]
b := v01.cfg.Users[userB]
a.State.Player, b.State.Player = b.State.Player, a.State.Player
v01.cfg.Users[userA] = a
v01.cfg.Users[userB] = b
v01.cfg.Quiet = false
v01.servePutBroadcastValue(fmt.Sprintf(`%s is now player %v and %s is now player %v`, userA, a.State.Player, userB, b.State.Player))
return http.StatusOK, nil
}

View File

@@ -1,13 +1,17 @@
package v01
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path"
"strconv"
"strings"
"sync"
"testing"
"time"
"gopkg.in/yaml.v2"
)
@@ -23,19 +27,19 @@ func TestPatchConfig(t *testing.T) {
"replace entire doc": {
was: config{
Feedback: configFeedback{Addr: "a", TTSURL: "a"},
Users: map[string]configUser{"a": configUser{Player: 1, Message: "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": {"Player": 2, "Message": "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{Player: 2, Message: "b"}},
Users: map[string]configUser{"b": configUser{State: configUserState{Player: 2, Message: "b"}}},
Players: []configPlayer{configPlayer{Transformation: transformation{"b": "b"}}},
Quiet: false,
},
@@ -53,10 +57,11 @@ func TestPatchConfig(t *testing.T) {
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.patchConfig(w, r)
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)
}
@@ -72,3 +77,498 @@ func TestPatchConfig(t *testing.T) {
}
}
}
func TestServeGM(t *testing.T) {
ctx, can := context.WithCancel(context.Background())
defer can()
do := func(v01 *V01, path, body string, method ...string) *httptest.ResponseRecorder {
m := http.MethodPost
if len(method) > 0 {
m = method[0]
}
w := httptest.NewRecorder()
r := httptest.NewRequest(m, path, strings.NewReader(body))
v01.ServeHTTP(w, r)
return w
}
t.Run("status", func(t *testing.T) {
v01 := NewV01(ctx, nil)
var result struct {
Players int `yaml:"Players"`
Users map[string]struct {
Player int
Lag string
IdleFor string `yaml:"idle_for"`
} `yaml:"Users"`
}
t.Run("empty", func(t *testing.T) {
resp := do(v01, "/gm/rpc/status", "")
if resp.Code != http.StatusOK {
t.Error(resp.Code)
}
t.Log(string(resp.Body.Bytes()))
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
t.Fatal(err)
}
if result.Players != 0 {
t.Error(result.Players)
}
if len(result.Users) != 0 {
t.Error(result.Users)
}
})
t.Run("full", func(t *testing.T) {
v01.cfg.Players = []configPlayer{
{},
{},
{},
{},
}
v01.cfg.Users = map[string]configUser{
"bel": configUser{
State: configUserState{Player: 3},
Meta: configUserMeta{
LastTSMS: time.Now().Add(-1*time.Minute).UnixNano() / int64(time.Millisecond),
LastLag: int64(time.Second / time.Millisecond),
},
},
"zach": configUser{},
"chase": configUser{},
"mason": configUser{},
"nat": configUser{},
"roxy": configUser{},
"bill": configUser{},
}
resp := do(v01, "/gm/rpc/status", "")
if resp.Code != http.StatusOK {
t.Error(resp.Code)
}
t.Log(string(resp.Body.Bytes()))
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
t.Fatal(err)
}
if result.Players != 4 {
t.Error(result.Players)
}
if len(result.Users) != 7 {
t.Error(result.Users)
}
if d, err := time.ParseDuration(result.Users["bel"].Lag); err != nil {
t.Error(err)
} else if d != time.Second {
t.Error(d)
}
if d, err := time.ParseDuration(result.Users["bel"].IdleFor); err != nil {
t.Error(err)
} else if d < time.Minute || d > 2*time.Minute {
t.Error(d)
}
if result.Users["bel"].Player != 3 {
t.Error(result.Users["bel"].Player)
}
})
})
t.Run("broadcastSomeoneSaidAlias", func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.Quiet = false
v01.cfg.Users = map[string]configUser{
"bel": configUser{State: configUserState{
GM: configUserStateGM{
Alias: "driver",
},
Message: "if someone else says 'driver', then you get to play",
}},
"broadcast": configUser{State: configUserState{
Message: ":)",
}},
}
do(v01, "/gm/rpc/broadcastSomeoneSaidAlias", "")
if !v01.cfg.Quiet {
t.Error(v01.cfg.Quiet)
}
if v := v01.cfg.Users["bel"]; v.State.GM.Alias != "" {
t.Error(v.State.GM.Alias)
} else if v.State.GM.LastAlias != "driver" {
t.Error(v.State.GM.LastAlias)
}
if bc := v01.cfg.Users["broadcast"]; bc.State.Message == ":)" {
t.Error(bc)
}
})
t.Run("fillNonPlayerAliases", func(t *testing.T) {
t.Run("empty", func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.Users = nil
resp := do(v01, "/gm/rpc/fillNonPlayerAliases", "[qt]")
if resp.Code != http.StatusNoContent {
t.Error(resp.Code)
}
})
t.Run("not enough", func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.Users = map[string]configUser{
"zach": configUser{State: configUserState{Player: 0}},
}
resp := do(v01, "/gm/rpc/fillNonPlayerAliases", "[]")
if resp.Code != http.StatusBadRequest {
t.Error(resp.Code)
}
})
t.Run("happy", func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.Users = map[string]configUser{
"bel": configUser{State: configUserState{Player: 1}},
"zach": configUser{State: configUserState{Player: 0}},
}
do(v01, "/gm/rpc/fillNonPlayerAliases", "[qt]")
if v := v01.cfg.Users["bel"]; v.State.GM.Alias != "" {
t.Error(v.State.GM.Alias)
} else if v.State.Player != 1 {
t.Error(v.State.Player)
}
if v := v01.cfg.Users["zach"]; v.State.GM.Alias != "qt" {
t.Error(v.State.GM.Alias)
} else if v.State.Player != 0 {
t.Error(v.State.Player)
}
})
})
t.Run("vote", func(t *testing.T) {
type result map[string]string
t.Run("cast bad vote", func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.Users = map[string]configUser{"bel": {}}
resp := do(v01, "/gm/rpc/vote?user=bel&payload=?", "")
if resp.Code != http.StatusBadRequest {
t.Error(resp)
}
if v01.cfg.Users["bel"].State.Message != "" {
t.Error(v01.cfg.Users["bel"].State.Message)
}
})
t.Run("cast vote", func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.Users = map[string]configUser{"bel": {}, "zach": {}}
do(v01, "/gm/rpc/vote?user=bel&payload=zach", "")
if v01.cfg.Users["bel"].State.GM.Vote != "zach" {
t.Error(v01.cfg.Users["bel"].State.GM.Vote)
}
})
t.Run("get non vote", func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.Users = map[string]configUser{"bel": {}}
resp := do(v01, "/gm/rpc/vote", "", "GET")
var result result
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
t.Error(err)
}
if len(result) != 1 {
t.Error(result)
}
if result["bel"] != "voting" {
t.Error(result)
}
t.Logf("%+v", result)
})
t.Run("get mid vote", func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.Users = map[string]configUser{"bel": {State: configUserState{GM: configUserStateGM{Vote: "zach"}, Message: "driver"}}}
resp := do(v01, "/gm/rpc/vote", "", "GET")
var result result
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
t.Error(err)
}
if len(result) != 1 {
t.Error(result)
}
if result["bel"] != "voted" {
t.Error(result)
}
t.Logf("%+v", result)
})
t.Run("get empty", func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.Users = nil
resp := do(v01, "/gm/rpc/vote", "", "GET")
var result result
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
t.Error(err)
}
if len(result) != 0 {
t.Error(result)
}
t.Logf("%+v", result)
})
})
t.Run("elect", func(t *testing.T) {
type result map[string]int
t.Run("happy", func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.Users = map[string]configUser{
"bel": configUser{State: configUserState{GM: configUserStateGM{Vote: "zach", LastAlias: "driver"}, Player: 1}},
"zach": configUser{State: configUserState{GM: configUserStateGM{Vote: "bel", LastAlias: "pizza"}}},
"bill": configUser{State: configUserState{GM: configUserStateGM{Vote: "bel"}, Player: 2}},
}
resp := do(v01, "/gm/rpc/elect?alias=pizza", "")
var result result
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
t.Errorf("%s => %v", resp.Body.Bytes(), err)
}
if len(result) != 2 {
t.Error(result)
} else if result["bel"] != 2 {
t.Error(result)
} else if result["zach"] != 1 {
t.Error(result)
}
if v01.cfg.Users["bel"].State.Player != 0 {
t.Error(v01.cfg.Users["bel"].State.Player)
} else if v01.cfg.Users["zach"].State.Player != 1 {
t.Error(v01.cfg.Users["zach"].State.Player)
}
if v01.cfg.Users["broadcast"].State.Message != `bel is now player 0 and zach is now player 1` {
t.Error(v01.cfg.Users["broadcast"].State.Message)
}
})
t.Run("self", func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.Players = []configPlayer{{}}
v01.cfg.Users = map[string]configUser{
"bel": configUser{State: configUserState{GM: configUserStateGM{Vote: "zach", LastAlias: "driver"}, Player: 1}},
"zach": configUser{State: configUserState{GM: configUserStateGM{Vote: "bel"}}},
"bill": configUser{State: configUserState{GM: configUserStateGM{Vote: "bel"}}},
}
resp := do(v01, "/gm/rpc/elect?alias=driver", "")
var result result
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
t.Error(err)
}
if len(result) != 2 {
t.Error(result)
} else if result["bel"] != 2 {
t.Error(result)
} else if result["zach"] != 1 {
t.Error(result)
}
if !strings.HasSuffix(v01.cfg.Users["broadcast"].State.Message, `is now player 1`) || strings.Contains(v01.cfg.Users["broadcast"].State.Message, ",") {
t.Error(v01.cfg.Users["broadcast"].State.Message)
}
assignments := map[int]int{}
for _, v := range v01.cfg.Users {
assignments[v.State.Player] = assignments[v.State.Player] + 1
}
if len(assignments) != 2 {
t.Error(assignments)
} else if assignments[0] != 3 {
t.Error(assignments[0])
} else if assignments[1] != 1 {
t.Error(assignments[1])
}
})
t.Run("tie", func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.Players = []configPlayer{{}}
v01.cfg.Users = map[string]configUser{
"bel": configUser{State: configUserState{GM: configUserStateGM{Vote: "zach", LastAlias: "driver"}, Player: 1}},
"zach": configUser{State: configUserState{GM: configUserStateGM{Vote: "bel", LastAlias: "pizza"}}},
}
resp := do(v01, "/gm/rpc/elect?alias=pizza", "")
var result result
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
t.Error(err)
}
if len(result) != 2 {
t.Error(result)
} else if result["bel"] != 1 {
t.Error(result)
} else if result["zach"] != 1 {
t.Error(result)
}
if !strings.HasSuffix(v01.cfg.Users["broadcast"].State.Message, `is now player 1`) || strings.Contains(v01.cfg.Users["broadcast"].State.Message, ",") {
t.Error(v01.cfg.Users["broadcast"].State.Message)
}
assignments := map[int]int{}
for _, v := range v01.cfg.Users {
assignments[v.State.Player] = assignments[v.State.Player] + 1
}
if len(assignments) != 2 {
t.Error(assignments)
} else if assignments[0] != 2 {
t.Error(assignments[0])
} else if assignments[1] != 1 {
t.Error(assignments[1])
}
})
})
t.Run("shuffle", func(t *testing.T) {
t.Run("many 2u 2p", func(t *testing.T) {
v01 := NewV01(ctx, nil)
for i := 0; i < 100; i++ {
v01.cfg.Quiet = true
v01.cfg.Users = map[string]configUser{
"bel": configUser{State: configUserState{Player: 1}},
"zach": configUser{State: configUserState{Player: 2}},
}
v01.cfg.Players = []configPlayer{{}, {}}
do(v01, "/gm/rpc/shuffle", "")
if v01.cfg.Quiet {
t.Error(v01.cfg.Quiet)
}
if len(v01.cfg.Users) != 3 {
t.Error(v01.cfg.Users)
} else if len(v01.cfg.Players) != 2 {
t.Error(v01.cfg.Users)
} else if bp := v01.cfg.Users["bel"].State.Player; bp != 1 && bp != 2 {
t.Error(bp)
} else if zp := v01.cfg.Users["zach"].State.Player; zp != 1 && zp != 2 {
t.Error(zp)
} else if bp == zp {
t.Error(bp, zp)
}
}
})
cases := map[string]struct {
users int
usersAssigned int
players int
}{
"empty": {},
"just users": {users: 2},
"just players": {players: 2},
"2 unassigned users and 2 players": {users: 2, players: 2},
"2 users and 2 players": {users: 2, usersAssigned: 2, players: 2},
"1 users and 2 players": {users: 1, usersAssigned: 1, players: 2},
"1 unassigned users and 2 players": {users: 1, players: 2},
"4 players for 7 users 0 assigned": {users: 7, players: 4},
"4 players for 7 users 4 assigned": {users: 7, players: 4, usersAssigned: 4},
}
for name, d := range cases {
c := d
t.Run(name, func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.Quiet = true
v01.cfg.Users = map[string]configUser{}
for i := 0; i < c.users; i++ {
v01.cfg.Users[strconv.Itoa(i)] = configUser{}
if i < c.usersAssigned {
v01.cfg.Users[strconv.Itoa(i)] = configUser{State: configUserState{Player: i}}
}
}
v01.cfg.Players = make([]configPlayer, c.players)
do(v01, "/gm/rpc/shuffle", "")
if v01.cfg.Quiet {
t.Error(v01.cfg.Quiet)
}
if len(v01.cfg.Users) != c.users+1 {
t.Error(v01.cfg.Users)
} else if len(v01.cfg.Players) != c.players {
t.Error(v01.cfg.Users)
}
for i := 0; i < c.users; i++ {
if _, ok := v01.cfg.Users[strconv.Itoa(i)]; !ok {
t.Error(i)
}
}
assignments := map[int]int{}
for _, v := range v01.cfg.Users {
if v.State.Player > 0 {
assignments[v.State.Player] = assignments[v.State.Player] + 1
}
}
lesser := c.users
if c.players < lesser {
lesser = c.players
}
if len(assignments) != lesser {
t.Error(assignments)
}
for _, v := range assignments {
if v != 1 {
t.Error(v)
}
}
})
}
})
t.Run("swap", func(t *testing.T) {
t.Run("self", func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.Quiet = true
v01.cfg.Users = map[string]configUser{
"bel": configUser{State: configUserState{Player: 1}},
}
resp := do(v01, "/gm/rpc/swap?a=bel&b=bel", "")
if resp.Code != http.StatusConflict {
t.Error(resp.Code)
}
if !v01.cfg.Quiet {
t.Error(v01.cfg.Quiet)
}
})
t.Run("who", func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.Quiet = true
resp := do(v01, "/gm/rpc/swap?a=bel", "")
if resp.Code != http.StatusBadRequest {
t.Error(resp.Code)
}
if !v01.cfg.Quiet {
t.Error(v01.cfg.Quiet)
}
})
t.Run("happy", func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.Quiet = true
v01.cfg.Users = map[string]configUser{
"bel": configUser{State: configUserState{Player: 1}},
"zach": configUser{State: configUserState{Player: 2}},
}
resp := do(v01, "/gm/rpc/swap?a=bel&b=zach", "")
if resp.Code != http.StatusOK {
t.Error(resp.Code)
}
if v01.cfg.Quiet {
t.Error(v01.cfg.Quiet)
}
if v01.cfg.Users["bel"].State.Player != 2 {
t.Error(v01.cfg.Users["bel"])
} else if v01.cfg.Users["zach"].State.Player != 1 {
t.Error(v01.cfg.Users["zach"])
}
})
})
t.Run("404", func(t *testing.T) {
v01 := NewV01(ctx, nil)
resp := do(v01, "/gm/teehee", "")
if resp.Code != http.StatusNotFound {
t.Error(resp.Code)
}
})
}

View File

@@ -3,8 +3,13 @@ feedback:
ttsurl: http://localhost:15002
users:
bel:
player: 0
message: "hi"
state:
player: 0
message: "hi"
alias: driver
meta:
tsms: 1
lastlag: 2
players:
- buttons:
up: "w"

View File

@@ -68,6 +68,9 @@ func (v01 *V01) _tts(text string) error {
speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/30))
speaker.Play(&effects.Volume{Streamer: beep.ResampleRatio(4, 1, &beep.Ctrl{Streamer: beep.Loop(1, decoder)})})
duration := time.Duration(decoder.Len()) * format.SampleRate.D(1)
time.Sleep(duration)
select {
case <-v01.ctx.Done():
case <-time.After(duration):
}
return nil
}

View File

@@ -8,6 +8,7 @@ import (
"mayhem-party/src/device/input/button"
"mayhem-party/src/device/input/raw"
"os"
"sync"
"time"
"gopkg.in/yaml.v2"
@@ -20,25 +21,29 @@ var (
type (
V01 struct {
ctx context.Context
can context.CancelFunc
src raw.Raw
cfg config
ctx context.Context
can context.CancelFunc
src raw.Raw
cfg config
telemetryc chan message
}
)
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),
}
go result.listen()
go result.dotelemetry()
return result
}
@@ -69,23 +74,51 @@ func (v01 *V01) Read() []button.Button {
return buttons
}
func (v01 *V01) telemetry(msg message) {
if FlagDebug {
log.Printf("%s|%dms", msg.U, time.Now().UnixNano()/int64(time.Millisecond)-msg.T)
func (v01 *V01) dotelemetry() {
for {
select {
case <-v01.ctx.Done():
return
case msg := <-v01.telemetryc:
v01._telemetry(msg)
}
}
}
func (v01 *V01) telemetry(msg message) {
select {
case v01.telemetryc <- msg:
default:
}
}
func (v01 *V01) _telemetry(msg message) {
// TODO oof
v01.cfg.lock.Lock()
defer v01.cfg.lock.Unlock()
if v01.cfg.Users == nil {
v01.cfg.Users = map[string]configUser{}
}
u := v01.cfg.Users[msg.U]
u.Meta.LastLag = time.Now().UnixNano()/int64(time.Millisecond) - msg.T
u.Meta.LastTSMS = msg.T
if FlagDebug {
log.Printf("%s|%dms", msg.U, u.Meta.LastLag)
}
v01.cfg.Users[msg.U] = u
}
func (v01 *V01) transform(msg message) message {
if len(v01.cfg.Players) == 0 {
return msg
}
user := v01.cfg.Users[msg.U]
if user.Player < 1 {
if user.State.Player < 1 {
msg.Y = ""
msg.N = ""
return msg
}
player := v01.cfg.Players[user.Player-1]
player := v01.cfg.Players[user.State.Player-1]
msg.Y = player.Transformation.pipe(msg.Y)
msg.N = player.Transformation.pipe(msg.N)
return msg

View File

@@ -44,7 +44,8 @@ func TestV01WithCfg(t *testing.T) {
os.WriteFile(p, []byte(`
users:
bel:
player: 2
state:
player: 2
players:
- transformation:
w: t
@@ -87,10 +88,12 @@ func TestV01Feedback(t *testing.T) {
ttsurl: http://localhost:15002
users:
bel:
player: 2
message: to bel
state:
player: 2
message: to bel
broadcast:
message: to everyone
state:
message: to everyone
players:
- transformation:
w: t

View File

@@ -17,9 +17,9 @@ func Main(ctx context.Context) error {
defer reader.Close()
interval := time.Millisecond * 50
if intervalS := os.Getenv("MAIN_INTERVAL_DURATION"); intervalS != "" {
if intervalS := os.Getenv("MAIN_INTERVAL_DURATION"); intervalS == "" {
} else if v, err := time.ParseDuration(intervalS); err != nil {
panic(err)
return err
} else {
interval = v
}

View File

@@ -1,17 +1,20 @@
todo:
- https via home.blapointe and rproxy
- json patch endpoint for config
- 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
- trigger a vote
- trigger an election
- trigger a shuffle for init
- assign aliases
- display vote progress
- single docker image to run all
- trigger dolphin pause via query param mapping to a button that is a pause hotkey
- todo: rotation triggers
subtasks:
- ui for election start, election votes, election end stuff
- todo: stdin
subtasks:
- minigame end
- todo: voice recognition of hotwords to vote who dun it
subtasks:
- random word from cur wikipedia page
- each person has their own hotword
- only spectators have hotwords and must get a player to speak it
- tribunal to vote who said it
scheduled: []
@@ -80,3 +83,43 @@ done:
ts: Sat Mar 25 23:16:47 MDT 2023
- todo: tts for when someone said the word via larynx docker + http.get + https://pkg.go.dev/github.com/faiface/beep@v1.1.0/wav
ts: Sun Mar 26 09:57:02 MDT 2023
- todo: endpoint for v01 to start read-only mode so when hotword spoken, players are
dcd without losing players; press a hotkey that is bound to dolphin emulator pause
ts: Sun Mar 26 14:28:46 MDT 2023
- todo: game master to coordinate config change
ts: Mon Mar 27 11:01:10 MDT 2023
- todo: rotation triggers
subtasks:
- todo: stdin
subtasks:
- minigame end
- todo: voice recognition of hotwords to vote who dun it
subtasks:
- random word from cur wikipedia page
- each person has their own hotword
- only spectators have hotwords and must get a player to speak it
- tribunal to vote who said it
ts: Mon Mar 27 11:04:56 MDT 2023
- todo: clients can send STT via box
ts: Mon Mar 27 17:55:41 MDT 2023
- todo: -venue needs to update for new env variables for GUI
ts: Mon Mar 27 21:43:40 MDT 2023
- todo: -venue needs to udpate hits hotword path for new Users.[].State.GM.Alias
ts: Mon Mar 27 21:43:40 MDT 2023
- todo: clients can vote
ts: Mon Mar 27 21:43:40 MDT 2023
- todo: rotation triggers
subtasks:
- ui for election start, election votes, election end stuff
- todo: stdin
subtasks:
- minigame end
- todo: voice recognition of hotwords to vote who dun it
subtasks:
- random word from cur wikipedia page
- each person has their own hotword
- only spectators have hotwords and must get a player to speak it
- tribunal to vote who said it
ts: Mon Mar 27 21:43:40 MDT 2023
- todo: https via home.blapointe and rproxy
ts: Mon Mar 27 21:43:53 MDT 2023