hermeticum/client/cmd/main.go

334 lines
8.3 KiB
Go
Raw Normal View History

2022-07-07 22:12:22 +00:00
package main
import (
2022-07-08 04:07:34 +00:00
"context"
"errors"
"flag"
2022-07-07 22:12:22 +00:00
"fmt"
2022-07-09 07:15:24 +00:00
"io"
2022-07-07 22:12:22 +00:00
"log"
2022-12-30 04:01:12 +00:00
"os"
"os/signal"
2022-07-22 21:10:12 +00:00
"strings"
2022-07-08 04:07:34 +00:00
"time"
2022-07-22 21:10:12 +00:00
"github.com/gdamore/tcell/v2"
2022-07-08 05:18:52 +00:00
"github.com/rivo/tview"
2022-07-08 04:07:34 +00:00
"github.com/vilmibm/hermeticum/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
var (
tls = flag.Bool("tls", false, "Connection uses TLS if true, else plain TCP")
caFile = flag.String("ca_file", "", "The file containing the CA root cert file")
serverAddr = flag.String("addr", "localhost:6666", "The server address in the format of host:port")
serverHostOverride = flag.String("server_host_override", "x.test.example.com", "The server name used to verify the hostname returned by the TLS handshake")
2022-07-07 22:12:22 +00:00
)
2022-07-22 21:57:15 +00:00
type ClientState struct {
App *tview.Application
Client proto.GameWorldClient
SessionInfo *proto.SessionInfo
MaxMessages int
messagesView *tview.TextView
messages []*proto.ClientMessage
cmdStream proto.GameWorld_CommandsClient
}
func (cs *ClientState) Messages() error {
ctx, cancel := context.WithCancel(context.Background())
2022-07-09 07:15:24 +00:00
defer cancel()
stream, err := cs.Client.Messages(ctx, cs.SessionInfo)
if err != nil {
return err
}
for {
msg, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
return err
}
cs.AddMessage(msg)
}
return nil
}
2022-12-30 04:01:12 +00:00
func (cs *ClientState) HandleSIGINT(sigC chan os.Signal) {
for range sigC {
cm := &proto.Command{
SessionInfo: cs.SessionInfo,
Verb: "quit",
}
err := cs.cmdStream.Send(cm)
if err != nil {
fmt.Printf("failed to send quit verb to server: %s\n", err.Error())
}
_, err = cs.cmdStream.Recv()
if err != nil {
fmt.Printf("failed to receive an ACK from server: %s\n", err.Error())
}
cs.App.Stop()
}
}
2022-07-22 21:10:12 +00:00
func (cs *ClientState) HandleInput(input string) error {
var verb string
rest := input
if strings.HasPrefix(input, "/") {
2022-12-30 00:41:11 +00:00
verb, rest, _ = strings.Cut(input[1:], " ")
2022-07-22 21:10:12 +00:00
} else {
verb = "say"
}
cmd := &proto.Command{
SessionInfo: cs.SessionInfo,
Verb: verb,
Rest: rest,
}
// TODO I'm punting on handling CommandAcks for now but it will be a nice UX thing later for showing connectivity problems
2022-07-28 02:05:48 +00:00
err := cs.cmdStream.Send(cmd)
if err != nil {
return err
}
2022-12-30 04:01:12 +00:00
_, err = cs.cmdStream.Recv()
if err != nil {
fmt.Printf("failed to receive an ACK from server: %s\n", err.Error())
}
2022-07-28 02:05:48 +00:00
if verb == "quit" || verb == "q" {
cs.App.Stop()
}
return nil
2022-07-22 21:10:12 +00:00
}
func (cs *ClientState) InitCommandStream() error {
ctx := context.Background()
stream, err := cs.Client.Commands(ctx)
if err != nil {
return err
}
cs.cmdStream = stream
return nil
2022-07-09 07:15:24 +00:00
}
func (cs *ClientState) AddMessage(msg *proto.ClientMessage) {
// TODO i don't like this function
cs.messages = append(cs.messages, msg)
if len(cs.messages) > cs.MaxMessages {
cs.messages = cs.messages[1 : len(cs.messages)-1]
}
// TODO look into using the SetChangedFunc thing.
2022-07-09 07:15:24 +00:00
cs.App.QueueUpdateDraw(func() {
// TODO trim content of messagesView /or/ see if tview has a buffer size that does it for me. use cs.messages to re-constitute.
switch msg.Type {
case proto.ClientMessage_OVERHEARD:
fmt.Fprintf(cs.messagesView, "%s: %s\n", msg.GetSpeaker(), msg.GetText())
case proto.ClientMessage_EMOTE:
fmt.Fprintf(cs.messagesView, "%s %s\n", msg.GetSpeaker(), msg.GetText())
default:
fmt.Fprintf(cs.messagesView, "%#v\n", msg)
}
cs.messagesView.ScrollToEnd()
2022-07-09 07:15:24 +00:00
})
}
2022-07-07 22:12:22 +00:00
func _main() error {
2022-07-08 04:07:34 +00:00
var opts []grpc.DialOption
if *tls {
return errors.New("TODO tls unsupported")
/*
if *caFile == "" {
*caFile = data.Path("x509/ca_cert.pem")
}
creds, err := credentials.NewClientTLSFromFile(*caFile, *serverHostOverride)
if err != nil {
log.Fatalf("Failed to create TLS credentials %v", err)
}
opts = append(opts, grpc.WithTransportCredentials(creds))
*/
} else {
opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
}
conn, err := grpc.Dial(*serverAddr, opts...)
if err != nil {
return fmt.Errorf("fail to dial: %w", err)
}
defer conn.Close()
client := proto.NewGameWorldClient(conn)
2022-07-09 07:15:24 +00:00
app := tview.NewApplication()
// TODO make a NewClientState
2022-07-28 03:30:23 +00:00
// TODO rename this, like, UI
2022-07-09 07:15:24 +00:00
cs := &ClientState{
App: app,
SessionInfo: &proto.SessionInfo{},
Client: client,
MaxMessages: 15, // TODO for testing
messages: []*proto.ClientMessage{},
}
2022-07-22 21:10:12 +00:00
err = cs.InitCommandStream()
if err != nil {
return fmt.Errorf("could not create command stream: %w", err)
}
2022-07-08 04:07:34 +00:00
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
2022-12-30 04:01:12 +00:00
if _, err = cs.Client.Ping(ctx, cs.SessionInfo); err != nil {
2022-07-09 07:15:24 +00:00
log.Fatalf("%v.Ping -> %v", cs.Client, err)
2022-07-08 04:07:34 +00:00
}
2022-07-07 22:12:22 +00:00
2022-07-08 05:18:52 +00:00
pages := tview.NewPages()
pages.AddPage("splash",
tview.NewModal().
AddButtons([]string{"hey. let's go"}).
SetDoneFunc(func(_ int, _ string) {
pages.SwitchToPage("main")
2022-07-08 05:56:47 +00:00
app.ResizeToFullScreen(pages)
2022-07-08 05:18:52 +00:00
}).SetText("h e r m e t i c u m"),
2022-07-08 05:56:47 +00:00
true,
2022-07-08 05:18:52 +00:00
true)
2022-07-22 21:10:12 +00:00
commandInput := tview.NewInputField().SetLabel("> ")
handleInput := func(_ tcell.Key) {
input := commandInput.GetText()
2022-12-14 07:52:29 +00:00
// TODO command history
commandInput.SetText("")
2022-07-22 21:10:12 +00:00
// TODO do i need to clear the input's text?
go cs.HandleInput(input)
}
commandInput.SetDoneFunc(handleInput)
2022-07-08 05:18:52 +00:00
mainPage := tview.NewList().
2022-07-08 05:56:47 +00:00
AddItem("jack in", "connect using an existing account", '1', func() {
pages.SwitchToPage("login")
2022-07-08 05:56:47 +00:00
}).
2022-07-10 06:10:02 +00:00
AddItem("rez a toon", "create a new account", '2', func() {
pages.SwitchToPage("register")
}).
2022-07-08 05:18:52 +00:00
AddItem("open the hood", "client configuration", '3', nil).
AddItem("get outta here", "quit the client", '4', func() {
app.Stop()
})
2022-07-08 05:56:47 +00:00
pages.AddPage("main", mainPage, true, false)
2022-12-30 04:01:12 +00:00
sigC := make(chan os.Signal, 1)
signal.Notify(sigC, os.Interrupt)
lunfi := tview.NewInputField().SetLabel("account name")
lpwfi := tview.NewInputField().SetLabel("password").SetMaskCharacter('~')
2022-07-10 06:10:02 +00:00
loginPage := tview.NewForm().AddFormItem(lunfi).AddFormItem(lpwfi).
2022-07-10 06:10:02 +00:00
SetCancelFunc(func() {
pages.SwitchToPage("main")
})
loginSubmitFn := func() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
si, err := cs.Client.Login(ctx, &proto.AuthInfo{
Username: lunfi.GetText(),
Password: lpwfi.GetText(),
})
if err != nil {
panic(err.Error())
}
cs.SessionInfo = si
pages.SwitchToPage("game")
2022-07-22 21:10:12 +00:00
app.SetFocus(commandInput)
2022-12-30 04:01:12 +00:00
go cs.HandleSIGINT(sigC)
2022-07-22 21:57:15 +00:00
// TODO error handle
go cs.Messages()
}
2022-07-22 19:46:54 +00:00
// TODO login and register pages should refuse blank entries
// TODO password should have rules
loginPage.AddButton("gimme that shit", loginSubmitFn)
loginPage.AddButton("nah get outta here", func() {
pages.SwitchToPage("main")
})
pages.AddPage("login", loginPage, true, false)
runfi := tview.NewInputField().SetLabel("account name")
rpwfi := tview.NewInputField().SetLabel("password").SetMaskCharacter('~')
registerPage := tview.NewForm().AddFormItem(runfi).AddFormItem(rpwfi).
SetCancelFunc(func() {
pages.SwitchToPage("main")
})
registerSubmitFn := func() {
2022-07-10 06:10:02 +00:00
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
si, err := cs.Client.Register(ctx, &proto.AuthInfo{
Username: runfi.GetText(),
Password: rpwfi.GetText(),
2022-07-10 06:10:02 +00:00
})
if err != nil {
panic(err.Error())
}
cs.SessionInfo = si
pages.SwitchToPage("game")
2022-07-28 03:30:23 +00:00
app.SetFocus(commandInput)
2022-12-30 04:01:12 +00:00
go cs.HandleSIGINT(sigC)
2022-07-28 03:30:23 +00:00
// TODO error handle
go cs.Messages()
2022-07-10 06:10:02 +00:00
}
registerPage.AddButton("gimme that shit", registerSubmitFn)
2022-07-10 06:10:02 +00:00
registerPage.AddButton("nah get outta here", func() {
pages.SwitchToPage("main")
})
pages.AddPage("register", registerPage, true, false)
msgView := tview.NewTextView().SetScrollable(true).SetWrap(true).SetWordWrap(true)
2022-07-09 07:15:24 +00:00
cs.messagesView = msgView
2022-07-08 05:56:47 +00:00
gamePage := tview.NewGrid().
SetRows(1, 40, 3).
SetColumns(-1, -1).
SetBorders(true).
AddItem(
tview.NewTextView().SetTextAlign(tview.AlignLeft).SetText("h e r m e t i c u m"),
0, 0, 1, 1, 1, 1, false).
AddItem(
tview.NewTextView().SetTextAlign(tview.AlignRight).SetText("TODO server status"),
0, 1, 1, 1, 1, 1, false).
AddItem(
2022-07-09 07:15:24 +00:00
msgView,
2022-07-08 05:56:47 +00:00
1, 0, 1, 1, 10, 20, false).
AddItem(
2022-07-22 21:10:12 +00:00
tview.NewTextView().SetText("TODO details"),
2022-07-08 05:56:47 +00:00
1, 1, 1, 1, 10, 10, false).
AddItem(
2022-07-22 21:10:12 +00:00
commandInput,
2022-07-08 05:56:47 +00:00
2, 0, 1, 2, 1, 30, false)
pages.AddPage("game", gamePage, true, false)
2022-07-08 05:18:52 +00:00
return app.SetRoot(pages, true).SetFocus(pages).Run()
2022-07-07 22:12:22 +00:00
}
func main() {
err := _main()
if err != nil {
log.Fatal(err.Error())
}
}