Early response parsing work
This commit is contained in:
1 parent
0243fc3e13
commit
adb42dea54
5 files changed
+354
-36
No files matched your search
@@ -0,0 +1,33 @@
|
|||||||
|
// This file is part of https://github.com/racingmars/go3270/
|
||||||
|
// Copyright 2020 by Matthew R. Wilson, licensed under the MIT license. See
|
||||||
|
// LICENSE in the project root for license information.
|
||||||
|
|
||||||
|
package go3270
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEncode(t *testing.T) {
|
||||||
|
encoded := getpos(0, 0)
|
||||||
|
if encoded[0] != 0x40 || encoded[1] != 0x40 {
|
||||||
|
t.Error("Position (0, 0) not correctly encoded")
|
||||||
|
}
|
||||||
|
|
||||||
|
encoded = getpos(11, 39)
|
||||||
|
if encoded[0] != 0x4e || encoded[1] != 0xd7 {
|
||||||
|
t.Error("Position (11, 39) not correctly encoded")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecode(t *testing.T) {
|
||||||
|
decoded := decodeBufAddr([2]byte{0x40, 0x40})
|
||||||
|
if decoded != 0 {
|
||||||
|
t.Error("Buffer address incorrectly decoded")
|
||||||
|
}
|
||||||
|
|
||||||
|
decoded = decodeBufAddr([2]byte{0x4e, 0xd7})
|
||||||
|
if decoded != 919 {
|
||||||
|
t.Error("Buffer address incorrectly decoded")
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
-15
@@ -7,14 +7,22 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/racingmars/go3270"
|
"github.com/racingmars/go3270"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
go3270.Debug = os.Stderr
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Screen is an array of go3270.Field structs:
|
||||||
var loginScreen = go3270.Screen{
|
var loginScreen = go3270.Screen{
|
||||||
{Row: 0, Col: 0, Intense: true, Content: "Testing . . ."},
|
{Row: 0, Col: 30, Intense: true, Content: "3270 Example Screen"},
|
||||||
{Row: 1, Col: 0, Content: "Name . . ."},
|
{Row: 1, Col: 0, Content: "First Name . . ."},
|
||||||
{Row: 1, Col: 14, Name: "name", Write: true},
|
{Row: 1, Col: 18, Name: "fname", Write: true, Content: "Test"},
|
||||||
|
{Row: 2, Col: 0, Content: "Last Name . . . ."},
|
||||||
|
{Row: 2, Col: 18, Name: "lname", Write: true},
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -35,21 +43,11 @@ func handle(conn net.Conn) {
|
|||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
go3270.NegotiateTelnet(conn)
|
go3270.NegotiateTelnet(conn)
|
||||||
err := go3270.WriteScreen(loginScreen, 10, 15, conn)
|
_, err := go3270.ShowScreen(loginScreen, nil, 1, 19, conn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for {
|
fmt.Println("Connection closed")
|
||||||
rbuf := make([]byte, 255)
|
|
||||||
n, err := conn.Read(rbuf)
|
|
||||||
if err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
for i := 0; i < n; i++ {
|
|
||||||
fmt.Printf("%x", rbuf[i])
|
|
||||||
}
|
|
||||||
fmt.Printf("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
+192
@@ -0,0 +1,192 @@
|
|||||||
|
// This file is part of https://github.com/racingmars/go3270/
|
||||||
|
// Copyright 2020 by Matthew R. Wilson, licensed under the MIT license. See
|
||||||
|
// LICENSE in the project root for license information.
|
||||||
|
|
||||||
|
package go3270
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Response encapsulates data received from a 3270 client in response to the
|
||||||
|
// previously sent screen.
|
||||||
|
type Response struct {
|
||||||
|
// Which Action ID key did the user press?
|
||||||
|
AID AID
|
||||||
|
|
||||||
|
// Row the cursor was on (0-based).
|
||||||
|
Row int
|
||||||
|
|
||||||
|
// Column the cursor was on (0-based).
|
||||||
|
Col int
|
||||||
|
|
||||||
|
// Field values.
|
||||||
|
Values map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// AID is an Action ID character.
|
||||||
|
type AID byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
AIDNone AID = 0x60
|
||||||
|
AIDEnter AID = 0x7D
|
||||||
|
AIDPF1 AID = 0xF1
|
||||||
|
AIDPF2 AID = 0xF2
|
||||||
|
AIDPF3 AID = 0xF3
|
||||||
|
AIDPF4 AID = 0xF4
|
||||||
|
AIDPF5 AID = 0xF5
|
||||||
|
AIDPF6 AID = 0xF6
|
||||||
|
AIDPF7 AID = 0xF7
|
||||||
|
AIDPF8 AID = 0xF8
|
||||||
|
AIDPF9 AID = 0xF9
|
||||||
|
AIDPF10 AID = 0x7A
|
||||||
|
AIDPF11 AID = 0x7B
|
||||||
|
AIDPF12 AID = 0x7C
|
||||||
|
AIDPF13 AID = 0xC1
|
||||||
|
AIDPF14 AID = 0xC2
|
||||||
|
AIDPF15 AID = 0xC3
|
||||||
|
AIDPF16 AID = 0xC4
|
||||||
|
AIDPF17 AID = 0xC5
|
||||||
|
AIDPF18 AID = 0xC6
|
||||||
|
AIDPF19 AID = 0xC7
|
||||||
|
AIDPF20 AID = 0xC8
|
||||||
|
AIDPF21 AID = 0xC9
|
||||||
|
AIDPF22 AID = 0x4A
|
||||||
|
AIDPF23 AID = 0x4B
|
||||||
|
AIDPF24 AID = 0x4C
|
||||||
|
AIDPA1 AID = 0x6C
|
||||||
|
AIDPA2 AID = 0x6E
|
||||||
|
AIDPA3 AID = 0x6B
|
||||||
|
AIDClear AID = 0x6D
|
||||||
|
)
|
||||||
|
|
||||||
|
func readResponse(c net.Conn) (Response, error) {
|
||||||
|
var r Response
|
||||||
|
aid, err := readAID(c)
|
||||||
|
if err != nil {
|
||||||
|
return r, err
|
||||||
|
}
|
||||||
|
r.AID = aid
|
||||||
|
|
||||||
|
row, col, _, err := readPosition(c)
|
||||||
|
if err != nil {
|
||||||
|
return r, err
|
||||||
|
}
|
||||||
|
r.Col = col
|
||||||
|
r.Row = row
|
||||||
|
|
||||||
|
if err = readFields(c); err != nil {
|
||||||
|
return r, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readAID(c net.Conn) (AID, error) {
|
||||||
|
buf := make([]byte, 1)
|
||||||
|
for {
|
||||||
|
_, err := c.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
return AIDNone, err
|
||||||
|
}
|
||||||
|
b := buf[0]
|
||||||
|
if (b == 0x60) || (b >= 0x6b && b <= 0x6e) ||
|
||||||
|
(b >= 0x7a && b <= 0x7d) || (b >= 0x4a && b <= 0x4c) ||
|
||||||
|
(b >= 0xf1 && b <= 0xf9) || (b >= 0xc1 && b <= 0xc9) {
|
||||||
|
// We found a valid AID
|
||||||
|
debugf("Got AID byte: %x\n", b)
|
||||||
|
return AID(b), nil
|
||||||
|
}
|
||||||
|
// Consume non-AID bytes continuing loop
|
||||||
|
debugf("Got non-AID byte: %x\n", b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readPosition(c net.Conn) (row, col, addr int, err error) {
|
||||||
|
buf := make([]byte, 1)
|
||||||
|
raw := make([]byte, 2)
|
||||||
|
|
||||||
|
// Read two bytes
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
if _, err := c.Read(buf); err != nil {
|
||||||
|
return 0, 0, 0, err
|
||||||
|
}
|
||||||
|
raw[i] = buf[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode the raw position
|
||||||
|
addr = decodeBufAddr([2]byte{raw[0], raw[1]})
|
||||||
|
row = addr % 80
|
||||||
|
col = (addr - row) / 80
|
||||||
|
|
||||||
|
debugf("Got position bytes %02x %02x, decoded to %d\n", raw[0], raw[1],
|
||||||
|
addr)
|
||||||
|
|
||||||
|
return row, col, addr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readFields(c net.Conn) error {
|
||||||
|
buf := make([]byte, 1)
|
||||||
|
var infield bool
|
||||||
|
var fieldpos int
|
||||||
|
var fieldval bytes.Buffer
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// consume bytes until we get 0xffef
|
||||||
|
for {
|
||||||
|
// Read a byte
|
||||||
|
if _, err = c.Read(buf); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for end of data stream (0xffef)
|
||||||
|
if buf[0] == 0xff {
|
||||||
|
// Finish the current field
|
||||||
|
if infield {
|
||||||
|
// TODO
|
||||||
|
debugf("Field %d: %s\n", fieldpos, e2a(fieldval.Bytes()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// consume the next byte, which is probably 0xef
|
||||||
|
if _, err = c.Read(buf); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// No? Check for start-of-field
|
||||||
|
if buf[0] == 0x11 {
|
||||||
|
// Finish the previous field, if necessary
|
||||||
|
if infield {
|
||||||
|
// TODO
|
||||||
|
debugf("Field %d: %s\n", fieldpos, e2a(fieldval.Bytes()))
|
||||||
|
}
|
||||||
|
// Start a new field
|
||||||
|
infield = true
|
||||||
|
fieldval = bytes.Buffer{}
|
||||||
|
fieldpos = 0
|
||||||
|
|
||||||
|
if _, _, fieldpos, err = readPosition(c); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consume all other bytes as field contents if we're in a field
|
||||||
|
if !infield {
|
||||||
|
debugf("Got unexpected byte while processing fields: %02x\n", buf[0])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fieldval.WriteByte(buf[0])
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// decodeBufAddr decodes a raw 2-byte encoded buffer address and returns the
|
||||||
|
// integer value of the address (i.e. 0-1919)
|
||||||
|
func decodeBufAddr(raw [2]byte) int {
|
||||||
|
hi := decodes[raw[0]] << 6
|
||||||
|
lo := decodes[raw[1]]
|
||||||
|
return hi | lo
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ package go3270
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"net"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Field is a field on the 3270 screen.
|
// Field is a field on the 3270 screen.
|
||||||
@@ -41,13 +41,19 @@ type Field struct {
|
|||||||
// names,
|
// names,
|
||||||
type Screen []Field
|
type Screen []Field
|
||||||
|
|
||||||
// WriteScreen writes the 3270 datastream for the screen to a writer. Fields
|
// ShowScreen writes the 3270 datastream for the screen to a connection.
|
||||||
// that aren't valid (e.g. outside of the 24x80 screen) are silently ignored.
|
// Fields that aren't valid (e.g. outside of the 24x80 screen) are silently
|
||||||
// After writing the fields, the curser is set to crow, ccol, which are
|
// ignored. If a named field has an entry in the values map, the content of
|
||||||
// 0-based positions: row 0-23 and col 0-79. Errors from io.Writer.Write()
|
// the field from the values map is used INSTEAD OF the Field struct's Content
|
||||||
// are returned if encountered.
|
// field. The values map may be nil if no overrides are needed. After writing
|
||||||
func WriteScreen(screen Screen, crow, ccol int, w io.Writer) error {
|
// the fields, the cursor is set to crow, ccol, which are 0-based positions:
|
||||||
|
// row 0-23 and col 0-79. Errors from conn.Write() are returned if
|
||||||
|
// encountered.
|
||||||
|
func ShowScreen(screen Screen, values map[string]string, crow, ccol int,
|
||||||
|
conn net.Conn) (Response, error) {
|
||||||
|
|
||||||
var b bytes.Buffer
|
var b bytes.Buffer
|
||||||
|
var fieldmap = make(map[int]string) // field buffer positions -> name
|
||||||
|
|
||||||
b.WriteByte(0xf5) // Erase/Write to terminal
|
b.WriteByte(0xf5) // Erase/Write to terminal
|
||||||
b.WriteByte(0xc3) // WCC = Reset, Unlock Keyboard, Reset MDT
|
b.WriteByte(0xc3) // WCC = Reset, Unlock Keyboard, Reset MDT
|
||||||
@@ -61,8 +67,23 @@ func WriteScreen(screen Screen, crow, ccol int, w io.Writer) error {
|
|||||||
|
|
||||||
b.Write(sba(fld.Row, fld.Col))
|
b.Write(sba(fld.Row, fld.Col))
|
||||||
b.Write(sf(fld.Write, fld.Intense))
|
b.Write(sf(fld.Write, fld.Intense))
|
||||||
if fld.Content != "" {
|
|
||||||
b.Write(a2e([]byte(fld.Content)))
|
// Use fld.Content, unless the field is named and appears in the
|
||||||
|
// value map.
|
||||||
|
content := fld.Content
|
||||||
|
if fld.Name != "" {
|
||||||
|
if val, ok := values[fld.Name]; ok {
|
||||||
|
content = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if content != "" {
|
||||||
|
b.Write(a2e([]byte(content)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a writable field, add it to the field map
|
||||||
|
if fld.Write {
|
||||||
|
bufaddr := fld.Row*80 + fld.Col
|
||||||
|
fieldmap[bufaddr] = fld.Name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,14 +94,33 @@ func WriteScreen(screen Screen, crow, ccol int, w io.Writer) error {
|
|||||||
if ccol < 0 || ccol > 79 {
|
if ccol < 0 || ccol > 79 {
|
||||||
ccol = 0
|
ccol = 0
|
||||||
}
|
}
|
||||||
//b.Write(ic(crow, ccol))
|
b.Write(ic(crow, ccol))
|
||||||
|
|
||||||
b.Write([]byte{0xff, 0xef}) // Telnet IAC EOR
|
b.Write([]byte{0xff, 0xef}) // Telnet IAC EOR
|
||||||
|
|
||||||
// Now write the datastream to the writer, returning any potential error.
|
// Now write the datastream to the writer, returning any potential error.
|
||||||
fmt.Printf("%x\n", b.Bytes())
|
if Debug != nil {
|
||||||
_, err := w.Write(b.Bytes())
|
fmt.Fprintf(Debug, "%x\n", b.Bytes())
|
||||||
return err
|
}
|
||||||
|
if _, err := conn.Write(b.Bytes()); err != nil {
|
||||||
|
return Response{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now wait for the response. We want to read bytes that start with an AID
|
||||||
|
// and end with 0xFFEF.
|
||||||
|
// for {
|
||||||
|
// rbuf := make([]byte, 1)
|
||||||
|
// n, err := conn.Read(rbuf)
|
||||||
|
// if err != nil {
|
||||||
|
// return Response{}, err
|
||||||
|
// }
|
||||||
|
// for i := 0; i < n; i++ {
|
||||||
|
// fmt.Printf("%x", rbuf[i])
|
||||||
|
// }
|
||||||
|
// fmt.Printf("\n")
|
||||||
|
// }
|
||||||
|
|
||||||
|
return readResponse(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
// sba is the "set buffer address" 3270 command.
|
// sba is the "set buffer address" 3270 command.
|
||||||
@@ -97,6 +137,11 @@ func sf(write, intense bool) []byte {
|
|||||||
result[0] = 0x1d // SF
|
result[0] = 0x1d // SF
|
||||||
if !write {
|
if !write {
|
||||||
result[1] |= 1 << 5 // set "bit 2"
|
result[1] |= 1 << 5 // set "bit 2"
|
||||||
|
} else {
|
||||||
|
// The MDT bit -- we always want writable field values returned,
|
||||||
|
// even if unchanged
|
||||||
|
result[1] |= 1 // set "bit 7"
|
||||||
|
|
||||||
}
|
}
|
||||||
if intense {
|
if intense {
|
||||||
result[1] |= 1 << 3 // set "bit 4"
|
result[1] |= 1 << 3 // set "bit 4"
|
||||||
@@ -126,11 +171,15 @@ func getpos(row, col int) []byte {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// codes are the 3270 control character I/O codes, pre-computed as provided
|
type readerState int
|
||||||
// at http://www.tommysprinkle.com/mvs/P3270/iocodes.htm
|
|
||||||
var codes = []byte{0x40, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8,
|
const (
|
||||||
0xc9, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0xd1, 0xd2, 0xd3, 0xd4,
|
stateNone readerState = iota
|
||||||
0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, 0x60,
|
stateGotAID
|
||||||
0x61, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0x6a, 0x6b, 0x6c,
|
stateGotFirstAddr
|
||||||
0x6d, 0x6e, 0x6f, 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8,
|
stateGotSecondAddr
|
||||||
0xf9, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f}
|
stateInField
|
||||||
|
stateGotFirstFieldAddr
|
||||||
|
stateGotSecondFieldAddr
|
||||||
|
stateGot
|
||||||
|
)
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
// This file is part of https://github.com/racingmars/go3270/
|
||||||
|
// Copyright 2020 by Matthew R. Wilson, licensed under the MIT license. See
|
||||||
|
// LICENSE in the project root for license information.
|
||||||
|
|
||||||
|
package go3270
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Debug io.Writer
|
||||||
|
|
||||||
|
func debugf(format string, a ...interface{}) {
|
||||||
|
if Debug == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(Debug, format, a...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// codes are the 3270 control character I/O codes, pre-computed as provided
|
||||||
|
// at http://www.tommysprinkle.com/mvs/P3270/iocodes.htm
|
||||||
|
var codes = []byte{0x40, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8,
|
||||||
|
0xc9, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0xd1, 0xd2, 0xd3, 0xd4,
|
||||||
|
0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, 0x60,
|
||||||
|
0x61, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0x6a, 0x6b, 0x6c,
|
||||||
|
0x6d, 0x6e, 0x6f, 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8,
|
||||||
|
0xf9, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f}
|
||||||
|
|
||||||
|
// decodes is the inverse of the above table; -1 is used in invalid positions
|
||||||
|
var decodes = []int{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||||
|
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||||
|
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||||
|
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, -1, -1, -1,
|
||||||
|
-1, -1, -1, -1, -1, -1, 10, 11, 12, 13, 14, 15, 16, -1, -1, -1, -1, -1,
|
||||||
|
-1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, -1, -1, -1, -1, -1, -1,
|
||||||
|
-1, -1, 42, 43, 44, 45, 46, 47, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||||
|
58, 59, 60, 61, 62, 63, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||||
|
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||||
|
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||||
|
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 1, 2,
|
||||||
|
3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, -1, 17, 18, 19, 20, 21, 22,
|
||||||
|
23, 24, 25, -1, -1, -1, -1, -1, -1, -1, -1, 34, 35, 36, 37, 38, 39, 40,
|
||||||
|
41, -1, -1, -1, -1, -1, -1, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, -1,
|
||||||
|
-1, -1, -1, -1}
|
||||||
Reference in new issue
Block a user