From ca90ebdfa8789654766c5d7969baa7afacd9ebd2 Mon Sep 17 00:00:00 2001 From: BanceDev Date: Mon, 16 Feb 2026 16:31:54 -0500 Subject: initial commit --- common/args/args.go | 79 ++++++++++++ common/args/args_test.go | 83 ++++++++++++ common/ascii/ascii.go | 75 +++++++++++ common/buffer/buffer.go | 34 +++++ common/buffer/get.go | 189 ++++++++++++++++++++++++++++ common/buffer/get_test.go | 107 ++++++++++++++++ common/buffer/option.go | 9 ++ common/buffer/put.go | 87 +++++++++++++ common/context/context.go | 236 +++++++++++++++++++++++++++++++++++ common/context/option.go | 27 ++++ common/crc/crc.go | 106 ++++++++++++++++ common/crc/crc_test.go | 48 +++++++ common/death/death.go | 132 ++++++++++++++++++++ common/death/parser.go | 55 ++++++++ common/death/template.go | 155 +++++++++++++++++++++++ common/infostring/infostring.go | 72 +++++++++++ common/infostring/infostring_test.go | 49 ++++++++ common/infostring/option.go | 9 ++ common/loc/loc.go | 82 ++++++++++++ common/loc/loc_test.go | 24 ++++ common/rand/rand.go | 7 ++ common/sequencer/option.go | 21 ++++ common/sequencer/sequencer.go | 145 +++++++++++++++++++++ 23 files changed, 1831 insertions(+) create mode 100644 common/args/args.go create mode 100644 common/args/args_test.go create mode 100644 common/ascii/ascii.go create mode 100644 common/buffer/buffer.go create mode 100644 common/buffer/get.go create mode 100644 common/buffer/get_test.go create mode 100644 common/buffer/option.go create mode 100644 common/buffer/put.go create mode 100644 common/context/context.go create mode 100644 common/context/option.go create mode 100644 common/crc/crc.go create mode 100644 common/crc/crc_test.go create mode 100644 common/death/death.go create mode 100644 common/death/parser.go create mode 100644 common/death/template.go create mode 100644 common/infostring/infostring.go create mode 100644 common/infostring/infostring_test.go create mode 100644 common/infostring/option.go create mode 100644 common/loc/loc.go create mode 100644 common/loc/loc_test.go create mode 100644 common/rand/rand.go create mode 100644 common/sequencer/option.go create mode 100644 common/sequencer/sequencer.go (limited to 'common') diff --git a/common/args/args.go b/common/args/args.go new file mode 100644 index 0000000..28c3b5f --- /dev/null +++ b/common/args/args.go @@ -0,0 +1,79 @@ +package args + +import ( + "strings" + + "github.com/osm/quake/common/buffer" +) + +type Arg struct { + Cmd string + Args []string +} + +func Parse(input string) []Arg { + var arg string + var args []string + var ret []Arg + var inQuotes bool + + for _, c := range input { + switch c { + case '\n', ';': + if inQuotes { + arg += string(c) + continue + } + + if arg != "" { + args = append(args, strings.TrimSpace(arg)) + } + + ret = append(ret, Arg{Cmd: strings.TrimSpace(args[0]), Args: args[1:]}) + arg = "" + args = []string{} + case ' ': + if inQuotes { + arg += string(c) + continue + } + + if arg != "" { + args = append(args, strings.TrimSpace(arg)) + arg = "" + } + case '"': + inQuotes = !inQuotes + arg += string(c) + default: + arg += string(c) + } + } + + if arg != "" { + args = append(args, strings.TrimSpace(arg)) + } + + if len(args) > 0 { + ret = append(ret, Arg{Cmd: strings.TrimSpace(args[0]), Args: args[1:]}) + } + + return ret +} + +func (s Arg) Bytes() []byte { + buf := buffer.New() + + buf.PutBytes([]byte(s.Cmd)) + + if len(s.Args) > 0 { + buf.PutByte(byte(' ')) + buf.PutBytes([]byte(strings.Join(s.Args, " "))) + } + + return buf.Bytes() +} + +func (s Arg) String() string { + return string(s.Bytes()) +} diff --git a/common/args/args_test.go b/common/args/args_test.go new file mode 100644 index 0000000..4f1967c --- /dev/null +++ b/common/args/args_test.go @@ -0,0 +1,83 @@ +package args + +import ( + "encoding/base64" + "reflect" + "testing" +) + +type argsTest struct { + name string + input string + expected []Arg +} + +var argsTests = []argsTest{ + { + name: "foo bar baz", + input: "Zm9vIGJhciBiYXo=", + expected: []Arg{ + { + Cmd: "foo", + Args: []string{"bar", "baz"}, + }, + }, + }, + { + name: "foo bar baz\n", + input: "Zm9vIGJhciBiYXoK", + expected: []Arg{ + { + Cmd: "foo", + Args: []string{"bar", "baz"}, + }, + }, + }, + { + name: "foo \"bar baz\"", + input: "Zm9vICJiYXIgYmF6Ig==", + expected: []Arg{ + { + Cmd: "foo", + Args: []string{"\"bar baz\""}, + }, + }, + }, + { + name: "foo \"bar baz\";foo bar baz\nfoo bar", + input: "Zm9vICJiYXIgYmF6Ijtmb28gYmFyIGJhegpmb28gYmFy", + expected: []Arg{ + { + Cmd: "foo", + Args: []string{"\"bar baz\""}, + }, + { + Cmd: "foo", + Args: []string{"bar", "baz"}, + }, + { + Cmd: "foo", + Args: []string{"bar"}, + }, + }, + }, +} + +func TestArgs(t *testing.T) { + for _, at := range argsTests { + t.Run(at.name, func(t *testing.T) { + input, err := base64.StdEncoding.DecodeString(at.input) + if err != nil { + t.Errorf("unable to decode input: %#v", err) + } + + args := Parse(string(input)) + + if !reflect.DeepEqual(at.expected, args) { + t.Errorf("parsed args output didn't match") + t.Logf("output: %#v", args) + t.Logf("expected: %#v", at.expected) + } + }) + } +} diff --git a/common/ascii/ascii.go b/common/ascii/ascii.go new file mode 100644 index 0000000..30a5b17 --- /dev/null +++ b/common/ascii/ascii.go @@ -0,0 +1,75 @@ +package ascii + +import ( + "strings" +) + +func Parse(input string) string { + var str strings.Builder + + for i := 0; i < len(input); i++ { + c := input[i] + + if c > 0 && c < 5 { + str.WriteByte('#') + } else if c == 5 { + str.WriteByte('.') + } else if c > 5 && c < 10 { + str.WriteByte('#') + } else if c == 10 { + str.WriteByte(10) + } else if c == 11 { + str.WriteByte('#') + } else if c > 11 && c < 14 { + str.WriteByte(' ') + } else if c > 14 && c < 16 { + str.WriteByte('.') + } else if c == 16 { + str.WriteByte('[') + } else if c == 17 { + str.WriteByte(']') + } else if c > 17 && c < 28 { + str.WriteByte(c + 30) + } else if c >= 28 && c < 32 { + str.WriteByte('.') + } else if c == 32 { + str.WriteByte(' ') + } else if c > 32 && c < 127 { + str.WriteByte(c) + } else if c > 127 && c < 129 { + str.WriteByte('<') + } else if c == 129 { + str.WriteByte('=') + } else if c == 130 { + str.WriteByte('>') + } else if c > 130 && c < 133 { + str.WriteByte('#') + } else if c == 133 { + str.WriteByte('.') + } else if c > 133 && c < 141 { + str.WriteByte('#') + } else if c > 141 && c < 144 { + str.WriteByte('.') + } else if c == 144 { + str.WriteByte('[') + } else if c == 145 { + str.WriteByte(']') + } else if c > 145 && c < 156 { + str.WriteByte(c - 98) + } else if c == 156 { + str.WriteByte('.') + } else if c == 157 { + str.WriteByte('<') + } else if c == 158 { + str.WriteByte('=') + } else if c == 159 { + str.WriteByte('>') + } else if c == 160 { + str.WriteByte(' ') + } else { + str.WriteByte(c - 128) + } + } + + return str.String() +} diff --git a/common/buffer/buffer.go b/common/buffer/buffer.go new file mode 100644 index 0000000..afc5f7f --- /dev/null +++ b/common/buffer/buffer.go @@ -0,0 +1,34 @@ +package buffer + +import ( + "errors" +) + +var ErrBadRead = errors.New("bad read") + +type Buffer struct { + buf []byte + off int +} + +func New(opts ...Option) *Buffer { + b := &Buffer{} + + for _, opt := range opts { + opt(b) + } + + return b +} + +func (b *Buffer) Len() int { + return len(b.buf) +} + +func (b *Buffer) Off() int { + return b.off +} + +func (b *Buffer) Bytes() []byte { + return b.buf +} diff --git a/common/buffer/get.go b/common/buffer/get.go new file mode 100644 index 0000000..4f27b70 --- /dev/null +++ b/common/buffer/get.go @@ -0,0 +1,189 @@ +package buffer + +import ( + "encoding/binary" + "math" + "strings" +) + +func (b *Buffer) ReadByte() (byte, error) { + if b.off+1 > len(b.buf) { + return 0, ErrBadRead + } + + r := b.buf[b.off] + b.off += 1 + return r, nil +} + +func (b *Buffer) GetBytes(n int) ([]byte, error) { + if b.off+n > len(b.buf) { + return nil, ErrBadRead + } + + r := b.buf[b.off : b.off+n] + b.off += n + return r, nil +} + +func (b *Buffer) GetUint8() (uint8, error) { + if b.off+1 > len(b.buf) { + return 0, ErrBadRead + } + + r := b.buf[b.off] + b.off++ + return r, nil +} + +func (b *Buffer) GetInt8() (int8, error) { + r, err := b.GetUint8() + if err != nil { + return 0, err + } + + return int8(r), nil +} + +func (b *Buffer) GetInt16() (int16, error) { + r, err := b.GetUint16() + if err != nil { + return 0, err + } + + return int16(r), nil +} + +func (b *Buffer) GetUint16() (uint16, error) { + if b.off+2 > len(b.buf) { + return 0, ErrBadRead + } + + r := binary.LittleEndian.Uint16(b.buf[b.off : b.off+2]) + b.off += 2 + return r, nil +} + +func (b *Buffer) GetInt32() (int32, error) { + r, err := b.GetUint32() + if err != nil { + return 0, err + } + + return int32(r), nil +} + +func (b *Buffer) GetUint32() (uint32, error) { + if b.off+4 > len(b.buf) { + return 0, ErrBadRead + } + + r := binary.LittleEndian.Uint32(b.buf[b.off : b.off+4]) + b.off += 4 + return r, nil +} + +func (b *Buffer) GetFloat32() (float32, error) { + r, err := b.GetUint32() + if err != nil { + return 0, err + } + + return math.Float32frombits(r), nil +} + +func (b *Buffer) GetAngle8() (float32, error) { + r, err := b.ReadByte() + if err != nil { + return 0, err + } + + return float32(int8(r)) * (360.0 / 256), nil +} + +func (b *Buffer) GetAngle16() (float32, error) { + r, err := b.GetUint16() + if err != nil { + return 0, err + } + + return float32(r) * (360.0 / 65536), nil +} + +func (b *Buffer) GetCoord16() (float32, error) { + r, err := b.GetInt16() + if err != nil { + return 0, err + } + + return float32(r) / 8.0, nil +} + +func (b *Buffer) GetCoord32() (float32, error) { + return b.GetFloat32() +} + +func (b *Buffer) GetString() (string, error) { + var str strings.Builder + + for b.off < b.Len() { + r, err := b.ReadByte() + if err != nil { + return "", err + } + + if r == 0xff { + continue + } + + if r == 0x00 { + break + } + + str.WriteByte(r) + } + + return str.String(), nil +} + +func (b *Buffer) PeekInt32() (int32, error) { + if b.off+4 > len(b.buf) { + return 0, ErrBadRead + } + + return int32(binary.LittleEndian.Uint32(b.buf[b.off : b.off+4])), nil +} + +func (b *Buffer) PeekBytes(n int) ([]byte, error) { + if b.off+n > len(b.buf) { + return nil, ErrBadRead + } + + return b.buf[b.off : b.off+n], nil +} + +func (b *Buffer) PeekBytesAt(off, size int) ([]byte, error) { + if off+size > len(b.buf) { + return nil, ErrBadRead + } + + return b.buf[off : off+size], nil +} + +func (b *Buffer) Skip(n int) error { + if b.off+n > len(b.buf) { + return ErrBadRead + } + + b.off += n + return nil +} + +func (b *Buffer) Seek(n int) error { + if n > len(b.buf) { + return ErrBadRead + } + + b.off = n + return nil +} diff --git a/common/buffer/get_test.go b/common/buffer/get_test.go new file mode 100644 index 0000000..a4cd8a2 --- /dev/null +++ b/common/buffer/get_test.go @@ -0,0 +1,107 @@ +package buffer + +import ( + "math" + "reflect" + "testing" +) + +type test[T any] struct { + name string + input []byte + expected T +} + +var byteTests = []test[byte]{ + {"byte test 0", []byte{0x00}, byte(0)}, + {"byte test 255", []byte{0xff}, byte(255)}, +} + +var int16Tests = []test[int16]{ + {"int16 test 0", []byte{0x00, 0x00}, int16(0)}, + {"int16 test -32768", []byte{0x00, 0x80}, math.MinInt16}, + {"int16 test 32767", []byte{0xff, 0x7f}, math.MaxInt16}, +} + +var uint16Tests = []test[uint16]{ + {"uint16 test 0", []byte{0x00, 0x00}, uint16(0)}, + {"uint16 test 65535", []byte{0xff, 0xff}, math.MaxUint16}, +} + +var int32Tests = []test[int32]{ + {"int32 test 0", []byte{0x00, 0x00, 0x00, 0x00}, int32(0)}, + {"int32 test -2147483648", []byte{0x00, 0x00, 0x00, 0x80}, math.MinInt32}, + {"int32 test 2147483647", []byte{0xff, 0xff, 0xff, 0x7f}, math.MaxInt32}, +} + +var uint32Tests = []test[uint32]{ + {"uint32 test 0", []byte{0x00, 0x00, 0x00, 0x00}, uint32(0)}, + {"uint32 test 4294967295", []byte{0xff, 0xff, 0xff, 0xff}, uint32(math.MaxUint32)}, +} + +var float32Tests = []test[float32]{ + {"float32 test 0", []byte{0x00, 0x00, 0x00, 0x00}, float32(0)}, + {"float32 test 1e-45", []byte{0x01, 0x00, 0x00, 0x00}, math.SmallestNonzeroFloat32}, + {"float32 test 3.4028235e+38", []byte{0xff, 0xff, 0x7f, 0x7f}, math.MaxFloat32}, +} + +var angle8Tests = []test[float32]{ + {"angle8 test 0", []byte{0x00}, float32(0)}, + {"angle8 test 90", []byte{0x40}, float32(90)}, + {"angle8 test -90", []byte{0xc0}, float32(-90)}, +} + +var angle16Tests = []test[float32]{ + {"angle16 test 0", []byte{0x00, 0x00}, float32(0)}, + {"angle16 test 90", []byte{0x00, 0x40}, float32(90)}, + {"angle16 test 180", []byte{0x00, 0x80}, float32(180)}, +} + +var coord16Tests = []test[float32]{ + {"coord16 test 0", []byte{0x00, 0x00}, float32(0)}, + {"coord16 test 4095", []byte{0xf8, 0x7f}, float32(4095)}, + {"coord16 test -4096", []byte{0x00, 0x80}, float32(-4096)}, +} + +var coord32Tests = []test[float32]{ + {"coord32 test 0", []byte{0x00, 0x00, 0x00, 0x00}, float32(0)}, + {"coord32 test 90", []byte{0x00, 0x00, 0xb4, 0x42}, float32(90)}, + {"coord32 test -90", []byte{0x00, 0x00, 0xb4, 0xc2}, float32(-90)}, +} + +var stringTests = []test[string]{ + {"string test foo bar baz", []byte{0x66, 0x6f, 0x6f, 0x20, 0x62, 0x61, 0x72, 0x00}, "foo bar"}, + {"string test foo bar baz", []byte{0x66, 0x6f, 0x6f, 0x0a, 0x00}, "foo\n"}, +} + +func runTest[T any](t *testing.T, tests []test[T], f func(*Buffer) (T, error)) { + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + val, err := f(New(WithData(tt.input))) + + if err != nil { + t.Errorf("%s: should not return an error", tt.name) + } + + if !reflect.DeepEqual(val, tt.expected) { + t.Errorf("%s: invalid value returned", tt.name) + t.Logf("actual: %#v", val) + t.Logf("expected: %#v", tt.expected) + } + }) + } +} + +func TestBuffer(t *testing.T) { + runTest(t, byteTests, func(buf *Buffer) (byte, error) { return buf.ReadByte() }) + runTest(t, int16Tests, func(buf *Buffer) (int16, error) { return buf.GetInt16() }) + runTest(t, uint16Tests, func(buf *Buffer) (uint16, error) { return buf.GetUint16() }) + runTest(t, int32Tests, func(buf *Buffer) (int32, error) { return buf.GetInt32() }) + runTest(t, uint32Tests, func(buf *Buffer) (uint32, error) { return buf.GetUint32() }) + runTest(t, float32Tests, func(buf *Buffer) (float32, error) { return buf.GetFloat32() }) + runTest(t, angle8Tests, func(buf *Buffer) (float32, error) { return buf.GetAngle8() }) + runTest(t, angle16Tests, func(buf *Buffer) (float32, error) { return buf.GetAngle16() }) + runTest(t, coord16Tests, func(buf *Buffer) (float32, error) { return buf.GetCoord16() }) + runTest(t, coord32Tests, func(buf *Buffer) (float32, error) { return buf.GetCoord32() }) + runTest(t, stringTests, func(buf *Buffer) (string, error) { return buf.GetString() }) +} diff --git a/common/buffer/option.go b/common/buffer/option.go new file mode 100644 index 0000000..2e802e6 --- /dev/null +++ b/common/buffer/option.go @@ -0,0 +1,9 @@ +package buffer + +type Option func(*Buffer) + +func WithData(data []byte) Option { + return func(b *Buffer) { + b.buf = data + } +} diff --git a/common/buffer/put.go b/common/buffer/put.go new file mode 100644 index 0000000..8a5064e --- /dev/null +++ b/common/buffer/put.go @@ -0,0 +1,87 @@ +package buffer + +import ( + "encoding/binary" + "math" +) + +func (b *Buffer) PutByte(v byte) { + b.off += 1 + b.buf = append(b.buf, v) +} + +func (b *Buffer) PutBytes(v []byte) { + b.off += len(v) + b.buf = append(b.buf, v...) +} + +func (b *Buffer) PutInt8(v int8) { + b.PutUint8(uint8(v)) +} + +func (b *Buffer) PutUint8(v uint8) { + b.off++ + b.buf = append(b.buf, v) +} + +func (b *Buffer) PutInt16(v int16) { + b.PutUint16(uint16(v)) +} + +func (b *Buffer) PutUint16(v uint16) { + b.off += 2 + + tmp := make([]byte, 2) + binary.LittleEndian.PutUint16(tmp, v) + b.buf = append(b.buf, tmp...) +} + +func (b *Buffer) PutInt32(v int32) { + b.PutUint32(uint32(v)) +} + +func (b *Buffer) PutUint32(v uint32) { + b.off += 4 + + tmp := make([]byte, 4) + binary.LittleEndian.PutUint32(tmp, v) + b.buf = append(b.buf, tmp...) +} + +func (b *Buffer) PutFloat32(v float32) { + b.off += 4 + + tmp := make([]byte, 4) + binary.LittleEndian.PutUint32(tmp, math.Float32bits(v)) + b.buf = append(b.buf, tmp...) +} + +func (b *Buffer) PutString(v string) { + b.off += len(v) + 1 + + for i := 0; i < len(v); i++ { + b.PutByte(byte(v[i])) + } + + b.PutByte(0) +} + +func (b *Buffer) PutCoord16(v float32) { + b.PutUint16(uint16(v * 8.0)) +} + +func (b *Buffer) PutCoord32(v float32) { + b.PutFloat32(v) +} + +func (b *Buffer) PutAngle8(v float32) { + b.PutByte(byte(v / (360.0 / 256))) +} + +func (b *Buffer) PutAngle16(v float32) { + b.PutUint16(uint16(v / (360.0 / 65536))) +} + +func (b *Buffer) PutAngle32(v float32) { + b.PutFloat32(v) +} diff --git a/common/context/context.go b/common/context/context.go new file mode 100644 index 0000000..6aec81d --- /dev/null +++ b/common/context/context.go @@ -0,0 +1,236 @@ +package context + +import ( + "sync" + + "github.com/osm/quake/protocol" +) + +type Context struct { + angleSizeMu sync.Mutex + angleSize uint8 + + coordSizeMU sync.Mutex + coordSize uint8 + + protocolVersioncolVersionMu sync.Mutex + protocolVersion uint32 + + isMVDEnabledMu sync.Mutex + isMVDEnabled bool + + mvdProtocolExtensionMu sync.Mutex + mvdProtocolExtension uint32 + + isFTEEnabledMu sync.Mutex + isFTEEnabled bool + + fteProtocolExtensionMu sync.Mutex + fteProtocolExtension uint32 + + isFTE2EnabledMu sync.Mutex + isFTE2Enabled bool + + fte2ProtocolExtensionMu sync.Mutex + fte2ProtocolExtension uint32 + + isZQuakeEnabledMu sync.Mutex + isZQuakeEnabled bool + + zQuakeProtocolExtensionMu sync.Mutex + zQuakeProtocolExtension uint32 + + isDemMu sync.Mutex + isDem bool + + isQWDMu sync.Mutex + isQWD bool + + isMVDMu sync.Mutex + isMVD bool +} + +func New(opts ...Option) *Context { + ctx := &Context{ + angleSize: 1, + coordSize: 2, + } + + for _, opt := range opts { + opt(ctx) + } + + return ctx +} + +func (ctx *Context) GetAngleSize() uint8 { + ctx.angleSizeMu.Lock() + defer ctx.angleSizeMu.Unlock() + return ctx.angleSize +} + +func (ctx *Context) SetAngleSize(v uint8) { + ctx.angleSizeMu.Lock() + defer ctx.angleSizeMu.Unlock() + ctx.angleSize = v +} + +func (ctx *Context) GetCoordSize() uint8 { + ctx.coordSizeMU.Lock() + defer ctx.coordSizeMU.Unlock() + return ctx.coordSize +} + +func (ctx *Context) SetCoordSize(v uint8) { + ctx.coordSizeMU.Lock() + defer ctx.coordSizeMU.Unlock() + ctx.coordSize = v +} + +func (ctx *Context) GetProtocolVersion() uint32 { + ctx.protocolVersioncolVersionMu.Lock() + defer ctx.protocolVersioncolVersionMu.Unlock() + return ctx.protocolVersion +} + +func (ctx *Context) SetProtocolVersion(v uint32) { + ctx.protocolVersioncolVersionMu.Lock() + defer ctx.protocolVersioncolVersionMu.Unlock() + ctx.protocolVersion = v +} + +func (ctx *Context) GetIsMVDEnabled() bool { + ctx.isMVDEnabledMu.Lock() + defer ctx.isMVDEnabledMu.Unlock() + return ctx.isMVDEnabled +} + +func (ctx *Context) SetIsMVDEnabled(v bool) { + ctx.isMVDEnabledMu.Lock() + defer ctx.isMVDEnabledMu.Unlock() + ctx.isMVDEnabled = v +} + +func (ctx *Context) GetMVDProtocolExtension() uint32 { + ctx.mvdProtocolExtensionMu.Lock() + defer ctx.mvdProtocolExtensionMu.Unlock() + return ctx.mvdProtocolExtension +} + +func (ctx *Context) SetMVDProtocolExtension(v uint32) { + ctx.mvdProtocolExtensionMu.Lock() + defer ctx.mvdProtocolExtensionMu.Unlock() + ctx.mvdProtocolExtension = v +} + +func (ctx *Context) GetIsFTEEnabled() bool { + ctx.isFTEEnabledMu.Lock() + defer ctx.isFTEEnabledMu.Unlock() + return ctx.isFTEEnabled +} + +func (ctx *Context) SetIsFTEEnabled(v bool) { + ctx.isFTEEnabledMu.Lock() + defer ctx.isFTEEnabledMu.Unlock() + ctx.isFTEEnabled = v +} + +func (ctx *Context) GetFTEProtocolExtension() uint32 { + ctx.fteProtocolExtensionMu.Lock() + defer ctx.fteProtocolExtensionMu.Unlock() + return ctx.fteProtocolExtension +} + +func (ctx *Context) SetFTEProtocolExtension(v uint32) { + ctx.fteProtocolExtensionMu.Lock() + defer ctx.fteProtocolExtensionMu.Unlock() + ctx.fteProtocolExtension = v +} + +func (ctx *Context) GetIsFTE2Enabled() bool { + ctx.isFTE2EnabledMu.Lock() + defer ctx.isFTE2EnabledMu.Unlock() + return ctx.isFTE2Enabled +} + +func (ctx *Context) SetIsFTE2Enabled(v bool) { + ctx.isFTE2EnabledMu.Lock() + defer ctx.isFTE2EnabledMu.Unlock() + ctx.isFTE2Enabled = v +} + +func (ctx *Context) GetFTE2ProtocolExtension() uint32 { + ctx.fte2ProtocolExtensionMu.Lock() + defer ctx.fte2ProtocolExtensionMu.Unlock() + return ctx.fte2ProtocolExtension +} + +func (ctx *Context) SetFTE2ProtocolExtension(v uint32) { + ctx.fte2ProtocolExtensionMu.Lock() + defer ctx.fte2ProtocolExtensionMu.Unlock() + ctx.fte2ProtocolExtension = v +} + +func (ctx *Context) GetIsZQuakeEnabled() bool { + ctx.isZQuakeEnabledMu.Lock() + defer ctx.isZQuakeEnabledMu.Unlock() + return ctx.isZQuakeEnabled +} + +func (ctx *Context) SetIsZQuakeEnabled(v bool) { + ctx.isZQuakeEnabledMu.Lock() + defer ctx.isZQuakeEnabledMu.Unlock() + ctx.isZQuakeEnabled = v +} + +func (ctx *Context) GetZQuakeProtocolExtension() uint32 { + ctx.zQuakeProtocolExtensionMu.Lock() + defer ctx.zQuakeProtocolExtensionMu.Unlock() + return ctx.zQuakeProtocolExtension +} + +func (ctx *Context) SetZQuakeProtocolExtension(v uint32) { + ctx.zQuakeProtocolExtensionMu.Lock() + defer ctx.zQuakeProtocolExtensionMu.Unlock() + ctx.zQuakeProtocolExtension = v +} + +func (ctx *Context) GetIsDem() bool { + ctx.isDemMu.Lock() + defer ctx.isDemMu.Unlock() + return ctx.isDem +} + +func (ctx *Context) SetIsDem(v bool) { + ctx.isDemMu.Lock() + defer ctx.isDemMu.Unlock() + ctx.isDem = v +} + +func (ctx *Context) GetIsQWD() bool { + ctx.isQWDMu.Lock() + defer ctx.isQWDMu.Unlock() + return ctx.isQWD +} + +func (ctx *Context) SetIsQWD(v bool) { + ctx.isQWDMu.Lock() + defer ctx.isQWDMu.Unlock() + ctx.isQWD = v +} + +func (ctx *Context) GetIsMVD() bool { + ctx.isMVDMu.Lock() + defer ctx.isMVDMu.Unlock() + return ctx.isMVD +} + +func (ctx *Context) SetIsMVD(v bool) { + ctx.isMVDMu.Lock() + defer ctx.isMVDMu.Unlock() + ctx.isMVD = v +} + +func (ctx *Context) GetIsNQ() bool { + return ctx.GetIsDem() || ctx.GetProtocolVersion() == protocol.VersionNQ +} diff --git a/common/context/option.go b/common/context/option.go new file mode 100644 index 0000000..7c40cd4 --- /dev/null +++ b/common/context/option.go @@ -0,0 +1,27 @@ +package context + +type Option func(*Context) + +func WithIsDem(isDem bool) Option { + return func(ctx *Context) { + ctx.isDem = isDem + } +} + +func WithIsQWD(isQWD bool) Option { + return func(ctx *Context) { + ctx.isQWD = isQWD + } +} + +func WithIsMVD(isMVD bool) Option { + return func(ctx *Context) { + ctx.isMVD = isMVD + } +} + +func WithProtocolVersion(protocolVersion uint32) Option { + return func(ctx *Context) { + ctx.protocolVersion = protocolVersion + } +} diff --git a/common/crc/crc.go b/common/crc/crc.go new file mode 100644 index 0000000..fcd3ffd --- /dev/null +++ b/common/crc/crc.go @@ -0,0 +1,106 @@ +package crc + +var crcTable = []uint16{ + 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, + 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, + 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, + 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, + 0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485, + 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, + 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4, + 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc, + 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, + 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, + 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12, + 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, + 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, + 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49, + 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70, + 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78, + 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f, + 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, + 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, + 0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, + 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, + 0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, + 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c, + 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, + 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab, + 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3, + 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, + 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, + 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, + 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, + 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, + 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0, +} + +var checksumTable = [1024]byte{ + 0x78, 0xd2, 0x94, 0xe3, 0x41, 0xec, 0xd6, 0xd5, 0xcb, 0xfc, 0xdb, 0x8a, + 0x4b, 0xcc, 0x85, 0x01, 0x23, 0xd2, 0xe5, 0xf2, 0x29, 0xa7, 0x45, 0x94, + 0x4a, 0x62, 0xe3, 0xa5, 0x6f, 0x3f, 0xe1, 0x7a, 0x64, 0xed, 0x5c, 0x99, + 0x29, 0x87, 0xa8, 0x78, 0x59, 0x0d, 0xaa, 0x0f, 0x25, 0x0a, 0x5c, 0x58, + 0xfb, 0x00, 0xa7, 0xa8, 0x8a, 0x1d, 0x86, 0x80, 0xc5, 0x1f, 0xd2, 0x28, + 0x69, 0x71, 0x58, 0xc3, 0x51, 0x90, 0xe1, 0xf8, 0x6a, 0xf3, 0x8f, 0xb0, + 0x68, 0xdf, 0x95, 0x40, 0x5c, 0xe4, 0x24, 0x6b, 0x29, 0x19, 0x71, 0x3f, + 0x42, 0x63, 0x6c, 0x48, 0xe7, 0xad, 0xa8, 0x4b, 0x91, 0x8f, 0x42, 0x36, + 0x34, 0xe7, 0x32, 0x55, 0x59, 0x2d, 0x36, 0x38, 0x38, 0x59, 0x9b, 0x08, + 0x16, 0x4d, 0x8d, 0xf8, 0x0a, 0xa4, 0x52, 0x01, 0xbb, 0x52, 0xa9, 0xfd, + 0x40, 0x18, 0x97, 0x37, 0xff, 0xc9, 0x82, 0x27, 0xb2, 0x64, 0x60, 0xce, + 0x00, 0xd9, 0x04, 0xf0, 0x9e, 0x99, 0xbd, 0xce, 0x8f, 0x90, 0x4a, 0xdd, + 0xe1, 0xec, 0x19, 0x14, 0xb1, 0xfb, 0xca, 0x1e, 0x98, 0x0f, 0xd4, 0xcb, + 0x80, 0xd6, 0x05, 0x63, 0xfd, 0xa0, 0x74, 0xa6, 0x86, 0xf6, 0x19, 0x98, + 0x76, 0x27, 0x68, 0xf7, 0xe9, 0x09, 0x9a, 0xf2, 0x2e, 0x42, 0xe1, 0xbe, + 0x64, 0x48, 0x2a, 0x74, 0x30, 0xbb, 0x07, 0xcc, 0x1f, 0xd4, 0x91, 0x9d, + 0xac, 0x55, 0x53, 0x25, 0xb9, 0x64, 0xf7, 0x58, 0x4c, 0x34, 0x16, 0xbc, + 0xf6, 0x12, 0x2b, 0x65, 0x68, 0x25, 0x2e, 0x29, 0x1f, 0xbb, 0xb9, 0xee, + 0x6d, 0x0c, 0x8e, 0xbb, 0xd2, 0x5f, 0x1d, 0x8f, 0xc1, 0x39, 0xf9, 0x8d, + 0xc0, 0x39, 0x75, 0xcf, 0x25, 0x17, 0xbe, 0x96, 0xaf, 0x98, 0x9f, 0x5f, + 0x65, 0x15, 0xc4, 0x62, 0xf8, 0x55, 0xfc, 0xab, 0x54, 0xcf, 0xdc, 0x14, + 0x06, 0xc8, 0xfc, 0x42, 0xd3, 0xf0, 0xad, 0x10, 0x08, 0xcd, 0xd4, 0x11, + 0xbb, 0xca, 0x67, 0xc6, 0x48, 0x5f, 0x9d, 0x59, 0xe3, 0xe8, 0x53, 0x67, + 0x27, 0x2d, 0x34, 0x9e, 0x9e, 0x24, 0x29, 0xdb, 0x69, 0x99, 0x86, 0xf9, + 0x20, 0xb5, 0xbb, 0x5b, 0xb0, 0xf9, 0xc3, 0x67, 0xad, 0x1c, 0x9c, 0xf7, + 0xcc, 0xef, 0xce, 0x69, 0xe0, 0x26, 0x8f, 0x79, 0xbd, 0xca, 0x10, 0x17, + 0xda, 0xa9, 0x88, 0x57, 0x9b, 0x15, 0x24, 0xba, 0x84, 0xd0, 0xeb, 0x4d, + 0x14, 0xf5, 0xfc, 0xe6, 0x51, 0x6c, 0x6f, 0x64, 0x6b, 0x73, 0xec, 0x85, + 0xf1, 0x6f, 0xe1, 0x67, 0x25, 0x10, 0x77, 0x32, 0x9e, 0x85, 0x6e, 0x69, + 0xb1, 0x83, 0x00, 0xe4, 0x13, 0xa4, 0x45, 0x34, 0x3b, 0x40, 0xff, 0x41, + 0x82, 0x89, 0x79, 0x57, 0xfd, 0xd2, 0x8e, 0xe8, 0xfc, 0x1d, 0x19, 0x21, + 0x12, 0x00, 0xd7, 0x66, 0xe5, 0xc7, 0x10, 0x1d, 0xcb, 0x75, 0xe8, 0xfa, + 0xb6, 0xee, 0x7b, 0x2f, 0x1a, 0x25, 0x24, 0xb9, 0x9f, 0x1d, 0x78, 0xfb, + 0x84, 0xd0, 0x17, 0x05, 0x71, 0xb3, 0xc8, 0x18, 0xff, 0x62, 0xee, 0xed, + 0x53, 0xab, 0x78, 0xd3, 0x65, 0x2d, 0xbb, 0xc7, 0xc1, 0xe7, 0x70, 0xa2, + 0x43, 0x2c, 0x7c, 0xc7, 0x16, 0x04, 0xd2, 0x45, 0xd5, 0x6b, 0x6c, 0x7a, + 0x5e, 0xa1, 0x50, 0x2e, 0x31, 0x5b, 0xcc, 0xe8, 0x65, 0x8b, 0x16, 0x85, + 0xbf, 0x82, 0x83, 0xfb, 0xde, 0x9f, 0x36, 0x48, 0x32, 0x79, 0xd6, 0x9b, + 0xfb, 0x52, 0x45, 0xbf, 0x43, 0xf7, 0x0b, 0x0b, 0x19, 0x19, 0x31, 0xc3, + 0x85, 0xec, 0x1d, 0x8c, 0x20, 0xf0, 0x3a, 0xfa, 0x80, 0x4d, 0x2c, 0x7d, + 0xac, 0x60, 0x09, 0xc0, 0x40, 0xee, 0xb9, 0xeb, 0x13, 0x5b, 0xe8, 0x2b, + 0xb1, 0x20, 0xf0, 0xce, 0x4c, 0xbd, 0xc6, 0x04, 0x86, 0x70, 0xc6, 0x33, + 0xc3, 0x15, 0x0f, 0x65, 0x19, 0xfd, 0xc2, 0xd3, +} + +func Byte(base []byte, seq int) byte { + var buf [60 + 4]byte + + l := len(base) + if l > 60 { + l = 60 + } + copy(buf[:], base[:l]) + + i := (uint32(seq) % uint32(len(checksumTable)-4)) + buf[l+0] = byte(seq&0xff) ^ checksumTable[i+0] + buf[l+1] = checksumTable[i+1] + buf[l+2] = byte((seq>>8)&0xff) ^ checksumTable[i+2] + buf[l+3] = checksumTable[i+3] + + var crc uint16 = 0xffff + for _, b := range buf[:l+4] { + crc = (crc << 8) ^ crcTable[(crc>>8)^uint16(b)] + + } + + return byte(crc & 0xff) +} diff --git a/common/crc/crc_test.go b/common/crc/crc_test.go new file mode 100644 index 0000000..74e77cd --- /dev/null +++ b/common/crc/crc_test.go @@ -0,0 +1,48 @@ +package crc + +import ( + "testing" +) + +type crcTest struct { + name string + input []byte + seq int + expected byte +} + +var crcTests = []crcTest{ + { + name: "sequence 0", + input: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + seq: 0, + expected: 0x2f, + }, + { + name: "sequence 1", + input: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + seq: 1, + expected: 0x2d, + }, + { + name: "sequence 2", + input: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + seq: 2, + expected: 0x37, + }, +} + +func TestCRCs(t *testing.T) { + for _, ct := range crcTests { + t.Run(ct.name, func(t *testing.T) { + crc := Byte(ct.input, ct.seq) + + if crc != ct.expected { + t.Errorf("crc byte didn't match expected") + t.Logf("output: %#v", crc) + t.Logf("expected: %#v", ct.expected) + } + + }) + } +} diff --git a/common/death/death.go b/common/death/death.go new file mode 100644 index 0000000..c71f862 --- /dev/null +++ b/common/death/death.go @@ -0,0 +1,132 @@ +package death + +import ( + "fmt" + "sort" + "strings" +) + +type Weapon string + +const ( + Axe Weapon = "axe" + Discharge Weapon = "discharge" + Drown Weapon = "drown" + Fall Weapon = "fall" + GrenadeLauncher Weapon = "gl" + Lava Weapon = "lava" + LightningGun Weapon = "lg" + Nailgun Weapon = "ng" + NoWeapon Weapon = "no weapon" + QLightningGun Weapon = "quad lg" + QRocketLauncher Weapon = "quad rl" + QShotgun Weapon = "quad sg" + QSuperNailgun Weapon = "quad sng" + QSuperShotgun Weapon = "quad ssg" + RocketLauncher Weapon = "rl" + Shotgun Weapon = "sg" + Slime Weapon = "slime" + Squish Weapon = "squish" + Stomp Weapon = "stomp" + SuperNailgun Weapon = "sng" + SuperShotgun Weapon = "ssg" + TeamKill Weapon = "tk" + Telefrag Weapon = "telefrag" + Trap Weapon = "trap" +) + +type Type uint8 + +const ( + Unknown Type = iota + Death + Suicide + Kill + TeamKillUnknownKiller + TeamKillUnknownVictim +) + +func (t Type) String() string { + names := []string{ + "unknown", + "death", + "suicide", + "kill", + "teamkill", + "teamkill", + } + + if t < 0 || int(t) >= len(names) { + return "type(?)" + } + + return names[t] +} + +type Obituary struct { + Type Type + Killer string + Victim string + Weapon Weapon +} + +func (o Obituary) String() string { + hasKiller := o.Killer != "" + hasVictim := o.Victim != "" + + switch { + case hasKiller && hasVictim: + return fmt.Sprintf("%s %s %s %s", o.Type, o.Killer, o.Weapon, o.Victim) + case hasKiller: + return fmt.Sprintf("%s %s %s", o.Type, o.Killer, o.Weapon) + default: + return fmt.Sprintf("%s %s %s", o.Type, o.Victim, o.Weapon) + } +} + +type Parser struct { + templates []template +} + +func init() { + sort.SliceStable(templates, func(i, j int) bool { + return templates[i].priority > templates[j].priority + }) +} + +func Parse(input string) (*Obituary, bool) { + s := strings.TrimSpace(input) + + for _, t := range templates { + x, y, ok := t.parser.parse(s) + if !ok { + continue + } + + ob := &Obituary{Weapon: t.weapon, Type: toObituaryType(t.typ)} + + switch t.typ { + case PlayerDeath: + ob.Victim = x + case PlayerSuicide: + ob.Victim = x + case XFraggedByY: + ob.Victim = x + ob.Killer = y + case XFragsY: + ob.Killer = x + ob.Victim = y + case XTeamKilled: + ob.Victim = x + case XTeamKills: + ob.Killer = x + default: + ob.Victim = x + ob.Killer = y + } + + return ob, true + } + + return nil, false +} diff --git a/common/death/parser.go b/common/death/parser.go new file mode 100644 index 0000000..e86cffb --- /dev/null +++ b/common/death/parser.go @@ -0,0 +1,55 @@ +package death + +import "strings" + +type parser interface { + parse(msg string) (string, string, bool) +} + +type suffixParser struct { + suffix string +} + +func (m suffixParser) parse(msg string) (x, y string, ok bool) { + if !strings.HasSuffix(msg, m.suffix) { + return "", "", false + } + + x = strings.TrimSpace(strings.TrimSuffix(msg, m.suffix)) + if x == "" { + return "", "", false + } + + return x, "", true +} + +type infixParser struct { + sep string + suffix string +} + +func (m infixParser) parse(msg string) (x, y string, ok bool) { + before, after, found := strings.Cut(msg, m.sep) + if !found { + return "", "", false + } + + x = strings.TrimSpace(before) + if x == "" { + return "", "", false + } + + if m.suffix != "" { + if !strings.HasSuffix(after, m.suffix) { + return "", "", false + } + after = strings.TrimSuffix(after, m.suffix) + } + + y = strings.TrimSpace(after) + if y == "" { + return "", "", false + } + + return x, y, true +} diff --git a/common/death/template.go b/common/death/template.go new file mode 100644 index 0000000..3f77c5b --- /dev/null +++ b/common/death/template.go @@ -0,0 +1,155 @@ +package death + +import "strings" + +type templateType uint8 + +const ( + PlayerDeath templateType = iota + PlayerSuicide + XFraggedByY + XFragsY + XTeamKilled + XTeamKills +) + +func toObituaryType(t templateType) Type { + switch t { + case PlayerDeath: + return Death + case PlayerSuicide: + return Suicide + case XFraggedByY, XFragsY: + return Kill + case XTeamKilled: + return TeamKillUnknownKiller + case XTeamKills: + return TeamKillUnknownVictim + default: + return Unknown + } +} + +type template struct { + parser parser + parts []string + priority int + typ templateType + weapon Weapon +} + +func newTemplate(typ templateType, weapon Weapon, parts ...string) template { + prio := 0 + for _, pr := range parts { + prio += len(pr) + } + + var pa parser + switch typ { + case XFraggedByY, XFragsY: + pa = infixParser{ + sep: parts[0], + suffix: strings.Join(parts[1:], ""), + } + default: + pa = suffixParser{ + suffix: strings.Join(parts, ""), + } + } + + return template{ + parser: pa, + parts: parts, + priority: prio, + typ: typ, + weapon: weapon, + } +} + +var templates = []template{ + newTemplate(PlayerDeath, Drown, " sleeps with the fishes"), + newTemplate(PlayerDeath, Drown, " sucks it down"), + newTemplate(PlayerDeath, Fall, " cratered"), + newTemplate(PlayerDeath, Fall, " fell to her death"), + newTemplate(PlayerDeath, Fall, " fell to his death"), + newTemplate(PlayerDeath, Lava, " burst into flames"), + newTemplate(PlayerDeath, Lava, " turned into hot slag"), + newTemplate(PlayerDeath, Lava, " visits the Volcano God"), + newTemplate(PlayerDeath, NoWeapon, " died"), + newTemplate(PlayerDeath, NoWeapon, " tried to leave"), + newTemplate(PlayerDeath, Slime, " can't exist on slime alone"), + newTemplate(PlayerDeath, Slime, " gulped a load of slime"), + newTemplate(PlayerDeath, Squish, " was squished"), + newTemplate(PlayerDeath, Trap, " ate a lavaball"), + newTemplate(PlayerDeath, Trap, " blew up"), + newTemplate(PlayerDeath, Trap, " was spiked"), + newTemplate(PlayerDeath, Trap, " was zapped"), + newTemplate(PlayerSuicide, Discharge, " discharges into the lava"), + newTemplate(PlayerSuicide, Discharge, " discharges into the slime"), + newTemplate(PlayerSuicide, Discharge, " discharges into the water"), + newTemplate(PlayerSuicide, Discharge, " electrocutes herself"), + newTemplate(PlayerSuicide, Discharge, " electrocutes himself"), + newTemplate(PlayerSuicide, Discharge, " heats up the water"), + newTemplate(PlayerSuicide, Discharge, " railcutes herself"), + newTemplate(PlayerSuicide, Discharge, " railcutes himself"), + newTemplate(PlayerSuicide, GrenadeLauncher, " tries to put the pin back in"), + newTemplate(PlayerSuicide, NoWeapon, " suicides"), + newTemplate(PlayerSuicide, RocketLauncher, " becomes bored with life"), + newTemplate(PlayerSuicide, RocketLauncher, " discovers blast radius"), + newTemplate(XFraggedByY, "Axe", " was ax-murdered by "), + newTemplate(XFraggedByY, "Axe", " was axed to pieces by "), + newTemplate(XFraggedByY, QShotgun, " was lead poisoned by "), + newTemplate(XFraggedByY, QSuperNailgun, " was straw-cuttered by "), + newTemplate(XFraggedByY, Discharge, " accepts ", "' discharge"), + newTemplate(XFraggedByY, Discharge, " accepts ", "'s discharge"), + newTemplate(XFraggedByY, Discharge, " drains ", "' batteries"), + newTemplate(XFraggedByY, Discharge, " drains ", "'s batteries"), + newTemplate(XFraggedByY, GrenadeLauncher, " eats ", "' pineapple"), + newTemplate(XFraggedByY, GrenadeLauncher, " eats ", "'s pineapple"), + newTemplate(XFraggedByY, GrenadeLauncher, " was gibbed by ", "' grenade"), + newTemplate(XFraggedByY, GrenadeLauncher, " was gibbed by ", "'s grenade"), + newTemplate(XFraggedByY, LightningGun, " accepts ", "' shaft"), + newTemplate(XFraggedByY, LightningGun, " accepts ", "'s shaft"), + newTemplate(XFraggedByY, Nailgun, " was body pierced by "), + newTemplate(XFraggedByY, Nailgun, " was nailed by "), + newTemplate(XFraggedByY, QLightningGun, " gets a natural disaster from "), + newTemplate(XFraggedByY, QRocketLauncher, " was brutalized by ", "' quad rocket"), + newTemplate(XFraggedByY, QRocketLauncher, " was brutalized by ", "'s quad rocket"), + newTemplate(XFraggedByY, QRocketLauncher, " was smeared by ", "' quad rocket"), + newTemplate(XFraggedByY, QRocketLauncher, " was smeared by ", "'s quad rocket"), + newTemplate(XFraggedByY, QSuperShotgun, " ate 8 loads of ", "' buckshot"), + newTemplate(XFraggedByY, QSuperShotgun, " ate 8 loads of ", "'s buckshot"), + newTemplate(XFraggedByY, RocketLauncher, " rides ", "' rocket"), + newTemplate(XFraggedByY, RocketLauncher, " rides ", "'s rocket"), + newTemplate(XFraggedByY, RocketLauncher, " was gibbed by ", "' rocket"), + newTemplate(XFraggedByY, RocketLauncher, " was gibbed by ", "'s rocket"), + newTemplate(XFraggedByY, Shotgun, " chewed on ", "' boomstick"), + newTemplate(XFraggedByY, Shotgun, " chewed on ", "'s boomstick"), + newTemplate(XFraggedByY, Stomp, " softens ", "' fall"), + newTemplate(XFraggedByY, Stomp, " softens ", "'s fall"), + newTemplate(XFraggedByY, Stomp, " tried to catch "), + newTemplate(XFraggedByY, Stomp, " was crushed by "), + newTemplate(XFraggedByY, Stomp, " was jumped by "), + newTemplate(XFraggedByY, Stomp, " was literally stomped into particles by "), + newTemplate(XFraggedByY, SuperNailgun, " was perforated by "), + newTemplate(XFraggedByY, SuperNailgun, " was punctured by "), + newTemplate(XFraggedByY, SuperNailgun, " was ventilated by "), + newTemplate(XFraggedByY, SuperShotgun, " ate 2 loads of ", "' buckshot"), + newTemplate(XFraggedByY, SuperShotgun, " ate 2 loads of ", "'s buckshot"), + newTemplate(XFraggedByY, Telefrag, " was telefragged by "), + newTemplate(XFragsY, QRocketLauncher, " rips ", " a new one"), + newTemplate(XFragsY, Squish, " squishes "), + newTemplate(XFragsY, Stomp, " stomps "), + newTemplate(XTeamKilled, Stomp, " was crushed by her teammate"), + newTemplate(XTeamKilled, Stomp, " was crushed by his teammate"), + newTemplate(XTeamKilled, Stomp, " was jumped by her teammate"), + newTemplate(XTeamKilled, Stomp, " was jumped by his teammate"), + newTemplate(XTeamKilled, Telefrag, " was telefragged by her teammate"), + newTemplate(XTeamKilled, Telefrag, " was telefragged by his teammate"), + newTemplate(XTeamKills, Squish, " squished a teammate"), + newTemplate(XTeamKills, TeamKill, " checks her glasses"), + newTemplate(XTeamKills, TeamKill, " checks his glasses"), + newTemplate(XTeamKills, TeamKill, " gets a frag for the other team"), + newTemplate(XTeamKills, TeamKill, " loses another friend"), + newTemplate(XTeamKills, TeamKill, " mows down a teammate"), +} diff --git a/common/infostring/infostring.go b/common/infostring/infostring.go new file mode 100644 index 0000000..114058a --- /dev/null +++ b/common/infostring/infostring.go @@ -0,0 +1,72 @@ +package infostring + +import ( + "strings" + + "github.com/osm/quake/common/buffer" +) + +type InfoString struct { + Info []Info +} + +type Info struct { + Key string + Value string +} + +func New(opts ...Option) *InfoString { + var infoString InfoString + + for _, opt := range opts { + opt(&infoString) + } + + return &infoString +} + +func (is *InfoString) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(byte('"')) + + for i := 0; i < len(is.Info); i++ { + buf.PutBytes([]byte("\\" + is.Info[i].Key)) + buf.PutBytes([]byte("\\" + is.Info[i].Value)) + } + + buf.PutByte(byte('"')) + + return buf.Bytes() +} + +func Parse(input string) *InfoString { + var ret InfoString + + trimmed := strings.Trim(input, "\"") + parts := strings.Split(trimmed, "\\") + + for i := 1; i < len(parts)-1; i += 2 { + ret.Info = append(ret.Info, Info{parts[i], parts[i+1]}) + } + + return &ret +} + +func (is *InfoString) Get(key string) string { + for i := 0; i < len(is.Info); i++ { + if is.Info[i].Key == key { + return is.Info[i].Value + } + } + + return "" +} + +func (is *InfoString) Set(key, value string) { + for i := 0; i < len(is.Info); i++ { + if is.Info[i].Key == key { + is.Info[i].Value = value + } + } +} diff --git a/common/infostring/infostring_test.go b/common/infostring/infostring_test.go new file mode 100644 index 0000000..977090d --- /dev/null +++ b/common/infostring/infostring_test.go @@ -0,0 +1,49 @@ +package infostring + +import ( + "reflect" + "testing" +) + +type infoStringTest struct { + name string + input string + expected InfoString +} + +var infoStringTests = []infoStringTest{ + { + name: "foo", + input: "\"\\FOO\\foo\\BAR\\bar\\BAZ\\baz\"", + expected: InfoString{ + Info: []Info{ + Info{Key: "FOO", Value: "foo"}, + Info{Key: "BAR", Value: "bar"}, + Info{Key: "BAZ", Value: "baz"}, + }, + }, + }, + { + name: "\\foo\\with spaces", + input: "\"\\foo\\with spaces\"", + expected: InfoString{ + Info: []Info{ + Info{Key: "foo", Value: "with spaces"}, + }, + }, + }, +} + +func TestInfoString(t *testing.T) { + for _, is := range infoStringTests { + t.Run(is.name, func(t *testing.T) { + infoString := Parse(is.input) + + if !reflect.DeepEqual(is.expected.Bytes(), infoString.Bytes()) { + t.Errorf("parsed infostring output didn't match") + t.Logf("output: %#v\n", infoString) + t.Logf("expected: %#v\n", is.expected) + } + }) + } +} diff --git a/common/infostring/option.go b/common/infostring/option.go new file mode 100644 index 0000000..f64fefa --- /dev/null +++ b/common/infostring/option.go @@ -0,0 +1,9 @@ +package infostring + +type Option func(*InfoString) + +func WithKeyValue(key, value string) Option { + return func(is *InfoString) { + is.Info = append(is.Info, Info{key, value}) + } +} diff --git a/common/loc/loc.go b/common/loc/loc.go new file mode 100644 index 0000000..c82c816 --- /dev/null +++ b/common/loc/loc.go @@ -0,0 +1,82 @@ +package loc + +import ( + "fmt" + "strings" +) + +type Locations struct { + locations []Location +} + +type Location struct { + Coord [3]float32 + Name string +} + +func Parse(data []byte) (*Locations, error) { + var locs []Location + + for _, line := range strings.Split(string(data), "\n") { + line = strings.TrimSpace(line) + if len(line) == 0 { + continue + } + + var x, y, z float64 + var name string + _, err := fmt.Sscanf(line, "%f %f %f %s", &x, &y, &z, &name) + if err != nil { + return nil, fmt.Errorf("error parsing line '%v'", err) + } + + locs = append(locs, Location{ + Coord: [3]float32{ + float32(x / 8.0), + float32(y / 8.0), + float32(z / 8.0), + }, + Name: trim(name), + }) + } + + return &Locations{locs}, nil +} + +func trim(input string) string { + m := map[string]string{ + "$loc_name_ga": "ga", + "$loc_name_mh": "mh", + "$loc_name_pent": "pent", + "$loc_name_quad": "quad", + "$loc_name_ra": "ra", + "$loc_name_ring": "ring", + "$loc_name_separator": "-", + "$loc_name_ya": "ya", + } + + for x, y := range m { + input = strings.Replace(input, x, y, -1) + } + + return input +} + +func (l *Locations) Get(coord [3]float32) *Location { + var loc *Location + var min float32 + + for _, n := range l.locations { + x := coord[0] - n.Coord[0] + y := coord[1] - n.Coord[1] + z := coord[2] - n.Coord[2] + d := x*x + y*y + z*z + + if loc == nil || d < min { + loc = &n + min = d + } + } + + return loc +} diff --git a/common/loc/loc_test.go b/common/loc/loc_test.go new file mode 100644 index 0000000..5b68cc8 --- /dev/null +++ b/common/loc/loc_test.go @@ -0,0 +1,24 @@ +package loc + +import ( + "testing" +) + +func TestLoc(t *testing.T) { + dm3Loc := []byte(`-7040 -1856 -128 sng-mega +1536 -1664 -1408 ra-tunnel +11776 -7424 -192 ya +12160 3456 -704 rl +-5056 -5440 -128 sng-ra +4096 6144 1728 lifts`) + + loc, err := Parse(dm3Loc) + if err != nil { + t.Errorf("error when parsing loc data, %v", err) + } + + l := loc.Get([3]float32{1300, -700, -24}) + if l.Name != "ya" { + t.Errorf("expected to be at ya, got %v", l.Name) + } +} diff --git a/common/rand/rand.go b/common/rand/rand.go new file mode 100644 index 0000000..79f9efb --- /dev/null +++ b/common/rand/rand.go @@ -0,0 +1,7 @@ +package rand + +import "math/rand" + +func Uint16() uint16 { + return uint16(rand.Uint32() & 0xffff) +} diff --git a/common/sequencer/option.go b/common/sequencer/option.go new file mode 100644 index 0000000..1b3f613 --- /dev/null +++ b/common/sequencer/option.go @@ -0,0 +1,21 @@ +package sequencer + +type Option func(*Sequencer) + +func WithIncomingSeq(incomingSeq uint32) Option { + return func(s *Sequencer) { + s.incomingSeq = incomingSeq + } +} + +func WithOutgoingSeq(outgoingSeq uint32) Option { + return func(s *Sequencer) { + s.outgoingSeq = outgoingSeq + } +} + +func WithPing(ping int16) Option { + return func(s *Sequencer) { + s.ping = ping + } +} diff --git a/common/sequencer/sequencer.go b/common/sequencer/sequencer.go new file mode 100644 index 0000000..fe83580 --- /dev/null +++ b/common/sequencer/sequencer.go @@ -0,0 +1,145 @@ +package sequencer + +import ( + "errors" + "time" + + "github.com/osm/quake/packet/command" +) + +var ErrRateLimit = errors.New("rate limit reached") + +type State byte + +const ( + Disconnected State = 0 + Handshake State = 1 + Connecting State = 2 + Connected State = 3 +) + +type Sequencer struct { + ping int16 + lastWrite time.Time + + state State + + incomingSeq uint32 + incomingAck uint32 + lastReliableSeq uint32 + outgoingSeq uint32 + + isIncomingAckReliable bool + isOutgoingSeqReliable bool + isIncomingSeqReliable bool + + outgoingCommands []command.Command + outgoingCommandsBuf []command.Command +} + +func New(opts ...Option) *Sequencer { + s := Sequencer{ + ping: 999, + } + + for _, opt := range opts { + opt(&s) + } + + return &s +} + +func (s *Sequencer) SetState(state State) { s.state = state } +func (s *Sequencer) GetState() State { return s.state } +func (s *Sequencer) SetPing(ping int16) { s.ping = ping } +func (s *Sequencer) GetPing() int16 { return s.ping } + +func (s *Sequencer) Reset() { + s.incomingSeq = 0 + s.incomingAck = 0 + s.lastReliableSeq = 0 + s.outgoingSeq = 0 + + s.isIncomingAckReliable = false + s.isOutgoingSeqReliable = false + s.isIncomingSeqReliable = false + + s.outgoingCommands = []command.Command{} + s.outgoingCommandsBuf = []command.Command{} +} + +func (s *Sequencer) Process( + incomingSeq, incomingAck uint32, + cmds []command.Command, +) (uint32, uint32, []command.Command, error) { + s.incoming(incomingSeq, incomingAck) + return s.outgoing(cmds) +} + +func (s *Sequencer) incoming(incomingSeq, incomingAck uint32) { + isIncomingSeqReliable := incomingSeq>>31 == 1 + isIncomingAckReliable := incomingAck>>31 == 1 + + incomingSeq = incomingSeq & 0x7fffffff + incomingAck = incomingAck & 0x7fffffff + + if incomingSeq < s.incomingSeq { + return + } + + if isIncomingAckReliable == s.isOutgoingSeqReliable { + s.outgoingCommandsBuf = []command.Command{} + } + + if isIncomingSeqReliable { + s.isIncomingSeqReliable = !s.isIncomingSeqReliable + } + + s.incomingSeq = incomingSeq + s.incomingAck = incomingAck + s.isIncomingAckReliable = isIncomingAckReliable +} + +func (s *Sequencer) outgoing(cmds []command.Command) (uint32, uint32, []command.Command, error) { + s.outgoingCommands = append(s.outgoingCommands, cmds...) + + if s.state == Connected && time.Since(s.lastWrite).Milliseconds() < int64(s.ping) { + return 0, 0, nil, ErrRateLimit + } + + var isReliable bool + + if s.incomingAck > s.lastReliableSeq && + s.isIncomingAckReliable != s.isOutgoingSeqReliable { + isReliable = true + } + + if len(s.outgoingCommandsBuf) == 0 && len(s.outgoingCommands) > 0 { + s.outgoingCommandsBuf = s.outgoingCommands + s.isOutgoingSeqReliable = !s.isOutgoingSeqReliable + isReliable = true + s.outgoingCommands = []command.Command{} + } + + outgoingSeq := s.outgoingSeq + if isReliable { + outgoingSeq = s.outgoingSeq | (1 << 31) + } + + outgoingAck := s.incomingSeq + if s.isIncomingSeqReliable { + outgoingAck = s.incomingSeq | (1 << 31) + } + + outgoingCmds := []command.Command{} + + s.outgoingSeq++ + + if isReliable { + outgoingCmds = append(outgoingCmds, s.outgoingCommandsBuf...) + s.lastReliableSeq = s.outgoingSeq + } + + s.lastWrite = time.Now() + return outgoingSeq, outgoingAck, outgoingCmds, nil +} -- cgit v1.2.3-59-g8ed1b