Add basic command parsing and handling

This commit is contained in:
Trevor Slocum 2023-08-03 22:29:54 -07:00
parent a74df0d5d2
commit 17056eb77e
7 changed files with 260 additions and 37 deletions

View file

@ -1,7 +1,5 @@
package bgammon
type Client interface {
ReadCommand() (Command, error)
WriteEvent() (Event, error)
Terminate() error
Terminate(reason string) error
}

View file

@ -0,0 +1,6 @@
package main
type account struct {
id int
username string
}

View file

@ -0,0 +1,14 @@
package main
import "code.rocket9labs.com/tslocum/bgammon"
type serverClient struct {
id int
name []byte
account int
connected int64
lastActive int64
commands <-chan []byte
events chan<- []byte
bgammon.Client
}

View file

@ -0,0 +1,55 @@
package main
import (
"bufio"
"fmt"
"log"
"net"
"code.rocket9labs.com/tslocum/bgammon"
)
var _ bgammon.Client = &socketClient{}
type socketClient struct {
conn net.Conn
events <-chan []byte
commands chan<- []byte
}
func newSocketClient(conn net.Conn, commands chan<- []byte, events <-chan []byte) *socketClient {
c := &socketClient{
conn: conn,
events: events,
commands: commands,
}
go c.readCommands()
go c.writeEvents()
return c
}
func (c *socketClient) readCommands() {
var scanner = bufio.NewScanner(c.conn)
for scanner.Scan() {
log.Printf("READ COMMAND %s", scanner.Text())
buf := make([]byte, len(scanner.Bytes()))
copy(buf, scanner.Bytes())
c.commands <- buf
}
}
func (c *socketClient) writeEvents() {
var event []byte
for event = range c.events {
c.conn.Write(event)
c.conn.Write([]byte("\n"))
log.Printf("write event %s", event)
}
}
func (c *socketClient) Terminate(reason string) error {
c.conn.Write([]byte(fmt.Sprintf("Connection closed: %s\n", reason)))
c.conn.Close()
return nil
}

View file

@ -1,19 +1,17 @@
package main
import (
"bytes"
"fmt"
"log"
"math/rand"
"net"
"strings"
"time"
"code.rocket9labs.com/tslocum/bgammon"
)
type serverClient struct {
id int
connected int64
lastActive int64
bgammon.Client
}
type serverGame struct {
id int
created int64
@ -23,14 +21,28 @@ type serverGame struct {
bgammon.Game
}
type serverCommand struct {
client *serverClient
command []byte
}
type server struct {
clients []*serverClient
games []*serverGame
listeners []net.Listener
clients []*serverClient
games []*serverGame
listeners []net.Listener
newClientIDs chan int
commands chan serverCommand
}
func newServer() *server {
return &server{}
const bufferSize = 10
s := &server{
newClientIDs: make(chan int),
commands: make(chan serverCommand, bufferSize),
}
go s.handleNewClientIDs()
go s.handleCommands()
return s
}
func (s *server) listen(network string, address string) {
@ -55,4 +67,154 @@ func (s *server) handleListener(listener net.Listener) {
func (s *server) handleConnection(conn net.Conn) {
log.Printf("new conn %+v", conn)
const bufferSize = 8
commands := make(chan []byte, bufferSize)
events := make(chan []byte, bufferSize)
now := time.Now().Unix()
c := &serverClient{
id: <-s.newClientIDs,
account: -1,
connected: now,
lastActive: now,
commands: commands,
events: events,
Client: newSocketClient(conn, commands, events),
}
log.Println("socket client", c)
s.handleClientCommands(c)
}
func (s *server) handleClientCommands(c *serverClient) {
var command []byte
for command = range c.commands {
s.commands <- serverCommand{
client: c,
command: command,
}
}
}
func (s *server) handleNewClientIDs() {
clientID := 1
for {
s.newClientIDs <- clientID
clientID++
}
}
func (s *server) randomUsername() []byte {
// TODO check if username is available
i := 100 + rand.Intn(900)
return []byte(fmt.Sprintf("Guest%d", i))
}
func (s *server) sendWelcome(c *serverClient) {
c.events <- []byte(fmt.Sprintf("welcome %s there are %d clients playing %d games", c.name, len(s.clients), len(s.games)))
}
func (s *server) handleCommands() {
var cmd serverCommand
for cmd = range s.commands {
if cmd.client == nil {
log.Panicf("nil client with command %s", cmd.command)
}
cmd.command = bytes.TrimSpace(cmd.command)
firstSpace := bytes.IndexByte(cmd.command, ' ')
var keyword string
var startParameters int
if firstSpace == -1 {
keyword = string(cmd.command)
startParameters = len(cmd.command)
} else {
keyword = string(cmd.command[:firstSpace])
startParameters = firstSpace + 1
}
if keyword == "" {
continue
}
keyword = strings.ToLower(keyword)
log.Printf("server client %+v command %s with keyword %s", cmd.client, cmd.command, keyword)
params := bytes.Fields(cmd.command[startParameters:])
log.Printf("params %+v", params)
// Require users to send login command first.
if cmd.client.account == -1 {
if keyword == bgammon.CommandLogin {
var username []byte
var password []byte
switch len(params) {
case 0:
username = s.randomUsername()
case 1:
username = params[0]
default:
username = params[0]
password = bytes.Join(params[1:], []byte(" "))
}
if len(password) > 0 {
cmd.client.account = 1
} else {
cmd.client.account = 0
}
cmd.client.name = username
s.sendWelcome(cmd.client)
log.Printf("login as %s - %s", username, password)
continue
} else {
cmd.client.Terminate("You must login before using other commands.")
continue
}
}
switch keyword {
case bgammon.CommandSay:
if len(params) == 0 {
continue
}
case bgammon.CommandList:
case bgammon.CommandCreate:
sendUsage := func() {
cmd.client.events <- []byte("notice To create a public game specify whether it is public or private.")
cmd.client.events <- []byte("notice When creating a private game, a password must be set.")
}
if len(params) == 0 {
sendUsage()
continue
}
var gamePassword []byte
gameType := bytes.ToLower(params[0])
var gameName []byte
switch {
case bytes.Equal(gameType, []byte("public")):
gameName = bytes.Join(params[1:], []byte(" "))
case bytes.Equal(gameType, []byte("private")):
if len(params) < 2 {
sendUsage()
continue
}
gamePassword = params[1]
gameName = bytes.Join(params[2:], []byte(" "))
default:
sendUsage()
continue
}
log.Printf("create game (password %s) name: %s", gamePassword, gameName)
case bgammon.CommandJoin:
case bgammon.CommandRoll:
case bgammon.CommandMove:
default:
log.Printf("unknown command %s", keyword)
}
}
}

View file

@ -2,16 +2,14 @@ package bgammon
// commands are always sent TO the server
type Command struct {
Type int
}
type Command string
type CommandChat struct {
Command
Message string
}
type CommandMove struct {
Event
Spaces []int // One or more sets of moves from A->B as A,B,A,B,A,B...
}
const (
CommandLogin = "login" // Log in with username and password, or as a guest.
CommandSay = "say" // Send chat message.
CommandList = "list" // List available games.
CommandCreate = "create" // Create game.
CommandJoin = "join" // Join game.
CommandRoll = "roll" // Roll dice.
CommandMove = "move" // Move checkers.
)

View file

@ -3,16 +3,6 @@ package bgammon
// events are always received FROM the server
type Event struct {
Type int
Player int
}
type EventChat struct {
Event
Message string
}
type EventMove struct {
Event
Spaces []int // One or more sets of moves from A->B as A,B,A,B,A,B...
Command
}