Making client GUI with fake data

live
noah metz 2024-04-24 19:09:17 -06:00
parent 15a24c7d05
commit 108bc44411
3 changed files with 307 additions and 80 deletions

@ -1,26 +1,26 @@
package main package main
import ( import (
"crypto/ed25519" "crypto/ed25519"
"crypto/rand" "crypto/rand"
"crypto/x509" "crypto/x509"
"encoding/binary" "encoding/binary"
"encoding/pem" "encoding/pem"
"flag" "flag"
"fmt" "fmt"
"os" "os"
"os/signal" "os/signal"
"syscall" "syscall"
"time" "time"
"git.metznet.ca/MetzNet/go-ncurses" "git.metznet.ca/MetzNet/go-ncurses"
"git.metznet.ca/MetzNet/pnyx" "git.metznet.ca/MetzNet/pnyx"
"github.com/gen2brain/malgo" "github.com/gen2brain/malgo"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/hraban/opus" "github.com/hraban/opus"
) )
var decoders = map[pnyx.PeerID]chan[]byte{} var decoders = map[pnyx.PeerID]chan []byte{}
var encoder *opus.Encoder var encoder *opus.Encoder
var sample_rate int = 0 var sample_rate int = 0
@ -33,18 +33,18 @@ func set_sample_rate(audio_data chan []int16, new_sample_rate int) error {
return err return err
} }
for peer_id, decoder_chan := range(decoders) { for peer_id, decoder_chan := range decoders {
if decoder_chan != nil { if decoder_chan != nil {
decoder_chan <- nil decoder_chan <- nil
} }
new_chan := make(chan[]byte, 1000) new_chan := make(chan []byte, 1000)
decoders[peer_id] = new_chan decoders[peer_id] = new_chan
go handle_peer_decode(audio_data, peer_id, decoders[peer_id], sample_rate) go handle_peer_decode(audio_data, peer_id, decoders[peer_id], sample_rate)
} }
return nil return nil
} }
func handle_peer_decode(audio_data chan []int16, peer_id pnyx.PeerID, decode_chan chan[]byte, sample_rate int){ func handle_peer_decode(audio_data chan []int16, peer_id pnyx.PeerID, decode_chan chan []byte, sample_rate int) {
decoder, err := opus.NewDecoder(sample_rate, 1) decoder, err := opus.NewDecoder(sample_rate, 1)
if err != nil { if err != nil {
panic(err) panic(err)
@ -54,11 +54,11 @@ func handle_peer_decode(audio_data chan []int16, peer_id pnyx.PeerID, decode_cha
missed := 0 missed := 0
for running { for running {
select { select {
case <-time.After(20*time.Millisecond): case <-time.After(20 * time.Millisecond):
missed += 1 missed += 1
if missed > 100 { if missed > 100 {
decode_chan <- <- decode_chan decode_chan <- <-decode_chan
} }
pcm := make([]int16, sample_rate/50) pcm := make([]int16, sample_rate/50)
@ -88,11 +88,11 @@ func mixer(data_chan chan []int16, speaker_chan chan []int16) {
var samples []int16 = nil var samples []int16 = nil
for true { for true {
if samples == nil { if samples == nil {
samples = <- data_chan samples = <-data_chan
} else { } else {
select { select {
case new_samples := <- data_chan: case new_samples := <-data_chan:
for i, sample := range(new_samples) { for i, sample := range new_samples {
samples[i] += sample samples[i] += sample
} }
case speaker_chan <- samples: case speaker_chan <- samples:
@ -108,13 +108,13 @@ func setup_audio(mic chan []byte, speaker chan []int16) (*malgo.AllocatedContext
panic(err) panic(err)
} }
infos, err := ctx.Devices(malgo.Playback) infos, err := ctx.Devices(malgo.Playback)
if err != nil { if err != nil {
panic(err) panic(err)
} }
var playback_device *malgo.DeviceInfo = nil var playback_device *malgo.DeviceInfo = nil
for _, info := range infos { for _, info := range infos {
if info.IsDefault != 0 { if info.IsDefault != 0 {
full, err := ctx.DeviceInfo(malgo.Playback, info.ID, malgo.Shared) full, err := ctx.DeviceInfo(malgo.Playback, info.ID, malgo.Shared)
if err != nil { if err != nil {
@ -122,19 +122,19 @@ func setup_audio(mic chan []byte, speaker chan []int16) (*malgo.AllocatedContext
} }
playback_device = &full playback_device = &full
} }
} }
if playback_device == nil { if playback_device == nil {
panic("No default playback device") panic("No default playback device")
} }
infos, err = ctx.Devices(malgo.Capture) infos, err = ctx.Devices(malgo.Capture)
if err != nil { if err != nil {
panic(err) panic(err)
} }
var capture_device *malgo.DeviceInfo = nil var capture_device *malgo.DeviceInfo = nil
for _, info := range infos { for _, info := range infos {
if info.IsDefault != 0 { if info.IsDefault != 0 {
full, err := ctx.DeviceInfo(malgo.Capture, info.ID, malgo.Shared) full, err := ctx.DeviceInfo(malgo.Capture, info.ID, malgo.Shared)
if err != nil { if err != nil {
@ -142,33 +142,33 @@ func setup_audio(mic chan []byte, speaker chan []int16) (*malgo.AllocatedContext
} }
capture_device = &full capture_device = &full
} }
} }
if capture_device == nil { if capture_device == nil {
panic("No default capture device") panic("No default capture device")
} }
inDeviceConfig := malgo.DefaultDeviceConfig(malgo.Capture) inDeviceConfig := malgo.DefaultDeviceConfig(malgo.Capture)
inDeviceConfig.Capture.Format = malgo.FormatS16 inDeviceConfig.Capture.Format = malgo.FormatS16
inDeviceConfig.Capture.Channels = 1 inDeviceConfig.Capture.Channels = 1
inDeviceConfig.Capture.DeviceID = capture_device.ID.Pointer() inDeviceConfig.Capture.DeviceID = capture_device.ID.Pointer()
inDeviceConfig.SampleRate = 48000 inDeviceConfig.SampleRate = 48000
inDeviceConfig.PeriodSizeInFrames = 960 inDeviceConfig.PeriodSizeInFrames = 960
inDeviceConfig.Alsa.NoMMap = 1 inDeviceConfig.Alsa.NoMMap = 1
inDeviceConfig.Capture.ShareMode = malgo.Shared inDeviceConfig.Capture.ShareMode = malgo.Shared
outDeviceConfig := malgo.DefaultDeviceConfig(malgo.Playback) outDeviceConfig := malgo.DefaultDeviceConfig(malgo.Playback)
outDeviceConfig.Playback.Format = malgo.FormatS16 outDeviceConfig.Playback.Format = malgo.FormatS16
outDeviceConfig.Playback.Channels = 1 outDeviceConfig.Playback.Channels = 1
outDeviceConfig.Playback.DeviceID = playback_device.ID.Pointer() outDeviceConfig.Playback.DeviceID = playback_device.ID.Pointer()
outDeviceConfig.SampleRate = 48000 outDeviceConfig.SampleRate = 48000
outDeviceConfig.PeriodSizeInFrames = 960 outDeviceConfig.PeriodSizeInFrames = 960
outDeviceConfig.Alsa.NoMMap = 1 outDeviceConfig.Alsa.NoMMap = 1
outDeviceConfig.Playback.ShareMode = malgo.Shared outDeviceConfig.Playback.ShareMode = malgo.Shared
onSendFrames := func(output_samples []byte, input_samples []byte, framecount uint32) { onSendFrames := func(output_samples []byte, input_samples []byte, framecount uint32) {
select { select {
case pcm := <- speaker: case pcm := <-speaker:
for i := 0; i < sample_rate/50; i++ { for i := 0; i < sample_rate/50; i++ {
binary.LittleEndian.PutUint16(output_samples[i*2:], uint16(pcm[i])) binary.LittleEndian.PutUint16(output_samples[i*2:], uint16(pcm[i]))
} }
@ -176,9 +176,9 @@ func setup_audio(mic chan []byte, speaker chan []int16) (*malgo.AllocatedContext
} }
} }
playbackCallbacks := malgo.DeviceCallbacks{ playbackCallbacks := malgo.DeviceCallbacks{
Data: onSendFrames, Data: onSendFrames,
} }
outDevice, err := malgo.InitDevice(ctx.Context, outDeviceConfig, playbackCallbacks) outDevice, err := malgo.InitDevice(ctx.Context, outDeviceConfig, playbackCallbacks)
if err != nil { if err != nil {
@ -190,7 +190,7 @@ func setup_audio(mic chan []byte, speaker chan []int16) (*malgo.AllocatedContext
panic(err) panic(err)
} }
onRecvFrames := func(output_samples []byte, input_samples []byte, framecount uint32) { onRecvFrames := func(output_samples []byte, input_samples []byte, framecount uint32) {
if encoder != nil { if encoder != nil {
pcm := make([]int16, len(input_samples)/2) pcm := make([]int16, len(input_samples)/2)
for i := 0; i < len(input_samples)/2; i++ { for i := 0; i < len(input_samples)/2; i++ {
@ -207,21 +207,21 @@ func setup_audio(mic chan []byte, speaker chan []int16) (*malgo.AllocatedContext
default: default:
} }
} }
} }
captureCallbacks := malgo.DeviceCallbacks{ captureCallbacks := malgo.DeviceCallbacks{
Data: onRecvFrames, Data: onRecvFrames,
} }
inDevice, err := malgo.InitDevice(ctx.Context, inDeviceConfig, captureCallbacks) inDevice, err := malgo.InitDevice(ctx.Context, inDeviceConfig, captureCallbacks)
if err != nil { if err != nil {
panic(err) panic(err)
} }
err = inDevice.Start() err = inDevice.Start()
if err != nil { if err != nil {
panic(err) panic(err)
} }
return ctx, outDevice, inDevice return ctx, outDevice, inDevice
} }
@ -257,7 +257,7 @@ func get_private_key(path string, generate bool) ed25519.PrivateKey {
} }
key_pem := pem.EncodeToMemory(&pem.Block{ key_pem := pem.EncodeToMemory(&pem.Block{
Type: "PRIVATE KEY", Type: "PRIVATE KEY",
Bytes: key_pkcs8, Bytes: key_pkcs8,
}) })
@ -271,21 +271,21 @@ func get_private_key(path string, generate bool) ed25519.PrivateKey {
} }
} }
func print_decoration(window, title, channels, body ncurses.Window, top_left string) { func print_decoration(window, title, channels, body ncurses.Window, top_left string) {
max_y := ncurses.GetMaxY(*ncurses.CurScr) max_y := ncurses.GetMaxY(*ncurses.CurScr)
max_x := ncurses.GetMaxX(*ncurses.CurScr) max_x := ncurses.GetMaxX(*ncurses.CurScr)
ncurses.WResize(window, max_y, max_x) ncurses.WResize(window, max_y, max_x)
ncurses.WResize(title, 1, max_x/4-2) ncurses.WResize(title, 1, max_x/4-2)
ncurses.MvWin(title, 1, 1) ncurses.MvWin(title, 1, 1)
ncurses.WResize(channels, max_y - 4, (max_x / 4) - 2) ncurses.WResize(channels, max_y-4, (max_x/4)-1)
ncurses.MvWin(channels, 3, 1) ncurses.MvWin(channels, 3, 1)
ncurses.WResize(body, max_y - 2, max_x - (max_x / 4) - 2) ncurses.WResize(body, max_y-2, max_x-(max_x/4)-2)
ncurses.MvWin(body, 1, (max_x / 4) + 1) ncurses.MvWin(body, 1, (max_x/4)+1)
ncurses.MvWAddStr(title, 0, 0, top_left) ncurses.MvWAddStr(title, 0, 0, top_left)
for i := 1; i < max_x - 1; i++ { for i := 1; i < max_x-1; i++ {
ncurses.MvWAddStr(window, 0, i, "═") ncurses.MvWAddStr(window, 0, i, "═")
} }
@ -306,7 +306,7 @@ func print_decoration(window, title, channels, body ncurses.Window, top_left str
} }
for i := 1; i < max_x-1; i++ { for i := 1; i < max_x-1; i++ {
ncurses.MvWAddStr(window, max_y - 1, i, "═") ncurses.MvWAddStr(window, max_y-1, i, "═")
} }
ncurses.MvWAddStr(window, 0, 0, "╔") ncurses.MvWAddStr(window, 0, 0, "╔")
@ -324,12 +324,231 @@ func print_decoration(window, title, channels, body ncurses.Window, top_left str
ncurses.WRefresh(body) ncurses.WRefresh(body)
} }
type ChannelState struct {
ID pnyx.ChannelID
Name string
Members []pnyx.PeerID
Modes []pnyx.ModeID
}
type PeerState struct {
Name string
}
type UIState struct {
SelectedBody int
SelectedPeer int
SelectedChannel int
SelectedArea int
Channels []ChannelState
Peers map[pnyx.PeerID]PeerState
}
const (
AREA_CHANNELS = 0
AREA_BODY = 1
BODY_CHANNEL = 0
BODY_DETAIL = 1
BODY_QUIT = 2
)
func handle_back(state *UIState) {
if state.SelectedArea == AREA_BODY {
if state.SelectedBody == BODY_QUIT {
state.SelectedBody = BODY_CHANNEL
} else if state.SelectedBody == BODY_DETAIL {
state.SelectedBody = BODY_CHANNEL
}
state.SelectedArea = AREA_CHANNELS
}
}
func handle_up(state *UIState) {
if state.SelectedArea == AREA_CHANNELS {
if state.SelectedPeer > 0 {
state.SelectedPeer -= 1
} else if state.SelectedChannel > 0 {
state.SelectedChannel -= 1
state.SelectedPeer = len(state.Channels[state.SelectedChannel].Members)
}
}
}
func handle_down(state *UIState) {
if state.SelectedArea == AREA_CHANNELS {
if state.SelectedPeer < len(state.Channels[state.SelectedChannel].Members) {
state.SelectedPeer += 1
} else if state.SelectedChannel < len(state.Channels) - 1 {
state.SelectedChannel += 1
state.SelectedPeer = 0
}
}
}
func handle_select(state *UIState) {
if state.SelectedArea == AREA_CHANNELS {
state.SelectedArea = AREA_BODY
state.SelectedBody = BODY_CHANNEL
}
}
func handle_edit(state *UIState) {
if state.SelectedArea == AREA_CHANNELS {
state.SelectedArea = AREA_BODY
state.SelectedBody = BODY_DETAIL
}
}
func handle_quit(state *UIState) {
state.SelectedArea = AREA_BODY
state.SelectedBody = BODY_QUIT
}
func handle_input(state *UIState, char []byte) {
switch char[0] {
case 'q':
handle_quit(state)
case 'e':
handle_edit(state)
case '\r':
handle_select(state)
case 0x1B:
if len(char) == 1 {
handle_back(state)
} else {
switch char[1] {
case '[':
if len(char) == 2 {
} else {
switch char[2] {
case 'A': // Up
handle_up(state)
case 'B': // Down
handle_down(state)
case 'C': // Right
handle_select(state)
case 'D': // Left
handle_back(state)
}
}
}
}
}
}
func draw_channels(state *UIState, channels ncurses.Window) {
channel_highlight_color := 2
if state.SelectedArea == AREA_CHANNELS {
channel_highlight_color = 1
}
ncurses.WErase(channels)
ncurses.WMove(channels, 0, 0)
for i, channel_state := range(state.Channels) {
if i == state.SelectedChannel && state.SelectedPeer == 0 {
ncurses.WAttrOn(channels, ncurses.COLOR_PAIR(channel_highlight_color))
}
ncurses.WAddStr(channels, channel_state.Name)
ncurses.WAddCh(channels, '\n')
for j, peer_id := range(channel_state.Members) {
ncurses.WAddStr(channels, " ↳")
peer_state, found := state.Peers[peer_id]
if state.SelectedChannel == i && state.SelectedPeer == (j+1) {
ncurses.WAttrOn(channels, ncurses.COLOR_PAIR(channel_highlight_color))
}
if found {
ncurses.WAddStr(channels, peer_state.Name)
} else {
ncurses.WAddStr(channels, fmt.Sprintf("%s", peer_id))
}
if state.SelectedChannel == i && state.SelectedPeer == (j+1) {
ncurses.WAttrOff(channels, ncurses.COLOR_PAIR(channel_highlight_color))
}
ncurses.WAddCh(channels, '\n')
}
if i == state.SelectedChannel && state.SelectedPeer == 0 {
ncurses.WAttrOff(channels, ncurses.COLOR_PAIR(channel_highlight_color))
}
}
ncurses.WRefresh(channels)
}
func draw_body_channel(state *UIState, body ncurses.Window) {
ncurses.WErase(body)
ncurses.MvWAddStr(body, 0, 0, "channel")
ncurses.WRefresh(body)
}
func draw_body_detail(state *UIState, body ncurses.Window) {
ncurses.WErase(body)
ncurses.MvWAddStr(body, 0, 0, "detail")
ncurses.WRefresh(body)
}
func draw_body_quit(state *UIState, body ncurses.Window) {
ncurses.WErase(body)
ncurses.MvWAddStr(body, 0, 0, "quit")
ncurses.WRefresh(body)
}
func draw_gui(state *UIState, channels, body ncurses.Window) {
draw_channels(state, channels)
switch state.SelectedBody {
case BODY_CHANNEL:
draw_body_channel(state, body)
case BODY_DETAIL:
draw_body_detail(state, body)
case BODY_QUIT:
draw_body_quit(state, body)
}
}
func main_loop(client *pnyx.Client, audio_data chan []int16, window ncurses.Window, packet_chan chan pnyx.Payload, user_chan chan []byte, sigwinch_channel chan os.Signal) { func main_loop(client *pnyx.Client, audio_data chan []int16, window ncurses.Window, packet_chan chan pnyx.Payload, user_chan chan []byte, sigwinch_channel chan os.Signal) {
channels := ncurses.NewWin(0, 0, 0, 0) channels := ncurses.NewWin(0, 0, 0, 0)
body := ncurses.NewWin(0, 0, 0, 0) body := ncurses.NewWin(0, 0, 0, 0)
title := ncurses.NewWin(0, 0, 0, 0) title := ncurses.NewWin(0, 0, 0, 0)
state := &UIState{
Peers: map[pnyx.PeerID]PeerState{
[32]byte{}: {
Name: "Test User 0",
},
[32]byte{0x01}: {
Name: "Test User 1",
},
[32]byte{0x02}: {
Name: "Test User 2",
},
},
Channels: []ChannelState{
{
ID: 0,
Name: "Channel 0",
Members: []pnyx.PeerID{[32]byte{}, [32]byte{0x01}},
},
{
ID: 1,
Name: "Channel 1",
Members: []pnyx.PeerID{[32]byte{0x02}},
},
},
}
ncurses.InitColor(1, 1000, 1000, 1000)
ncurses.InitColor(2, 300, 300, 300)
ncurses.InitColor(3, 150, 150, 150)
ncurses.InitColor(4, 150, 150, 300)
ncurses.InitPair(1, 1, 2)
ncurses.InitPair(2, 1, 3)
ncurses.InitPair(3, 1, 4)
ncurses.ScrollOk(body, true)
print_decoration(window, title, channels, body, client.Remote()) print_decoration(window, title, channels, body, client.Remote())
draw_gui(state, channels, body)
for client.Active() { for client.Active() {
select { select {
@ -338,6 +557,7 @@ func main_loop(client *pnyx.Client, audio_data chan []int16, window ncurses.Wind
ncurses.WRefresh(window) ncurses.WRefresh(window)
ncurses.WClear(window) ncurses.WClear(window)
print_decoration(window, title, channels, body, client.Remote()) print_decoration(window, title, channels, body, client.Remote())
draw_gui(state, channels, body)
case packet := <-packet_chan: case packet := <-packet_chan:
switch packet := packet.(type) { switch packet := packet.(type) {
case pnyx.ChannelCommandPacket: case pnyx.ChannelCommandPacket:
@ -366,7 +586,7 @@ func main_loop(client *pnyx.Client, audio_data chan []int16, window ncurses.Wind
decode_chan, exists := decoders[packet.Peer] decode_chan, exists := decoders[packet.Peer]
if exists == false { if exists == false {
if sample_rate != 0 { if sample_rate != 0 {
decode_chan = make(chan[]byte, 1000) decode_chan = make(chan []byte, 1000)
decoders[packet.Peer] = decode_chan decoders[packet.Peer] = decode_chan
go handle_peer_decode(audio_data, packet.Peer, decoders[packet.Peer], sample_rate) go handle_peer_decode(audio_data, packet.Peer, decoders[packet.Peer], sample_rate)
decode_chan <- packet.Data decode_chan <- packet.Data
@ -379,15 +599,15 @@ func main_loop(client *pnyx.Client, audio_data chan []int16, window ncurses.Wind
} }
} }
case char := <-user_chan: case char := <-user_chan:
ncurses.WAddStr(body, string(char)) handle_input(state, char)
ncurses.WRefresh(body) draw_gui(state, channels, body)
} }
} }
} }
func process_mic(client *pnyx.Client, mic chan []byte) { func process_mic(client *pnyx.Client, mic chan []byte) {
for true { for true {
data := <- mic data := <-mic
err := client.Send(pnyx.NewDataPacket(pnyx.ChannelID(0), pnyx.MODE_AUDIO, data)) err := client.Send(pnyx.NewDataPacket(pnyx.ChannelID(0), pnyx.MODE_AUDIO, data))
if err != nil { if err != nil {
panic(err) panic(err)
@ -415,7 +635,6 @@ func main() {
defer inDevice.Uninit() defer inDevice.Uninit()
defer inDevice.Stop() defer inDevice.Stop()
user_chan := make(chan []byte, 1024) user_chan := make(chan []byte, 1024)
packet_chan := make(chan pnyx.Payload, 1024) packet_chan := make(chan pnyx.Payload, 1024)
@ -431,7 +650,7 @@ func main() {
if err != nil { if err != nil {
panic(err) panic(err)
} }
join_packet := pnyx.NewChannelCommandPacket(uuid.New(), pnyx.ChannelID(0), pnyx.MODE_CHANNEL, pnyx.CHANNEL_COMMAND_JOIN, nil) join_packet := pnyx.NewChannelCommandPacket(uuid.New(), pnyx.ChannelID(0), pnyx.MODE_CHANNEL, pnyx.CHANNEL_COMMAND_JOIN, nil)
err = client.Send(join_packet) err = client.Send(join_packet)
if err != nil { if err != nil {
@ -446,11 +665,11 @@ func main() {
go process_mic(client, mic) go process_mic(client, mic)
locale := ncurses.SetLocale(0, "") ncurses.SetLocale(0, "")
fmt.Printf("locale: %s\n", locale)
user_chan, stdin_active := ncurses.UTF8Listener(100, os.Stdin) user_chan, stdin_active := ncurses.UTF8Listener(100, os.Stdin)
window := ncurses.InitScr() window := ncurses.InitScr()
ncurses.CursSet(0)
ret := ncurses.StartColor() ret := ncurses.StartColor()
if ret != 0 { if ret != 0 {
panic(ret) panic(ret)

@ -1 +1 @@
Subproject commit 6b121ac061f1182cf5adf8d540cf3d5d6aae8c0b Subproject commit 14b1c310e749554c5e290108e50190cbe71b2d23

@ -36,11 +36,16 @@ func(session *ServerSession) Send(payload Payload) {
} }
} }
type SessionPayload struct {
Session *ServerSession
Payload Payload
}
type Server struct { type Server struct {
key ed25519.PrivateKey key ed25519.PrivateKey
active atomic.Bool active atomic.Bool
connection *net.UDPConn connection *net.UDPConn
commands chan Payload commands chan SessionPayload
threads sync.WaitGroup threads sync.WaitGroup
@ -80,7 +85,7 @@ func NewServer(listen string, key ed25519.PrivateKey, channels map[ChannelID]*Ch
key: key, key: key,
connection: connection, connection: connection,
active: atomic.Bool{}, active: atomic.Bool{},
commands: make(chan Payload, SERVER_COMMAND_BUFFER_SIZE), commands: make(chan SessionPayload, SERVER_COMMAND_BUFFER_SIZE),
sessions: map[SessionID]*ServerSession{}, sessions: map[SessionID]*ServerSession{},
channels: atomic.Value{}, channels: atomic.Value{},
@ -198,7 +203,10 @@ func handle_session_incoming(session *ServerSession, server *Server) {
switch packet := packet.(type) { switch packet := packet.(type) {
case CommandPacket: case CommandPacket:
server.commands<-packet server.commands<-SessionPayload{
Session: session,
Payload: packet,
}
case ChannelCommandPacket: case ChannelCommandPacket:
channels := server.channels.Load().(map[ChannelID]*Channel) channels := server.channels.Load().(map[ChannelID]*Channel)
channel, exists := channels[packet.Channel] channel, exists := channels[packet.Channel]
@ -380,7 +388,7 @@ func(server *Server) update_state() {
for server.active.Load() { for server.active.Load() {
select { select {
case command := <-server.commands: case command := <-server.commands:
if command == nil { if command.Payload == nil {
break break
} }
server.Log("Incoming server command %+v", command) server.Log("Incoming server command %+v", command)