Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50e89492cf | ||
|
|
3d9ea1296c | ||
|
|
db69f76aa0 | ||
|
|
0ee3a8b6e8 | ||
|
|
b379f1d82c | ||
|
|
c83f9d8700 | ||
|
|
6289222b69 | ||
|
|
607a65e22e | ||
|
|
6bbb297c59 | ||
|
|
95866f7df0 | ||
|
|
aaa949cc2a | ||
|
|
ed2b7b7cb9 |
1
go.mod
1
go.mod
@@ -5,4 +5,5 @@ go 1.19
|
||||
require (
|
||||
github.com/go-yaml/yaml v2.1.0+incompatible // 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/micmonay/keybd_event v1.1.1 h1:rv7omwXWYL9Lgf3PUq6uBgJI2k1yGkL/GD6dxc6nmSs=
|
||||
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=
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export DEBUG=1
|
||||
export DEBUG=true
|
||||
export RAW_UDP=17070
|
||||
export BUTTON_PARSER_V01=true
|
||||
export WRAP_REFRESH_ON_SIGUSR1=true
|
||||
export WRAP_REMAP_FILE=./config.d/mayhem-party.d/remap.d/live.yaml
|
||||
export MAIN_INTERVAL_DURATION=5ms
|
||||
export OUTPUT_KEYBOARD=false
|
||||
|
||||
@@ -4,21 +4,23 @@ streams:
|
||||
engine:
|
||||
name: gui
|
||||
gui:
|
||||
user: bel
|
||||
press: {prefix: "", suffix: ""}
|
||||
release: {prefix: "!", suffix: ""}
|
||||
release: {prefix: "", suffix: ""}
|
||||
format: '{"T":{{ms}},"U":"{{user}}","Y":"{{pressed}}","N":"{{released}}"}'
|
||||
buttons:
|
||||
up: '1'
|
||||
down: '2'
|
||||
left: '3'
|
||||
right: '4'
|
||||
l: '5'
|
||||
r: '6'
|
||||
a: '7'
|
||||
b: '8'
|
||||
x: '9'
|
||||
y: '0'
|
||||
up: 'w'
|
||||
down: 's'
|
||||
left: 'a'
|
||||
right: 'd'
|
||||
l: 'e'
|
||||
r: 'q'
|
||||
a: '1'
|
||||
b: '2'
|
||||
x: '3'
|
||||
y: '4'
|
||||
output:
|
||||
debug: false
|
||||
debug: true
|
||||
engine:
|
||||
name: udp
|
||||
udp:
|
||||
|
||||
@@ -4,21 +4,23 @@ streams:
|
||||
engine:
|
||||
name: gui
|
||||
gui:
|
||||
user: zach
|
||||
press: {prefix: "", suffix: ""}
|
||||
release: {prefix: "!", suffix: ""}
|
||||
release: {prefix: "", suffix: ""}
|
||||
format: '{"T":{{ms}},"U":"{{user}}","Y":"{{pressed}}","N":"{{released}}"}'
|
||||
buttons:
|
||||
up: 'q'
|
||||
down: 'w'
|
||||
left: 'e'
|
||||
right: 'r'
|
||||
l: 't'
|
||||
r: 'y'
|
||||
a: 'u'
|
||||
b: 'i'
|
||||
x: 'o'
|
||||
y: 'p'
|
||||
up: 'w'
|
||||
down: 's'
|
||||
left: 'a'
|
||||
right: 'd'
|
||||
l: 'e'
|
||||
r: 'q'
|
||||
a: '1'
|
||||
b: '2'
|
||||
x: '3'
|
||||
y: '4'
|
||||
output:
|
||||
debug: false
|
||||
debug: true
|
||||
engine:
|
||||
name: udp
|
||||
udp:
|
||||
|
||||
@@ -4,21 +4,23 @@ streams:
|
||||
engine:
|
||||
name: gui
|
||||
gui:
|
||||
user: mason
|
||||
press: {prefix: "", suffix: ""}
|
||||
release: {prefix: "!", suffix: ""}
|
||||
release: {prefix: "", suffix: ""}
|
||||
format: '{"T":{{ms}},"U":"{{user}}","Y":"{{pressed}}","N":"{{released}}"}'
|
||||
buttons:
|
||||
up: 'a'
|
||||
up: 'w'
|
||||
down: 's'
|
||||
left: 'd'
|
||||
right: 'f'
|
||||
l: 'g'
|
||||
r: 'h'
|
||||
a: 'j'
|
||||
b: 'k'
|
||||
x: 'l'
|
||||
y: ';'
|
||||
left: 'a'
|
||||
right: 'd'
|
||||
l: 'e'
|
||||
r: 'q'
|
||||
a: '1'
|
||||
b: '2'
|
||||
x: '3'
|
||||
y: '4'
|
||||
output:
|
||||
debug: false
|
||||
debug: true
|
||||
engine:
|
||||
name: udp
|
||||
udp:
|
||||
|
||||
@@ -4,21 +4,23 @@ streams:
|
||||
engine:
|
||||
name: gui
|
||||
gui:
|
||||
user: nat
|
||||
press: {prefix: "", suffix: ""}
|
||||
release: {prefix: "!", suffix: ""}
|
||||
release: {prefix: "", suffix: ""}
|
||||
format: '{"T":{{ms}},"U":"{{user}}","Y":"{{pressed}}","N":"{{released}}"}'
|
||||
buttons:
|
||||
up: 'z'
|
||||
down: 'x'
|
||||
left: 'c'
|
||||
right: 'v'
|
||||
l: 'b'
|
||||
r: 'n'
|
||||
a: 'm'
|
||||
b: ','
|
||||
x: '-'
|
||||
y: '/'
|
||||
up: 'w'
|
||||
down: 's'
|
||||
left: 'a'
|
||||
right: 'd'
|
||||
l: 'e'
|
||||
r: 'q'
|
||||
a: '1'
|
||||
b: '2'
|
||||
x: '3'
|
||||
y: '4'
|
||||
output:
|
||||
debug: false
|
||||
debug: true
|
||||
engine:
|
||||
name: udp
|
||||
udp:
|
||||
|
||||
@@ -4,21 +4,23 @@ streams:
|
||||
engine:
|
||||
name: gui
|
||||
gui:
|
||||
user: roxy
|
||||
press: {prefix: "", suffix: ""}
|
||||
release: {prefix: "!", suffix: ""}
|
||||
release: {prefix: "", suffix: ""}
|
||||
format: '{"T":{{ms}},"U":"{{user}}","Y":"{{pressed}}","N":"{{released}}"}'
|
||||
buttons:
|
||||
up: '!'
|
||||
down: '@'
|
||||
left: '#'
|
||||
right: '$'
|
||||
l: '%'
|
||||
r: '^'
|
||||
a: '&'
|
||||
b: '*'
|
||||
x: '('
|
||||
y: ')'
|
||||
up: 'w'
|
||||
down: 's'
|
||||
left: 'a'
|
||||
right: 'd'
|
||||
l: 'e'
|
||||
r: 'q'
|
||||
a: '1'
|
||||
b: '2'
|
||||
x: '3'
|
||||
y: '4'
|
||||
output:
|
||||
debug: false
|
||||
debug: true
|
||||
engine:
|
||||
name: udp
|
||||
udp:
|
||||
|
||||
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
|
||||
@@ -12,8 +12,8 @@ type Parser interface {
|
||||
}
|
||||
|
||||
func New(ctx context.Context, src raw.Raw) Parser {
|
||||
if os.Getenv("BUTTON_PARSER_V1") == "true" {
|
||||
return NewV1(src)
|
||||
if os.Getenv("BUTTON_PARSER_V01") == "true" {
|
||||
return NewV01(src)
|
||||
}
|
||||
return NewPlaintext(src)
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@ import (
|
||||
|
||||
func TestParser(t *testing.T) {
|
||||
var _ button.Parser = button.Plaintext{}
|
||||
var _ button.Parser = button.V1{}
|
||||
var _ button.Parser = button.V01{}
|
||||
}
|
||||
|
||||
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"
|
||||
107
src/device/input/button/v01.go
Normal file
107
src/device/input/button/v01.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package button
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"mayhem-party/src/device/input/raw"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
var debugging = os.Getenv("DEBUG") == "true"
|
||||
|
||||
type (
|
||||
V01 struct {
|
||||
src raw.Raw
|
||||
cfg v01Cfg
|
||||
}
|
||||
v01Msg struct {
|
||||
T int64
|
||||
U string
|
||||
Y string
|
||||
N string
|
||||
}
|
||||
v01Cfg struct {
|
||||
Users map[string]struct {
|
||||
Player int
|
||||
Message string
|
||||
}
|
||||
Players []struct {
|
||||
Transformation v01Transformation
|
||||
}
|
||||
}
|
||||
v01Transformation map[string]string
|
||||
)
|
||||
|
||||
func NewV01(src raw.Raw) V01 {
|
||||
var cfg v01Cfg
|
||||
b, _ := ioutil.ReadFile(os.Getenv("BUTTON_PARSER_V01_CONFIG"))
|
||||
yaml.Unmarshal(b, &cfg)
|
||||
return V01{
|
||||
src: src,
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func (v01 V01) Close() {
|
||||
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 debugging {
|
||||
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 debugging {
|
||||
log.Printf("%+v", msg)
|
||||
}
|
||||
return buttons
|
||||
}
|
||||
71
src/device/input/button/v01_exported_test.go
Normal file
71
src/device/input/button/v01_exported_test.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package button_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mayhem-party/src/device/input/button"
|
||||
"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(src)
|
||||
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)
|
||||
os.Setenv("BUTTON_PARSER_V01_CONFIG", p)
|
||||
|
||||
t.Run("unknown user ignored", func(t *testing.T) {
|
||||
v01 := button.NewV01(constSrc(`{"U":"qt","Y":"w"}`))
|
||||
got := v01.Read()
|
||||
if len(got) != 0 {
|
||||
t.Error(got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("player2", func(t *testing.T) {
|
||||
v01 := button.NewV01(constSrc(`{"U":"bel","Y":"w","N":"w"}`))
|
||||
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])
|
||||
}
|
||||
})
|
||||
}
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package button
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"mayhem-party/src/device/input/raw"
|
||||
"os"
|
||||
)
|
||||
|
||||
var debugging = os.Getenv("DEBUG") == "true"
|
||||
|
||||
type (
|
||||
V1 struct {
|
||||
src raw.Raw
|
||||
}
|
||||
v1Msg struct {
|
||||
T int64
|
||||
U string
|
||||
Y string
|
||||
N string
|
||||
}
|
||||
)
|
||||
|
||||
func NewV1(src raw.Raw) V1 {
|
||||
return V1{
|
||||
src: src,
|
||||
}
|
||||
}
|
||||
|
||||
func (v1 V1) Close() {
|
||||
v1.src.Close()
|
||||
}
|
||||
|
||||
func (v1 V1) Read() []Button {
|
||||
line := v1.src.Read()
|
||||
var msg v1Msg
|
||||
if err := json.Unmarshal(line, &msg); err != nil {
|
||||
log.Printf("%v: %s", err, line)
|
||||
}
|
||||
return msg.buttons()
|
||||
}
|
||||
|
||||
func (msg v1Msg) 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 debugging {
|
||||
log.Printf("%+v", msg)
|
||||
}
|
||||
return buttons
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package button_test
|
||||
|
||||
import (
|
||||
"mayhem-party/src/device/input/button"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestV1(t *testing.T) {
|
||||
src := constSrc(`{"T":1,"U":"bel","Y":"abc","N":"cde"}`)
|
||||
t.Logf("(%v) %s", len(src), src.Read())
|
||||
v1 := button.NewV1(src)
|
||||
got := v1.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])
|
||||
}
|
||||
}
|
||||
}
|
||||
13
todo.yaml
13
todo.yaml
@@ -1,6 +1,5 @@
|
||||
todo:
|
||||
- change from 'a','b','c' from rust to just 11,21,31,41 so playerName is known implicitly
|
||||
- lag via UDP formatted inputs as space-delimited TS PID buttonIdx buttonIdx buttonIdx
|
||||
- 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
|
||||
@@ -42,3 +41,13 @@ done:
|
||||
- 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
|
||||
|
||||
Reference in New Issue
Block a user