Compare commits

...

62 Commits

Author SHA1 Message Date
cafca9093c login_test: use TestCharCreate from testutil 2024-03-03 13:14:37 -06:00
d84fcd2c93 testutil: added account.go 2024-03-03 13:11:46 -06:00
1f63f9856e testutil: refactoring/organizing
split helpers.go into env.go && dummy.go
2024-03-03 13:11:37 -06:00
de3e067b48 cnet/service: RandomPort no longer binds on 127.0.0.1 2024-02-23 18:34:10 -06:00
02afe67ac3 fix: os.Exit() kills any deferred cleanup functions
os.Exit() itself is now also a deferred function, which will be the last to run.
2024-02-05 11:59:50 -06:00
79f68187bf testutil: DummyPeer now holds onto the *is.Is
makes SendAndRecv a bit cleaner imo
2024-02-04 11:26:48 -06:00
cd93a058ce shard: no longer panics if tdata/NPCs.json isn't found 2024-02-04 11:21:02 -06:00
0a28dbcc3e removed util
- WaitWithTimeout && SelectWithTimeout have been moved to internal/testutil
- GetTime has been moved to cnet/protocol
2024-02-01 18:25:49 -06:00
1a6de671e5 moved 'testutil' to 'internal/testutil' 2024-02-01 17:28:00 -06:00
261ea6505f testutil: fix possible orphaned container in SetupEnvironment 2024-02-01 17:25:11 -06:00
556878544d testutil: refactoring && cleanup
added a simple DummyPeer struct to simplify creation, send/recv and cleanup
2024-02-01 17:21:56 -06:00
bfcbe6d3d6 started testutil: login_test now uses these helpers
should simplify new tests in the future
2024-02-01 17:11:50 -06:00
e5a9ed1481 shardserver: added Service()
also, Start() now returns an error result
2024-02-01 16:53:27 -06:00
23170093ee login_test: fix minor memory leak
defer PutBuffer so that the event packet is returned to the pool
2023-12-07 21:37:16 -06:00
2bd61dc571 login_test: more stale comments lol 2023-12-06 20:23:29 -06:00
cba01a877d login_test: fix sendAndRecv
removed the stale typecast lol
2023-12-06 20:17:36 -06:00
e1b9fa5d99 login_test: refactor, abstracted send and recv
validation
2023-12-06 20:15:06 -06:00
77751a2aa0 login_tests: annotate tests 2023-12-06 17:35:02 -06:00
8e84f0c7b2 more better README
haven't decided if showing an asciinema of the docker compose
environment is really useful or informative. maybe i'll
record a GIF of some gameplay in the future
2023-12-06 17:20:58 -06:00
c902559eac LICENSE: finally added! 2023-12-06 17:19:32 -06:00
1c40998cb6 README: oops, wrong link 2023-12-06 17:12:39 -06:00
988368c307 added unit tests workflow badge to README 2023-12-06 17:10:39 -06:00
01ebf4499f login_test: added TestCharacterSequence
tests the complete account/character creation sequence of packets
2023-12-06 17:08:59 -06:00
3a14d807d2 login: minor refactoring 2023-12-06 17:08:05 -06:00
141858d6c3 db/account: removed debug log 2023-12-06 16:40:06 -06:00
335fdb417c fix: add player to current chunk on LOAD_COMPLETE 2023-12-05 19:17:58 -06:00
2a6fb25f03 use passed context.Context 2023-12-04 20:45:23 -06:00
0ebd162af0 login_test: minor cleanup 2023-12-04 20:40:48 -06:00
d1763418a8 removed useless closure 2023-12-04 20:33:53 -06:00
c4d885cf6d Login && Password need to be at least 4 long 2023-12-04 20:30:58 -06:00
afd5c9ef23 added login_test 2023-12-04 20:28:17 -06:00
ac62f7d64e moved config/ -> internal/config 2023-12-02 22:09:11 -06:00
58afc9df1f protocol/packet: use append() w/ temp buf to grow 2023-12-02 21:54:54 -06:00
e257bf998f protocol/encrypt: minor cleanup 2023-12-02 21:53:02 -06:00
96eed66831 add cnet/ util/ to unit-tests push event 2023-12-02 21:52:42 -06:00
12f16645e1 updated README 2023-12-02 17:04:06 -06:00
76e9bdf7e7 minor protocol_test.go refactor 2023-12-02 17:03:56 -06:00
44f3b31965 start redis_test.go 2023-12-02 16:56:45 -06:00
899b95b4e6 SelectWithTimeout && WaitWithTImeout now use time.Duration 2023-12-01 20:23:27 -06:00
e33b7c0556 util: added test 2023-12-01 20:13:53 -06:00
3445b852fd util: added SelectWithTimeout && WaitWithTImeout 2023-12-01 20:13:42 -06:00
557117f093 moved internal/protocol -> cnet/protocol 2023-12-01 19:56:23 -06:00
b07e9ddbcb merged internal/service -> cnet/service 2023-12-01 19:22:49 -06:00
af867ccff2 renamed cnet.CNPeer -> cnet.Peer 2023-12-01 19:15:00 -06:00
c60017f78f rename cnpeer package to cnet 2023-12-01 17:11:41 -06:00
e1804a1042 merged entity/chunk && entity/chunkposition 2023-12-01 17:03:46 -06:00
bcc999db38 moved internal/entity to shard/entity 2023-12-01 16:56:55 -06:00
e355af19ab moved internal/protocol/cnpeer to cnpeer
also started a util package
2023-12-01 15:29:19 -06:00
0ed19ad6c5 tests workflow: add timeout
some of the tests can fail in really bad, slow ways. make sure we don't
spin our wheels waiting for a failed test to never fail.
2023-12-01 14:10:45 -06:00
66fe3c9738 TestService: minor refactor; clearer waitgroup Add 2023-12-01 14:08:17 -06:00
72dbfe2541 TestService: wait for OnConnect && OnDisconnect 2023-12-01 13:58:57 -06:00
8e65a78d07 testing refactor; use github.com/matryer/is
It is syntactically pretty, simple, and also makes failures have
pretty colors. what more could you ask for :)
2023-12-01 13:49:50 -06:00
f4b17906ce more protocol/service refactor
- removed protocol.Event: CNPeers now send protocol.PacketEvents
- peer uData is held in CNPeer, use SetUserData() and UserData() to
set/read it
- Service.PacketHandler calback has changed, removed uData:
switched calls to peer.SetUserData() and peer.UserData() where appropriate
- service.Service lots of tidying up, removed dependence on old
protocol.Event.
- service.Service && protocol.CNPeer now accept a cancelable context.
hooray graceful shutdowns and unit tests!
- general cleanup
2023-12-01 00:56:34 -06:00
c0ba365cf5 CNPeer/Service refactor
- each CNPeer is given a unique chan *protocol.Event to pass events to
the service.handleEvents() loop. this is now passed to CNPeer.Handler()
as opposed to NewCNPeer().
- service has basically been rewritten. handleEvents() main loop uses
reflect.SelectCase() now to handle all of the eRecv channels for each
peer
- new protocol Event type: EVENT_CLIENT_CONNECT
- Added service_test.go; blackbox-styled testing like the others.
TestService() starts a service and spins up  a bunch of dummy peers
and verifies that each packet sent causes the corresponding packet
handler to be called.
2023-11-29 19:57:45 -06:00
d0346b2382 use proper errors for db.Err
- switch to using errors.Is where applicable
2023-11-29 19:52:10 -06:00
18a6c5ab42 oh, this is safe actually
https://stackoverflow.com/a/23230406
2023-11-28 21:21:24 -06:00
3abba0ca3c this should be CompareAndSwap oopsd D: 2023-11-27 21:34:39 -06:00
1f66acfd25 holy refactor
started out as me making a service abstraction..

- db.Player exists again, and entity.Player uses it as an embedded struct
- chunk.ForEachEntity() lets you add/remove entities during iteration now
- removed account related fields from CNPeer
- protocol/pool has been merged with protocol.
use protocol.GetBuffer() and protocol.PutBuffer().
- new protocol/internal/service!
service.Service is an abstraction layer to handle multiple CNPeer*
connections and allows you to associate each with an interface{} uData.
In the future it might also handle a task queue for jobs that
modify/interact with the player's uData, called from service.handleEvents()
- PacketHandler callback type has a new param! uData is passed as well now
- much of loginserver/shardserver is now handled by the shared service
abstraction
- SHARD: NPC_ENTER packets are now sent on player loading complete
rather than on enter.
2023-11-27 21:23:28 -06:00
d8277ea89c fix workflow event trigger 2023-11-26 17:00:37 -06:00
81de857670 protocol_test: split TestPacketEncodeDecode 2023-11-26 16:58:33 -06:00
e8f5e5fc9c protocol_test: added TestDataEncrypt, TestDataDecrypt, TestCreateNewKey 2023-11-26 16:54:22 -06:00
6f55cbbad5 minor renaming of unit-tests workflow 2023-11-26 16:19:43 -06:00
50 changed files with 1527 additions and 689 deletions

View File

@@ -1,15 +1,21 @@
name: Go
name: unit-tests
on:
push:
paths:
- ./**.go
- cmd/**
- config/**
- cnet/**
- internal/**
- login/**
- shard/**
- util/**
- go.mod
- go.sum
- .github/workflows/tests.yaml
jobs:
tests:
run:
runs-on: ubuntu-latest
steps:
@@ -19,4 +25,4 @@ jobs:
with:
go-version: '1.21.x'
- name: Test with the Go CLI
run: go test -v ./...
run: go test -timeout 10s -v ./...

7
LICENSE.md Normal file
View 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.

View File

@@ -1,6 +1,13 @@
# 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

View File

@@ -5,7 +5,7 @@ import (
"flag"
"log"
"github.com/CPunch/gopenfusion/config"
"github.com/CPunch/gopenfusion/internal/config"
"github.com/CPunch/gopenfusion/login"
"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 {
loginServer, err := login.NewLoginServer(dbHndlr, redisHndlr, s.port)
loginServer, err := login.NewLoginServer(ctx, dbHndlr, redisHndlr, s.port)
if err != nil {
log.Panicf("failed to create shard server: %v", err)
}

View File

@@ -6,7 +6,7 @@ import (
"log"
"os"
"github.com/CPunch/gopenfusion/config"
"github.com/CPunch/gopenfusion/internal/config"
"github.com/CPunch/gopenfusion/internal/db"
"github.com/CPunch/gopenfusion/internal/redis"

View File

@@ -5,7 +5,7 @@ import (
"flag"
"log"
"github.com/CPunch/gopenfusion/config"
"github.com/CPunch/gopenfusion/internal/config"
"github.com/CPunch/gopenfusion/shard"
"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 {
shardServer, err := shard.NewShardServer(dbHndlr, redisHndlr, s.port)
shardServer, err := shard.NewShardServer(ctx, dbHndlr, redisHndlr, s.port)
if err != nil {
log.Panicf("failed to create shard server: %v", err)
}

165
cnet/peer.go Normal file
View 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}
}
}
}

View File

@@ -6,7 +6,7 @@ import (
)
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
)
@@ -50,13 +50,14 @@ func DecryptData(buff, key []byte) {
}
func CreateNewKey(uTime, iv1, iv2 uint64) []byte {
dEKey := binary.LittleEndian.Uint64([]byte(DEFAULT_KEY))
num := iv1 + 1
num2 := iv2 + 1
dEKey := uint64(binary.LittleEndian.Uint64([]byte(DEFAULT_KEY)))
key := dEKey * (uTime * num * num2)
buf := make([]byte, 8)
binary.LittleEndian.PutUint64(buf, uint64(key))
binary.LittleEndian.PutUint64(buf, key)
return buf
}

View File

@@ -42,10 +42,7 @@ func (pkt Packet) encodeStructField(field reflect.StructField, value reflect.Val
buf16 = buf16[:sz]
} else {
// grow
// TODO: probably a better way to do this?
for len(buf16) < sz {
buf16 = append(buf16, 0)
}
buf16 = append(buf16, make([]uint16, sz-len(buf16))...)
}
// write
@@ -125,8 +122,7 @@ func (pkt Packet) decodeStructField(field reflect.StructField, value reflect.Val
// consume padding bytes
pad, err := strconv.Atoi(field.Tag.Get("pad"))
if err == nil {
dummy := make([]byte, pad)
if _, err := pkt.readWriter.Read(dummy); err != nil {
if _, err := pkt.readWriter.Read(make([]byte, pad)); err != nil {
return err
}
}

View File

@@ -1,4 +1,4 @@
package pool
package protocol
import (
"bytes"
@@ -9,11 +9,13 @@ var allocator = &sync.Pool{
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)
}
func Put(buf *bytes.Buffer) {
// returns a *bytes.Buffer to the pool
func PutBuffer(buf *bytes.Buffer) {
buf.Reset()
allocator.Put(buf)
}

View 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
View File

@@ -0,0 +1,9 @@
package protocol
import (
"time"
)
func GetTime() uint64 {
return uint64(time.Now().UnixMilli())
}

282
cnet/service.go Normal file
View 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
View 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
View File

@@ -3,11 +3,13 @@ module github.com/CPunch/gopenfusion
go 1.19
require (
github.com/alicebob/miniredis/v2 v2.31.0
github.com/bitcomplete/sqltestutil v1.0.1
github.com/blockloop/scan v1.3.0
github.com/georgysavva/scany/v2 v2.0.0
github.com/google/subcommands v1.2.0
github.com/lib/pq v1.10.9
github.com/matryer/is v1.4.1
github.com/redis/go-redis/v9 v9.0.5
golang.org/x/crypto v0.7.0
)
@@ -15,6 +17,7 @@ require (
require (
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // 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/davecgh/go-spew v1.1.1 // 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/sirupsen/logrus v1.8.1 // 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/sys v0.6.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect

14
go.sum
View File

@@ -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/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/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/go.mod h1:ZgpEnW6t2RBsCo9EIEYsAvjxJeZDwOzC8aVYXK0+gdE=
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/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
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/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=
@@ -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/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
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/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
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.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
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/yuin/goldmark v1.1.27/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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
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-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/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

View File

@@ -1,12 +1,11 @@
package db
import (
"fmt"
"log"
"errors"
"golang.org/x/crypto/bcrypt"
"github.com/CPunch/gopenfusion/internal/protocol"
"github.com/CPunch/gopenfusion/cnet/protocol"
"github.com/georgysavva/scany/v2/sqlscan"
)
@@ -44,8 +43,8 @@ func (db *DBHandler) NewAccount(Login, Password string) (*Account, error) {
}
var (
ErrLoginInvalidID = fmt.Errorf("invalid Login ID")
ErrLoginInvalidPassword = fmt.Errorf("invalid ID && Password combo")
ErrLoginInvalidID = errors.New("invalid Login ID")
ErrLoginInvalidPassword = errors.New("invalid ID && Password combo")
)
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
}
// make sure id && pw are valid
if len(Login) < 4 || len(Password) < 4 {
return nil, ErrLoginInvalidPassword
}
var account Account
row.Next()
if err := sqlscan.ScanRow(&account, row); err != nil {
log.Printf("Error scanning row: %v", err)
return nil, ErrLoginInvalidID
}

View File

@@ -2,11 +2,14 @@ package db_test
import (
"context"
"errors"
"os"
"testing"
"github.com/matryer/is"
"github.com/CPunch/gopenfusion/cnet/protocol"
"github.com/CPunch/gopenfusion/internal/db"
"github.com/CPunch/gopenfusion/internal/protocol"
"github.com/bitcomplete/sqltestutil"
)
@@ -15,6 +18,11 @@ var (
)
func TestMain(m *testing.M) {
ret := 1
defer func() {
os.Exit(ret)
}()
ctx := context.Background()
psql, err := sqltestutil.StartPostgresContainer(ctx, "15")
if err != nil {
@@ -32,27 +40,24 @@ func TestMain(m *testing.M) {
panic(err)
}
os.Exit(m.Run())
ret = m.Run()
}
func TestDBAccount(t *testing.T) {
if _, err := testDB.NewAccount("test", "test"); err != nil {
t.Error(err)
}
is := is.New(t)
// create new account
_, err := testDB.NewAccount("test", "test")
is.NoErr(err)
// now try to retrieve account data
acc, err := testDB.TryLogin("test", "test")
if err != nil {
t.Error(err)
}
is.NoErr(err)
if acc.Login != "test" {
t.Error("account username is not test")
}
_, err = testDB.TryLogin("test", "wrongpassword")
if _, err = testDB.TryLogin("test", "wrongpassword"); err != db.ErrLoginInvalidPassword {
t.Error("expected ErrLoginInvalidPassword")
}
is.True(acc.Login == "test") // login data should match created account
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) {
if _, err := testDB.NewAccount("testplayer", "test"); err != nil {
t.Error(err)
}
is := is.New(t)
_, err := testDB.NewAccount("testplayer", "test")
is.NoErr(err)
// now try to retrieve account data
acc, err := testDB.TryLogin("testplayer", "test")
if err != nil {
t.Error(err)
}
is.NoErr(err)
plrID, err := testDB.NewPlayer(acc.AccountID, "Neil", "Mcscout", 1)
if err != nil {
t.Error(err)
}
is.NoErr(err)
if err = testDB.FinishPlayer(&protocol.SP_CL2LS_REQ_CHAR_CREATE{
err = testDB.FinishPlayer(&protocol.SP_CL2LS_REQ_CHAR_CREATE{
PCStyle: protocol.SPCStyle{
IPC_UID: int64(plrID),
INameCheck: 1,
@@ -115,11 +116,9 @@ func TestDBPlayer(t *testing.T) {
IEquipLBID: 359,
IEquipFootID: 194,
},
}, acc.AccountID); err != nil {
t.Error(err)
}
}, acc.AccountID)
is.NoErr(err)
if err = testDB.FinishTutorial(plrID, acc.AccountID); err != nil {
t.Error(err)
}
err = testDB.FinishTutorial(plrID, acc.AccountID)
is.NoErr(err)
}

View File

@@ -3,7 +3,7 @@ package db
import (
"database/sql"
"github.com/CPunch/gopenfusion/internal/protocol"
"github.com/CPunch/gopenfusion/cnet/protocol"
)
type Inventory struct {

View File

@@ -3,12 +3,42 @@ package db
import (
"database/sql"
"github.com/CPunch/gopenfusion/config"
"github.com/CPunch/gopenfusion/internal/entity"
"github.com/CPunch/gopenfusion/internal/protocol"
"github.com/CPunch/gopenfusion/cnet/protocol"
"github.com/CPunch/gopenfusion/internal/config"
"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
func (db *DBHandler) NewPlayer(AccountID int, FirstName, LastName string, slot int) (int, error) {
nameCheck := 1 // for now, we approve all names
@@ -121,8 +151,8 @@ const (
INNER JOIN Accounts as acc ON p.AccountID = acc.AccountID `
)
func (db *DBHandler) readPlayer(rows *sql.Rows) (*entity.Player, error) {
plr := entity.Player{ActiveNanoSlotNum: 0}
func (db *DBHandler) readPlayer(rows *sql.Rows) (*Player, error) {
plr := Player{ActiveNanoSlotNum: 0}
if err := rows.Scan(
&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
}
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)
if err != nil {
return nil, err
}
var plr *entity.Player
var plr *Player
for rows.Next() {
plr, err = db.readPlayer(rows)
if err != nil {
@@ -179,13 +209,13 @@ func (db *DBHandler) GetPlayer(PlayerID int) (*entity.Player, error) {
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)
if err != nil {
return nil, err
}
var plrs []entity.Player
var plrs []Player
for rows.Next() {
plr, err := db.readPlayer(rows)
if err != nil {

View File

@@ -10,7 +10,7 @@ import (
_ "embed"
"fmt"
"github.com/CPunch/gopenfusion/config"
"github.com/CPunch/gopenfusion/internal/config"
_ "github.com/lib/pq"
)

View File

@@ -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),
}
}

View File

@@ -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
}
}
}

View File

@@ -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
}

View File

@@ -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!")
}
}

View File

@@ -5,7 +5,7 @@ import (
"fmt"
"strconv"
"github.com/CPunch/gopenfusion/config"
"github.com/CPunch/gopenfusion/internal/config"
)
type LoginMetadata struct {

View File

@@ -7,7 +7,7 @@ package redis
import (
"context"
"github.com/CPunch/gopenfusion/config"
"github.com/CPunch/gopenfusion/internal/config"
"github.com/redis/go-redis/v9"
)
@@ -17,10 +17,6 @@ type RedisHandler struct {
ctx context.Context
}
const (
SHARD_SET = "shards"
)
func OpenRedis(addr string) (*RedisHandler, error) {
client := redis.NewClient(&redis.Options{
Addr: addr,

View 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
}

View File

@@ -7,6 +7,10 @@ type ShardMetadata struct {
Port int
}
const (
SHARD_SET = "shards"
)
func (r *RedisHandler) RegisterShard(shard ShardMetadata) error {
value, err := json.Marshal(shard)
if err != nil {

View 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,
})
}

View 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
View 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()
}
}

View 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)
}

View 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
}

View File

@@ -2,13 +2,15 @@ package login
import (
"encoding/binary"
"errors"
"fmt"
"log"
"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/protocol"
"github.com/CPunch/gopenfusion/internal/redis"
)
@@ -24,9 +26,7 @@ const (
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 {
peer.SzID = SzID
func (server *LoginServer) AcceptLogin(peer *cnet.Peer, SzID string, IClientVerC int32, ISlotNum int8, data []protocol.SP_LS2CL_REP_CHAR_INFO) error {
resp := protocol.SP_LS2CL_REP_LOGIN_SUCC{
SzID: SzID,
ICharCount: int8(len(data)),
@@ -62,7 +62,7 @@ func (server *LoginServer) AcceptLogin(peer *protocol.CNPeer, SzID string, IClie
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
pkt.Decode(&loginPkt)
@@ -74,14 +74,14 @@ func (server *LoginServer) Login(peer *protocol.CNPeer, pkt protocol.Packet) err
}
// client is resending a login packet??
if peer.AccountID != -1 {
if peer.UserData() != nil {
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
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
account, err = server.dbHndlr.NewAccount(loginPkt.SzID, loginPkt.SzPassword)
if err != nil {
@@ -89,7 +89,7 @@ func (server *LoginServer) Login(peer *protocol.CNPeer, pkt protocol.Packet) err
SendError(LOGIN_DATABASE_ERROR)
return err
}
} else if err == db.ErrLoginInvalidPassword {
} else if errors.Is(err, db.ErrLoginInvalidPassword) {
// respond with invalid password
SendError(LOGIN_ID_AND_PASSWORD_DO_NOT_MATCH)
return nil
@@ -99,7 +99,7 @@ func (server *LoginServer) Login(peer *protocol.CNPeer, pkt protocol.Packet) err
}
// grab player data
peer.AccountID = account.AccountID
peer.SetUserData(account)
plrs, err := server.dbHndlr.GetPlayers(account.AccountID)
if err != nil {
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)])
}
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
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
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{})
return fmt.Errorf("out of order P_LS2CL_REP_SAVE_CHAR_NAME_FAIL")
}
// 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 {
peer.Send(protocol.P_LS2CL_REP_SAVE_CHAR_NAME_FAIL, protocol.SP_LS2CL_REP_SAVE_CHAR_NAME_FAIL{})
return err
@@ -201,7 +202,7 @@ func validateCharacterCreation(character *protocol.SP_CL2LS_REQ_CHAR_CREATE) boo
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{
IErrorCode: 2,
}); err != nil {
@@ -211,16 +212,21 @@ func SendFail(peer *protocol.CNPeer) error {
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
pkt.Decode(&charPkt)
account, ok := peer.UserData().(*db.Account)
if !ok || account == nil {
return SendFail(peer)
}
if !validateCharacterCreation(&charPkt) {
log.Printf("Invalid character creation packet: %+v", charPkt)
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)
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
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 {
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
pkt.Decode(&selection)
account, ok := peer.UserData().(*db.Account)
if !ok || account == nil {
return SendFail(peer)
}
shards := server.redisHndlr.GetShards()
if len(shards) == 0 {
SendFail(peer)
return fmt.Errorf("loginServer has found no linked shards")
}
key, err := protocol.GenSerialKey()
if err != nil {
return err
}
// TODO: better shard selection logic pls
// for now, pick random shard
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)
return SendFail(peer)
}
accountID := account.AccountID
if plr.AccountID != peer.AccountID {
log.Printf("HACK: player %d tried to join shard as player %d", peer.AccountID, plr.AccountID)
if plr.AccountID != accountID {
log.Printf("HACK: player %d tried to join shard as player %d", accountID, plr.AccountID)
return SendFail(peer)
}
key, err := protocol.GenSerialKey()
if err != nil {
return err
}
// share the login attempt
server.redisHndlr.QueueLogin(key, redis.LoginMetadata{
FEKey: peer.FE_key,
PlayerID: int32(selection.IPC_UID),
AccountID: peer.AccountID,
AccountID: accountID,
})
// 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)
}
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
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)
}

179
login/login_test.go Normal file
View 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
}

View File

@@ -1,126 +1,51 @@
package login
import (
"fmt"
"log"
"net"
"sync"
"context"
"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/protocol"
"github.com/CPunch/gopenfusion/internal/protocol/pool"
"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 {
listener net.Listener
port int
dbHndlr *db.DBHandler
redisHndlr *redis.RedisHandler
eRecv chan *protocol.Event
peers map[*protocol.CNPeer]bool
packetHandlers map[uint32]PacketHandler
peerLock sync.Mutex
service *cnet.Service
dbHndlr *db.DBHandler
redisHndlr *redis.RedisHandler
}
func NewLoginServer(dbHndlr *db.DBHandler, redisHndlr *redis.RedisHandler, port int) (*LoginServer, error) {
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
if err != nil {
return nil, err
}
func NewLoginServer(ctx context.Context, dbHndlr *db.DBHandler, redisHndlr *redis.RedisHandler, port int) (*LoginServer, error) {
srvc := cnet.NewService(ctx, "LOGIN", port)
server := &LoginServer{
listener: listener,
port: port,
service: srvc,
dbHndlr: dbHndlr,
redisHndlr: redisHndlr,
peers: make(map[*protocol.CNPeer]bool),
eRecv: make(chan *protocol.Event),
}
server.packetHandlers = map[uint32]PacketHandler{
protocol.P_CL2LS_REQ_LOGIN: server.Login,
protocol.P_CL2LS_REQ_CHECK_CHAR_NAME: server.CheckCharacterName,
protocol.P_CL2LS_REQ_SAVE_CHAR_NAME: server.SaveCharacterName,
protocol.P_CL2LS_REQ_CHAR_CREATE: server.CharacterCreate,
protocol.P_CL2LS_REQ_CHAR_SELECT: server.ShardSelect,
protocol.P_CL2LS_REQ_CHAR_DELETE: server.CharacterDelete,
protocol.P_CL2LS_REQ_SHARD_SELECT: stubbedPacket,
protocol.P_CL2LS_REQ_SHARD_LIST_INFO: stubbedPacket,
protocol.P_CL2LS_CHECK_NAME_LIST: stubbedPacket,
protocol.P_CL2LS_REQ_SAVE_CHAR_TUTOR: server.FinishTutorial,
protocol.P_CL2LS_REQ_PC_EXIT_DUPLICATE: stubbedPacket,
protocol.P_CL2LS_REP_LIVE_CHECK: stubbedPacket,
protocol.P_CL2LS_REQ_CHANGE_CHAR_NAME: stubbedPacket,
protocol.P_CL2LS_REQ_SERVER_SELECT: stubbedPacket,
}
srvc.AddPacketHandler(protocol.P_CL2LS_REQ_LOGIN, server.Login)
srvc.AddPacketHandler(protocol.P_CL2LS_REQ_CHECK_CHAR_NAME, server.CheckCharacterName)
srvc.AddPacketHandler(protocol.P_CL2LS_REQ_SAVE_CHAR_NAME, server.SaveCharacterName)
srvc.AddPacketHandler(protocol.P_CL2LS_REQ_CHAR_CREATE, server.CharacterCreate)
srvc.AddPacketHandler(protocol.P_CL2LS_REQ_CHAR_SELECT, server.ShardSelect)
srvc.AddPacketHandler(protocol.P_CL2LS_REQ_CHAR_DELETE, server.CharacterDelete)
srvc.AddPacketHandler(protocol.P_CL2LS_REQ_SHARD_SELECT, cnet.StubbedPacket)
srvc.AddPacketHandler(protocol.P_CL2LS_REQ_SHARD_LIST_INFO, cnet.StubbedPacket)
srvc.AddPacketHandler(protocol.P_CL2LS_CHECK_NAME_LIST, cnet.StubbedPacket)
srvc.AddPacketHandler(protocol.P_CL2LS_REQ_SAVE_CHAR_TUTOR, server.FinishTutorial)
srvc.AddPacketHandler(protocol.P_CL2LS_REQ_PC_EXIT_DUPLICATE, cnet.StubbedPacket)
srvc.AddPacketHandler(protocol.P_CL2LS_REP_LIVE_CHECK, cnet.StubbedPacket)
srvc.AddPacketHandler(protocol.P_CL2LS_REQ_CHANGE_CHAR_NAME, cnet.StubbedPacket)
srvc.AddPacketHandler(protocol.P_CL2LS_REQ_SERVER_SELECT, cnet.StubbedPacket)
return server, nil
}
func (server *LoginServer) Start() {
log.Printf("Login 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 *LoginServer) Service() *cnet.Service {
return server.service
}
func (server *LoginServer) handleEvents() {
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 {
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
func (server *LoginServer) Start() error {
return server.service.Start()
}

View File

@@ -1,15 +1,20 @@
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
pkt.Decode(&chat)
// sanity check
plr, err := server.getPlayer(peer)
if err != nil {
return err
plr, ok := peer.UserData().(*entity.Player)
if !ok || plr == nil {
return fmt.Errorf("freeChat: plr is nil")
}
// 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
pkt.Decode(&chat)
// sanity check
plr, err := server.getPlayer(peer)
if err != nil {
return err
plr, ok := peer.UserData().(*entity.Player)
if !ok || plr == nil {
return fmt.Errorf("menuChat: plr is nil")
}
// 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
pkt.Decode(&chat)
// sanity check
plr, err := server.getPlayer(peer)
if err != nil {
return err
plr, ok := peer.UserData().(*entity.Player)
if !ok || plr == nil {
return fmt.Errorf("emoteChat: plr is nil")
}
// spread message

View File

@@ -3,8 +3,22 @@ package entity
import (
"log"
"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 {
Position ChunkPosition
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
// f can safely add/remove entities from the chunk
func (c *Chunk) ForEachEntity(f func(entity Entity) bool) {
c.lock.Lock()
defer c.lock.Unlock()
for entity := range c.entities {
if f(entity) {
break

View File

@@ -1,6 +1,6 @@
package entity
import "github.com/CPunch/gopenfusion/internal/protocol"
import "github.com/CPunch/gopenfusion/cnet"
type EntityKind int
@@ -20,6 +20,6 @@ type Entity interface {
SetPosition(x, y, z int)
SetAngle(angle int)
DisappearFromViewOf(peer *protocol.CNPeer)
EnterIntoViewOf(peer *protocol.CNPeer)
DisappearFromViewOf(peer *cnet.Peer)
EnterIntoViewOf(peer *cnet.Peer)
}

View File

@@ -3,10 +3,12 @@ package entity_test
import (
"testing"
"github.com/CPunch/gopenfusion/internal/entity"
"github.com/CPunch/gopenfusion/shard/entity"
"github.com/matryer/is"
)
func TestChunkSliceDifference(t *testing.T) {
is := is.New(t)
chunks := []*entity.Chunk{
entity.NewChunk(entity.MakeChunkPosition(0, 0)),
entity.NewChunk(entity.MakeChunkPosition(0, 1)),
@@ -28,13 +30,7 @@ func TestChunkSliceDifference(t *testing.T) {
}
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] {
t.Logf("%+v", diff)
t.Error("wrong difference")
}
is.True(len(diff) == 1) // should be 1 chunk in difference
is.True(diff[0] == chunks[3]) // should be chunks[3] in difference
}

View File

@@ -3,7 +3,8 @@ package entity
import (
"sync/atomic"
"github.com/CPunch/gopenfusion/internal/protocol"
"github.com/CPunch/gopenfusion/cnet"
"github.com/CPunch/gopenfusion/cnet/protocol"
)
type NPC struct {
@@ -62,13 +63,13 @@ func (npc *NPC) SetAngle(angle int) {
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{
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{
NPCAppearanceData: npc.GetAppearanceData(),
})

View File

@@ -1,41 +1,23 @@
package entity
import (
"github.com/CPunch/gopenfusion/config"
"github.com/CPunch/gopenfusion/internal/protocol"
"github.com/CPunch/gopenfusion/cnet"
"github.com/CPunch/gopenfusion/cnet/protocol"
"github.com/CPunch/gopenfusion/internal/db"
)
type Player struct {
Peer *protocol.CNPeer
Chunk ChunkPosition
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
db.Player
Peer *cnet.Peer
Chunk ChunkPosition
}
func NewPlayer(peer *cnet.Peer, player *db.Player) *Player {
return &Player{
Player: *player,
Peer: peer,
Chunk: MakeChunkPosition(player.X, player.Y),
}
}
// ==================== Entity interface ====================
@@ -70,13 +52,13 @@ func (plr *Player) SetAngle(angle int) {
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{
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{
PCAppearanceData: plr.GetAppearanceData(),
})

View File

@@ -1,7 +1,7 @@
package shard
import (
"github.com/CPunch/gopenfusion/internal/entity"
"github.com/CPunch/gopenfusion/shard/entity"
)
func (server *ShardServer) addEntity(e entity.Entity) {

View File

@@ -4,33 +4,37 @@ import (
"fmt"
"log"
"github.com/CPunch/gopenfusion/internal/entity"
"github.com/CPunch/gopenfusion/internal/protocol"
"github.com/CPunch/gopenfusion/cnet"
"github.com/CPunch/gopenfusion/cnet/protocol"
"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) {
// resending a shard enter packet?
old, _ := server.getPlayer(peer)
if old != nil {
return nil, fmt.Errorf("resent enter packet")
}
// attach player
plr, err := server.dbHndlr.GetPlayer(int(meta.PlayerID))
func (server *ShardServer) attachPlayer(peer *cnet.Peer, meta redis.LoginMetadata) (*entity.Player, error) {
dbPlr, err := server.dbHndlr.GetPlayer(int(meta.PlayerID))
if err != nil {
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
}
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
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)
if err != nil {
// 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
peer.E_key = protocol.CreateNewKey(resp.UiSvrTime, uint64(resp.IID+1), uint64(resp.PCLoadData2CL.IFusionMatter+1))
peer.FE_key = loginData.FEKey
peer.PlayerID = loginData.PlayerID
peer.AccountID = loginData.AccountID
peer.SetActiveKey(protocol.USE_FE)
peer.SetActiveKey(cnet.USE_FE)
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 {
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
}
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
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 {
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
}

View File

@@ -1,8 +1,11 @@
package shard
import (
"github.com/CPunch/gopenfusion/internal/entity"
"github.com/CPunch/gopenfusion/internal/protocol"
"fmt"
"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) {
@@ -13,14 +16,13 @@ func (server *ShardServer) updatePlayerPosition(plr *entity.Player, X, Y, Z, Ang
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
pkt.Decode(&move)
// sanity check
plr, err := server.getPlayer(peer)
if err != nil {
return err
plr, ok := peer.UserData().(*entity.Player)
if !ok || plr == nil {
return fmt.Errorf("playerMove: plr is nil")
}
// 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
pkt.Decode(&stop)
// sanity check
plr, err := server.getPlayer(peer)
if err != nil {
return err
plr, ok := peer.UserData().(*entity.Player)
if !ok || plr == nil {
return fmt.Errorf("playerStop: plr is nil")
}
// 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
pkt.Decode(&jump)
// sanity check
plr, err := server.getPlayer(peer)
if err != nil {
return err
plr, ok := peer.UserData().(*entity.Player)
if !ok || plr == nil {
return fmt.Errorf("playerJump: _plr is nil")
}
// update chunking

View File

@@ -5,8 +5,8 @@ import (
"log"
"os"
"github.com/CPunch/gopenfusion/config"
"github.com/CPunch/gopenfusion/internal/entity"
"github.com/CPunch/gopenfusion/internal/config"
"github.com/CPunch/gopenfusion/shard/entity"
)
type NPCData struct {
@@ -18,7 +18,8 @@ func (server *ShardServer) LoadNPCs() {
data, err := os.ReadFile(config.GetTDataPath() + "/NPCs.json")
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

View File

@@ -1,60 +1,46 @@
package shard
import (
"fmt"
"log"
"net"
"sync"
"context"
"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/entity"
"github.com/CPunch/gopenfusion/internal/protocol"
"github.com/CPunch/gopenfusion/internal/protocol/pool"
"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 {
listener net.Listener
port int
dbHndlr *db.DBHandler
redisHndlr *redis.RedisHandler
eRecv chan *protocol.Event
packetHandlers map[uint32]PacketHandler
peers map[*protocol.CNPeer]*entity.Player
chunks map[entity.ChunkPosition]*entity.Chunk
peerLock sync.Mutex
service *cnet.Service
dbHndlr *db.DBHandler
redisHndlr *redis.RedisHandler
chunks map[entity.ChunkPosition]*entity.Chunk
}
func NewShardServer(dbHndlr *db.DBHandler, redisHndlr *redis.RedisHandler, port int) (*ShardServer, error) {
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
if err != nil {
return nil, err
}
func NewShardServer(ctx context.Context, dbHndlr *db.DBHandler, redisHndlr *redis.RedisHandler, port int) (*ShardServer, error) {
srvc := cnet.NewService(ctx, "SHARD", port)
server := &ShardServer{
listener: listener,
port: port,
dbHndlr: dbHndlr,
redisHndlr: redisHndlr,
packetHandlers: make(map[uint32]PacketHandler),
peers: make(map[*protocol.CNPeer]*entity.Player),
chunks: make(map[entity.ChunkPosition]*entity.Chunk),
eRecv: make(chan *protocol.Event),
service: srvc,
dbHndlr: dbHndlr,
redisHndlr: redisHndlr,
chunks: make(map[entity.ChunkPosition]*entity.Chunk),
}
server.packetHandlers = map[uint32]PacketHandler{
protocol.P_CL2FE_REQ_PC_ENTER: server.RequestEnter,
protocol.P_CL2FE_REQ_PC_LOADING_COMPLETE: server.LoadingComplete,
protocol.P_CL2FE_REQ_PC_MOVE: server.playerMove,
protocol.P_CL2FE_REQ_PC_STOP: server.playerStop,
protocol.P_CL2FE_REQ_PC_JUMP: server.playerJump,
protocol.P_CL2FE_REQ_SEND_FREECHAT_MESSAGE: server.freeChat,
protocol.P_CL2FE_REQ_SEND_MENUCHAT_MESSAGE: server.menuChat,
protocol.P_CL2FE_REQ_PC_AVATAR_EMOTES_CHAT: server.emoteChat,
}
srvc.AddPacketHandler(protocol.P_CL2FE_REQ_PC_ENTER, server.RequestEnter)
srvc.AddPacketHandler(protocol.P_CL2FE_REQ_PC_LOADING_COMPLETE, server.LoadingComplete)
srvc.AddPacketHandler(protocol.P_CL2FE_REQ_PC_MOVE, server.playerMove)
srvc.AddPacketHandler(protocol.P_CL2FE_REQ_PC_STOP, server.playerStop)
srvc.AddPacketHandler(protocol.P_CL2FE_REQ_PC_JUMP, server.playerJump)
srvc.AddPacketHandler(protocol.P_CL2FE_REQ_SEND_FREECHAT_MESSAGE, server.freeChat)
srvc.AddPacketHandler(protocol.P_CL2FE_REQ_SEND_MENUCHAT_MESSAGE, server.menuChat)
srvc.AddPacketHandler(protocol.P_CL2FE_REQ_PC_AVATAR_EMOTES_CHAT, server.emoteChat)
srvc.OnConnect = server.onConnect
srvc.OnDisconnect = server.onDisconnect
redisHndlr.RegisterShard(redis.ShardMetadata{
IP: config.GetAnnounceIP(),
@@ -64,97 +50,23 @@ func NewShardServer(dbHndlr *db.DBHandler, redisHndlr *redis.RedisHandler, port
return server, nil
}
func (server *ShardServer) handleEvents() {
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() {
func (server *ShardServer) Start() error {
server.LoadNPCs()
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()
}
return server.service.Start()
}
func (server *ShardServer) GetPort() int {
return server.port
}
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)
func (server *ShardServer) onDisconnect(peer *cnet.Peer) {
// remove from chunks
plr, ok := peer.UserData().(*entity.Player)
if ok && plr != nil {
server.removeEntity(plr)
}
log.Printf("Peer %p disconnected from SHARD\n", peer)
delete(server.peers, peer)
}
func (server *ShardServer) connect(peer *protocol.CNPeer) {
server.peerLock.Lock()
defer server.peerLock.Unlock()
func (server *ShardServer) onConnect(peer *cnet.Peer) {
log.Printf("New peer %p connected to SHARD\n", peer)
server.peers[peer] = nil
}
func (server *ShardServer) getPlayer(peer *protocol.CNPeer) (*entity.Player, error) {
plr, ok := server.peers[peer]
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
}
}
func (server *ShardServer) Service() *cnet.Service {
return server.service
}