mirror of
https://github.com/CPunch/gopenfusion.git
synced 2025-09-30 23:10:06 +00:00
Compare commits
62 Commits
b02c141000
...
main
Author | SHA1 | Date | |
---|---|---|---|
cafca9093c | |||
d84fcd2c93 | |||
1f63f9856e | |||
de3e067b48 | |||
02afe67ac3 | |||
79f68187bf | |||
cd93a058ce | |||
0a28dbcc3e | |||
1a6de671e5 | |||
261ea6505f | |||
556878544d | |||
bfcbe6d3d6 | |||
e5a9ed1481 | |||
23170093ee | |||
2bd61dc571 | |||
cba01a877d | |||
e1b9fa5d99 | |||
77751a2aa0 | |||
8e84f0c7b2 | |||
c902559eac | |||
1c40998cb6 | |||
988368c307 | |||
01ebf4499f | |||
3a14d807d2 | |||
141858d6c3 | |||
335fdb417c | |||
2a6fb25f03 | |||
0ebd162af0 | |||
d1763418a8 | |||
c4d885cf6d | |||
afd5c9ef23 | |||
ac62f7d64e | |||
58afc9df1f | |||
e257bf998f | |||
96eed66831 | |||
12f16645e1 | |||
76e9bdf7e7 | |||
44f3b31965 | |||
899b95b4e6 | |||
e33b7c0556 | |||
3445b852fd | |||
557117f093 | |||
b07e9ddbcb | |||
af867ccff2 | |||
c60017f78f | |||
e1804a1042 | |||
bcc999db38 | |||
e355af19ab | |||
0ed19ad6c5 | |||
66fe3c9738 | |||
72dbfe2541 | |||
8e65a78d07 | |||
f4b17906ce | |||
c0ba365cf5 | |||
d0346b2382 | |||
18a6c5ab42 | |||
3abba0ca3c | |||
1f66acfd25 | |||
d8277ea89c | |||
81de857670 | |||
e8f5e5fc9c | |||
6f55cbbad5 |
14
.github/workflows/tests.yaml
vendored
14
.github/workflows/tests.yaml
vendored
@@ -1,15 +1,21 @@
|
|||||||
name: Go
|
name: unit-tests
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
paths:
|
paths:
|
||||||
- ./**.go
|
- cmd/**
|
||||||
|
- config/**
|
||||||
|
- cnet/**
|
||||||
|
- internal/**
|
||||||
|
- login/**
|
||||||
|
- shard/**
|
||||||
|
- util/**
|
||||||
- go.mod
|
- go.mod
|
||||||
- go.sum
|
- go.sum
|
||||||
- .github/workflows/tests.yaml
|
- .github/workflows/tests.yaml
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
tests:
|
run:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -19,4 +25,4 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
go-version: '1.21.x'
|
go-version: '1.21.x'
|
||||||
- name: Test with the Go CLI
|
- name: Test with the Go CLI
|
||||||
run: go test -v ./...
|
run: go test -timeout 10s -v ./...
|
||||||
|
7
LICENSE.md
Normal file
7
LICENSE.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Copyright © 2022 Gopenfusion Contributors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
@@ -1,6 +1,13 @@
|
|||||||
# gopenfusion
|
# gopenfusion
|
||||||
|
|
||||||
A toy implementation of the [Fusionfall Packet Protocol](https://openpunk.com/pages/fusionfall-openfusion/) and accompanying services, written in Go.
|
<p align="center">
|
||||||
|
<a href="https://github.com/CPunch/gopenfusion/actions/workflows/tests.yaml"><img src="https://github.com/CPunch/gopenfusion/actions/workflows/tests.yaml/badge.svg?branch=main" alt="Workflow"></a>
|
||||||
|
<a href="https://github.com/CPunch/gopenfusion/blob/main/LICENSE.md"><img src="https://img.shields.io/github/license/CPunch/gopenfusion" alt="License"></a>
|
||||||
|
<br>
|
||||||
|
<a href="https://asciinema.org/a/625524" target="_blank"><img src="https://asciinema.org/a/625524.svg" /></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
A toy implementation of the [Fusionfall Packet Protocol](https://openpunk.com/pages/fusionfall-openfusion/) (see: `cnet/`) and accompanying services, written in Go.
|
||||||
|
|
||||||
## Landwalker demo
|
## Landwalker demo
|
||||||
|
|
||||||
|
@@ -5,7 +5,7 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"github.com/CPunch/gopenfusion/config"
|
"github.com/CPunch/gopenfusion/internal/config"
|
||||||
"github.com/CPunch/gopenfusion/login"
|
"github.com/CPunch/gopenfusion/login"
|
||||||
"github.com/google/subcommands"
|
"github.com/google/subcommands"
|
||||||
)
|
)
|
||||||
@@ -31,7 +31,7 @@ func (s *loginCommand) SetFlags(f *flag.FlagSet) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *loginCommand) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
|
func (s *loginCommand) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
|
||||||
loginServer, err := login.NewLoginServer(dbHndlr, redisHndlr, s.port)
|
loginServer, err := login.NewLoginServer(ctx, dbHndlr, redisHndlr, s.port)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Panicf("failed to create shard server: %v", err)
|
log.Panicf("failed to create shard server: %v", err)
|
||||||
}
|
}
|
||||||
|
@@ -6,7 +6,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/CPunch/gopenfusion/config"
|
"github.com/CPunch/gopenfusion/internal/config"
|
||||||
"github.com/CPunch/gopenfusion/internal/db"
|
"github.com/CPunch/gopenfusion/internal/db"
|
||||||
"github.com/CPunch/gopenfusion/internal/redis"
|
"github.com/CPunch/gopenfusion/internal/redis"
|
||||||
|
|
||||||
|
@@ -5,7 +5,7 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"github.com/CPunch/gopenfusion/config"
|
"github.com/CPunch/gopenfusion/internal/config"
|
||||||
"github.com/CPunch/gopenfusion/shard"
|
"github.com/CPunch/gopenfusion/shard"
|
||||||
"github.com/google/subcommands"
|
"github.com/google/subcommands"
|
||||||
)
|
)
|
||||||
@@ -31,7 +31,7 @@ func (s *shardCommand) SetFlags(f *flag.FlagSet) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *shardCommand) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
|
func (s *shardCommand) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
|
||||||
shardServer, err := shard.NewShardServer(dbHndlr, redisHndlr, s.port)
|
shardServer, err := shard.NewShardServer(ctx, dbHndlr, redisHndlr, s.port)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Panicf("failed to create shard server: %v", err)
|
log.Panicf("failed to create shard server: %v", err)
|
||||||
}
|
}
|
||||||
|
165
cnet/peer.go
Normal file
165
cnet/peer.go
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
package cnet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"github.com/CPunch/gopenfusion/cnet/protocol"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
USE_E = iota
|
||||||
|
USE_FE
|
||||||
|
)
|
||||||
|
|
||||||
|
type PacketEvent struct {
|
||||||
|
Type int
|
||||||
|
Pkt *bytes.Buffer
|
||||||
|
PktID uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// Peer is a simple wrapper for net.Conn connections to send/recv packets over the Fusionfall packet protocol.
|
||||||
|
type Peer struct {
|
||||||
|
uData interface{}
|
||||||
|
conn net.Conn
|
||||||
|
ctx context.Context
|
||||||
|
whichKey int
|
||||||
|
alive *atomic.Bool
|
||||||
|
|
||||||
|
// May not be set while Send() or Handler() are concurrently running.
|
||||||
|
E_key []byte
|
||||||
|
|
||||||
|
// May not be set while Send() or Handler() are concurrently running.
|
||||||
|
FE_key []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPeer(ctx context.Context, conn net.Conn) *Peer {
|
||||||
|
p := &Peer{
|
||||||
|
conn: conn,
|
||||||
|
ctx: ctx,
|
||||||
|
whichKey: USE_E,
|
||||||
|
alive: &atomic.Bool{},
|
||||||
|
|
||||||
|
E_key: []byte(protocol.DEFAULT_KEY),
|
||||||
|
FE_key: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (peer *Peer) SetUserData(uData interface{}) {
|
||||||
|
peer.uData = uData
|
||||||
|
}
|
||||||
|
|
||||||
|
func (peer *Peer) UserData() interface{} {
|
||||||
|
return peer.uData
|
||||||
|
}
|
||||||
|
|
||||||
|
func (peer *Peer) Send(typeID uint32, data ...interface{}) error {
|
||||||
|
// grab buffer from pool
|
||||||
|
buf := protocol.GetBuffer()
|
||||||
|
defer protocol.PutBuffer(buf)
|
||||||
|
|
||||||
|
// allocate space for packet size
|
||||||
|
buf.Write(make([]byte, 4))
|
||||||
|
|
||||||
|
// body start
|
||||||
|
pkt := protocol.NewPacket(buf)
|
||||||
|
|
||||||
|
// encode type id
|
||||||
|
if err := pkt.Encode(typeID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// encode data
|
||||||
|
for _, trailer := range data {
|
||||||
|
if err := pkt.Encode(trailer); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepend the packet size
|
||||||
|
binary.LittleEndian.PutUint32(buf.Bytes()[:4], uint32(buf.Len()-4))
|
||||||
|
|
||||||
|
// encrypt body
|
||||||
|
var key []byte
|
||||||
|
switch peer.whichKey {
|
||||||
|
case USE_E:
|
||||||
|
key = peer.E_key
|
||||||
|
case USE_FE:
|
||||||
|
key = peer.FE_key
|
||||||
|
}
|
||||||
|
protocol.EncryptData(buf.Bytes()[4:], key)
|
||||||
|
|
||||||
|
// send full packet
|
||||||
|
// log.Printf("Sending %#v, sizeof: %d, buffer: %v", data, buf.Len(), buf.Bytes())
|
||||||
|
if _, err := peer.conn.Write(buf.Bytes()); err != nil {
|
||||||
|
return fmt.Errorf("failed to write packet body! %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (peer *Peer) SetActiveKey(whichKey int) {
|
||||||
|
peer.whichKey = whichKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (peer *Peer) Kill() {
|
||||||
|
// de-bounce: only kill if alive
|
||||||
|
if !peer.alive.CompareAndSwap(true, false) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
peer.conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// meant to be invoked as a goroutine
|
||||||
|
func (peer *Peer) Handler(eRecv chan<- *PacketEvent) error {
|
||||||
|
defer func() {
|
||||||
|
close(eRecv)
|
||||||
|
peer.Kill()
|
||||||
|
}()
|
||||||
|
|
||||||
|
peer.alive.Store(true)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-peer.ctx.Done():
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
// read packet size, the goroutine spends most of it's time parked here
|
||||||
|
var sz uint32
|
||||||
|
if err := binary.Read(peer.conn, binary.LittleEndian, &sz); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// client should never send a packet size outside of this range
|
||||||
|
if sz > protocol.CN_PACKET_BUFFER_SIZE || sz < 4 {
|
||||||
|
return fmt.Errorf("invalid packet size: %d", sz)
|
||||||
|
}
|
||||||
|
|
||||||
|
// grab buffer && read packet body
|
||||||
|
buf := protocol.GetBuffer()
|
||||||
|
if _, err := buf.ReadFrom(io.LimitReader(peer.conn, int64(sz))); err != nil {
|
||||||
|
return fmt.Errorf("failed to read packet body: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// decrypt
|
||||||
|
protocol.DecryptData(buf.Bytes(), peer.E_key)
|
||||||
|
pkt := protocol.NewPacket(buf)
|
||||||
|
|
||||||
|
// create packet && read pktID
|
||||||
|
var pktID uint32
|
||||||
|
if err := pkt.Decode(&pktID); err != nil {
|
||||||
|
return fmt.Errorf("failed to read packet type! %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// dispatch packet
|
||||||
|
// log.Printf("Got packet ID: %x, with a sizeof: %d\n", pktID, sz)
|
||||||
|
eRecv <- &PacketEvent{Pkt: buf, PktID: pktID}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -6,7 +6,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
DEFAULT_KEY = "m@rQn~W#"
|
DEFAULT_KEY = "m@rQn~W#" // if you change this, make sure to update the test data in protocol_test.go
|
||||||
KEY_LENGTH = 8
|
KEY_LENGTH = 8
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -50,13 +50,14 @@ func DecryptData(buff, key []byte) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func CreateNewKey(uTime, iv1, iv2 uint64) []byte {
|
func CreateNewKey(uTime, iv1, iv2 uint64) []byte {
|
||||||
|
dEKey := binary.LittleEndian.Uint64([]byte(DEFAULT_KEY))
|
||||||
|
|
||||||
num := iv1 + 1
|
num := iv1 + 1
|
||||||
num2 := iv2 + 1
|
num2 := iv2 + 1
|
||||||
|
|
||||||
dEKey := uint64(binary.LittleEndian.Uint64([]byte(DEFAULT_KEY)))
|
|
||||||
key := dEKey * (uTime * num * num2)
|
key := dEKey * (uTime * num * num2)
|
||||||
|
|
||||||
buf := make([]byte, 8)
|
buf := make([]byte, 8)
|
||||||
binary.LittleEndian.PutUint64(buf, uint64(key))
|
binary.LittleEndian.PutUint64(buf, key)
|
||||||
return buf
|
return buf
|
||||||
}
|
}
|
||||||
|
|
@@ -42,10 +42,7 @@ func (pkt Packet) encodeStructField(field reflect.StructField, value reflect.Val
|
|||||||
buf16 = buf16[:sz]
|
buf16 = buf16[:sz]
|
||||||
} else {
|
} else {
|
||||||
// grow
|
// grow
|
||||||
// TODO: probably a better way to do this?
|
buf16 = append(buf16, make([]uint16, sz-len(buf16))...)
|
||||||
for len(buf16) < sz {
|
|
||||||
buf16 = append(buf16, 0)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// write
|
// write
|
||||||
@@ -125,8 +122,7 @@ func (pkt Packet) decodeStructField(field reflect.StructField, value reflect.Val
|
|||||||
// consume padding bytes
|
// consume padding bytes
|
||||||
pad, err := strconv.Atoi(field.Tag.Get("pad"))
|
pad, err := strconv.Atoi(field.Tag.Get("pad"))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
dummy := make([]byte, pad)
|
if _, err := pkt.readWriter.Read(make([]byte, pad)); err != nil {
|
||||||
if _, err := pkt.readWriter.Read(dummy); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
package pool
|
package protocol
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -9,11 +9,13 @@ var allocator = &sync.Pool{
|
|||||||
New: func() any { return new(bytes.Buffer) },
|
New: func() any { return new(bytes.Buffer) },
|
||||||
}
|
}
|
||||||
|
|
||||||
func Get() *bytes.Buffer {
|
// grabs a *bytes.Buffer from the pool
|
||||||
|
func GetBuffer() *bytes.Buffer {
|
||||||
return allocator.Get().(*bytes.Buffer)
|
return allocator.Get().(*bytes.Buffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Put(buf *bytes.Buffer) {
|
// returns a *bytes.Buffer to the pool
|
||||||
|
func PutBuffer(buf *bytes.Buffer) {
|
||||||
buf.Reset()
|
buf.Reset()
|
||||||
allocator.Put(buf)
|
allocator.Put(buf)
|
||||||
}
|
}
|
102
cnet/protocol/protocol_test.go
Normal file
102
cnet/protocol/protocol_test.go
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
package protocol_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/CPunch/gopenfusion/cnet/protocol"
|
||||||
|
"github.com/matryer/is"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TestPacketData struct {
|
||||||
|
A int32
|
||||||
|
B int32
|
||||||
|
UTF16Str string `size:"32"`
|
||||||
|
Pad int16 `pad:"2"`
|
||||||
|
C int32
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
testStruct = TestPacketData{
|
||||||
|
A: 1,
|
||||||
|
B: 2,
|
||||||
|
UTF16Str: "hello world",
|
||||||
|
C: 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is the data we expect to get from encoding the above struct
|
||||||
|
testData = [...]byte{
|
||||||
|
0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00,
|
||||||
|
0x68, 0x00, 0x65, 0x00, 0x6c, 0x00, 0x6c, 0x00,
|
||||||
|
0x6f, 0x00, 0x20, 0x00, 0x77, 0x00, 0x6f, 0x00,
|
||||||
|
0x72, 0x00, 0x6c, 0x00, 0x64, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00,
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is the data we expect to get from EncryptData(testData, []byte(protocol.DEFAULT_KEY))
|
||||||
|
encTestData = []byte{
|
||||||
|
0x23, 0x40, 0x72, 0x51, 0x6c, 0x7e, 0x57, 0x6c,
|
||||||
|
0x05, 0x3b, 0x17, 0x51, 0x02, 0x7e, 0x40, 0x23,
|
||||||
|
0x02, 0x40, 0x7e, 0x51, 0x19, 0x52, 0x38, 0x23,
|
||||||
|
0x1f, 0x40, 0x1e, 0x0a, 0x51, 0x7e, 0x57, 0x23,
|
||||||
|
0x6d, 0x40, 0x72, 0x6e, 0x51, 0x7e, 0x57, 0x23,
|
||||||
|
0x23, 0x40, 0x72, 0x51, 0x6e, 0x7e, 0x57, 0x6d,
|
||||||
|
0x6d, 0x57, 0x72, 0x51, 0x6e, 0x7e, 0x40, 0x23,
|
||||||
|
0x6d, 0x40, 0x7e, 0x51, 0x6e, 0x72, 0x57, 0x23,
|
||||||
|
0x6d, 0x40, 0x72, 0x6e, 0x51, 0x7e, 0x57, 0x23,
|
||||||
|
0x6d, 0x40, 0x72, 0x6d, 0x51, 0x7e, 0x57, 0x23,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPacketEncode(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
pkt := protocol.NewPacket(buf)
|
||||||
|
|
||||||
|
err := pkt.Encode(testStruct)
|
||||||
|
is.NoErr(err)
|
||||||
|
|
||||||
|
is.Equal(buf.Bytes(), testData[:]) // encoded data should match expected data
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPacketDecode(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
pkt := protocol.NewPacket(buf)
|
||||||
|
buf.Write(testData[:])
|
||||||
|
|
||||||
|
var test TestPacketData
|
||||||
|
err := pkt.Decode(&test)
|
||||||
|
is.NoErr(err)
|
||||||
|
is.Equal(test, testStruct) // decoded data should match testStruct
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDataEncrypt(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
buf := make([]byte, len(testData))
|
||||||
|
copy(buf, testData[:])
|
||||||
|
|
||||||
|
protocol.EncryptData(buf, []byte(protocol.DEFAULT_KEY))
|
||||||
|
is.Equal(buf, encTestData) // encrypted data should match expected data
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDataDecrypt(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
buf := make([]byte, len(encTestData))
|
||||||
|
copy(buf, encTestData)
|
||||||
|
|
||||||
|
protocol.DecryptData(buf, []byte(protocol.DEFAULT_KEY))
|
||||||
|
is.Equal(buf, testData[:]) // decrypted data should match expected data
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateNewKey(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
key := protocol.CreateNewKey(123456789, 0x1234567890abcdef, 0x1234567890abcdef)
|
||||||
|
|
||||||
|
is.Equal(key, []byte{0x0, 0x31, 0xb8, 0xcd, 0xd, 0xc3, 0xad, 0x67}) // key should match expected data
|
||||||
|
}
|
9
cnet/protocol/time.go
Normal file
9
cnet/protocol/time.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package protocol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetTime() uint64 {
|
||||||
|
return uint64(time.Now().UnixMilli())
|
||||||
|
}
|
282
cnet/service.go
Normal file
282
cnet/service.go
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
package cnet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/CPunch/gopenfusion/cnet/protocol"
|
||||||
|
"github.com/CPunch/gopenfusion/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PacketHandler func(peer *Peer, pkt protocol.Packet) error
|
||||||
|
|
||||||
|
func StubbedPacket(_ *Peer, _ protocol.Packet) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
listener net.Listener
|
||||||
|
port int
|
||||||
|
Name string
|
||||||
|
ctx context.Context
|
||||||
|
started chan struct{}
|
||||||
|
stopped chan struct{}
|
||||||
|
packetHandlers map[uint32]PacketHandler
|
||||||
|
peers map[chan *PacketEvent]*Peer
|
||||||
|
stateLock sync.Mutex
|
||||||
|
|
||||||
|
// OnDisconnect is called when a peer disconnects from the service.
|
||||||
|
// uData is the stored value of the key/value pair in the peer map.
|
||||||
|
// It may not be set while the service is running. (eg. srvc.Start() has been called)
|
||||||
|
OnDisconnect func(peer *Peer)
|
||||||
|
|
||||||
|
// OnConnect is called when a peer connects to the service.
|
||||||
|
// return value is used as the value in the peer map.
|
||||||
|
// It may not be set while the service is running. (eg. srvc.Start() has been called)
|
||||||
|
OnConnect func(peer *Peer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RandomPort() (int, error) {
|
||||||
|
l, err := net.Listen("tcp", ":0")
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
defer l.Close()
|
||||||
|
|
||||||
|
_, port, err := net.SplitHostPort(l.Addr().String())
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return strconv.Atoi(port)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(ctx context.Context, name string, port int) *Service {
|
||||||
|
srvc := &Service{
|
||||||
|
port: port,
|
||||||
|
Name: name,
|
||||||
|
}
|
||||||
|
|
||||||
|
srvc.Reset(ctx)
|
||||||
|
return srvc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srvc *Service) Reset(ctx context.Context) {
|
||||||
|
srvc.ctx = ctx
|
||||||
|
srvc.packetHandlers = make(map[uint32]PacketHandler)
|
||||||
|
srvc.peers = make(map[chan *PacketEvent]*Peer)
|
||||||
|
srvc.started = make(chan struct{})
|
||||||
|
srvc.stopped = make(chan struct{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// may not be called while the service is running (eg. srvc.Start() has been called)
|
||||||
|
func (srvc *Service) AddPacketHandler(pktID uint32, handler PacketHandler) {
|
||||||
|
srvc.packetHandlers[pktID] = handler
|
||||||
|
}
|
||||||
|
|
||||||
|
type newPeerConnection struct {
|
||||||
|
peer *Peer
|
||||||
|
channel chan *PacketEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srvc *Service) Start() error {
|
||||||
|
peerConnections := make(chan newPeerConnection)
|
||||||
|
defer close(peerConnections)
|
||||||
|
go srvc.handleEvents(peerConnections)
|
||||||
|
|
||||||
|
// open listener socket
|
||||||
|
var err error
|
||||||
|
srvc.listener, err = net.Listen("tcp", fmt.Sprintf(":%d", srvc.port))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer srvc.listener.Close()
|
||||||
|
|
||||||
|
log.Printf("%s service hosted on %s:%d\n", srvc.Name, config.GetAnnounceIP(), srvc.port)
|
||||||
|
|
||||||
|
close(srvc.started) // signal that the service has started
|
||||||
|
for {
|
||||||
|
conn, err := srvc.listener.Accept()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
// we expect this to happen when the service is stopped
|
||||||
|
if errors.Is(err, net.ErrClosed) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a new peer and pass it to the event loop
|
||||||
|
peer := NewPeer(srvc.ctx, conn)
|
||||||
|
eRecv := make(chan *PacketEvent)
|
||||||
|
peerConnections <- newPeerConnection{channel: eRecv, peer: peer}
|
||||||
|
go peer.Handler(eRecv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srvc *Service) getPeer(channel chan *PacketEvent) *Peer {
|
||||||
|
return srvc.peers[channel]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srvc *Service) setPeer(channel chan *PacketEvent, peer *Peer) {
|
||||||
|
srvc.peers[channel] = peer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srvc *Service) removePeer(channel chan *PacketEvent) {
|
||||||
|
delete(srvc.peers, channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns a channel that is closed when the service has started.
|
||||||
|
// this is useful if you need to wait until after the service has started.
|
||||||
|
func (srvc *Service) Started() <-chan struct{} {
|
||||||
|
return srvc.started
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns a channel that is closed when the service has stopped.
|
||||||
|
// this is useful if you need wait until after the service has stopped.
|
||||||
|
func (srvc *Service) Stopped() <-chan struct{} {
|
||||||
|
return srvc.stopped
|
||||||
|
}
|
||||||
|
|
||||||
|
// calls f for each peer in the service passing the peer and the stored uData.
|
||||||
|
// if f returns false, the iteration is stopped.
|
||||||
|
// NOTE: the peer map is not locked while iterating, if you're calling this
|
||||||
|
// outside of the service's event loop, you'll need to lock the peer map yourself.
|
||||||
|
func (srvc *Service) RangePeers(f func(peer *Peer) bool) {
|
||||||
|
for _, peer := range srvc.peers {
|
||||||
|
if !f(peer) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// locks the peer map.
|
||||||
|
func (srvc *Service) Lock() {
|
||||||
|
srvc.stateLock.Lock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// unlocks the peer map.
|
||||||
|
func (srvc *Service) Unlock() {
|
||||||
|
srvc.stateLock.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srvc *Service) stop() {
|
||||||
|
// OnDisconnect handler might need to do something important
|
||||||
|
srvc.RangePeers(func(peer *Peer) bool {
|
||||||
|
peer.Kill()
|
||||||
|
if srvc.OnDisconnect != nil {
|
||||||
|
srvc.OnDisconnect(peer)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
log.Printf("%s service stopped\n", srvc.Name)
|
||||||
|
close(srvc.stopped)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleEvents is the main event loop for the service.
|
||||||
|
// it handles all events from the peers and calls the appropriate handlers.
|
||||||
|
func (srvc *Service) handleEvents(peerPipe <-chan newPeerConnection) {
|
||||||
|
defer srvc.stop()
|
||||||
|
|
||||||
|
poll := make([]reflect.SelectCase, 0, 4)
|
||||||
|
|
||||||
|
// add the stop channel and the peer connection channel to our poll queue
|
||||||
|
poll = append(poll, reflect.SelectCase{
|
||||||
|
Dir: reflect.SelectRecv,
|
||||||
|
Chan: reflect.ValueOf(srvc.ctx.Done()),
|
||||||
|
})
|
||||||
|
poll = append(poll, reflect.SelectCase{
|
||||||
|
Dir: reflect.SelectRecv,
|
||||||
|
Chan: reflect.ValueOf(peerPipe),
|
||||||
|
})
|
||||||
|
|
||||||
|
addPoll := func(channel chan *PacketEvent) {
|
||||||
|
poll = append(poll, reflect.SelectCase{
|
||||||
|
Dir: reflect.SelectRecv,
|
||||||
|
Chan: reflect.ValueOf(channel),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
removePoll := func(index int) {
|
||||||
|
poll = append(poll[:index], poll[index+1:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
chosen, value, recvOK := reflect.Select(poll)
|
||||||
|
switch chosen {
|
||||||
|
case 0: // cancel signal received, stop the service
|
||||||
|
return
|
||||||
|
case 1: // new peer, add it to our poll queue
|
||||||
|
if !recvOK {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
evnt := value.Interface().(newPeerConnection)
|
||||||
|
addPoll(evnt.channel)
|
||||||
|
srvc.connect(evnt.channel, evnt.peer)
|
||||||
|
default: // peer event
|
||||||
|
channel := poll[chosen].Chan.Interface().(chan *PacketEvent)
|
||||||
|
peer := srvc.getPeer(channel)
|
||||||
|
if peer == nil {
|
||||||
|
log.Printf("Unknown peer event: %v", value)
|
||||||
|
removePoll(chosen)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
evnt, ok := value.Interface().(*PacketEvent)
|
||||||
|
if !recvOK || !ok || evnt == nil {
|
||||||
|
// peer disconnected, remove it from our poll queue
|
||||||
|
removePoll(chosen)
|
||||||
|
srvc.disconnect(channel, peer)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
srvc.Lock()
|
||||||
|
if err := srvc.handlePacket(peer, evnt.PktID, protocol.NewPacket(evnt.Pkt)); err != nil {
|
||||||
|
log.Printf("Error handling packet: %v", err)
|
||||||
|
peer.Kill()
|
||||||
|
}
|
||||||
|
srvc.Unlock()
|
||||||
|
|
||||||
|
// the packet buffer is given to us by the event, so we'll need to make sure to return it to the pool
|
||||||
|
protocol.PutBuffer(evnt.Pkt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srvc *Service) handlePacket(peer *Peer, typeID uint32, pkt protocol.Packet) error {
|
||||||
|
if hndlr, ok := srvc.packetHandlers[typeID]; ok {
|
||||||
|
// fmt.Printf("Handling packet %x\n", typeID)
|
||||||
|
if err := hndlr(peer, pkt); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Printf("[WARN] unknown packet ID: %x\n", typeID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srvc *Service) disconnect(channel chan *PacketEvent, peer *Peer) {
|
||||||
|
log.Printf("Peer %p disconnected from %s\n", peer, srvc.Name)
|
||||||
|
if srvc.OnDisconnect != nil {
|
||||||
|
srvc.OnDisconnect(peer)
|
||||||
|
}
|
||||||
|
|
||||||
|
srvc.removePeer(channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srvc *Service) connect(channel chan *PacketEvent, peer *Peer) {
|
||||||
|
log.Printf("New peer %p connected to %s\n", peer, srvc.Name)
|
||||||
|
if srvc.OnConnect != nil {
|
||||||
|
srvc.OnConnect(peer)
|
||||||
|
}
|
||||||
|
|
||||||
|
srvc.setPeer(channel, peer)
|
||||||
|
}
|
98
cnet/service_test.go
Normal file
98
cnet/service_test.go
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
package cnet_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/CPunch/gopenfusion/cnet"
|
||||||
|
"github.com/CPunch/gopenfusion/cnet/protocol"
|
||||||
|
"github.com/CPunch/gopenfusion/internal/testutil"
|
||||||
|
"github.com/matryer/is"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
srvcPort int
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
timeout = 2 * time.Second
|
||||||
|
maxDummyPeers = 5
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
var err error
|
||||||
|
srvcPort, err = cnet.RandomPort()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is fine since we don't defer anything
|
||||||
|
os.Exit(m.Run())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestService(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
srvc := cnet.NewService(ctx, "TEST", srvcPort)
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
|
||||||
|
// shutdown service when test is done
|
||||||
|
defer func() {
|
||||||
|
cancel()
|
||||||
|
is.True(testutil.SelectWithTimeout(srvc.Stopped(), timeout)) // wait for service to stop with timeout
|
||||||
|
}()
|
||||||
|
|
||||||
|
// our dummy packet handler
|
||||||
|
wg.Add(maxDummyPeers)
|
||||||
|
srvc.AddPacketHandler(0x1234, func(peer *cnet.Peer, pkt protocol.Packet) error {
|
||||||
|
log.Printf("Received packet %#v", pkt)
|
||||||
|
wg.Done()
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// wait for all dummy peers to connect and disconnect
|
||||||
|
wg.Add(maxDummyPeers)
|
||||||
|
srvc.OnConnect = func(peer *cnet.Peer) {
|
||||||
|
wg.Done()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Add(maxDummyPeers)
|
||||||
|
srvc.OnDisconnect = func(peer *cnet.Peer) {
|
||||||
|
wg.Done()
|
||||||
|
}
|
||||||
|
|
||||||
|
// run service
|
||||||
|
go func() { is.NoErr(srvc.Start()) }() // srvc.Start error
|
||||||
|
is.True(testutil.SelectWithTimeout(srvc.Started(), timeout)) // wait for service to start with timeout
|
||||||
|
|
||||||
|
wg.Add(maxDummyPeers * 2) // 2 wg.Done() per peer for receiving packets
|
||||||
|
for i := 0; i < maxDummyPeers; i++ {
|
||||||
|
go func() {
|
||||||
|
// make dummy client
|
||||||
|
conn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", srvcPort))
|
||||||
|
is.NoErr(err) // net.Dial error
|
||||||
|
|
||||||
|
peer := cnet.NewPeer(ctx, conn)
|
||||||
|
go func() {
|
||||||
|
defer peer.Kill()
|
||||||
|
|
||||||
|
// send dummy packets
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
is.NoErr(peer.Send(0x1234)) // peer.Send error
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// we wait until Handler gracefully exits (peer was killed)
|
||||||
|
peer.Handler(make(chan *cnet.PacketEvent))
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
is.True(testutil.WaitWithTimeout(&wg, timeout)) // wait for all dummy peers to be done with timeout
|
||||||
|
}
|
4
go.mod
4
go.mod
@@ -3,11 +3,13 @@ module github.com/CPunch/gopenfusion
|
|||||||
go 1.19
|
go 1.19
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/alicebob/miniredis/v2 v2.31.0
|
||||||
github.com/bitcomplete/sqltestutil v1.0.1
|
github.com/bitcomplete/sqltestutil v1.0.1
|
||||||
github.com/blockloop/scan v1.3.0
|
github.com/blockloop/scan v1.3.0
|
||||||
github.com/georgysavva/scany/v2 v2.0.0
|
github.com/georgysavva/scany/v2 v2.0.0
|
||||||
github.com/google/subcommands v1.2.0
|
github.com/google/subcommands v1.2.0
|
||||||
github.com/lib/pq v1.10.9
|
github.com/lib/pq v1.10.9
|
||||||
|
github.com/matryer/is v1.4.1
|
||||||
github.com/redis/go-redis/v9 v9.0.5
|
github.com/redis/go-redis/v9 v9.0.5
|
||||||
golang.org/x/crypto v0.7.0
|
golang.org/x/crypto v0.7.0
|
||||||
)
|
)
|
||||||
@@ -15,6 +17,7 @@ require (
|
|||||||
require (
|
require (
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
|
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
|
||||||
github.com/Microsoft/go-winio v0.5.2 // indirect
|
github.com/Microsoft/go-winio v0.5.2 // indirect
|
||||||
|
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
@@ -33,6 +36,7 @@ require (
|
|||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/sirupsen/logrus v1.8.1 // indirect
|
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||||
github.com/stretchr/testify v1.8.0 // indirect
|
github.com/stretchr/testify v1.8.0 // indirect
|
||||||
|
github.com/yuin/gopher-lua v1.1.0 // indirect
|
||||||
golang.org/x/net v0.8.0 // indirect
|
golang.org/x/net v0.8.0 // indirect
|
||||||
golang.org/x/sys v0.6.0 // indirect
|
golang.org/x/sys v0.6.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
14
go.sum
14
go.sum
@@ -1,7 +1,12 @@
|
|||||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
|
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||||
|
github.com/DmitriyVTitov/size v1.5.0/go.mod h1:le6rNI4CoLQV1b9gzp1+3d7hMAD/uu2QcJ+aYbNgiU0=
|
||||||
github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA=
|
github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA=
|
||||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||||
|
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk=
|
||||||
|
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
|
||||||
|
github.com/alicebob/miniredis/v2 v2.31.0 h1:ObEFUNlJwoIiyjxdrYF0QIDE7qXcLc7D3WpSH4c22PU=
|
||||||
|
github.com/alicebob/miniredis/v2 v2.31.0/go.mod h1:UB/T2Uztp7MlFSDakaX1sTXUv5CASoprx0wulRT6HBg=
|
||||||
github.com/bitcomplete/sqltestutil v1.0.1 h1:rj/RgrXXyuPB8KYrFmxiSjORb1hrhK6sXHpDPaSEBII=
|
github.com/bitcomplete/sqltestutil v1.0.1 h1:rj/RgrXXyuPB8KYrFmxiSjORb1hrhK6sXHpDPaSEBII=
|
||||||
github.com/bitcomplete/sqltestutil v1.0.1/go.mod h1:ZgpEnW6t2RBsCo9EIEYsAvjxJeZDwOzC8aVYXK0+gdE=
|
github.com/bitcomplete/sqltestutil v1.0.1/go.mod h1:ZgpEnW6t2RBsCo9EIEYsAvjxJeZDwOzC8aVYXK0+gdE=
|
||||||
github.com/blockloop/scan v1.3.0 h1:p8xnajpGA3d/V6o23IBFdQ764+JnNJ+PQj+OwT+rkdg=
|
github.com/blockloop/scan v1.3.0 h1:p8xnajpGA3d/V6o23IBFdQ764+JnNJ+PQj+OwT+rkdg=
|
||||||
@@ -10,6 +15,9 @@ github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao=
|
|||||||
github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y=
|
github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y=
|
||||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||||
|
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||||
|
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||||
github.com/cockroachdb/cockroach-go/v2 v2.2.0 h1:/5znzg5n373N/3ESjHF5SMLxiW4RKB05Ql//KWfeTFs=
|
github.com/cockroachdb/cockroach-go/v2 v2.2.0 h1:/5znzg5n373N/3ESjHF5SMLxiW4RKB05Ql//KWfeTFs=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
@@ -33,6 +41,7 @@ github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaL
|
|||||||
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
|
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
|
||||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE=
|
github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE=
|
||||||
@@ -48,6 +57,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
|
|||||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
|
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
|
||||||
|
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||||
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||||
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||||
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
|
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
|
||||||
@@ -77,6 +88,8 @@ github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PK
|
|||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/gopher-lua v1.1.0 h1:BojcDhfyDWgU2f2TOzYK/g5p2gxMrku8oupLDqlnSqE=
|
||||||
|
github.com/yuin/gopher-lua v1.1.0/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
@@ -93,6 +106,7 @@ golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
|||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
@@ -1,12 +1,11 @@
|
|||||||
package db
|
package db
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"errors"
|
||||||
"log"
|
|
||||||
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
"github.com/CPunch/gopenfusion/internal/protocol"
|
"github.com/CPunch/gopenfusion/cnet/protocol"
|
||||||
"github.com/georgysavva/scany/v2/sqlscan"
|
"github.com/georgysavva/scany/v2/sqlscan"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -44,8 +43,8 @@ func (db *DBHandler) NewAccount(Login, Password string) (*Account, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrLoginInvalidID = fmt.Errorf("invalid Login ID")
|
ErrLoginInvalidID = errors.New("invalid Login ID")
|
||||||
ErrLoginInvalidPassword = fmt.Errorf("invalid ID && Password combo")
|
ErrLoginInvalidPassword = errors.New("invalid ID && Password combo")
|
||||||
)
|
)
|
||||||
|
|
||||||
func (db *DBHandler) TryLogin(Login, Password string) (*Account, error) {
|
func (db *DBHandler) TryLogin(Login, Password string) (*Account, error) {
|
||||||
@@ -54,10 +53,14 @@ func (db *DBHandler) TryLogin(Login, Password string) (*Account, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// make sure id && pw are valid
|
||||||
|
if len(Login) < 4 || len(Password) < 4 {
|
||||||
|
return nil, ErrLoginInvalidPassword
|
||||||
|
}
|
||||||
|
|
||||||
var account Account
|
var account Account
|
||||||
row.Next()
|
row.Next()
|
||||||
if err := sqlscan.ScanRow(&account, row); err != nil {
|
if err := sqlscan.ScanRow(&account, row); err != nil {
|
||||||
log.Printf("Error scanning row: %v", err)
|
|
||||||
return nil, ErrLoginInvalidID
|
return nil, ErrLoginInvalidID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -2,11 +2,14 @@ package db_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/matryer/is"
|
||||||
|
|
||||||
|
"github.com/CPunch/gopenfusion/cnet/protocol"
|
||||||
"github.com/CPunch/gopenfusion/internal/db"
|
"github.com/CPunch/gopenfusion/internal/db"
|
||||||
"github.com/CPunch/gopenfusion/internal/protocol"
|
|
||||||
"github.com/bitcomplete/sqltestutil"
|
"github.com/bitcomplete/sqltestutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -15,6 +18,11 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
|
ret := 1
|
||||||
|
defer func() {
|
||||||
|
os.Exit(ret)
|
||||||
|
}()
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
psql, err := sqltestutil.StartPostgresContainer(ctx, "15")
|
psql, err := sqltestutil.StartPostgresContainer(ctx, "15")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -32,27 +40,24 @@ func TestMain(m *testing.M) {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
os.Exit(m.Run())
|
ret = m.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDBAccount(t *testing.T) {
|
func TestDBAccount(t *testing.T) {
|
||||||
if _, err := testDB.NewAccount("test", "test"); err != nil {
|
is := is.New(t)
|
||||||
t.Error(err)
|
|
||||||
}
|
// create new account
|
||||||
|
_, err := testDB.NewAccount("test", "test")
|
||||||
|
is.NoErr(err)
|
||||||
|
|
||||||
// now try to retrieve account data
|
// now try to retrieve account data
|
||||||
acc, err := testDB.TryLogin("test", "test")
|
acc, err := testDB.TryLogin("test", "test")
|
||||||
if err != nil {
|
is.NoErr(err)
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if acc.Login != "test" {
|
_, err = testDB.TryLogin("test", "wrongpassword")
|
||||||
t.Error("account username is not test")
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err = testDB.TryLogin("test", "wrongpassword"); err != db.ErrLoginInvalidPassword {
|
is.True(acc.Login == "test") // login data should match created account
|
||||||
t.Error("expected ErrLoginInvalidPassword")
|
is.True(errors.Is(err, db.ErrLoginInvalidPassword)) // wrong password passed to TryLogin() should fail with ErrLoginInvalidPassword
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -80,22 +85,18 @@ gopenfusion=# SELECT * FROM Inventory;
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
func TestDBPlayer(t *testing.T) {
|
func TestDBPlayer(t *testing.T) {
|
||||||
if _, err := testDB.NewAccount("testplayer", "test"); err != nil {
|
is := is.New(t)
|
||||||
t.Error(err)
|
_, err := testDB.NewAccount("testplayer", "test")
|
||||||
}
|
is.NoErr(err)
|
||||||
|
|
||||||
// now try to retrieve account data
|
// now try to retrieve account data
|
||||||
acc, err := testDB.TryLogin("testplayer", "test")
|
acc, err := testDB.TryLogin("testplayer", "test")
|
||||||
if err != nil {
|
is.NoErr(err)
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
plrID, err := testDB.NewPlayer(acc.AccountID, "Neil", "Mcscout", 1)
|
plrID, err := testDB.NewPlayer(acc.AccountID, "Neil", "Mcscout", 1)
|
||||||
if err != nil {
|
is.NoErr(err)
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = testDB.FinishPlayer(&protocol.SP_CL2LS_REQ_CHAR_CREATE{
|
err = testDB.FinishPlayer(&protocol.SP_CL2LS_REQ_CHAR_CREATE{
|
||||||
PCStyle: protocol.SPCStyle{
|
PCStyle: protocol.SPCStyle{
|
||||||
IPC_UID: int64(plrID),
|
IPC_UID: int64(plrID),
|
||||||
INameCheck: 1,
|
INameCheck: 1,
|
||||||
@@ -115,11 +116,9 @@ func TestDBPlayer(t *testing.T) {
|
|||||||
IEquipLBID: 359,
|
IEquipLBID: 359,
|
||||||
IEquipFootID: 194,
|
IEquipFootID: 194,
|
||||||
},
|
},
|
||||||
}, acc.AccountID); err != nil {
|
}, acc.AccountID)
|
||||||
t.Error(err)
|
is.NoErr(err)
|
||||||
}
|
|
||||||
|
|
||||||
if err = testDB.FinishTutorial(plrID, acc.AccountID); err != nil {
|
err = testDB.FinishTutorial(plrID, acc.AccountID)
|
||||||
t.Error(err)
|
is.NoErr(err)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -3,7 +3,7 @@ package db
|
|||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
|
||||||
"github.com/CPunch/gopenfusion/internal/protocol"
|
"github.com/CPunch/gopenfusion/cnet/protocol"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Inventory struct {
|
type Inventory struct {
|
||||||
|
@@ -3,12 +3,42 @@ package db
|
|||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
|
||||||
"github.com/CPunch/gopenfusion/config"
|
"github.com/CPunch/gopenfusion/cnet/protocol"
|
||||||
"github.com/CPunch/gopenfusion/internal/entity"
|
"github.com/CPunch/gopenfusion/internal/config"
|
||||||
"github.com/CPunch/gopenfusion/internal/protocol"
|
|
||||||
"github.com/blockloop/scan"
|
"github.com/blockloop/scan"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Player struct {
|
||||||
|
PlayerID int
|
||||||
|
AccountID int
|
||||||
|
AccountLevel int
|
||||||
|
Slot int
|
||||||
|
PCStyle protocol.SPCStyle
|
||||||
|
PCStyle2 protocol.SPCStyle2
|
||||||
|
EquippedNanos [3]int
|
||||||
|
Nanos [config.NANO_COUNT]protocol.SNano
|
||||||
|
Equip [config.AEQUIP_COUNT]protocol.SItemBase
|
||||||
|
Inven [config.AINVEN_COUNT]protocol.SItemBase
|
||||||
|
Bank [config.ABANK_COUNT]protocol.SItemBase
|
||||||
|
SkywayLocationFlag []byte
|
||||||
|
FirstUseFlag []byte
|
||||||
|
Quests []byte
|
||||||
|
HP int
|
||||||
|
Level int
|
||||||
|
Taros int
|
||||||
|
FusionMatter int
|
||||||
|
Mentor int
|
||||||
|
X, Y, Z int
|
||||||
|
Angle int
|
||||||
|
BatteryN int
|
||||||
|
BatteryW int
|
||||||
|
WarpLocationFlag int
|
||||||
|
ActiveNanoSlotNum int
|
||||||
|
Fatigue int
|
||||||
|
CurrentMissionID int
|
||||||
|
IPCState int8
|
||||||
|
}
|
||||||
|
|
||||||
// returns PlayerID, error
|
// returns PlayerID, error
|
||||||
func (db *DBHandler) NewPlayer(AccountID int, FirstName, LastName string, slot int) (int, error) {
|
func (db *DBHandler) NewPlayer(AccountID int, FirstName, LastName string, slot int) (int, error) {
|
||||||
nameCheck := 1 // for now, we approve all names
|
nameCheck := 1 // for now, we approve all names
|
||||||
@@ -121,8 +151,8 @@ const (
|
|||||||
INNER JOIN Accounts as acc ON p.AccountID = acc.AccountID `
|
INNER JOIN Accounts as acc ON p.AccountID = acc.AccountID `
|
||||||
)
|
)
|
||||||
|
|
||||||
func (db *DBHandler) readPlayer(rows *sql.Rows) (*entity.Player, error) {
|
func (db *DBHandler) readPlayer(rows *sql.Rows) (*Player, error) {
|
||||||
plr := entity.Player{ActiveNanoSlotNum: 0}
|
plr := Player{ActiveNanoSlotNum: 0}
|
||||||
|
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
&plr.PlayerID, &plr.AccountID, &plr.Slot, &plr.PCStyle.SzFirstName, &plr.PCStyle.SzLastName,
|
&plr.PlayerID, &plr.AccountID, &plr.Slot, &plr.PCStyle.SzFirstName, &plr.PCStyle.SzLastName,
|
||||||
@@ -162,13 +192,13 @@ func (db *DBHandler) readPlayer(rows *sql.Rows) (*entity.Player, error) {
|
|||||||
return &plr, nil
|
return &plr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *DBHandler) GetPlayer(PlayerID int) (*entity.Player, error) {
|
func (db *DBHandler) GetPlayer(PlayerID int) (*Player, error) {
|
||||||
rows, err := db.Query(QUERY_PLAYERS+"WHERE p.PlayerID = $1", PlayerID)
|
rows, err := db.Query(QUERY_PLAYERS+"WHERE p.PlayerID = $1", PlayerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var plr *entity.Player
|
var plr *Player
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
plr, err = db.readPlayer(rows)
|
plr, err = db.readPlayer(rows)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -179,13 +209,13 @@ func (db *DBHandler) GetPlayer(PlayerID int) (*entity.Player, error) {
|
|||||||
return plr, nil
|
return plr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *DBHandler) GetPlayers(AccountID int) ([]entity.Player, error) {
|
func (db *DBHandler) GetPlayers(AccountID int) ([]Player, error) {
|
||||||
rows, err := db.Query(QUERY_PLAYERS+"WHERE p.AccountID = $1", AccountID)
|
rows, err := db.Query(QUERY_PLAYERS+"WHERE p.AccountID = $1", AccountID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var plrs []entity.Player
|
var plrs []Player
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
plr, err := db.readPlayer(rows)
|
plr, err := db.readPlayer(rows)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@@ -10,7 +10,7 @@ import (
|
|||||||
_ "embed"
|
_ "embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/CPunch/gopenfusion/config"
|
"github.com/CPunch/gopenfusion/internal/config"
|
||||||
_ "github.com/lib/pq"
|
_ "github.com/lib/pq"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -1,15 +0,0 @@
|
|||||||
package entity
|
|
||||||
|
|
||||||
import "github.com/CPunch/gopenfusion/config"
|
|
||||||
|
|
||||||
type ChunkPosition struct {
|
|
||||||
X int
|
|
||||||
Y int
|
|
||||||
}
|
|
||||||
|
|
||||||
func MakeChunkPosition(x, y int) ChunkPosition {
|
|
||||||
return ChunkPosition{
|
|
||||||
X: x / (config.VIEW_DISTANCE / 3),
|
|
||||||
Y: y / (config.VIEW_DISTANCE / 3),
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,150 +0,0 @@
|
|||||||
package protocol
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/CPunch/gopenfusion/internal/protocol/pool"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
USE_E = iota
|
|
||||||
USE_FE
|
|
||||||
)
|
|
||||||
|
|
||||||
// CNPeer is a simple wrapper for net.Conn connections to send/recv packets over the Fusionfall packet protocol.
|
|
||||||
type CNPeer struct {
|
|
||||||
conn net.Conn
|
|
||||||
eRecv chan *Event
|
|
||||||
SzID string
|
|
||||||
E_key []byte
|
|
||||||
FE_key []byte
|
|
||||||
AccountID int
|
|
||||||
PlayerID int32
|
|
||||||
whichKey int
|
|
||||||
alive bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetTime() uint64 {
|
|
||||||
return uint64(time.Now().UnixMilli())
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCNPeer(eRecv chan *Event, conn net.Conn) *CNPeer {
|
|
||||||
return &CNPeer{
|
|
||||||
conn: conn,
|
|
||||||
eRecv: eRecv,
|
|
||||||
SzID: "",
|
|
||||||
E_key: []byte(DEFAULT_KEY),
|
|
||||||
FE_key: nil,
|
|
||||||
AccountID: -1,
|
|
||||||
whichKey: USE_E,
|
|
||||||
alive: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (peer *CNPeer) Send(typeID uint32, data ...interface{}) error {
|
|
||||||
// grab buffer from pool
|
|
||||||
buf := pool.Get()
|
|
||||||
defer pool.Put(buf)
|
|
||||||
|
|
||||||
// allocate space for packet size
|
|
||||||
buf.Write(make([]byte, 4))
|
|
||||||
|
|
||||||
// body start
|
|
||||||
pkt := NewPacket(buf)
|
|
||||||
|
|
||||||
// encode type id
|
|
||||||
if err := pkt.Encode(typeID); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// encode data
|
|
||||||
for _, trailer := range data {
|
|
||||||
if err := pkt.Encode(trailer); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// prepend the packet size
|
|
||||||
binary.LittleEndian.PutUint32(buf.Bytes()[:4], uint32(buf.Len()-4))
|
|
||||||
|
|
||||||
// encrypt body
|
|
||||||
switch peer.whichKey {
|
|
||||||
case USE_E:
|
|
||||||
EncryptData(buf.Bytes()[4:], peer.E_key)
|
|
||||||
case USE_FE:
|
|
||||||
EncryptData(buf.Bytes()[4:], peer.FE_key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// send full packet
|
|
||||||
log.Printf("Sending %#v, sizeof: %d, buffer: %v", data, buf.Len(), buf.Bytes())
|
|
||||||
if _, err := peer.conn.Write(buf.Bytes()); err != nil {
|
|
||||||
return fmt.Errorf("failed to write packet body! %v", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (peer *CNPeer) SetActiveKey(whichKey int) {
|
|
||||||
peer.whichKey = whichKey
|
|
||||||
}
|
|
||||||
|
|
||||||
func (peer *CNPeer) Kill() {
|
|
||||||
log.Printf("Killing peer %p", peer)
|
|
||||||
if !peer.alive {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
peer.alive = false
|
|
||||||
peer.conn.Close()
|
|
||||||
peer.eRecv <- &Event{Type: EVENT_CLIENT_DISCONNECT, Peer: peer}
|
|
||||||
}
|
|
||||||
|
|
||||||
// meant to be invoked as a goroutine
|
|
||||||
func (peer *CNPeer) Handler() {
|
|
||||||
defer peer.Kill()
|
|
||||||
|
|
||||||
for {
|
|
||||||
// read packet size, the goroutine spends most of it's time parked here
|
|
||||||
var sz uint32
|
|
||||||
if err := binary.Read(peer.conn, binary.LittleEndian, &sz); err != nil {
|
|
||||||
log.Printf("[FATAL] failed to read packet size! %v\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// client should never send a packet size outside of this range
|
|
||||||
if sz > CN_PACKET_BUFFER_SIZE || sz < 4 {
|
|
||||||
log.Printf("[FATAL] malicious packet size received! %d", sz)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// grab buffer && read packet body
|
|
||||||
if err := func() error {
|
|
||||||
buf := pool.Get()
|
|
||||||
if _, err := buf.ReadFrom(io.LimitReader(peer.conn, int64(sz))); err != nil {
|
|
||||||
return fmt.Errorf("failed to read packet body! %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// decrypt
|
|
||||||
DecryptData(buf.Bytes(), peer.E_key)
|
|
||||||
pkt := NewPacket(buf)
|
|
||||||
|
|
||||||
// create packet && read pktID
|
|
||||||
var pktID uint32
|
|
||||||
if err := pkt.Decode(&pktID); err != nil {
|
|
||||||
return fmt.Errorf("failed to read packet type! %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// dispatch packet
|
|
||||||
log.Printf("Got packet ID: %x, with a sizeof: %d\n", pktID, sz)
|
|
||||||
peer.eRecv <- &Event{Type: EVENT_CLIENT_PACKET, Peer: peer, Pkt: buf, PktID: pktID}
|
|
||||||
return nil
|
|
||||||
}(); err != nil {
|
|
||||||
log.Printf("[FATAL] %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,15 +0,0 @@
|
|||||||
package protocol
|
|
||||||
|
|
||||||
import "bytes"
|
|
||||||
|
|
||||||
const (
|
|
||||||
EVENT_CLIENT_DISCONNECT = iota
|
|
||||||
EVENT_CLIENT_PACKET
|
|
||||||
)
|
|
||||||
|
|
||||||
type Event struct {
|
|
||||||
Type int
|
|
||||||
Peer *CNPeer
|
|
||||||
Pkt *bytes.Buffer
|
|
||||||
PktID uint32
|
|
||||||
}
|
|
@@ -1,64 +0,0 @@
|
|||||||
package protocol_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/CPunch/gopenfusion/internal/protocol"
|
|
||||||
)
|
|
||||||
|
|
||||||
type TestPacketData struct {
|
|
||||||
A int32
|
|
||||||
B int32
|
|
||||||
UTF16Str string `size:"32"`
|
|
||||||
Pad int16 `pad:"2"`
|
|
||||||
C int32
|
|
||||||
}
|
|
||||||
|
|
||||||
// this is the data we expect to get from encoding the above struct with:
|
|
||||||
//
|
|
||||||
// Encode(TestPacketData{
|
|
||||||
// A: 1,
|
|
||||||
// B: 2,
|
|
||||||
// UTF16Str: "hello world",
|
|
||||||
// C: 3,
|
|
||||||
// })
|
|
||||||
var testData = [...]byte{
|
|
||||||
0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00,
|
|
||||||
0x68, 0x00, 0x65, 0x00, 0x6c, 0x00, 0x6c, 0x00,
|
|
||||||
0x6f, 0x00, 0x20, 0x00, 0x77, 0x00, 0x6f, 0x00,
|
|
||||||
0x72, 0x00, 0x6c, 0x00, 0x64, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00,
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPacketEncodeDecode(t *testing.T) {
|
|
||||||
buf := &bytes.Buffer{}
|
|
||||||
pkt := protocol.NewPacket(buf)
|
|
||||||
|
|
||||||
if err := pkt.Encode(TestPacketData{
|
|
||||||
A: 1,
|
|
||||||
B: 2,
|
|
||||||
UTF16Str: "hello world",
|
|
||||||
C: 3,
|
|
||||||
}); err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !bytes.Equal(buf.Bytes(), testData[:]) {
|
|
||||||
t.Error("packet data does not match!")
|
|
||||||
}
|
|
||||||
|
|
||||||
var test TestPacketData
|
|
||||||
if err := pkt.Decode(&test); err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if test.A != 1 || test.B != 2 || test.C != 3 || test.UTF16Str != "hello world" {
|
|
||||||
t.Error("decoded packet data does not match!")
|
|
||||||
}
|
|
||||||
}
|
|
@@ -5,7 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/CPunch/gopenfusion/config"
|
"github.com/CPunch/gopenfusion/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LoginMetadata struct {
|
type LoginMetadata struct {
|
||||||
|
@@ -7,7 +7,7 @@ package redis
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/CPunch/gopenfusion/config"
|
"github.com/CPunch/gopenfusion/internal/config"
|
||||||
|
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
)
|
)
|
||||||
@@ -17,10 +17,6 @@ type RedisHandler struct {
|
|||||||
ctx context.Context
|
ctx context.Context
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
|
||||||
SHARD_SET = "shards"
|
|
||||||
)
|
|
||||||
|
|
||||||
func OpenRedis(addr string) (*RedisHandler, error) {
|
func OpenRedis(addr string) (*RedisHandler, error) {
|
||||||
client := redis.NewClient(&redis.Options{
|
client := redis.NewClient(&redis.Options{
|
||||||
Addr: addr,
|
Addr: addr,
|
||||||
|
82
internal/redis/redis_test.go
Normal file
82
internal/redis/redis_test.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package redis_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/CPunch/gopenfusion/internal/redis"
|
||||||
|
"github.com/alicebob/miniredis/v2"
|
||||||
|
"github.com/matryer/is"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
rh *redis.RedisHandler
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
ret := 1
|
||||||
|
defer func() {
|
||||||
|
os.Exit(ret)
|
||||||
|
}()
|
||||||
|
|
||||||
|
r, err := miniredis.Run()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer r.Close()
|
||||||
|
|
||||||
|
rh, err = redis.OpenRedis(r.Addr())
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer rh.Close()
|
||||||
|
|
||||||
|
ret = m.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRedisLogin(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
|
||||||
|
// test data
|
||||||
|
serialKey := int64(1234)
|
||||||
|
data := redis.LoginMetadata{
|
||||||
|
FEKey: []byte("test"),
|
||||||
|
PlayerID: 1,
|
||||||
|
AccountID: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
// queue login
|
||||||
|
is.NoErr(rh.QueueLogin(serialKey, data))
|
||||||
|
|
||||||
|
// get login
|
||||||
|
loginData, err := rh.GetLogin(serialKey)
|
||||||
|
is.NoErr(err)
|
||||||
|
|
||||||
|
// compare
|
||||||
|
is.Equal(loginData, data) // received data should be the same as sent data
|
||||||
|
|
||||||
|
// delete login
|
||||||
|
is.NoErr(rh.RemoveLogin(serialKey))
|
||||||
|
|
||||||
|
// get login
|
||||||
|
_, err = rh.GetLogin(serialKey)
|
||||||
|
is.True(err != nil) // should fail to get removed login
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRedisShard(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
|
||||||
|
// test data
|
||||||
|
shard := redis.ShardMetadata{
|
||||||
|
IP: "0.0.0.0",
|
||||||
|
Port: 1234,
|
||||||
|
}
|
||||||
|
|
||||||
|
// register shard
|
||||||
|
is.NoErr(rh.RegisterShard(shard))
|
||||||
|
|
||||||
|
// get shards
|
||||||
|
shards := rh.GetShards()
|
||||||
|
is.True(len(shards) == 1) // should only be 1 shard
|
||||||
|
is.Equal(shards[0], shard) // received data should be the same as sent data
|
||||||
|
}
|
@@ -7,6 +7,10 @@ type ShardMetadata struct {
|
|||||||
Port int
|
Port int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
SHARD_SET = "shards"
|
||||||
|
)
|
||||||
|
|
||||||
func (r *RedisHandler) RegisterShard(shard ShardMetadata) error {
|
func (r *RedisHandler) RegisterShard(shard ShardMetadata) error {
|
||||||
value, err := json.Marshal(shard)
|
value, err := json.Marshal(shard)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
68
internal/testutil/account.go
Normal file
68
internal/testutil/account.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package testutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/CPunch/gopenfusion/cnet/protocol"
|
||||||
|
"github.com/CPunch/gopenfusion/internal/db"
|
||||||
|
"github.com/CPunch/gopenfusion/internal/redis"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
TestCharCreate = protocol.SP_CL2LS_REQ_CHAR_CREATE{
|
||||||
|
PCStyle: protocol.SPCStyle{
|
||||||
|
INameCheck: 1, SzFirstName: "Hector",
|
||||||
|
SzLastName: "Bannonvenom", IGender: 1, IFaceStyle: 1,
|
||||||
|
IHairStyle: 17, IHairColor: 11, ISkinColor: 10, IEyeColor: 2,
|
||||||
|
IHeight: 1, IBody: 0, IClass: 0,
|
||||||
|
},
|
||||||
|
SOn_Item: protocol.SOnItem{
|
||||||
|
IEquipHandID: 0, IEquipUBID: 53, IEquipLBID: 17, IEquipFootID: 58,
|
||||||
|
IEquipHeadID: 0, IEquipFaceID: 0, IEquipBackID: 0,
|
||||||
|
},
|
||||||
|
SOn_Item_Index: protocol.SOnItem_Index{
|
||||||
|
IEquipUBID_index: 15, IEquipLBID_index: 12, IEquipFootID_index: 17,
|
||||||
|
IFaceStyle: 2, IHairStyle: 18,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// creates a new account and player in the database
|
||||||
|
func MakeTestPlayer(db *db.DBHandler, id string, password string) (acc *db.Account, plr *db.Player, err error) {
|
||||||
|
acc, err = db.NewAccount(id, password)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var plrID int
|
||||||
|
plrID, err = db.NewPlayer(acc.AccountID, TestCharCreate.PCStyle.SzFirstName, TestCharCreate.PCStyle.SzLastName, 1)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
charCreate := TestCharCreate
|
||||||
|
charCreate.PCStyle.IPC_UID = int64(plrID)
|
||||||
|
err = db.FinishPlayer(&charCreate, acc.AccountID)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.FinishTutorial(plrID, acc.AccountID)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
plr, err = db.GetPlayer(plrID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func QueueLogin(redisHndlr *redis.RedisHandler, FEKey []byte, plrID, accID int) (int64, error) {
|
||||||
|
key, err := protocol.GenSerialKey()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return key, redisHndlr.QueueLogin(key, redis.LoginMetadata{
|
||||||
|
FEKey: FEKey,
|
||||||
|
PlayerID: int32(plrID),
|
||||||
|
AccountID: accID,
|
||||||
|
})
|
||||||
|
}
|
50
internal/testutil/dummy.go
Normal file
50
internal/testutil/dummy.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package testutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/CPunch/gopenfusion/cnet"
|
||||||
|
"github.com/CPunch/gopenfusion/cnet/protocol"
|
||||||
|
"github.com/matryer/is"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DummyPeer struct {
|
||||||
|
Recv chan *cnet.PacketEvent
|
||||||
|
Peer *cnet.Peer
|
||||||
|
is *is.I
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeDummyPeer creates a new dummy peer and returns it
|
||||||
|
func MakeDummyPeer(ctx context.Context, is *is.I, port int) *DummyPeer {
|
||||||
|
conn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port))
|
||||||
|
is.NoErr(err)
|
||||||
|
|
||||||
|
recv := make(chan *cnet.PacketEvent)
|
||||||
|
peer := cnet.NewPeer(ctx, conn)
|
||||||
|
go func() {
|
||||||
|
peer.Handler(recv)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return &DummyPeer{Recv: recv, Peer: peer, is: is}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendAndRecv sends a packet (sID & out), waits for the expected response (rID) and decodes it into in
|
||||||
|
func (dp *DummyPeer) SendAndRecv(sID, rID uint32, out, in interface{}) {
|
||||||
|
// send out packet
|
||||||
|
err := dp.Peer.Send(sID, out)
|
||||||
|
dp.is.NoErr(err) // peer.Send() should not return an error
|
||||||
|
|
||||||
|
// receive response
|
||||||
|
evnt := <-dp.Recv
|
||||||
|
defer protocol.PutBuffer(evnt.Pkt)
|
||||||
|
|
||||||
|
dp.is.Equal(evnt.PktID, rID) // should receive expected type
|
||||||
|
dp.is.NoErr(protocol.NewPacket(evnt.Pkt).Decode(in)) // packet.Decode() should not return an error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kill closes the peer's connection
|
||||||
|
func (dp *DummyPeer) Kill() {
|
||||||
|
dp.Peer.Kill()
|
||||||
|
}
|
52
internal/testutil/env.go
Normal file
52
internal/testutil/env.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package testutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/CPunch/gopenfusion/internal/db"
|
||||||
|
"github.com/CPunch/gopenfusion/internal/redis"
|
||||||
|
"github.com/alicebob/miniredis/v2"
|
||||||
|
"github.com/bitcomplete/sqltestutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetupEnvironment spawns a postgres container and returns a db and redis handler
|
||||||
|
// along with a cleanup function
|
||||||
|
func SetupEnvironment(ctx context.Context) (*db.DBHandler, *redis.RedisHandler, func()) {
|
||||||
|
// spawn postgres container
|
||||||
|
psql, err := sqltestutil.StartPostgresContainer(ctx, "15")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// open db handler
|
||||||
|
testDB, err := db.OpenFromConnectionString("postgres", psql.ConnectionString()+"?sslmode=disable")
|
||||||
|
if err != nil {
|
||||||
|
psql.Shutdown(ctx)
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = testDB.Setup(); err != nil {
|
||||||
|
psql.Shutdown(ctx)
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// start miniredis
|
||||||
|
r, err := miniredis.Run()
|
||||||
|
if err != nil {
|
||||||
|
psql.Shutdown(ctx)
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// open redis handler
|
||||||
|
rh, err := redis.OpenRedis(r.Addr())
|
||||||
|
if err != nil {
|
||||||
|
psql.Shutdown(ctx)
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return testDB, rh, func() {
|
||||||
|
psql.Shutdown(ctx)
|
||||||
|
rh.Close()
|
||||||
|
r.Close()
|
||||||
|
}
|
||||||
|
}
|
25
internal/testutil/helpers.go
Normal file
25
internal/testutil/helpers.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package testutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SelectWithTimeout(ch <-chan struct{}, timeout time.Duration) bool {
|
||||||
|
select {
|
||||||
|
case <-ch:
|
||||||
|
return true
|
||||||
|
case <-time.After(timeout):
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WaitWithTimeout(wg *sync.WaitGroup, timeout time.Duration) bool {
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(done)
|
||||||
|
wg.Wait()
|
||||||
|
}()
|
||||||
|
|
||||||
|
return SelectWithTimeout(done, timeout)
|
||||||
|
}
|
35
internal/testutil/testutil_test.go
Normal file
35
internal/testutil/testutil_test.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package testutil_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/CPunch/gopenfusion/internal/testutil"
|
||||||
|
"github.com/matryer/is"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWaitWithTimeout(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
go func() {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
is.True(!testutil.WaitWithTimeout(wg, 500*time.Millisecond)) // timeout should occur
|
||||||
|
is.True(testutil.WaitWithTimeout(wg, 750*time.Millisecond)) // timeout shouldn't occur
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSelectWithTimeout(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
ch := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
close(ch)
|
||||||
|
}()
|
||||||
|
|
||||||
|
is.True(!testutil.SelectWithTimeout(ch, 500*time.Millisecond)) // timeout should occur
|
||||||
|
is.True(testutil.SelectWithTimeout(ch, 750*time.Millisecond)) // timeout shouldn't occur
|
||||||
|
}
|
@@ -2,13 +2,15 @@ package login
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
|
||||||
"github.com/CPunch/gopenfusion/config"
|
"github.com/CPunch/gopenfusion/cnet"
|
||||||
|
"github.com/CPunch/gopenfusion/cnet/protocol"
|
||||||
|
"github.com/CPunch/gopenfusion/internal/config"
|
||||||
"github.com/CPunch/gopenfusion/internal/db"
|
"github.com/CPunch/gopenfusion/internal/db"
|
||||||
"github.com/CPunch/gopenfusion/internal/protocol"
|
|
||||||
"github.com/CPunch/gopenfusion/internal/redis"
|
"github.com/CPunch/gopenfusion/internal/redis"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -24,9 +26,7 @@ const (
|
|||||||
LOGIN_UPDATED_EUALA_REQUIRED = 9
|
LOGIN_UPDATED_EUALA_REQUIRED = 9
|
||||||
)
|
)
|
||||||
|
|
||||||
func (server *LoginServer) AcceptLogin(peer *protocol.CNPeer, SzID string, IClientVerC int32, ISlotNum int8, data []protocol.SP_LS2CL_REP_CHAR_INFO) error {
|
func (server *LoginServer) AcceptLogin(peer *cnet.Peer, SzID string, IClientVerC int32, ISlotNum int8, data []protocol.SP_LS2CL_REP_CHAR_INFO) error {
|
||||||
peer.SzID = SzID
|
|
||||||
|
|
||||||
resp := protocol.SP_LS2CL_REP_LOGIN_SUCC{
|
resp := protocol.SP_LS2CL_REP_LOGIN_SUCC{
|
||||||
SzID: SzID,
|
SzID: SzID,
|
||||||
ICharCount: int8(len(data)),
|
ICharCount: int8(len(data)),
|
||||||
@@ -62,7 +62,7 @@ func (server *LoginServer) AcceptLogin(peer *protocol.CNPeer, SzID string, IClie
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *LoginServer) Login(peer *protocol.CNPeer, pkt protocol.Packet) error {
|
func (server *LoginServer) Login(peer *cnet.Peer, pkt protocol.Packet) error {
|
||||||
var loginPkt protocol.SP_CL2LS_REQ_LOGIN
|
var loginPkt protocol.SP_CL2LS_REQ_LOGIN
|
||||||
pkt.Decode(&loginPkt)
|
pkt.Decode(&loginPkt)
|
||||||
|
|
||||||
@@ -74,14 +74,14 @@ func (server *LoginServer) Login(peer *protocol.CNPeer, pkt protocol.Packet) err
|
|||||||
}
|
}
|
||||||
|
|
||||||
// client is resending a login packet??
|
// client is resending a login packet??
|
||||||
if peer.AccountID != -1 {
|
if peer.UserData() != nil {
|
||||||
SendError(LOGIN_ERROR)
|
SendError(LOGIN_ERROR)
|
||||||
return fmt.Errorf("out of order P_CL2LS_REQ_LOGIN")
|
return fmt.Errorf("out of order P_CL2LS_REQ_LOGIN: %v", peer.UserData())
|
||||||
}
|
}
|
||||||
|
|
||||||
// attempt login
|
// attempt login
|
||||||
account, err := server.dbHndlr.TryLogin(loginPkt.SzID, loginPkt.SzPassword)
|
account, err := server.dbHndlr.TryLogin(loginPkt.SzID, loginPkt.SzPassword)
|
||||||
if err == db.ErrLoginInvalidID {
|
if errors.Is(err, db.ErrLoginInvalidID) {
|
||||||
// this is the default behavior, auto create the account if the ID isn't in use
|
// this is the default behavior, auto create the account if the ID isn't in use
|
||||||
account, err = server.dbHndlr.NewAccount(loginPkt.SzID, loginPkt.SzPassword)
|
account, err = server.dbHndlr.NewAccount(loginPkt.SzID, loginPkt.SzPassword)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -89,7 +89,7 @@ func (server *LoginServer) Login(peer *protocol.CNPeer, pkt protocol.Packet) err
|
|||||||
SendError(LOGIN_DATABASE_ERROR)
|
SendError(LOGIN_DATABASE_ERROR)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else if err == db.ErrLoginInvalidPassword {
|
} else if errors.Is(err, db.ErrLoginInvalidPassword) {
|
||||||
// respond with invalid password
|
// respond with invalid password
|
||||||
SendError(LOGIN_ID_AND_PASSWORD_DO_NOT_MATCH)
|
SendError(LOGIN_ID_AND_PASSWORD_DO_NOT_MATCH)
|
||||||
return nil
|
return nil
|
||||||
@@ -99,7 +99,7 @@ func (server *LoginServer) Login(peer *protocol.CNPeer, pkt protocol.Packet) err
|
|||||||
}
|
}
|
||||||
|
|
||||||
// grab player data
|
// grab player data
|
||||||
peer.AccountID = account.AccountID
|
peer.SetUserData(account)
|
||||||
plrs, err := server.dbHndlr.GetPlayers(account.AccountID)
|
plrs, err := server.dbHndlr.GetPlayers(account.AccountID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
SendError(LOGIN_DATABASE_ERROR)
|
SendError(LOGIN_DATABASE_ERROR)
|
||||||
@@ -138,7 +138,7 @@ func (server *LoginServer) Login(peer *protocol.CNPeer, pkt protocol.Packet) err
|
|||||||
return server.AcceptLogin(peer, loginPkt.SzID, loginPkt.IClientVerC, 1, charInfo[:len(plrs)])
|
return server.AcceptLogin(peer, loginPkt.SzID, loginPkt.IClientVerC, 1, charInfo[:len(plrs)])
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *LoginServer) CheckCharacterName(peer *protocol.CNPeer, pkt protocol.Packet) error {
|
func (server *LoginServer) CheckCharacterName(peer *cnet.Peer, pkt protocol.Packet) error {
|
||||||
var charPkt protocol.SP_CL2LS_REQ_CHECK_CHAR_NAME
|
var charPkt protocol.SP_CL2LS_REQ_CHECK_CHAR_NAME
|
||||||
pkt.Decode(&charPkt)
|
pkt.Decode(&charPkt)
|
||||||
|
|
||||||
@@ -149,17 +149,18 @@ func (server *LoginServer) CheckCharacterName(peer *protocol.CNPeer, pkt protoco
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *LoginServer) SaveCharacterName(peer *protocol.CNPeer, pkt protocol.Packet) error {
|
func (server *LoginServer) SaveCharacterName(peer *cnet.Peer, pkt protocol.Packet) error {
|
||||||
var charPkt protocol.SP_CL2LS_REQ_SAVE_CHAR_NAME
|
var charPkt protocol.SP_CL2LS_REQ_SAVE_CHAR_NAME
|
||||||
pkt.Decode(&charPkt)
|
pkt.Decode(&charPkt)
|
||||||
|
|
||||||
if peer.AccountID == -1 {
|
account, ok := peer.UserData().(*db.Account)
|
||||||
|
if !ok || account == nil {
|
||||||
peer.Send(protocol.P_LS2CL_REP_SAVE_CHAR_NAME_FAIL, protocol.SP_LS2CL_REP_SAVE_CHAR_NAME_FAIL{})
|
peer.Send(protocol.P_LS2CL_REP_SAVE_CHAR_NAME_FAIL, protocol.SP_LS2CL_REP_SAVE_CHAR_NAME_FAIL{})
|
||||||
return fmt.Errorf("out of order P_LS2CL_REP_SAVE_CHAR_NAME_FAIL")
|
return fmt.Errorf("out of order P_LS2CL_REP_SAVE_CHAR_NAME_FAIL")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: sanity check SzFirstName && SzLastName
|
// TODO: sanity check SzFirstName && SzLastName
|
||||||
PlayerID, err := server.dbHndlr.NewPlayer(peer.AccountID, charPkt.SzFirstName, charPkt.SzLastName, int(charPkt.ISlotNum))
|
PlayerID, err := server.dbHndlr.NewPlayer(account.AccountID, charPkt.SzFirstName, charPkt.SzLastName, int(charPkt.ISlotNum))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
peer.Send(protocol.P_LS2CL_REP_SAVE_CHAR_NAME_FAIL, protocol.SP_LS2CL_REP_SAVE_CHAR_NAME_FAIL{})
|
peer.Send(protocol.P_LS2CL_REP_SAVE_CHAR_NAME_FAIL, protocol.SP_LS2CL_REP_SAVE_CHAR_NAME_FAIL{})
|
||||||
return err
|
return err
|
||||||
@@ -201,7 +202,7 @@ func validateCharacterCreation(character *protocol.SP_CL2LS_REQ_CHAR_CREATE) boo
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func SendFail(peer *protocol.CNPeer) error {
|
func SendFail(peer *cnet.Peer) error {
|
||||||
if err := peer.Send(protocol.P_LS2CL_REP_SHARD_SELECT_FAIL, protocol.SP_LS2CL_REP_SHARD_SELECT_FAIL{
|
if err := peer.Send(protocol.P_LS2CL_REP_SHARD_SELECT_FAIL, protocol.SP_LS2CL_REP_SHARD_SELECT_FAIL{
|
||||||
IErrorCode: 2,
|
IErrorCode: 2,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
@@ -211,16 +212,21 @@ func SendFail(peer *protocol.CNPeer) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *LoginServer) CharacterCreate(peer *protocol.CNPeer, pkt protocol.Packet) error {
|
func (server *LoginServer) CharacterCreate(peer *cnet.Peer, pkt protocol.Packet) error {
|
||||||
var charPkt protocol.SP_CL2LS_REQ_CHAR_CREATE
|
var charPkt protocol.SP_CL2LS_REQ_CHAR_CREATE
|
||||||
pkt.Decode(&charPkt)
|
pkt.Decode(&charPkt)
|
||||||
|
|
||||||
|
account, ok := peer.UserData().(*db.Account)
|
||||||
|
if !ok || account == nil {
|
||||||
|
return SendFail(peer)
|
||||||
|
}
|
||||||
|
|
||||||
if !validateCharacterCreation(&charPkt) {
|
if !validateCharacterCreation(&charPkt) {
|
||||||
log.Printf("Invalid character creation packet: %+v", charPkt)
|
log.Printf("Invalid character creation packet: %+v", charPkt)
|
||||||
return SendFail(peer)
|
return SendFail(peer)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := server.dbHndlr.FinishPlayer(&charPkt, peer.AccountID); err != nil {
|
if err := server.dbHndlr.FinishPlayer(&charPkt, account.AccountID); err != nil {
|
||||||
log.Printf("Error finishing player: %v", err)
|
log.Printf("Error finishing player: %v", err)
|
||||||
return SendFail(peer)
|
return SendFail(peer)
|
||||||
}
|
}
|
||||||
@@ -239,11 +245,16 @@ func (server *LoginServer) CharacterCreate(peer *protocol.CNPeer, pkt protocol.P
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *LoginServer) CharacterDelete(peer *protocol.CNPeer, pkt protocol.Packet) error {
|
func (server *LoginServer) CharacterDelete(peer *cnet.Peer, pkt protocol.Packet) error {
|
||||||
var charPkt protocol.SP_CL2LS_REQ_CHAR_DELETE
|
var charPkt protocol.SP_CL2LS_REQ_CHAR_DELETE
|
||||||
pkt.Decode(&charPkt)
|
pkt.Decode(&charPkt)
|
||||||
|
|
||||||
slot, err := server.dbHndlr.DeletePlayer(int(charPkt.IPC_UID), peer.AccountID)
|
account, ok := peer.UserData().(*db.Account)
|
||||||
|
if !ok || account == nil {
|
||||||
|
return SendFail(peer)
|
||||||
|
}
|
||||||
|
|
||||||
|
slot, err := server.dbHndlr.DeletePlayer(int(charPkt.IPC_UID), account.AccountID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return SendFail(peer)
|
return SendFail(peer)
|
||||||
}
|
}
|
||||||
@@ -253,21 +264,21 @@ func (server *LoginServer) CharacterDelete(peer *protocol.CNPeer, pkt protocol.P
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *LoginServer) ShardSelect(peer *protocol.CNPeer, pkt protocol.Packet) error {
|
func (server *LoginServer) ShardSelect(peer *cnet.Peer, pkt protocol.Packet) error {
|
||||||
var selection protocol.SP_CL2LS_REQ_CHAR_SELECT
|
var selection protocol.SP_CL2LS_REQ_CHAR_SELECT
|
||||||
pkt.Decode(&selection)
|
pkt.Decode(&selection)
|
||||||
|
|
||||||
|
account, ok := peer.UserData().(*db.Account)
|
||||||
|
if !ok || account == nil {
|
||||||
|
return SendFail(peer)
|
||||||
|
}
|
||||||
|
|
||||||
shards := server.redisHndlr.GetShards()
|
shards := server.redisHndlr.GetShards()
|
||||||
if len(shards) == 0 {
|
if len(shards) == 0 {
|
||||||
SendFail(peer)
|
SendFail(peer)
|
||||||
return fmt.Errorf("loginServer has found no linked shards")
|
return fmt.Errorf("loginServer has found no linked shards")
|
||||||
}
|
}
|
||||||
|
|
||||||
key, err := protocol.GenSerialKey()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: better shard selection logic pls
|
// TODO: better shard selection logic pls
|
||||||
// for now, pick random shard
|
// for now, pick random shard
|
||||||
shard := shards[rand.Intn(len(shards))]
|
shard := shards[rand.Intn(len(shards))]
|
||||||
@@ -278,17 +289,23 @@ func (server *LoginServer) ShardSelect(peer *protocol.CNPeer, pkt protocol.Packe
|
|||||||
log.Printf("Error getting player: %v", err)
|
log.Printf("Error getting player: %v", err)
|
||||||
return SendFail(peer)
|
return SendFail(peer)
|
||||||
}
|
}
|
||||||
|
accountID := account.AccountID
|
||||||
|
|
||||||
if plr.AccountID != peer.AccountID {
|
if plr.AccountID != accountID {
|
||||||
log.Printf("HACK: player %d tried to join shard as player %d", peer.AccountID, plr.AccountID)
|
log.Printf("HACK: player %d tried to join shard as player %d", accountID, plr.AccountID)
|
||||||
return SendFail(peer)
|
return SendFail(peer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
key, err := protocol.GenSerialKey()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// share the login attempt
|
// share the login attempt
|
||||||
server.redisHndlr.QueueLogin(key, redis.LoginMetadata{
|
server.redisHndlr.QueueLogin(key, redis.LoginMetadata{
|
||||||
FEKey: peer.FE_key,
|
FEKey: peer.FE_key,
|
||||||
PlayerID: int32(selection.IPC_UID),
|
PlayerID: int32(selection.IPC_UID),
|
||||||
AccountID: peer.AccountID,
|
AccountID: accountID,
|
||||||
})
|
})
|
||||||
|
|
||||||
// craft response
|
// craft response
|
||||||
@@ -303,11 +320,16 @@ func (server *LoginServer) ShardSelect(peer *protocol.CNPeer, pkt protocol.Packe
|
|||||||
return peer.Send(protocol.P_LS2CL_REP_SHARD_SELECT_SUCC, resp)
|
return peer.Send(protocol.P_LS2CL_REP_SHARD_SELECT_SUCC, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *LoginServer) FinishTutorial(peer *protocol.CNPeer, pkt protocol.Packet) error {
|
func (server *LoginServer) FinishTutorial(peer *cnet.Peer, pkt protocol.Packet) error {
|
||||||
var charPkt protocol.SP_CL2LS_REQ_SAVE_CHAR_TUTOR
|
var charPkt protocol.SP_CL2LS_REQ_SAVE_CHAR_TUTOR
|
||||||
pkt.Decode(&charPkt)
|
pkt.Decode(&charPkt)
|
||||||
|
|
||||||
if err := server.dbHndlr.FinishTutorial(int(charPkt.IPC_UID), peer.AccountID); err != nil {
|
account, ok := peer.UserData().(*db.Account)
|
||||||
|
if !ok || account == nil {
|
||||||
|
return SendFail(peer)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := server.dbHndlr.FinishTutorial(int(charPkt.IPC_UID), account.AccountID); err != nil {
|
||||||
return SendFail(peer)
|
return SendFail(peer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
179
login/login_test.go
Normal file
179
login/login_test.go
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
package login_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/binary"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/CPunch/gopenfusion/cnet"
|
||||||
|
"github.com/CPunch/gopenfusion/cnet/protocol"
|
||||||
|
"github.com/CPunch/gopenfusion/internal/db"
|
||||||
|
"github.com/CPunch/gopenfusion/internal/redis"
|
||||||
|
"github.com/CPunch/gopenfusion/internal/testutil"
|
||||||
|
"github.com/CPunch/gopenfusion/login"
|
||||||
|
"github.com/matryer/is"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
loginSrv *login.LoginServer
|
||||||
|
loginPort int
|
||||||
|
testDB *db.DBHandler
|
||||||
|
rh *redis.RedisHandler
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
test data was scraped by dumping packets, just adding a println to the LoginService
|
||||||
|
to print the packet data
|
||||||
|
*/
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
ret := 1
|
||||||
|
defer func() {
|
||||||
|
os.Exit(ret)
|
||||||
|
}()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// setup environment
|
||||||
|
var closer func()
|
||||||
|
testDB, rh, closer = testutil.SetupEnvironment(ctx)
|
||||||
|
defer closer()
|
||||||
|
|
||||||
|
var err error
|
||||||
|
loginPort, err = cnet.RandomPort()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// start login server
|
||||||
|
loginSrv, err = login.NewLoginServer(ctx, testDB, rh, loginPort)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := loginSrv.Start(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// wait for login server to start, then start tests
|
||||||
|
<-loginSrv.Service().Started()
|
||||||
|
ret = m.Run()
|
||||||
|
cancel()
|
||||||
|
<-loginSrv.Service().Stopped()
|
||||||
|
}
|
||||||
|
|
||||||
|
// This test tries a typical login sequence.
|
||||||
|
func TestLoginSuccSequence(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
dummy := testutil.MakeDummyPeer(ctx, is, loginPort)
|
||||||
|
defer dummy.Kill()
|
||||||
|
|
||||||
|
// send login request (this should create an account)
|
||||||
|
var resp protocol.SP_LS2CL_REP_LOGIN_SUCC
|
||||||
|
dummy.SendAndRecv(protocol.P_CL2LS_REQ_LOGIN, protocol.P_LS2CL_REP_LOGIN_SUCC,
|
||||||
|
protocol.SP_CL2LS_REQ_LOGIN{
|
||||||
|
SzID: "testLoginSequence",
|
||||||
|
SzPassword: "test",
|
||||||
|
}, &resp)
|
||||||
|
|
||||||
|
// verify response
|
||||||
|
is.Equal(resp.SzID, "testLoginSequence") // should have the same ID
|
||||||
|
is.Equal(resp.ICharCount, int8(0)) // should have 0 characters
|
||||||
|
|
||||||
|
// verify account was created
|
||||||
|
_, err := testDB.TryLogin("testLoginSequence", "test")
|
||||||
|
is.NoErr(err) // TryLogin() should not return an error
|
||||||
|
}
|
||||||
|
|
||||||
|
// This test tries a typical login sequence, but with an invalid password.
|
||||||
|
func TestLoginFailSequence(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
dummy := testutil.MakeDummyPeer(ctx, is, loginPort)
|
||||||
|
defer dummy.Kill()
|
||||||
|
|
||||||
|
// send login request (this should not create an account)
|
||||||
|
var resp protocol.SP_LS2CL_REP_LOGIN_FAIL
|
||||||
|
dummy.SendAndRecv(protocol.P_CL2LS_REQ_LOGIN, protocol.P_LS2CL_REP_LOGIN_FAIL,
|
||||||
|
protocol.SP_CL2LS_REQ_LOGIN{
|
||||||
|
SzID: "",
|
||||||
|
SzPassword: "",
|
||||||
|
}, &resp)
|
||||||
|
|
||||||
|
// verify response
|
||||||
|
is.Equal(resp.SzID, "") // should have the same ID
|
||||||
|
is.Equal(resp.IErrorCode, int32(login.LOGIN_ID_AND_PASSWORD_DO_NOT_MATCH)) // should respond with LOGIN_ID_AND_PASSWORD_DO_NOT_MATCH
|
||||||
|
}
|
||||||
|
|
||||||
|
// This test tries a typical login sequence, and creates a character
|
||||||
|
func TestCharacterSequence(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
dummy := testutil.MakeDummyPeer(ctx, is, loginPort)
|
||||||
|
defer dummy.Kill()
|
||||||
|
|
||||||
|
// send login request (this should create an account)
|
||||||
|
var resp protocol.SP_LS2CL_REP_LOGIN_SUCC
|
||||||
|
dummy.SendAndRecv(protocol.P_CL2LS_REQ_LOGIN, protocol.P_LS2CL_REP_LOGIN_SUCC,
|
||||||
|
protocol.SP_CL2LS_REQ_LOGIN{
|
||||||
|
SzID: "testCharacterSequence",
|
||||||
|
SzPassword: "test",
|
||||||
|
}, &resp)
|
||||||
|
|
||||||
|
// verify response
|
||||||
|
is.Equal(resp.SzID, "testCharacterSequence") // should have the same ID
|
||||||
|
is.Equal(resp.ICharCount, int8(0)) // should have 0 characters
|
||||||
|
|
||||||
|
// perform key swap
|
||||||
|
dummy.Peer.E_key = protocol.CreateNewKey(
|
||||||
|
resp.UiSvrTime,
|
||||||
|
uint64(resp.ICharCount+1),
|
||||||
|
uint64(resp.ISlotNum+1),
|
||||||
|
)
|
||||||
|
dummy.Peer.FE_key = protocol.CreateNewKey(
|
||||||
|
binary.LittleEndian.Uint64([]byte(protocol.DEFAULT_KEY)),
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
|
||||||
|
// send character name check request
|
||||||
|
var charResp protocol.SP_LS2CL_REP_SAVE_CHAR_NAME_SUCC
|
||||||
|
dummy.SendAndRecv(protocol.P_CL2LS_REQ_SAVE_CHAR_NAME, protocol.P_LS2CL_REP_SAVE_CHAR_NAME_SUCC,
|
||||||
|
protocol.SP_CL2LS_REQ_SAVE_CHAR_NAME{
|
||||||
|
ISlotNum: 1,
|
||||||
|
IGender: 1,
|
||||||
|
IFNCode: 260,
|
||||||
|
ILNCode: 551,
|
||||||
|
IMNCode: 33,
|
||||||
|
SzFirstName: testutil.TestCharCreate.PCStyle.SzFirstName,
|
||||||
|
SzLastName: testutil.TestCharCreate.PCStyle.SzLastName,
|
||||||
|
}, &charResp)
|
||||||
|
|
||||||
|
// verify response
|
||||||
|
is.Equal(charResp.ISlotNum, int8(1)) // should have the same slot number
|
||||||
|
is.Equal(charResp.IGender, int8(1)) // should have the same gender
|
||||||
|
is.Equal(charResp.SzFirstName, testutil.TestCharCreate.PCStyle.SzFirstName) // should have the same first name
|
||||||
|
is.Equal(charResp.SzLastName, testutil.TestCharCreate.PCStyle.SzLastName) // should have the same last name
|
||||||
|
|
||||||
|
// send character create request
|
||||||
|
charCreate := testutil.TestCharCreate
|
||||||
|
charCreate.PCStyle.IPC_UID = charResp.IPC_UID
|
||||||
|
var charCreateResp protocol.SP_LS2CL_REP_CHAR_CREATE_SUCC
|
||||||
|
dummy.SendAndRecv(protocol.P_CL2LS_REQ_CHAR_CREATE, protocol.P_LS2CL_REP_CHAR_CREATE_SUCC,
|
||||||
|
charCreate, &charCreateResp)
|
||||||
|
|
||||||
|
// verify response
|
||||||
|
is.Equal(charCreate.PCStyle, charCreateResp.SPC_Style) // should have the same PCStyle
|
||||||
|
is.Equal(charCreate.SOn_Item, charCreateResp.SOn_Item) // should have the same SOn_Item
|
||||||
|
}
|
@@ -1,126 +1,51 @@
|
|||||||
package login
|
package login
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"context"
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/CPunch/gopenfusion/config"
|
"github.com/CPunch/gopenfusion/cnet"
|
||||||
|
"github.com/CPunch/gopenfusion/cnet/protocol"
|
||||||
"github.com/CPunch/gopenfusion/internal/db"
|
"github.com/CPunch/gopenfusion/internal/db"
|
||||||
"github.com/CPunch/gopenfusion/internal/protocol"
|
|
||||||
"github.com/CPunch/gopenfusion/internal/protocol/pool"
|
|
||||||
"github.com/CPunch/gopenfusion/internal/redis"
|
"github.com/CPunch/gopenfusion/internal/redis"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PacketHandler func(peer *protocol.CNPeer, pkt protocol.Packet) error
|
|
||||||
|
|
||||||
func stubbedPacket(_ *protocol.CNPeer, _ protocol.Packet) error { /* stubbed */ return nil }
|
|
||||||
|
|
||||||
type LoginServer struct {
|
type LoginServer struct {
|
||||||
listener net.Listener
|
service *cnet.Service
|
||||||
port int
|
|
||||||
dbHndlr *db.DBHandler
|
dbHndlr *db.DBHandler
|
||||||
redisHndlr *redis.RedisHandler
|
redisHndlr *redis.RedisHandler
|
||||||
eRecv chan *protocol.Event
|
|
||||||
peers map[*protocol.CNPeer]bool
|
|
||||||
packetHandlers map[uint32]PacketHandler
|
|
||||||
peerLock sync.Mutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLoginServer(dbHndlr *db.DBHandler, redisHndlr *redis.RedisHandler, port int) (*LoginServer, error) {
|
func NewLoginServer(ctx context.Context, dbHndlr *db.DBHandler, redisHndlr *redis.RedisHandler, port int) (*LoginServer, error) {
|
||||||
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
|
srvc := cnet.NewService(ctx, "LOGIN", port)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
server := &LoginServer{
|
server := &LoginServer{
|
||||||
listener: listener,
|
service: srvc,
|
||||||
port: port,
|
|
||||||
dbHndlr: dbHndlr,
|
dbHndlr: dbHndlr,
|
||||||
redisHndlr: redisHndlr,
|
redisHndlr: redisHndlr,
|
||||||
peers: make(map[*protocol.CNPeer]bool),
|
|
||||||
eRecv: make(chan *protocol.Event),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
server.packetHandlers = map[uint32]PacketHandler{
|
srvc.AddPacketHandler(protocol.P_CL2LS_REQ_LOGIN, server.Login)
|
||||||
protocol.P_CL2LS_REQ_LOGIN: server.Login,
|
srvc.AddPacketHandler(protocol.P_CL2LS_REQ_CHECK_CHAR_NAME, server.CheckCharacterName)
|
||||||
protocol.P_CL2LS_REQ_CHECK_CHAR_NAME: server.CheckCharacterName,
|
srvc.AddPacketHandler(protocol.P_CL2LS_REQ_SAVE_CHAR_NAME, server.SaveCharacterName)
|
||||||
protocol.P_CL2LS_REQ_SAVE_CHAR_NAME: server.SaveCharacterName,
|
srvc.AddPacketHandler(protocol.P_CL2LS_REQ_CHAR_CREATE, server.CharacterCreate)
|
||||||
protocol.P_CL2LS_REQ_CHAR_CREATE: server.CharacterCreate,
|
srvc.AddPacketHandler(protocol.P_CL2LS_REQ_CHAR_SELECT, server.ShardSelect)
|
||||||
protocol.P_CL2LS_REQ_CHAR_SELECT: server.ShardSelect,
|
srvc.AddPacketHandler(protocol.P_CL2LS_REQ_CHAR_DELETE, server.CharacterDelete)
|
||||||
protocol.P_CL2LS_REQ_CHAR_DELETE: server.CharacterDelete,
|
srvc.AddPacketHandler(protocol.P_CL2LS_REQ_SHARD_SELECT, cnet.StubbedPacket)
|
||||||
protocol.P_CL2LS_REQ_SHARD_SELECT: stubbedPacket,
|
srvc.AddPacketHandler(protocol.P_CL2LS_REQ_SHARD_LIST_INFO, cnet.StubbedPacket)
|
||||||
protocol.P_CL2LS_REQ_SHARD_LIST_INFO: stubbedPacket,
|
srvc.AddPacketHandler(protocol.P_CL2LS_CHECK_NAME_LIST, cnet.StubbedPacket)
|
||||||
protocol.P_CL2LS_CHECK_NAME_LIST: stubbedPacket,
|
srvc.AddPacketHandler(protocol.P_CL2LS_REQ_SAVE_CHAR_TUTOR, server.FinishTutorial)
|
||||||
protocol.P_CL2LS_REQ_SAVE_CHAR_TUTOR: server.FinishTutorial,
|
srvc.AddPacketHandler(protocol.P_CL2LS_REQ_PC_EXIT_DUPLICATE, cnet.StubbedPacket)
|
||||||
protocol.P_CL2LS_REQ_PC_EXIT_DUPLICATE: stubbedPacket,
|
srvc.AddPacketHandler(protocol.P_CL2LS_REP_LIVE_CHECK, cnet.StubbedPacket)
|
||||||
protocol.P_CL2LS_REP_LIVE_CHECK: stubbedPacket,
|
srvc.AddPacketHandler(protocol.P_CL2LS_REQ_CHANGE_CHAR_NAME, cnet.StubbedPacket)
|
||||||
protocol.P_CL2LS_REQ_CHANGE_CHAR_NAME: stubbedPacket,
|
srvc.AddPacketHandler(protocol.P_CL2LS_REQ_SERVER_SELECT, cnet.StubbedPacket)
|
||||||
protocol.P_CL2LS_REQ_SERVER_SELECT: stubbedPacket,
|
|
||||||
}
|
|
||||||
|
|
||||||
return server, nil
|
return server, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *LoginServer) Start() {
|
func (server *LoginServer) Service() *cnet.Service {
|
||||||
log.Printf("Login service hosted on %s:%d\n", config.GetAnnounceIP(), server.port)
|
return server.service
|
||||||
|
|
||||||
go server.handleEvents()
|
|
||||||
for {
|
|
||||||
conn, err := server.listener.Accept()
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Connection error: ", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
client := protocol.NewCNPeer(server.eRecv, conn)
|
|
||||||
server.connect(client)
|
|
||||||
go client.Handler()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *LoginServer) handleEvents() {
|
func (server *LoginServer) Start() error {
|
||||||
for event := range server.eRecv {
|
return server.service.Start()
|
||||||
switch event.Type {
|
|
||||||
case protocol.EVENT_CLIENT_DISCONNECT:
|
|
||||||
server.disconnect(event.Peer)
|
|
||||||
case protocol.EVENT_CLIENT_PACKET:
|
|
||||||
if err := server.handlePacket(event.Peer, event.PktID, protocol.NewPacket(event.Pkt)); err != nil {
|
|
||||||
log.Printf("Error handling packet: %v", err)
|
|
||||||
event.Peer.Kill()
|
|
||||||
}
|
|
||||||
|
|
||||||
// the packet is given to us by the event, so we'll need to make sure to return it to the pool
|
|
||||||
pool.Put(event.Pkt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (server *LoginServer) handlePacket(peer *protocol.CNPeer, typeID uint32, pkt protocol.Packet) error {
|
|
||||||
if hndlr, ok := server.packetHandlers[typeID]; ok {
|
|
||||||
if err := hndlr(peer, pkt); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.Printf("[WARN] unknown packet ID: %x\n", typeID)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (server *LoginServer) disconnect(peer *protocol.CNPeer) {
|
|
||||||
server.peerLock.Lock()
|
|
||||||
defer server.peerLock.Unlock()
|
|
||||||
|
|
||||||
log.Printf("Peer %p disconnected from LOGIN\n", peer)
|
|
||||||
delete(server.peers, peer)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (server *LoginServer) connect(peer *protocol.CNPeer) {
|
|
||||||
server.peerLock.Lock()
|
|
||||||
defer server.peerLock.Unlock()
|
|
||||||
|
|
||||||
log.Printf("New peer %p connected to LOGIN\n", peer)
|
|
||||||
server.peers[peer] = true
|
|
||||||
}
|
}
|
||||||
|
@@ -1,15 +1,20 @@
|
|||||||
package shard
|
package shard
|
||||||
|
|
||||||
import "github.com/CPunch/gopenfusion/internal/protocol"
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
func (server *ShardServer) freeChat(peer *protocol.CNPeer, pkt protocol.Packet) error {
|
"github.com/CPunch/gopenfusion/cnet"
|
||||||
|
"github.com/CPunch/gopenfusion/cnet/protocol"
|
||||||
|
"github.com/CPunch/gopenfusion/shard/entity"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (server *ShardServer) freeChat(peer *cnet.Peer, pkt protocol.Packet) error {
|
||||||
var chat protocol.SP_CL2FE_REQ_SEND_FREECHAT_MESSAGE
|
var chat protocol.SP_CL2FE_REQ_SEND_FREECHAT_MESSAGE
|
||||||
pkt.Decode(&chat)
|
pkt.Decode(&chat)
|
||||||
|
|
||||||
// sanity check
|
plr, ok := peer.UserData().(*entity.Player)
|
||||||
plr, err := server.getPlayer(peer)
|
if !ok || plr == nil {
|
||||||
if err != nil {
|
return fmt.Errorf("freeChat: plr is nil")
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// spread message
|
// spread message
|
||||||
@@ -20,14 +25,13 @@ func (server *ShardServer) freeChat(peer *protocol.CNPeer, pkt protocol.Packet)
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *ShardServer) menuChat(peer *protocol.CNPeer, pkt protocol.Packet) error {
|
func (server *ShardServer) menuChat(peer *cnet.Peer, pkt protocol.Packet) error {
|
||||||
var chat protocol.SP_CL2FE_REQ_SEND_MENUCHAT_MESSAGE
|
var chat protocol.SP_CL2FE_REQ_SEND_MENUCHAT_MESSAGE
|
||||||
pkt.Decode(&chat)
|
pkt.Decode(&chat)
|
||||||
|
|
||||||
// sanity check
|
plr, ok := peer.UserData().(*entity.Player)
|
||||||
plr, err := server.getPlayer(peer)
|
if !ok || plr == nil {
|
||||||
if err != nil {
|
return fmt.Errorf("menuChat: plr is nil")
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// spread message
|
// spread message
|
||||||
@@ -38,14 +42,13 @@ func (server *ShardServer) menuChat(peer *protocol.CNPeer, pkt protocol.Packet)
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *ShardServer) emoteChat(peer *protocol.CNPeer, pkt protocol.Packet) error {
|
func (server *ShardServer) emoteChat(peer *cnet.Peer, pkt protocol.Packet) error {
|
||||||
var chat protocol.SP_CL2FE_REQ_PC_AVATAR_EMOTES_CHAT
|
var chat protocol.SP_CL2FE_REQ_PC_AVATAR_EMOTES_CHAT
|
||||||
pkt.Decode(&chat)
|
pkt.Decode(&chat)
|
||||||
|
|
||||||
// sanity check
|
plr, ok := peer.UserData().(*entity.Player)
|
||||||
plr, err := server.getPlayer(peer)
|
if !ok || plr == nil {
|
||||||
if err != nil {
|
return fmt.Errorf("emoteChat: plr is nil")
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// spread message
|
// spread message
|
||||||
|
@@ -3,8 +3,22 @@ package entity
|
|||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/CPunch/gopenfusion/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ChunkPosition struct {
|
||||||
|
X int
|
||||||
|
Y int
|
||||||
|
}
|
||||||
|
|
||||||
|
func MakeChunkPosition(x, y int) ChunkPosition {
|
||||||
|
return ChunkPosition{
|
||||||
|
X: x / (config.VIEW_DISTANCE / 3),
|
||||||
|
Y: y / (config.VIEW_DISTANCE / 3),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type Chunk struct {
|
type Chunk struct {
|
||||||
Position ChunkPosition
|
Position ChunkPosition
|
||||||
entities map[Entity]struct{}
|
entities map[Entity]struct{}
|
||||||
@@ -38,10 +52,8 @@ func (c *Chunk) SendPacket(typeID uint32, pkt ...interface{}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// calls f for each entity in this chunk, if f returns true, stop iterating
|
// calls f for each entity in this chunk, if f returns true, stop iterating
|
||||||
|
// f can safely add/remove entities from the chunk
|
||||||
func (c *Chunk) ForEachEntity(f func(entity Entity) bool) {
|
func (c *Chunk) ForEachEntity(f func(entity Entity) bool) {
|
||||||
c.lock.Lock()
|
|
||||||
defer c.lock.Unlock()
|
|
||||||
|
|
||||||
for entity := range c.entities {
|
for entity := range c.entities {
|
||||||
if f(entity) {
|
if f(entity) {
|
||||||
break
|
break
|
@@ -1,6 +1,6 @@
|
|||||||
package entity
|
package entity
|
||||||
|
|
||||||
import "github.com/CPunch/gopenfusion/internal/protocol"
|
import "github.com/CPunch/gopenfusion/cnet"
|
||||||
|
|
||||||
type EntityKind int
|
type EntityKind int
|
||||||
|
|
||||||
@@ -20,6 +20,6 @@ type Entity interface {
|
|||||||
SetPosition(x, y, z int)
|
SetPosition(x, y, z int)
|
||||||
SetAngle(angle int)
|
SetAngle(angle int)
|
||||||
|
|
||||||
DisappearFromViewOf(peer *protocol.CNPeer)
|
DisappearFromViewOf(peer *cnet.Peer)
|
||||||
EnterIntoViewOf(peer *protocol.CNPeer)
|
EnterIntoViewOf(peer *cnet.Peer)
|
||||||
}
|
}
|
@@ -3,10 +3,12 @@ package entity_test
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/CPunch/gopenfusion/internal/entity"
|
"github.com/CPunch/gopenfusion/shard/entity"
|
||||||
|
"github.com/matryer/is"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestChunkSliceDifference(t *testing.T) {
|
func TestChunkSliceDifference(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
chunks := []*entity.Chunk{
|
chunks := []*entity.Chunk{
|
||||||
entity.NewChunk(entity.MakeChunkPosition(0, 0)),
|
entity.NewChunk(entity.MakeChunkPosition(0, 0)),
|
||||||
entity.NewChunk(entity.MakeChunkPosition(0, 1)),
|
entity.NewChunk(entity.MakeChunkPosition(0, 1)),
|
||||||
@@ -28,13 +30,7 @@ func TestChunkSliceDifference(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
diff := entity.ChunkSliceDifference(c1, c2)
|
diff := entity.ChunkSliceDifference(c1, c2)
|
||||||
if len(diff) != 1 {
|
|
||||||
t.Logf("%+v", diff)
|
|
||||||
t.Error("expected 1 chunk in difference")
|
|
||||||
}
|
|
||||||
|
|
||||||
if diff[0] != chunks[3] {
|
is.True(len(diff) == 1) // should be 1 chunk in difference
|
||||||
t.Logf("%+v", diff)
|
is.True(diff[0] == chunks[3]) // should be chunks[3] in difference
|
||||||
t.Error("wrong difference")
|
|
||||||
}
|
|
||||||
}
|
}
|
@@ -3,7 +3,8 @@ package entity
|
|||||||
import (
|
import (
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
"github.com/CPunch/gopenfusion/internal/protocol"
|
"github.com/CPunch/gopenfusion/cnet"
|
||||||
|
"github.com/CPunch/gopenfusion/cnet/protocol"
|
||||||
)
|
)
|
||||||
|
|
||||||
type NPC struct {
|
type NPC struct {
|
||||||
@@ -62,13 +63,13 @@ func (npc *NPC) SetAngle(angle int) {
|
|||||||
npc.Angle = angle
|
npc.Angle = angle
|
||||||
}
|
}
|
||||||
|
|
||||||
func (npc *NPC) DisappearFromViewOf(peer *protocol.CNPeer) {
|
func (npc *NPC) DisappearFromViewOf(peer *cnet.Peer) {
|
||||||
peer.Send(protocol.P_FE2CL_NPC_EXIT, protocol.SP_FE2CL_NPC_EXIT{
|
peer.Send(protocol.P_FE2CL_NPC_EXIT, protocol.SP_FE2CL_NPC_EXIT{
|
||||||
INPC_ID: int32(npc.ID),
|
INPC_ID: int32(npc.ID),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (npc *NPC) EnterIntoViewOf(peer *protocol.CNPeer) {
|
func (npc *NPC) EnterIntoViewOf(peer *cnet.Peer) {
|
||||||
peer.Send(protocol.P_FE2CL_NPC_NEW, protocol.SP_FE2CL_NPC_NEW{
|
peer.Send(protocol.P_FE2CL_NPC_NEW, protocol.SP_FE2CL_NPC_NEW{
|
||||||
NPCAppearanceData: npc.GetAppearanceData(),
|
NPCAppearanceData: npc.GetAppearanceData(),
|
||||||
})
|
})
|
@@ -1,41 +1,23 @@
|
|||||||
package entity
|
package entity
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/CPunch/gopenfusion/config"
|
"github.com/CPunch/gopenfusion/cnet"
|
||||||
"github.com/CPunch/gopenfusion/internal/protocol"
|
"github.com/CPunch/gopenfusion/cnet/protocol"
|
||||||
|
"github.com/CPunch/gopenfusion/internal/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Player struct {
|
type Player struct {
|
||||||
Peer *protocol.CNPeer
|
db.Player
|
||||||
|
Peer *cnet.Peer
|
||||||
Chunk ChunkPosition
|
Chunk ChunkPosition
|
||||||
PlayerID int
|
}
|
||||||
AccountID int
|
|
||||||
AccountLevel int
|
func NewPlayer(peer *cnet.Peer, player *db.Player) *Player {
|
||||||
Slot int
|
return &Player{
|
||||||
PCStyle protocol.SPCStyle
|
Player: *player,
|
||||||
PCStyle2 protocol.SPCStyle2
|
Peer: peer,
|
||||||
EquippedNanos [3]int
|
Chunk: MakeChunkPosition(player.X, player.Y),
|
||||||
Nanos [config.NANO_COUNT]protocol.SNano
|
}
|
||||||
Equip [config.AEQUIP_COUNT]protocol.SItemBase
|
|
||||||
Inven [config.AINVEN_COUNT]protocol.SItemBase
|
|
||||||
Bank [config.ABANK_COUNT]protocol.SItemBase
|
|
||||||
SkywayLocationFlag []byte
|
|
||||||
FirstUseFlag []byte
|
|
||||||
Quests []byte
|
|
||||||
HP int
|
|
||||||
Level int
|
|
||||||
Taros int
|
|
||||||
FusionMatter int
|
|
||||||
Mentor int
|
|
||||||
X, Y, Z int
|
|
||||||
Angle int
|
|
||||||
BatteryN int
|
|
||||||
BatteryW int
|
|
||||||
WarpLocationFlag int
|
|
||||||
ActiveNanoSlotNum int
|
|
||||||
Fatigue int
|
|
||||||
CurrentMissionID int
|
|
||||||
IPCState int8
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Entity interface ====================
|
// ==================== Entity interface ====================
|
||||||
@@ -70,13 +52,13 @@ func (plr *Player) SetAngle(angle int) {
|
|||||||
plr.Angle = angle
|
plr.Angle = angle
|
||||||
}
|
}
|
||||||
|
|
||||||
func (plr *Player) DisappearFromViewOf(peer *protocol.CNPeer) {
|
func (plr *Player) DisappearFromViewOf(peer *cnet.Peer) {
|
||||||
peer.Send(protocol.P_FE2CL_PC_EXIT, protocol.SP_FE2CL_PC_EXIT{
|
peer.Send(protocol.P_FE2CL_PC_EXIT, protocol.SP_FE2CL_PC_EXIT{
|
||||||
IID: int32(plr.PlayerID),
|
IID: int32(plr.PlayerID),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (plr *Player) EnterIntoViewOf(peer *protocol.CNPeer) {
|
func (plr *Player) EnterIntoViewOf(peer *cnet.Peer) {
|
||||||
peer.Send(protocol.P_FE2CL_PC_NEW, protocol.SP_FE2CL_PC_NEW{
|
peer.Send(protocol.P_FE2CL_PC_NEW, protocol.SP_FE2CL_PC_NEW{
|
||||||
PCAppearanceData: plr.GetAppearanceData(),
|
PCAppearanceData: plr.GetAppearanceData(),
|
||||||
})
|
})
|
@@ -1,7 +1,7 @@
|
|||||||
package shard
|
package shard
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/CPunch/gopenfusion/internal/entity"
|
"github.com/CPunch/gopenfusion/shard/entity"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (server *ShardServer) addEntity(e entity.Entity) {
|
func (server *ShardServer) addEntity(e entity.Entity) {
|
||||||
|
@@ -4,33 +4,37 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"github.com/CPunch/gopenfusion/internal/entity"
|
"github.com/CPunch/gopenfusion/cnet"
|
||||||
"github.com/CPunch/gopenfusion/internal/protocol"
|
"github.com/CPunch/gopenfusion/cnet/protocol"
|
||||||
"github.com/CPunch/gopenfusion/internal/redis"
|
"github.com/CPunch/gopenfusion/internal/redis"
|
||||||
|
"github.com/CPunch/gopenfusion/shard/entity"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (server *ShardServer) attachPlayer(peer *protocol.CNPeer, meta redis.LoginMetadata) (*entity.Player, error) {
|
func (server *ShardServer) attachPlayer(peer *cnet.Peer, meta redis.LoginMetadata) (*entity.Player, error) {
|
||||||
// resending a shard enter packet?
|
dbPlr, err := server.dbHndlr.GetPlayer(int(meta.PlayerID))
|
||||||
old, _ := server.getPlayer(peer)
|
|
||||||
if old != nil {
|
|
||||||
return nil, fmt.Errorf("resent enter packet")
|
|
||||||
}
|
|
||||||
|
|
||||||
// attach player
|
|
||||||
plr, err := server.dbHndlr.GetPlayer(int(meta.PlayerID))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
plr.Peer = peer
|
plr := entity.NewPlayer(peer, dbPlr)
|
||||||
|
|
||||||
server.setPlayer(peer, plr)
|
// once we create the player, it's memory address is owned by the
|
||||||
|
// server.Start() goroutine. the only functions allowed to access
|
||||||
|
// it are the packet handlers as no other goroutines will be
|
||||||
|
// concurrently accessing it.
|
||||||
|
peer.SetUserData(plr)
|
||||||
return plr, nil
|
return plr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *ShardServer) RequestEnter(peer *protocol.CNPeer, pkt protocol.Packet) error {
|
func (server *ShardServer) RequestEnter(peer *cnet.Peer, pkt protocol.Packet) error {
|
||||||
var enter protocol.SP_CL2FE_REQ_PC_ENTER
|
var enter protocol.SP_CL2FE_REQ_PC_ENTER
|
||||||
pkt.Decode(&enter)
|
pkt.Decode(&enter)
|
||||||
|
|
||||||
|
// resending a shard enter packet?
|
||||||
|
_plr, ok := peer.UserData().(*entity.Player)
|
||||||
|
if ok && _plr != nil {
|
||||||
|
return fmt.Errorf("resent enter packet")
|
||||||
|
}
|
||||||
|
|
||||||
loginData, err := server.redisHndlr.GetLogin(enter.IEnterSerialKey)
|
loginData, err := server.redisHndlr.GetLogin(enter.IEnterSerialKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// the error codes for P_FE2CL_REP_PC_ENTER_FAIL aren't referenced in the client :(
|
// the error codes for P_FE2CL_REP_PC_ENTER_FAIL aren't referenced in the client :(
|
||||||
@@ -52,29 +56,37 @@ func (server *ShardServer) RequestEnter(peer *protocol.CNPeer, pkt protocol.Pack
|
|||||||
// setup peer
|
// setup peer
|
||||||
peer.E_key = protocol.CreateNewKey(resp.UiSvrTime, uint64(resp.IID+1), uint64(resp.PCLoadData2CL.IFusionMatter+1))
|
peer.E_key = protocol.CreateNewKey(resp.UiSvrTime, uint64(resp.IID+1), uint64(resp.PCLoadData2CL.IFusionMatter+1))
|
||||||
peer.FE_key = loginData.FEKey
|
peer.FE_key = loginData.FEKey
|
||||||
peer.PlayerID = loginData.PlayerID
|
peer.SetActiveKey(cnet.USE_FE)
|
||||||
peer.AccountID = loginData.AccountID
|
|
||||||
peer.SetActiveKey(protocol.USE_FE)
|
|
||||||
|
|
||||||
log.Printf("Player %d (AccountID %d) entered\n", resp.IID, loginData.AccountID)
|
log.Printf("Player %d (AccountID %d) entered\n", resp.IID, loginData.AccountID)
|
||||||
|
|
||||||
if err := peer.Send(protocol.P_FE2CL_REP_PC_ENTER_SUCC, resp); err != nil {
|
if err := peer.Send(protocol.P_FE2CL_REP_PC_ENTER_SUCC, resp); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// we send the chunk updates (PC_NEW, NPC_NEW, etc.) after the enter packet
|
|
||||||
server.updatePlayerPosition(plr, int(plr.X), int(plr.Y), int(plr.Z), int(plr.Angle))
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *ShardServer) LoadingComplete(peer *protocol.CNPeer, pkt protocol.Packet) error {
|
func (server *ShardServer) LoadingComplete(peer *cnet.Peer, pkt protocol.Packet) error {
|
||||||
var loadComplete protocol.SP_CL2FE_REQ_PC_LOADING_COMPLETE
|
var loadComplete protocol.SP_CL2FE_REQ_PC_LOADING_COMPLETE
|
||||||
pkt.Decode(&loadComplete)
|
pkt.Decode(&loadComplete)
|
||||||
|
|
||||||
plr, err := server.getPlayer(peer)
|
// was the peer attached to a player?
|
||||||
|
plr, ok := peer.UserData().(*entity.Player)
|
||||||
|
if !ok || plr == nil {
|
||||||
|
return fmt.Errorf("loadingComplete: plr is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := peer.Send(protocol.P_FE2CL_REP_PC_LOADING_COMPLETE_SUCC, protocol.SP_FE2CL_REP_PC_LOADING_COMPLETE_SUCC{IPC_ID: int32(plr.PlayerID)})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return peer.Send(protocol.P_FE2CL_REP_PC_LOADING_COMPLETE_SUCC, protocol.SP_FE2CL_REP_PC_LOADING_COMPLETE_SUCC{IPC_ID: int32(plr.PlayerID)})
|
// we send the chunk updates (PC_NEW, NPC_NEW, etc.) after the enter packet
|
||||||
|
chunkPos := entity.MakeChunkPosition(plr.X, plr.Y)
|
||||||
|
viewableChunks := server.getViewableChunks(chunkPos)
|
||||||
|
|
||||||
|
plr.SetChunkPos(chunkPos)
|
||||||
|
server.getChunk(chunkPos).AddEntity(plr)
|
||||||
|
server.addEntityToChunks(plr, viewableChunks)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,11 @@
|
|||||||
package shard
|
package shard
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/CPunch/gopenfusion/internal/entity"
|
"fmt"
|
||||||
"github.com/CPunch/gopenfusion/internal/protocol"
|
|
||||||
|
"github.com/CPunch/gopenfusion/cnet"
|
||||||
|
"github.com/CPunch/gopenfusion/cnet/protocol"
|
||||||
|
"github.com/CPunch/gopenfusion/shard/entity"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (server *ShardServer) updatePlayerPosition(plr *entity.Player, X, Y, Z, Angle int) {
|
func (server *ShardServer) updatePlayerPosition(plr *entity.Player, X, Y, Z, Angle int) {
|
||||||
@@ -13,14 +16,13 @@ func (server *ShardServer) updatePlayerPosition(plr *entity.Player, X, Y, Z, Ang
|
|||||||
server.updateEntityChunk(plr, plr.GetChunkPos(), entity.MakeChunkPosition(X, Y))
|
server.updateEntityChunk(plr, plr.GetChunkPos(), entity.MakeChunkPosition(X, Y))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *ShardServer) playerMove(peer *protocol.CNPeer, pkt protocol.Packet) error {
|
func (server *ShardServer) playerMove(peer *cnet.Peer, pkt protocol.Packet) error {
|
||||||
var move protocol.SP_CL2FE_REQ_PC_MOVE
|
var move protocol.SP_CL2FE_REQ_PC_MOVE
|
||||||
pkt.Decode(&move)
|
pkt.Decode(&move)
|
||||||
|
|
||||||
// sanity check
|
plr, ok := peer.UserData().(*entity.Player)
|
||||||
plr, err := server.getPlayer(peer)
|
if !ok || plr == nil {
|
||||||
if err != nil {
|
return fmt.Errorf("playerMove: plr is nil")
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// update chunking
|
// update chunking
|
||||||
@@ -42,14 +44,13 @@ func (server *ShardServer) playerMove(peer *protocol.CNPeer, pkt protocol.Packet
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *ShardServer) playerStop(peer *protocol.CNPeer, pkt protocol.Packet) error {
|
func (server *ShardServer) playerStop(peer *cnet.Peer, pkt protocol.Packet) error {
|
||||||
var stop protocol.SP_CL2FE_REQ_PC_STOP
|
var stop protocol.SP_CL2FE_REQ_PC_STOP
|
||||||
pkt.Decode(&stop)
|
pkt.Decode(&stop)
|
||||||
|
|
||||||
// sanity check
|
plr, ok := peer.UserData().(*entity.Player)
|
||||||
plr, err := server.getPlayer(peer)
|
if !ok || plr == nil {
|
||||||
if err != nil {
|
return fmt.Errorf("playerStop: plr is nil")
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// update chunking
|
// update chunking
|
||||||
@@ -65,14 +66,13 @@ func (server *ShardServer) playerStop(peer *protocol.CNPeer, pkt protocol.Packet
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *ShardServer) playerJump(peer *protocol.CNPeer, pkt protocol.Packet) error {
|
func (server *ShardServer) playerJump(peer *cnet.Peer, pkt protocol.Packet) error {
|
||||||
var jump protocol.SP_CL2FE_REQ_PC_JUMP
|
var jump protocol.SP_CL2FE_REQ_PC_JUMP
|
||||||
pkt.Decode(&jump)
|
pkt.Decode(&jump)
|
||||||
|
|
||||||
// sanity check
|
plr, ok := peer.UserData().(*entity.Player)
|
||||||
plr, err := server.getPlayer(peer)
|
if !ok || plr == nil {
|
||||||
if err != nil {
|
return fmt.Errorf("playerJump: _plr is nil")
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// update chunking
|
// update chunking
|
||||||
|
@@ -5,8 +5,8 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/CPunch/gopenfusion/config"
|
"github.com/CPunch/gopenfusion/internal/config"
|
||||||
"github.com/CPunch/gopenfusion/internal/entity"
|
"github.com/CPunch/gopenfusion/shard/entity"
|
||||||
)
|
)
|
||||||
|
|
||||||
type NPCData struct {
|
type NPCData struct {
|
||||||
@@ -18,7 +18,8 @@ func (server *ShardServer) LoadNPCs() {
|
|||||||
|
|
||||||
data, err := os.ReadFile(config.GetTDataPath() + "/NPCs.json")
|
data, err := os.ReadFile(config.GetTDataPath() + "/NPCs.json")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
log.Printf("Warning: failed to load NPCs: %v", err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// yes, we have to do it this way so our NPCs IDs will be incremented and unique
|
// yes, we have to do it this way so our NPCs IDs will be incremented and unique
|
||||||
|
@@ -1,60 +1,46 @@
|
|||||||
package shard
|
package shard
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"context"
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/CPunch/gopenfusion/config"
|
"github.com/CPunch/gopenfusion/cnet"
|
||||||
|
"github.com/CPunch/gopenfusion/cnet/protocol"
|
||||||
|
"github.com/CPunch/gopenfusion/internal/config"
|
||||||
"github.com/CPunch/gopenfusion/internal/db"
|
"github.com/CPunch/gopenfusion/internal/db"
|
||||||
"github.com/CPunch/gopenfusion/internal/entity"
|
|
||||||
"github.com/CPunch/gopenfusion/internal/protocol"
|
|
||||||
"github.com/CPunch/gopenfusion/internal/protocol/pool"
|
|
||||||
"github.com/CPunch/gopenfusion/internal/redis"
|
"github.com/CPunch/gopenfusion/internal/redis"
|
||||||
|
"github.com/CPunch/gopenfusion/shard/entity"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PacketHandler func(peer *protocol.CNPeer, pkt protocol.Packet) error
|
type PacketHandler func(peer *cnet.Peer, pkt protocol.Packet) error
|
||||||
|
|
||||||
type ShardServer struct {
|
type ShardServer struct {
|
||||||
listener net.Listener
|
service *cnet.Service
|
||||||
port int
|
|
||||||
dbHndlr *db.DBHandler
|
dbHndlr *db.DBHandler
|
||||||
redisHndlr *redis.RedisHandler
|
redisHndlr *redis.RedisHandler
|
||||||
eRecv chan *protocol.Event
|
|
||||||
packetHandlers map[uint32]PacketHandler
|
|
||||||
peers map[*protocol.CNPeer]*entity.Player
|
|
||||||
chunks map[entity.ChunkPosition]*entity.Chunk
|
chunks map[entity.ChunkPosition]*entity.Chunk
|
||||||
peerLock sync.Mutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewShardServer(dbHndlr *db.DBHandler, redisHndlr *redis.RedisHandler, port int) (*ShardServer, error) {
|
func NewShardServer(ctx context.Context, dbHndlr *db.DBHandler, redisHndlr *redis.RedisHandler, port int) (*ShardServer, error) {
|
||||||
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
|
srvc := cnet.NewService(ctx, "SHARD", port)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
server := &ShardServer{
|
server := &ShardServer{
|
||||||
listener: listener,
|
service: srvc,
|
||||||
port: port,
|
|
||||||
dbHndlr: dbHndlr,
|
dbHndlr: dbHndlr,
|
||||||
redisHndlr: redisHndlr,
|
redisHndlr: redisHndlr,
|
||||||
packetHandlers: make(map[uint32]PacketHandler),
|
|
||||||
peers: make(map[*protocol.CNPeer]*entity.Player),
|
|
||||||
chunks: make(map[entity.ChunkPosition]*entity.Chunk),
|
chunks: make(map[entity.ChunkPosition]*entity.Chunk),
|
||||||
eRecv: make(chan *protocol.Event),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
server.packetHandlers = map[uint32]PacketHandler{
|
srvc.AddPacketHandler(protocol.P_CL2FE_REQ_PC_ENTER, server.RequestEnter)
|
||||||
protocol.P_CL2FE_REQ_PC_ENTER: server.RequestEnter,
|
srvc.AddPacketHandler(protocol.P_CL2FE_REQ_PC_LOADING_COMPLETE, server.LoadingComplete)
|
||||||
protocol.P_CL2FE_REQ_PC_LOADING_COMPLETE: server.LoadingComplete,
|
srvc.AddPacketHandler(protocol.P_CL2FE_REQ_PC_MOVE, server.playerMove)
|
||||||
protocol.P_CL2FE_REQ_PC_MOVE: server.playerMove,
|
srvc.AddPacketHandler(protocol.P_CL2FE_REQ_PC_STOP, server.playerStop)
|
||||||
protocol.P_CL2FE_REQ_PC_STOP: server.playerStop,
|
srvc.AddPacketHandler(protocol.P_CL2FE_REQ_PC_JUMP, server.playerJump)
|
||||||
protocol.P_CL2FE_REQ_PC_JUMP: server.playerJump,
|
srvc.AddPacketHandler(protocol.P_CL2FE_REQ_SEND_FREECHAT_MESSAGE, server.freeChat)
|
||||||
protocol.P_CL2FE_REQ_SEND_FREECHAT_MESSAGE: server.freeChat,
|
srvc.AddPacketHandler(protocol.P_CL2FE_REQ_SEND_MENUCHAT_MESSAGE, server.menuChat)
|
||||||
protocol.P_CL2FE_REQ_SEND_MENUCHAT_MESSAGE: server.menuChat,
|
srvc.AddPacketHandler(protocol.P_CL2FE_REQ_PC_AVATAR_EMOTES_CHAT, server.emoteChat)
|
||||||
protocol.P_CL2FE_REQ_PC_AVATAR_EMOTES_CHAT: server.emoteChat,
|
|
||||||
}
|
srvc.OnConnect = server.onConnect
|
||||||
|
srvc.OnDisconnect = server.onDisconnect
|
||||||
|
|
||||||
redisHndlr.RegisterShard(redis.ShardMetadata{
|
redisHndlr.RegisterShard(redis.ShardMetadata{
|
||||||
IP: config.GetAnnounceIP(),
|
IP: config.GetAnnounceIP(),
|
||||||
@@ -64,97 +50,23 @@ func NewShardServer(dbHndlr *db.DBHandler, redisHndlr *redis.RedisHandler, port
|
|||||||
return server, nil
|
return server, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *ShardServer) handleEvents() {
|
func (server *ShardServer) Start() error {
|
||||||
for event := range server.eRecv {
|
|
||||||
switch event.Type {
|
|
||||||
case protocol.EVENT_CLIENT_DISCONNECT:
|
|
||||||
server.disconnect(event.Peer)
|
|
||||||
case protocol.EVENT_CLIENT_PACKET:
|
|
||||||
if err := server.handlePacket(event.Peer, event.PktID, protocol.NewPacket(event.Pkt)); err != nil {
|
|
||||||
event.Peer.Kill()
|
|
||||||
}
|
|
||||||
|
|
||||||
// the packet is given to us by the event, so we'll need to make sure to return it to the pool
|
|
||||||
pool.Put(event.Pkt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (server *ShardServer) Start() {
|
|
||||||
server.LoadNPCs()
|
server.LoadNPCs()
|
||||||
|
return server.service.Start()
|
||||||
log.Printf("Shard service hosted on %s:%d\n", config.GetAnnounceIP(), server.port)
|
|
||||||
go server.handleEvents()
|
|
||||||
for {
|
|
||||||
conn, err := server.listener.Accept()
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Connection error: ", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
client := protocol.NewCNPeer(server.eRecv, conn)
|
|
||||||
server.connect(client)
|
|
||||||
go client.Handler()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *ShardServer) GetPort() int {
|
func (server *ShardServer) onDisconnect(peer *cnet.Peer) {
|
||||||
return server.port
|
// remove from chunks
|
||||||
}
|
plr, ok := peer.UserData().(*entity.Player)
|
||||||
|
if ok && plr != nil {
|
||||||
func (server *ShardServer) handlePacket(peer *protocol.CNPeer, typeID uint32, pkt protocol.Packet) error {
|
|
||||||
if hndlr, ok := server.packetHandlers[typeID]; ok {
|
|
||||||
if err := hndlr(peer, pkt); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.Printf("[WARN] unknown packet ID: %x\n", typeID)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (server *ShardServer) disconnect(peer *protocol.CNPeer) {
|
|
||||||
server.peerLock.Lock()
|
|
||||||
defer server.peerLock.Unlock()
|
|
||||||
|
|
||||||
// remove from chunk(s)
|
|
||||||
plr, ok := server.peers[peer]
|
|
||||||
if ok {
|
|
||||||
log.Printf("Player %d (AccountID %d) disconnected\n", plr.PlayerID, plr.AccountID)
|
|
||||||
server.removeEntity(plr)
|
server.removeEntity(plr)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Peer %p disconnected from SHARD\n", peer)
|
|
||||||
delete(server.peers, peer)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *ShardServer) connect(peer *protocol.CNPeer) {
|
func (server *ShardServer) onConnect(peer *cnet.Peer) {
|
||||||
server.peerLock.Lock()
|
|
||||||
defer server.peerLock.Unlock()
|
|
||||||
|
|
||||||
log.Printf("New peer %p connected to SHARD\n", peer)
|
|
||||||
server.peers[peer] = nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *ShardServer) getPlayer(peer *protocol.CNPeer) (*entity.Player, error) {
|
func (server *ShardServer) Service() *cnet.Service {
|
||||||
plr, ok := server.peers[peer]
|
return server.service
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("player not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
return plr, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (server *ShardServer) setPlayer(peer *protocol.CNPeer, plr *entity.Player) {
|
|
||||||
server.peers[peer] = plr
|
|
||||||
}
|
|
||||||
|
|
||||||
// If f returns false the iteration is stopped.
|
|
||||||
func (server *ShardServer) rangePeers(f func(peer *protocol.CNPeer, plr *entity.Player) bool) {
|
|
||||||
for peer, plr := range server.peers {
|
|
||||||
if f(peer, plr) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user