Compare commits

..

38 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
37 changed files with 578 additions and 99 deletions

View File

@@ -5,9 +5,11 @@ on:
paths:
- cmd/**
- config/**
- cnet/**
- internal/**
- login/**
- shard/**
- util/**
- go.mod
- go.sum
- .github/workflows/tests.yaml

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"
)

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"
)

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

@@ -61,7 +61,7 @@ func TestPacketEncode(t *testing.T) {
err := pkt.Encode(testStruct)
is.NoErr(err)
is.True(bytes.Equal(buf.Bytes(), testData[:])) // encoded data should match expected data
is.Equal(buf.Bytes(), testData[:]) // encoded data should match expected data
}
func TestPacketDecode(t *testing.T) {
@@ -73,7 +73,7 @@ func TestPacketDecode(t *testing.T) {
var test TestPacketData
err := pkt.Decode(&test)
is.NoErr(err)
is.True(test == testStruct) // decoded data should match testStruct
is.Equal(test, testStruct) // decoded data should match testStruct
}
func TestDataEncrypt(t *testing.T) {
@@ -82,8 +82,7 @@ func TestDataEncrypt(t *testing.T) {
copy(buf, testData[:])
protocol.EncryptData(buf, []byte(protocol.DEFAULT_KEY))
is.True(bytes.Equal(buf, encTestData)) // encrypted data should match expected data
is.Equal(buf, encTestData) // encrypted data should match expected data
}
func TestDataDecrypt(t *testing.T) {
@@ -92,13 +91,12 @@ func TestDataDecrypt(t *testing.T) {
copy(buf, encTestData)
protocol.DecryptData(buf, []byte(protocol.DEFAULT_KEY))
is.True(bytes.Equal(buf, testData[:])) // decrypted data should match expected data
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.True(bytes.Equal(key, []byte{0x0, 0x31, 0xb8, 0xcd, 0xd, 0xc3, 0xad, 0x67})) // key should match expected data
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())
}

View File

@@ -11,7 +11,7 @@ import (
"sync"
"github.com/CPunch/gopenfusion/cnet/protocol"
"github.com/CPunch/gopenfusion/config"
"github.com/CPunch/gopenfusion/internal/config"
)
type PacketHandler func(peer *Peer, pkt protocol.Packet) error
@@ -43,7 +43,7 @@ type Service struct {
}
func RandomPort() (int, error) {
l, err := net.Listen("tcp", "127.0.0.1:0")
l, err := net.Listen("tcp", ":0")
if err != nil {
return 0, err
}

View File

@@ -12,7 +12,7 @@ import (
"github.com/CPunch/gopenfusion/cnet"
"github.com/CPunch/gopenfusion/cnet/protocol"
"github.com/CPunch/gopenfusion/util"
"github.com/CPunch/gopenfusion/internal/testutil"
"github.com/matryer/is"
)
@@ -32,6 +32,7 @@ func TestMain(m *testing.M) {
panic(err)
}
// this is fine since we don't defer anything
os.Exit(m.Run())
}
@@ -44,7 +45,7 @@ func TestService(t *testing.T) {
// shutdown service when test is done
defer func() {
cancel()
is.True(util.SelectWithTimeout(srvc.Stopped(), timeout)) // wait for service to stop with timeout
is.True(testutil.SelectWithTimeout(srvc.Stopped(), timeout)) // wait for service to stop with timeout
}()
// our dummy packet handler
@@ -67,8 +68,8 @@ func TestService(t *testing.T) {
}
// run service
go func() { is.NoErr(srvc.Start()) }() // srvc.Start error
is.True(util.SelectWithTimeout(srvc.Started(), timeout)) // wait for service to start with timeout
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++ {
@@ -93,5 +94,5 @@ func TestService(t *testing.T) {
}()
}
is.True(util.WaitWithTimeout(&wg, timeout)) // wait for all dummy peers to be done with timeout
is.True(testutil.WaitWithTimeout(&wg, timeout)) // wait for all dummy peers to be done with timeout
}

3
go.mod
View File

@@ -3,6 +3,7 @@ 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
@@ -16,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
@@ -34,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

12
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=
@@ -79,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=
@@ -95,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

@@ -2,7 +2,6 @@ package db
import (
"errors"
"log"
"golang.org/x/crypto/bcrypt"
@@ -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

@@ -18,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 {
@@ -35,7 +40,7 @@ func TestMain(m *testing.M) {
panic(err)
}
os.Exit(m.Run())
ret = m.Run()
}
func TestDBAccount(t *testing.T) {

View File

@@ -4,7 +4,7 @@ import (
"database/sql"
"github.com/CPunch/gopenfusion/cnet/protocol"
"github.com/CPunch/gopenfusion/config"
"github.com/CPunch/gopenfusion/internal/config"
"github.com/blockloop/scan"
)

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

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

@@ -1,14 +1,10 @@
package util
package testutil
import (
"sync"
"time"
)
func GetTime() uint64 {
return uint64(time.Now().UnixMilli())
}
func SelectWithTimeout(ch <-chan struct{}, timeout time.Duration) bool {
select {
case <-ch:

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

@@ -9,10 +9,9 @@ import (
"github.com/CPunch/gopenfusion/cnet"
"github.com/CPunch/gopenfusion/cnet/protocol"
"github.com/CPunch/gopenfusion/config"
"github.com/CPunch/gopenfusion/internal/config"
"github.com/CPunch/gopenfusion/internal/db"
"github.com/CPunch/gopenfusion/internal/redis"
"github.com/CPunch/gopenfusion/util"
)
const (
@@ -34,7 +33,7 @@ func (server *LoginServer) AcceptLogin(peer *cnet.Peer, SzID string, IClientVerC
ISlotNum: ISlotNum,
IPaymentFlag: 1,
IOpenBetaFlag: 0,
UiSvrTime: util.GetTime(),
UiSvrTime: protocol.GetTime(),
}
if err := peer.Send(protocol.P_LS2CL_REP_LOGIN_SUCC, resp); err != nil {
@@ -280,11 +279,6 @@ func (server *LoginServer) ShardSelect(peer *cnet.Peer, pkt protocol.Packet) err
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))]
@@ -302,6 +296,11 @@ func (server *LoginServer) ShardSelect(peer *cnet.Peer, pkt protocol.Packet) err
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,

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

@@ -42,6 +42,10 @@ func NewLoginServer(ctx context.Context, dbHndlr *db.DBHandler, redisHndlr *redi
return server, nil
}
func (server *LoginServer) Service() *cnet.Service {
return server.service
}
func (server *LoginServer) Start() error {
return server.service.Start()
}

View File

@@ -4,7 +4,7 @@ import (
"log"
"sync"
"github.com/CPunch/gopenfusion/config"
"github.com/CPunch/gopenfusion/internal/config"
)
type ChunkPosition struct {

View File

@@ -8,7 +8,6 @@ import (
"github.com/CPunch/gopenfusion/cnet/protocol"
"github.com/CPunch/gopenfusion/internal/redis"
"github.com/CPunch/gopenfusion/shard/entity"
"github.com/CPunch/gopenfusion/util"
)
func (server *ShardServer) attachPlayer(peer *cnet.Peer, meta redis.LoginMetadata) (*entity.Player, error) {
@@ -51,7 +50,7 @@ func (server *ShardServer) RequestEnter(peer *cnet.Peer, pkt protocol.Packet) er
resp := &protocol.SP_FE2CL_REP_PC_ENTER_SUCC{
IID: int32(plr.PlayerID),
PCLoadData2CL: plr.ToPCLoadData2CL(),
UiSvrTime: util.GetTime(),
UiSvrTime: protocol.GetTime(),
}
// setup peer
@@ -85,7 +84,9 @@ func (server *ShardServer) LoadingComplete(peer *cnet.Peer, pkt protocol.Packet)
// 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

@@ -6,7 +6,6 @@ import (
"github.com/CPunch/gopenfusion/cnet"
"github.com/CPunch/gopenfusion/cnet/protocol"
"github.com/CPunch/gopenfusion/shard/entity"
"github.com/CPunch/gopenfusion/util"
)
func (server *ShardServer) updatePlayerPosition(plr *entity.Player, X, Y, Z, Angle int) {
@@ -41,7 +40,7 @@ func (server *ShardServer) playerMove(peer *cnet.Peer, pkt protocol.Packet) erro
CKeyValue: move.CKeyValue,
ISpeed: move.ISpeed,
IID: int32(plr.PlayerID),
ISvrTime: util.GetTime(),
ISvrTime: protocol.GetTime(),
})
}
@@ -63,7 +62,7 @@ func (server *ShardServer) playerStop(peer *cnet.Peer, pkt protocol.Packet) erro
IY: stop.IY,
IZ: stop.IZ,
IID: int32(plr.PlayerID),
ISvrTime: util.GetTime(),
ISvrTime: protocol.GetTime(),
})
}
@@ -91,6 +90,6 @@ func (server *ShardServer) playerJump(peer *cnet.Peer, pkt protocol.Packet) erro
CKeyValue: jump.CKeyValue,
ISpeed: jump.ISpeed,
IID: int32(plr.PlayerID),
ISvrTime: util.GetTime(),
ISvrTime: protocol.GetTime(),
})
}

View File

@@ -5,7 +5,7 @@ import (
"log"
"os"
"github.com/CPunch/gopenfusion/config"
"github.com/CPunch/gopenfusion/internal/config"
"github.com/CPunch/gopenfusion/shard/entity"
)
@@ -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

@@ -5,7 +5,7 @@ import (
"github.com/CPunch/gopenfusion/cnet"
"github.com/CPunch/gopenfusion/cnet/protocol"
"github.com/CPunch/gopenfusion/config"
"github.com/CPunch/gopenfusion/internal/config"
"github.com/CPunch/gopenfusion/internal/db"
"github.com/CPunch/gopenfusion/internal/redis"
"github.com/CPunch/gopenfusion/shard/entity"
@@ -50,9 +50,9 @@ func NewShardServer(ctx context.Context, dbHndlr *db.DBHandler, redisHndlr *redi
return server, nil
}
func (server *ShardServer) Start() {
func (server *ShardServer) Start() error {
server.LoadNPCs()
server.service.Start()
return server.service.Start()
}
func (server *ShardServer) onDisconnect(peer *cnet.Peer) {
@@ -66,3 +66,7 @@ func (server *ShardServer) onDisconnect(peer *cnet.Peer) {
func (server *ShardServer) onConnect(peer *cnet.Peer) {
}
func (server *ShardServer) Service() *cnet.Service {
return server.service
}

View File

@@ -1,35 +0,0 @@
package util_test
import (
"sync"
"testing"
"time"
"github.com/CPunch/gopenfusion/util"
"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(!util.WaitWithTimeout(wg, 500*time.Millisecond)) // timeout should occur
is.True(util.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(!util.SelectWithTimeout(ch, 500*time.Millisecond)) // timeout should occur
is.True(util.SelectWithTimeout(ch, 750*time.Millisecond)) // timeout shouldn't occur
}