Port
This commit is contained in:
37
internal/app/app.go
Normal file
37
internal/app/app.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"vrc_osc_go/internal/config"
|
||||
"vrc_osc_go/internal/osc"
|
||||
)
|
||||
|
||||
func Run(configPath string) error {
|
||||
cfg, err := config.Load(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
server := osc.NewServer(cfg.OSC.Host, cfg.OSC.Port)
|
||||
server.Map("DiscordSend", func(_ string, args []osc.Value) error {
|
||||
return fmt.Errorf("discord mute not implemented yet")
|
||||
})
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
go func() { errCh <- server.Serve() }()
|
||||
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
|
||||
select {
|
||||
case err := <-errCh:
|
||||
return err
|
||||
case <-sigCh:
|
||||
server.Close()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
58
internal/config/config.go
Normal file
58
internal/config/config.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
OSC OSCConfig
|
||||
}
|
||||
|
||||
type OSCConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
func Load(path string) (*Config, error) {
|
||||
cfg := &Config{OSC: OSCConfig{Host: "127.0.0.1", Port: 9001}}
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return cfg, nil
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var section string
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
|
||||
section = strings.Trim(line, "[]")
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
key := strings.TrimSpace(parts[0])
|
||||
val := strings.TrimSpace(parts[1])
|
||||
val = strings.Trim(val, "\"'")
|
||||
if section == "osc" {
|
||||
switch key {
|
||||
case "host":
|
||||
cfg.OSC.Host = val
|
||||
case "port":
|
||||
if n, err := strconv.Atoi(val); err == nil {
|
||||
cfg.OSC.Port = n
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return cfg, scanner.Err()
|
||||
}
|
||||
|
||||
33
internal/config/config_test.go
Normal file
33
internal/config/config_test.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoadDefaultsWhenMissing(t *testing.T) {
|
||||
cfg, err := Load("does-not-exist.toml")
|
||||
if err != nil {
|
||||
t.Fatalf("Load returned error: %v", err)
|
||||
}
|
||||
if cfg.OSC.Host != "127.0.0.1" || cfg.OSC.Port != 9001 {
|
||||
t.Fatalf("unexpected defaults: %+v", cfg.OSC)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadOSCValues(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "config.toml")
|
||||
if err := os.WriteFile(path, []byte("[osc]\nhost = \"0.0.0.0\"\nport = 9002\n"), 0o600); err != nil {
|
||||
t.Fatalf("WriteFile: %v", err)
|
||||
}
|
||||
cfg, err := Load(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Load returned error: %v", err)
|
||||
}
|
||||
if cfg.OSC.Host != "0.0.0.0" || cfg.OSC.Port != 9002 {
|
||||
t.Fatalf("unexpected values: %+v", cfg.OSC)
|
||||
}
|
||||
}
|
||||
|
||||
77
internal/osc/server.go
Normal file
77
internal/osc/server.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package osc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Value struct {
|
||||
Type byte
|
||||
Float float32
|
||||
Int int32
|
||||
Bool bool
|
||||
Str string
|
||||
}
|
||||
|
||||
type Handler func(address string, args []Value) error
|
||||
|
||||
type Server struct {
|
||||
addr string
|
||||
conn *net.UDPConn
|
||||
handlers map[string]Handler
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func NewServer(host string, port int) *Server {
|
||||
return &Server{
|
||||
addr: fmt.Sprintf("%s:%d", host, port),
|
||||
handlers: map[string]Handler{},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Map(param string, h Handler) { s.handlers["/avatar/parameters/"+param] = h }
|
||||
func (s *Server) Close() error { if s.conn != nil { return s.conn.Close() }; return nil }
|
||||
|
||||
func (s *Server) Serve() error {
|
||||
addr, err := net.ResolveUDPAddr("udp", s.addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conn, err := net.ListenUDP("udp", addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.conn = conn
|
||||
buf := make([]byte, 2048)
|
||||
for {
|
||||
n, _, err := conn.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a, v, err := parseMessage(buf[:n])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
s.mu.RLock()
|
||||
h := s.handlers[a]
|
||||
s.mu.RUnlock()
|
||||
if h != nil {
|
||||
_ = h(a, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func parseMessage(b []byte) (string, []Value, error) {
|
||||
parts := bytes.SplitN(b, []byte{0}, 2)
|
||||
if len(parts) == 0 {
|
||||
return "", nil, fmt.Errorf("invalid osc")
|
||||
}
|
||||
address := string(parts[0])
|
||||
if !strings.HasPrefix(address, "/") {
|
||||
return "", nil, fmt.Errorf("invalid address")
|
||||
}
|
||||
return address, nil, nil
|
||||
}
|
||||
14
internal/osc/server_test.go
Normal file
14
internal/osc/server_test.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package osc
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParseMessageAddress(t *testing.T) {
|
||||
addr, _, err := parseMessage([]byte("/avatar/parameters/DiscordSend\x00"))
|
||||
if err != nil {
|
||||
t.Fatalf("parseMessage returned error: %v", err)
|
||||
}
|
||||
if addr != "/avatar/parameters/DiscordSend" {
|
||||
t.Fatalf("unexpected address: %q", addr)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user