player.go 4.96 KB
Newer Older
1
2
3
4
5
package main

import (
	tg "fahrradwurstmond2.0/telnet_gui"
	"log"
Michael Gebhard's avatar
go fmt    
Michael Gebhard committed
6
	"net"
7
	"os"
Michael Gebhard's avatar
go fmt    
Michael Gebhard committed
8
	"time"
ceddral's avatar
ceddral committed
9
10
	"fmt"
	"regexp"
11
	//"errors"
12
13
)

ceddral's avatar
ceddral committed
14
15
16
17
18
const (
	nameMaxLen = 16
	nameWhitelistChars = "a-zA-Z0-9_"
)

ceddral's avatar
ceddral committed
19
var (
ceddral's avatar
ceddral committed
20
	nameWhitelist = regexp.MustCompile("[^"+nameWhitelistChars+"]")
ceddral's avatar
ceddral committed
21
22
)

23
24
type Context interface {
	Register(*Player)
ceddral's avatar
ceddral committed
25
	Unregister(string)
ceddral's avatar
ceddral committed
26
	Resize(int, int, *Player)
ceddral's avatar
ceddral committed
27
28
29
	Redraw(*Player)
	Lock()
	Unlock()
30
31
32
}

type Player struct {
Michael Gebhard's avatar
go fmt    
Michael Gebhard committed
33
34
	tnState    *TelnetState
	conn       net.Conn
35
	connClosed chan struct{}
36
	inputEvent chan []byte
Michael Gebhard's avatar
go fmt    
Michael Gebhard committed
37
	inputKill  chan struct{}
38
	windowSizeLoopKill  chan struct{}
Michael Gebhard's avatar
go fmt    
Michael Gebhard committed
39
40
41
	name       string
	context    Context
	scr        *tg.Screen
42
43
44
}

func NewPlayer(conn net.Conn) (p *Player) {
45
46
47
	tcpConn := conn.(*net.TCPConn)
	tcpConn.SetKeepAlivePeriod(time.Second)
	tcpConn.SetKeepAlive(true)
48
49
	var err error
	p = &Player{
Michael Gebhard's avatar
go fmt    
Michael Gebhard committed
50
51
		conn:       conn,
		tnState:    NewTelnetState(),
52
		inputEvent: make(chan []byte, 50),
Michael Gebhard's avatar
go fmt    
Michael Gebhard committed
53
		inputKill:  make(chan struct{}),
54
	}
55
	go p.inputLoop()
56
	p.name, err = queryUsername(p.conn, p.tnState)
57
58
59
	if err != nil {
		log.Println(err)
	}
ceddral's avatar
ceddral committed
60
61
62
63
64
65
66
67
68
	reason := ""
	if len(p.name) == 0 {
		reason = "Must set username. `telnet -l <name>`\n"
	} else if len(p.name) > nameMaxLen {
		reason = fmt.Sprintf("Username must contain at most %d characters.\n", nameMaxLen)
	} else if len(nameWhitelist.ReplaceAllString(p.name, "")) != len(p.name) {
		reason = fmt.Sprintf("Username must consist only of %s\n", nameWhitelistChars)
	}
	if reason != "" {
ceddral's avatar
ceddral committed
69
70
71
		p.inputKill <- struct{}{}
		close(p.inputKill)
		close(p.inputEvent)
ceddral's avatar
ceddral committed
72
		conn.Write([]byte(reason))
ceddral's avatar
ceddral committed
73
74
75
		conn.Close()
		return nil
	}
Jonny Schäfer's avatar
Jonny Schäfer committed
76

77
	if pOld, ok := gs.players[p.name]; ok {
Jonny Schäfer's avatar
Jonny Schäfer committed
78
		// reconnect with replace
79
		pNew := p
80
		// check if old player is still responsive
81
82
83
84
		select {
		case <-pOld.connClosed:
			// continue
		default:
85
86
87
88
			// old player is responsive, disconnect imposter
			pNew.inputKill <- struct{}{}
			close(pNew.inputKill)
			close(pNew.inputEvent)
ceddral's avatar
ceddral committed
89
			conn.Write([]byte(fmt.Sprintf("Username %s already taken\n", pNew.name)))
90
			conn.Close()
ceddral's avatar
ceddral committed
91
			log.Printf("%s tried to connect twice", p.name)
92
93
			return nil
		}
94
95
96

		// kill old player components to be replaced
		pOld.inputKill <- struct{}{}
97
		pOld.conn.Close()
98
99
100
101
102
103
104

		// clean up new player input, was only needed to query username
		pNew.inputKill <- struct{}{}
		close(pNew.inputKill)
		close(pNew.inputEvent)

		// assemble reconnected player
105
		p = pOld
106
		// no inputLoop for pOld and pNew may be running during these modifications to p
107
108
		p.conn = pNew.conn
		p.scr.Chown(conn)
109
		go p.inputLoop()
ceddral's avatar
ceddral committed
110
		log.Printf("%s reconnected\n", p.name)
111
	} else {
112
		p.windowSizeLoopKill = make(chan struct{})
ceddral's avatar
ceddral committed
113
		p.connClosed = make(chan struct{}, 1)
ceddral's avatar
ceddral committed
114
115
		// TODO new players need to be sent to the meta lobby context
		p.context = glgl
116
		p.scr = tg.NewScreenEmpty(conn, tg.ColorDefault, tg.ColorDefault)
ceddral's avatar
ceddral committed
117
		go p.windowSizeLoop()
118
		gs.players[p.name] = p
ceddral's avatar
ceddral committed
119
		log.Printf("%s connected\n", p.name)
120
	}
ceddral's avatar
ceddral committed
121
122
123
124
	err = p.scr.Redraw()
	if err != nil {
		log.Println(err)
	}
125
	return p
126
}
127

ceddral's avatar
ceddral committed
128
func (p *Player) Close() {
129
130
131
	// do not wait for inputLoop termination (inputKill <- struct{}{})
	// as Close is and must be only called from within inputLoop
	close(p.inputKill)
ceddral's avatar
ceddral committed
132
133
134
135
136
137
	p.conn.Close()
	p.windowSizeLoopKill <- struct{}{}
	p.windowSizeLoopKill <- struct{}{}
	close(p.connClosed)
	close(p.inputEvent)
	close(p.windowSizeLoopKill)
138
	delete(gs.players, p.name)
ceddral's avatar
ceddral committed
139
	log.Printf("%s disconnected\n", p.name)
ceddral's avatar
ceddral committed
140
141
}

142
143
144
145
146
147
148
149
150
func StopTimer(t *time.Timer) {
	if !t.Stop() {
		select {
		case <-t.C:
		default:
		}
	}
}

151
func (p *Player) inputLoop() {
152
	key := make([]byte, 0, 3)
153
	p.conn.SetReadDeadline(time.Now().Add(time.Second))
154
	for {
Jonny Schäfer's avatar
Jonny Schäfer committed
155
		select {
156
157
		// kill loop without closing connection in case of reconnect
		case <-p.inputKill:
Michael Gebhard's avatar
go fmt    
Michael Gebhard committed
158
			return
Jonny Schäfer's avatar
Jonny Schäfer committed
159
160
161
		default:
		}

162
		if 0 == len(key) {
163
			p.conn.SetReadDeadline(time.Now().Add(time.Second))
164
		}
165
		b, err := readByteTelnet(p.conn, p.tnState)
166
		if err != nil {
Michael Gebhard's avatar
go fmt    
Michael Gebhard committed
167
			if os.IsTimeout(err) { //errors.Is(err, os.ErrDeadlineExceeded) {
168
169
170
171
172
				if 0 < len(key) {
					goto out
				}
				continue
			}
173
			p.connClosed <- struct{}{}
ceddral's avatar
ceddral committed
174
175
176
			gs.Lock()
			p.context.Unregister(p.name)
			gs.Unlock()
177
178
			// await termination, to unblock a reconnect
			<-p.inputKill
179
180
			return
		}
181
182
		key = append(key, b)
		if b == 27 { // ESC
183
			p.conn.SetReadDeadline(time.Now().Add(connectionConfig.ANSIEscapeTimeout * time.Millisecond))
184
185
186
187
188
			continue
		}
		if 1 < len(key) {
			continue
		}
Michael Gebhard's avatar
go fmt    
Michael Gebhard committed
189
	out:
190
		if toKey(key) != keyNull {
191
			select {
192
			case p.inputEvent <- key:
193
194
195
			default:
				// discard inputs if they come too fast
			}
196
		}
197
198
199
		key = make([]byte, 0, 3)
	}
}
200
201
202

func (p *Player) windowSizeLoop() {
	p.tnState.Lock()
203
204
205
206
	defer func() {
		p.tnState.Unlock()
		<-p.windowSizeLoopKill
	}()
207
	for {
208
209
210
211
212
		select {
		case <-p.windowSizeLoopKill:
			return
		default:
		}
213
		timeoutChan := cond_set_timeout(p.tnState.lock, p.tnState.cond, time.Second)
214
		pthread_cond_timedwait_because_fuck_you(p.tnState.cond, timeoutChan)
215
216
217
218
219
		w := p.tnState.windowWidth
		h := p.tnState.windowHeight
		if p.scr.W == w && p.scr.H == h {
			continue
		}
ceddral's avatar
ceddral committed
220
		gs.Lock()
ceddral's avatar
ceddral committed
221
		p.context.Lock()
222
		p.scr.Resize(w, h)
ceddral's avatar
ceddral committed
223
		p.context.Resize(w, h, p)
ceddral's avatar
ceddral committed
224
225
		p.context.Redraw(p)
		p.context.Unlock()
ceddral's avatar
ceddral committed
226
		gs.Unlock()
227
228
	}
}