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 {
2022-07-09 07:29:08 +00:00
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 ]
}
2022-07-09 07:29:08 +00:00
// TODO look into using the SetChangedFunc thing.
2022-07-09 07:15:24 +00:00
cs . App . QueueUpdateDraw ( func ( ) {
2022-07-09 07:29:08 +00:00
// TODO trim content of messagesView /or/ see if tview has a buffer size that does it for me. use cs.messages to re-constitute.
2022-12-29 05:22:18 +00:00
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 )
}
2022-07-09 07:29:08 +00:00
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 ( ) {
2022-07-16 06:54:18 +00:00
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 )
2022-07-16 06:54:18 +00:00
lunfi := tview . NewInputField ( ) . SetLabel ( "account name" )
lpwfi := tview . NewInputField ( ) . SetLabel ( "password" ) . SetMaskCharacter ( '~' )
2022-07-10 06:10:02 +00:00
2022-07-16 06:54:18 +00:00
loginPage := tview . NewForm ( ) . AddFormItem ( lunfi ) . AddFormItem ( lpwfi ) .
2022-07-10 06:10:02 +00:00
SetCancelFunc ( func ( ) {
pages . SwitchToPage ( "main" )
} )
2022-07-16 06:54:18 +00:00
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-16 06:54:18 +00:00
}
2022-07-22 19:46:54 +00:00
// TODO login and register pages should refuse blank entries
// TODO password should have rules
2022-07-16 06:54:18 +00:00
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 ( )
2022-07-16 05:56:12 +00:00
si , err := cs . Client . Register ( ctx , & proto . AuthInfo {
2022-07-16 06:54:18 +00:00
Username : runfi . GetText ( ) ,
Password : rpwfi . GetText ( ) ,
2022-07-10 06:10:02 +00:00
} )
2022-07-16 05:56:12 +00:00
if err != nil {
panic ( err . Error ( ) )
}
2022-07-16 06:54:18 +00:00
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
}
2022-07-16 06:54:18 +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 )
2022-07-09 07:29:08 +00:00
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 ( ) )
}
}