mirror of
https://github.com/CPunch/gopenfusion.git
synced 2025-10-28 20:00:09 +00:00
Compare commits
111 Commits
850dd59967
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| cafca9093c | |||
| d84fcd2c93 | |||
| 1f63f9856e | |||
| de3e067b48 | |||
| 02afe67ac3 | |||
| 79f68187bf | |||
| cd93a058ce | |||
| 0a28dbcc3e | |||
| 1a6de671e5 | |||
| 261ea6505f | |||
| 556878544d | |||
| bfcbe6d3d6 | |||
| e5a9ed1481 | |||
| 23170093ee | |||
| 2bd61dc571 | |||
| cba01a877d | |||
| e1b9fa5d99 | |||
| 77751a2aa0 | |||
| 8e84f0c7b2 | |||
| c902559eac | |||
| 1c40998cb6 | |||
| 988368c307 | |||
| 01ebf4499f | |||
| 3a14d807d2 | |||
| 141858d6c3 | |||
| 335fdb417c | |||
| 2a6fb25f03 | |||
| 0ebd162af0 | |||
| d1763418a8 | |||
| c4d885cf6d | |||
| afd5c9ef23 | |||
| ac62f7d64e | |||
| 58afc9df1f | |||
| e257bf998f | |||
| 96eed66831 | |||
| 12f16645e1 | |||
| 76e9bdf7e7 | |||
| 44f3b31965 | |||
| 899b95b4e6 | |||
| e33b7c0556 | |||
| 3445b852fd | |||
| 557117f093 | |||
| b07e9ddbcb | |||
| af867ccff2 | |||
| c60017f78f | |||
| e1804a1042 | |||
| bcc999db38 | |||
| e355af19ab | |||
| 0ed19ad6c5 | |||
| 66fe3c9738 | |||
| 72dbfe2541 | |||
| 8e65a78d07 | |||
| f4b17906ce | |||
| c0ba365cf5 | |||
| d0346b2382 | |||
| 18a6c5ab42 | |||
| 3abba0ca3c | |||
| 1f66acfd25 | |||
| d8277ea89c | |||
| 81de857670 | |||
| e8f5e5fc9c | |||
| 6f55cbbad5 | |||
| b02c141000 | |||
| 5b78c3dbd4 | |||
| d31723e245 | |||
| bb50948935 | |||
| d32facf363 | |||
| 4419260cd0 | |||
| 9df5a3d0d5 | |||
| 7d83732e44 | |||
| 7eece044c5 | |||
| e8f8129b35 | |||
| f95cc2cae7 | |||
| 459b71a109 | |||
| 06f4a4d33f | |||
| 3e04103ae4 | |||
| 3559682d18 | |||
| a78dedcb89 | |||
| dcb86e2518 | |||
| 458e907c99 | |||
| 74b68863b1 | |||
| 83b664da93 | |||
| 670d4a514c | |||
| 7ebe80d6e3 | |||
| f27e72d68d | |||
| 215e313c43 | |||
| 3559d9ba9b | |||
| c4325475ed | |||
| fd41b32b70 | |||
| 80dc876517 | |||
| c09a6cfd25 | |||
| 8f00a0c492 | |||
| f6ab7a9b5d | |||
| f0b9bc6ed6 | |||
| dfc00bcb52 | |||
| 1da82ac750 | |||
| d42a34535b | |||
| d7445e0f0f | |||
| 983588b6c9 | |||
| 124bb77f8d | |||
| e4cf64d7bc | |||
| 727c68aef3 | |||
| 70e42b5d79 | |||
| bb29a988b3 | |||
| 5b2a8b838e | |||
| 735bdc5b36 | |||
| 1357de99aa | |||
| 7a26ffdcf7 | |||
| cbe308a0bd | |||
| c62ed5d101 | |||
| a0a108a605 |
28
.github/workflows/tests.yaml
vendored
Normal file
28
.github/workflows/tests.yaml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
name: unit-tests
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- cmd/**
|
||||
- config/**
|
||||
- cnet/**
|
||||
- internal/**
|
||||
- login/**
|
||||
- shard/**
|
||||
- util/**
|
||||
- go.mod
|
||||
- go.sum
|
||||
- .github/workflows/tests.yaml
|
||||
|
||||
jobs:
|
||||
run:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.21.x'
|
||||
- name: Test with the Go CLI
|
||||
run: go test -timeout 10s -v ./...
|
||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "tdata"]
|
||||
path = tdata
|
||||
url = https://github.com/OpenFusionProject/tabledata.git
|
||||
7
Dockerfile
Normal file
7
Dockerfile
Normal file
@@ -0,0 +1,7 @@
|
||||
FROM scratch
|
||||
|
||||
# grab binary
|
||||
WORKDIR /gopenfusion
|
||||
COPY --chmod=0755 ./bin/server ./
|
||||
|
||||
ENTRYPOINT [ "/gopenfusion/server" ]
|
||||
7
LICENSE.md
Normal file
7
LICENSE.md
Normal file
@@ -0,0 +1,7 @@
|
||||
Copyright © 2022 Gopenfusion Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
26
README.md
26
README.md
@@ -1,11 +1,29 @@
|
||||
# gopenfusion
|
||||
|
||||
A toy implementation of the [Fusionfall Packet Protocol](https://openpunk.com/pages/fusionfall-openfusion/) 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>
|
||||
|
||||
## Login Sever
|
||||
A toy implementation of the [Fusionfall Packet Protocol](https://openpunk.com/pages/fusionfall-openfusion/) (see: `cnet/`) and accompanying services, written in Go.
|
||||
|
||||
An example login server implementation exists in `server/`. This implementation should be compatible with existing OpenFusion databases, however this only exists as an example and doesn't direct clients to a shard server (they're softlocked after the tutorial, or during character selection).
|
||||
## Landwalker demo
|
||||
|
||||
An implementation of a landwalker server is located in `login/` && `shard/`. This includes a functional login server and a dummy shard (supporting the minimum amount of packets necessary). There is minimal support for NPCs, and minimal support for player interaction (chat & player movement being mostly it).
|
||||
|
||||
Startup the environment using
|
||||
|
||||
```sh
|
||||
$ chmod +x ./build.sh && ./build.sh
|
||||
$ docker compose up
|
||||
```
|
||||
|
||||
The environment consists of a shard service, login service, redis && postgres containers. redis is used to pass login metadata between the login and shard services, while postgres is just used to store player accounts and characters.
|
||||
|
||||
login server is hosted at `127.0.0.1:23000`, just join from your [favorite client](https://github.com/OpenFusionProject/OpenFusion/releases/latest)
|
||||
|
||||
## Generating structures
|
||||
|
||||
Dump and decompile the `Assembly - CSharp.dll` assembly from the fusionfall main.unity3d, using a tool like [ilspycmd](https://www.nuget.org/packages/ilspycmd/). The full output source can then be passed to `genstructs.py` script located in `tools/`, which will handle scraping constants and calculating structure padding. See the script for details on usage.
|
||||
Dump and decompile the `Assembly - CSharp.dll` assembly from the fusionfall main.unity3d, using a tool like [ilspycmd](https://www.nuget.org/packages/ilspycmd/). The full output source can then be passed to `genstructs.py` script located in `tools/`, which will handle scraping constants and calculating structure padding. See the script for details on usage.
|
||||
|
||||
2
build.sh
2
build.sh
@@ -1,5 +1,5 @@
|
||||
#!/bin/sh
|
||||
|
||||
mkdir -p bin
|
||||
go build -o ./bin/server
|
||||
CGO_ENABLED=0 GOOS=linux go build -o ./bin/server ./cmd
|
||||
echo 'Done'
|
||||
42
cmd/login.go
Normal file
42
cmd/login.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"log"
|
||||
|
||||
"github.com/CPunch/gopenfusion/internal/config"
|
||||
"github.com/CPunch/gopenfusion/login"
|
||||
"github.com/google/subcommands"
|
||||
)
|
||||
|
||||
type loginCommand struct {
|
||||
port int
|
||||
}
|
||||
|
||||
func (s *loginCommand) Name() string {
|
||||
return "login"
|
||||
}
|
||||
|
||||
func (s *loginCommand) Synopsis() string {
|
||||
return "Starts login service"
|
||||
}
|
||||
|
||||
func (s *loginCommand) Usage() string {
|
||||
return s.Name() + " - " + s.Synopsis() + ":\n"
|
||||
}
|
||||
|
||||
func (s *loginCommand) SetFlags(f *flag.FlagSet) {
|
||||
f.IntVar(&s.port, "port", config.LOGIN_PORT, "Hosts the service on this port")
|
||||
}
|
||||
|
||||
func (s *loginCommand) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
|
||||
loginServer, err := login.NewLoginServer(ctx, dbHndlr, redisHndlr, s.port)
|
||||
if err != nil {
|
||||
log.Panicf("failed to create shard server: %v", err)
|
||||
}
|
||||
|
||||
loginServer.Start()
|
||||
|
||||
return subcommands.ExitSuccess
|
||||
}
|
||||
43
cmd/main.go
Normal file
43
cmd/main.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/CPunch/gopenfusion/internal/config"
|
||||
"github.com/CPunch/gopenfusion/internal/db"
|
||||
"github.com/CPunch/gopenfusion/internal/redis"
|
||||
|
||||
"github.com/google/subcommands"
|
||||
)
|
||||
|
||||
var dbHndlr *db.DBHandler
|
||||
var redisHndlr *redis.RedisHandler
|
||||
|
||||
func main() {
|
||||
subcommands.Register(subcommands.HelpCommand(), "")
|
||||
subcommands.Register(subcommands.FlagsCommand(), "")
|
||||
subcommands.Register(subcommands.CommandsCommand(), "")
|
||||
subcommands.Register(&shardCommand{}, "")
|
||||
subcommands.Register(&loginCommand{}, "")
|
||||
|
||||
var err error
|
||||
dbHndlr, err = db.OpenPostgresDB(config.GetDBAddr())
|
||||
if err != nil {
|
||||
log.Panicf("failed to open db: %v", err)
|
||||
}
|
||||
|
||||
if err = dbHndlr.Setup(); err != nil {
|
||||
log.Panicf("failed to setup db: %v", err)
|
||||
}
|
||||
|
||||
redisHndlr, err = redis.OpenRedis(config.GetRedisAddr())
|
||||
if err != nil {
|
||||
log.Panicf("failed to open redis: %v", err)
|
||||
}
|
||||
|
||||
flag.Parse()
|
||||
os.Exit(int(subcommands.Execute(context.Background())))
|
||||
}
|
||||
42
cmd/shard.go
Normal file
42
cmd/shard.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"log"
|
||||
|
||||
"github.com/CPunch/gopenfusion/internal/config"
|
||||
"github.com/CPunch/gopenfusion/shard"
|
||||
"github.com/google/subcommands"
|
||||
)
|
||||
|
||||
type shardCommand struct {
|
||||
port int
|
||||
}
|
||||
|
||||
func (s *shardCommand) Name() string {
|
||||
return "shard"
|
||||
}
|
||||
|
||||
func (s *shardCommand) Synopsis() string {
|
||||
return "Starts shard service"
|
||||
}
|
||||
|
||||
func (s *shardCommand) Usage() string {
|
||||
return s.Name() + " - " + s.Synopsis() + ":\n"
|
||||
}
|
||||
|
||||
func (s *shardCommand) SetFlags(f *flag.FlagSet) {
|
||||
f.IntVar(&s.port, "port", config.SHARD_PORT, "Hosts the service on this port")
|
||||
}
|
||||
|
||||
func (s *shardCommand) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
|
||||
shardServer, err := shard.NewShardServer(ctx, dbHndlr, redisHndlr, s.port)
|
||||
if err != nil {
|
||||
log.Panicf("failed to create shard server: %v", err)
|
||||
}
|
||||
|
||||
shardServer.Start()
|
||||
|
||||
return subcommands.ExitSuccess
|
||||
}
|
||||
165
cnet/peer.go
Normal file
165
cnet/peer.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package cnet
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/CPunch/gopenfusion/cnet/protocol"
|
||||
)
|
||||
|
||||
const (
|
||||
USE_E = iota
|
||||
USE_FE
|
||||
)
|
||||
|
||||
type PacketEvent struct {
|
||||
Type int
|
||||
Pkt *bytes.Buffer
|
||||
PktID uint32
|
||||
}
|
||||
|
||||
// Peer is a simple wrapper for net.Conn connections to send/recv packets over the Fusionfall packet protocol.
|
||||
type Peer struct {
|
||||
uData interface{}
|
||||
conn net.Conn
|
||||
ctx context.Context
|
||||
whichKey int
|
||||
alive *atomic.Bool
|
||||
|
||||
// May not be set while Send() or Handler() are concurrently running.
|
||||
E_key []byte
|
||||
|
||||
// May not be set while Send() or Handler() are concurrently running.
|
||||
FE_key []byte
|
||||
}
|
||||
|
||||
func NewPeer(ctx context.Context, conn net.Conn) *Peer {
|
||||
p := &Peer{
|
||||
conn: conn,
|
||||
ctx: ctx,
|
||||
whichKey: USE_E,
|
||||
alive: &atomic.Bool{},
|
||||
|
||||
E_key: []byte(protocol.DEFAULT_KEY),
|
||||
FE_key: nil,
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func (peer *Peer) SetUserData(uData interface{}) {
|
||||
peer.uData = uData
|
||||
}
|
||||
|
||||
func (peer *Peer) UserData() interface{} {
|
||||
return peer.uData
|
||||
}
|
||||
|
||||
func (peer *Peer) Send(typeID uint32, data ...interface{}) error {
|
||||
// grab buffer from pool
|
||||
buf := protocol.GetBuffer()
|
||||
defer protocol.PutBuffer(buf)
|
||||
|
||||
// allocate space for packet size
|
||||
buf.Write(make([]byte, 4))
|
||||
|
||||
// body start
|
||||
pkt := protocol.NewPacket(buf)
|
||||
|
||||
// encode type id
|
||||
if err := pkt.Encode(typeID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// encode data
|
||||
for _, trailer := range data {
|
||||
if err := pkt.Encode(trailer); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// prepend the packet size
|
||||
binary.LittleEndian.PutUint32(buf.Bytes()[:4], uint32(buf.Len()-4))
|
||||
|
||||
// encrypt body
|
||||
var key []byte
|
||||
switch peer.whichKey {
|
||||
case USE_E:
|
||||
key = peer.E_key
|
||||
case USE_FE:
|
||||
key = peer.FE_key
|
||||
}
|
||||
protocol.EncryptData(buf.Bytes()[4:], key)
|
||||
|
||||
// send full packet
|
||||
// log.Printf("Sending %#v, sizeof: %d, buffer: %v", data, buf.Len(), buf.Bytes())
|
||||
if _, err := peer.conn.Write(buf.Bytes()); err != nil {
|
||||
return fmt.Errorf("failed to write packet body! %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (peer *Peer) SetActiveKey(whichKey int) {
|
||||
peer.whichKey = whichKey
|
||||
}
|
||||
|
||||
func (peer *Peer) Kill() {
|
||||
// de-bounce: only kill if alive
|
||||
if !peer.alive.CompareAndSwap(true, false) {
|
||||
return
|
||||
}
|
||||
|
||||
peer.conn.Close()
|
||||
}
|
||||
|
||||
// meant to be invoked as a goroutine
|
||||
func (peer *Peer) Handler(eRecv chan<- *PacketEvent) error {
|
||||
defer func() {
|
||||
close(eRecv)
|
||||
peer.Kill()
|
||||
}()
|
||||
|
||||
peer.alive.Store(true)
|
||||
for {
|
||||
select {
|
||||
case <-peer.ctx.Done():
|
||||
return nil
|
||||
default:
|
||||
// read packet size, the goroutine spends most of it's time parked here
|
||||
var sz uint32
|
||||
if err := binary.Read(peer.conn, binary.LittleEndian, &sz); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// client should never send a packet size outside of this range
|
||||
if sz > protocol.CN_PACKET_BUFFER_SIZE || sz < 4 {
|
||||
return fmt.Errorf("invalid packet size: %d", sz)
|
||||
}
|
||||
|
||||
// grab buffer && read packet body
|
||||
buf := protocol.GetBuffer()
|
||||
if _, err := buf.ReadFrom(io.LimitReader(peer.conn, int64(sz))); err != nil {
|
||||
return fmt.Errorf("failed to read packet body: %v", err)
|
||||
}
|
||||
|
||||
// decrypt
|
||||
protocol.DecryptData(buf.Bytes(), peer.E_key)
|
||||
pkt := protocol.NewPacket(buf)
|
||||
|
||||
// create packet && read pktID
|
||||
var pktID uint32
|
||||
if err := pkt.Decode(&pktID); err != nil {
|
||||
return fmt.Errorf("failed to read packet type! %v", err)
|
||||
}
|
||||
|
||||
// dispatch packet
|
||||
// log.Printf("Got packet ID: %x, with a sizeof: %d\n", pktID, sz)
|
||||
eRecv <- &PacketEvent{Pkt: buf, PktID: pktID}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
@@ -48,13 +49,24 @@ func DecryptData(buff, key []byte) {
|
||||
xorData(buff, key, size)
|
||||
}
|
||||
|
||||
func CreateNewKey(uTime uint64, iv1, iv2 uint64) []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
|
||||
}
|
||||
|
||||
func GenSerialKey() (int64, error) {
|
||||
tmp := [8]byte{}
|
||||
if _, err := rand.Read(tmp[:]); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// convert to uint64 && return
|
||||
return int64(binary.LittleEndian.Uint64(tmp[:])), nil
|
||||
}
|
||||
@@ -31,7 +31,7 @@ func (pkt Packet) encodeStructField(field reflect.StructField, value reflect.Val
|
||||
case reflect.String: // all strings in fusionfall packets are encoded as utf16, we'll need to encode it
|
||||
sz, err := strconv.Atoi(field.Tag.Get("size"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to grab string 'size' tag!!")
|
||||
return fmt.Errorf("failed to grab string 'size' tag")
|
||||
}
|
||||
|
||||
buf16 := utf16.Encode([]rune(value.String()))
|
||||
@@ -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
|
||||
@@ -61,10 +58,9 @@ func (pkt Packet) encodeStructField(field reflect.StructField, value reflect.Val
|
||||
// write padding bytes
|
||||
pad, err := strconv.Atoi(field.Tag.Get("pad"))
|
||||
if err == nil {
|
||||
for i := 0; i < pad; i++ {
|
||||
if _, err := pkt.readWriter.Write([]byte{0}); err != nil {
|
||||
return err
|
||||
}
|
||||
dummy := make([]byte, pad)
|
||||
if _, err := pkt.readWriter.Write(dummy); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +96,7 @@ func (pkt Packet) decodeStructField(field reflect.StructField, value reflect.Val
|
||||
case reflect.String: // all strings in fusionfall packets are encoded as utf16, we'll need to decode it
|
||||
sz, err := strconv.Atoi(field.Tag.Get("size"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to grab string 'size' tag!!")
|
||||
return fmt.Errorf("failed to grab string 'size' tag")
|
||||
}
|
||||
|
||||
buf16 := make([]uint16, sz)
|
||||
@@ -126,10 +122,8 @@ func (pkt Packet) decodeStructField(field reflect.StructField, value reflect.Val
|
||||
// consume padding bytes
|
||||
pad, err := strconv.Atoi(field.Tag.Get("pad"))
|
||||
if err == nil {
|
||||
for i := 0; i < pad; i++ {
|
||||
if _, err := pkt.readWriter.Read([]byte{0}); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := pkt.readWriter.Read(make([]byte, pad)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
21
cnet/protocol/pool.go
Normal file
21
cnet/protocol/pool.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var allocator = &sync.Pool{
|
||||
New: func() any { return new(bytes.Buffer) },
|
||||
}
|
||||
|
||||
// grabs a *bytes.Buffer from the pool
|
||||
func GetBuffer() *bytes.Buffer {
|
||||
return allocator.Get().(*bytes.Buffer)
|
||||
}
|
||||
|
||||
// returns a *bytes.Buffer to the pool
|
||||
func PutBuffer(buf *bytes.Buffer) {
|
||||
buf.Reset()
|
||||
allocator.Put(buf)
|
||||
}
|
||||
102
cnet/protocol/protocol_test.go
Normal file
102
cnet/protocol/protocol_test.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package protocol_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/CPunch/gopenfusion/cnet/protocol"
|
||||
"github.com/matryer/is"
|
||||
)
|
||||
|
||||
type TestPacketData struct {
|
||||
A int32
|
||||
B int32
|
||||
UTF16Str string `size:"32"`
|
||||
Pad int16 `pad:"2"`
|
||||
C int32
|
||||
}
|
||||
|
||||
var (
|
||||
testStruct = TestPacketData{
|
||||
A: 1,
|
||||
B: 2,
|
||||
UTF16Str: "hello world",
|
||||
C: 3,
|
||||
}
|
||||
|
||||
// this is the data we expect to get from encoding the above struct
|
||||
testData = [...]byte{
|
||||
0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00,
|
||||
0x68, 0x00, 0x65, 0x00, 0x6c, 0x00, 0x6c, 0x00,
|
||||
0x6f, 0x00, 0x20, 0x00, 0x77, 0x00, 0x6f, 0x00,
|
||||
0x72, 0x00, 0x6c, 0x00, 0x64, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00,
|
||||
}
|
||||
|
||||
// this is the data we expect to get from EncryptData(testData, []byte(protocol.DEFAULT_KEY))
|
||||
encTestData = []byte{
|
||||
0x23, 0x40, 0x72, 0x51, 0x6c, 0x7e, 0x57, 0x6c,
|
||||
0x05, 0x3b, 0x17, 0x51, 0x02, 0x7e, 0x40, 0x23,
|
||||
0x02, 0x40, 0x7e, 0x51, 0x19, 0x52, 0x38, 0x23,
|
||||
0x1f, 0x40, 0x1e, 0x0a, 0x51, 0x7e, 0x57, 0x23,
|
||||
0x6d, 0x40, 0x72, 0x6e, 0x51, 0x7e, 0x57, 0x23,
|
||||
0x23, 0x40, 0x72, 0x51, 0x6e, 0x7e, 0x57, 0x6d,
|
||||
0x6d, 0x57, 0x72, 0x51, 0x6e, 0x7e, 0x40, 0x23,
|
||||
0x6d, 0x40, 0x7e, 0x51, 0x6e, 0x72, 0x57, 0x23,
|
||||
0x6d, 0x40, 0x72, 0x6e, 0x51, 0x7e, 0x57, 0x23,
|
||||
0x6d, 0x40, 0x72, 0x6d, 0x51, 0x7e, 0x57, 0x23,
|
||||
}
|
||||
)
|
||||
|
||||
func TestPacketEncode(t *testing.T) {
|
||||
is := is.New(t)
|
||||
buf := bytes.NewBuffer(nil)
|
||||
pkt := protocol.NewPacket(buf)
|
||||
|
||||
err := pkt.Encode(testStruct)
|
||||
is.NoErr(err)
|
||||
|
||||
is.Equal(buf.Bytes(), testData[:]) // encoded data should match expected data
|
||||
}
|
||||
|
||||
func TestPacketDecode(t *testing.T) {
|
||||
is := is.New(t)
|
||||
buf := bytes.NewBuffer(nil)
|
||||
pkt := protocol.NewPacket(buf)
|
||||
buf.Write(testData[:])
|
||||
|
||||
var test TestPacketData
|
||||
err := pkt.Decode(&test)
|
||||
is.NoErr(err)
|
||||
is.Equal(test, testStruct) // decoded data should match testStruct
|
||||
}
|
||||
|
||||
func TestDataEncrypt(t *testing.T) {
|
||||
is := is.New(t)
|
||||
buf := make([]byte, len(testData))
|
||||
copy(buf, testData[:])
|
||||
|
||||
protocol.EncryptData(buf, []byte(protocol.DEFAULT_KEY))
|
||||
is.Equal(buf, encTestData) // encrypted data should match expected data
|
||||
}
|
||||
|
||||
func TestDataDecrypt(t *testing.T) {
|
||||
is := is.New(t)
|
||||
buf := make([]byte, len(encTestData))
|
||||
copy(buf, encTestData)
|
||||
|
||||
protocol.DecryptData(buf, []byte(protocol.DEFAULT_KEY))
|
||||
is.Equal(buf, testData[:]) // decrypted data should match expected data
|
||||
}
|
||||
|
||||
func TestCreateNewKey(t *testing.T) {
|
||||
is := is.New(t)
|
||||
key := protocol.CreateNewKey(123456789, 0x1234567890abcdef, 0x1234567890abcdef)
|
||||
|
||||
is.Equal(key, []byte{0x0, 0x31, 0xb8, 0xcd, 0xd, 0xc3, 0xad, 0x67}) // key should match expected data
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
9
cnet/protocol/time.go
Normal file
9
cnet/protocol/time.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func GetTime() uint64 {
|
||||
return uint64(time.Now().UnixMilli())
|
||||
}
|
||||
282
cnet/service.go
Normal file
282
cnet/service.go
Normal file
@@ -0,0 +1,282 @@
|
||||
package cnet
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/CPunch/gopenfusion/cnet/protocol"
|
||||
"github.com/CPunch/gopenfusion/internal/config"
|
||||
)
|
||||
|
||||
type PacketHandler func(peer *Peer, pkt protocol.Packet) error
|
||||
|
||||
func StubbedPacket(_ *Peer, _ protocol.Packet) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
listener net.Listener
|
||||
port int
|
||||
Name string
|
||||
ctx context.Context
|
||||
started chan struct{}
|
||||
stopped chan struct{}
|
||||
packetHandlers map[uint32]PacketHandler
|
||||
peers map[chan *PacketEvent]*Peer
|
||||
stateLock sync.Mutex
|
||||
|
||||
// OnDisconnect is called when a peer disconnects from the service.
|
||||
// uData is the stored value of the key/value pair in the peer map.
|
||||
// It may not be set while the service is running. (eg. srvc.Start() has been called)
|
||||
OnDisconnect func(peer *Peer)
|
||||
|
||||
// OnConnect is called when a peer connects to the service.
|
||||
// return value is used as the value in the peer map.
|
||||
// It may not be set while the service is running. (eg. srvc.Start() has been called)
|
||||
OnConnect func(peer *Peer)
|
||||
}
|
||||
|
||||
func RandomPort() (int, error) {
|
||||
l, err := net.Listen("tcp", ":0")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer l.Close()
|
||||
|
||||
_, port, err := net.SplitHostPort(l.Addr().String())
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return strconv.Atoi(port)
|
||||
}
|
||||
|
||||
func NewService(ctx context.Context, name string, port int) *Service {
|
||||
srvc := &Service{
|
||||
port: port,
|
||||
Name: name,
|
||||
}
|
||||
|
||||
srvc.Reset(ctx)
|
||||
return srvc
|
||||
}
|
||||
|
||||
func (srvc *Service) Reset(ctx context.Context) {
|
||||
srvc.ctx = ctx
|
||||
srvc.packetHandlers = make(map[uint32]PacketHandler)
|
||||
srvc.peers = make(map[chan *PacketEvent]*Peer)
|
||||
srvc.started = make(chan struct{})
|
||||
srvc.stopped = make(chan struct{})
|
||||
}
|
||||
|
||||
// may not be called while the service is running (eg. srvc.Start() has been called)
|
||||
func (srvc *Service) AddPacketHandler(pktID uint32, handler PacketHandler) {
|
||||
srvc.packetHandlers[pktID] = handler
|
||||
}
|
||||
|
||||
type newPeerConnection struct {
|
||||
peer *Peer
|
||||
channel chan *PacketEvent
|
||||
}
|
||||
|
||||
func (srvc *Service) Start() error {
|
||||
peerConnections := make(chan newPeerConnection)
|
||||
defer close(peerConnections)
|
||||
go srvc.handleEvents(peerConnections)
|
||||
|
||||
// open listener socket
|
||||
var err error
|
||||
srvc.listener, err = net.Listen("tcp", fmt.Sprintf(":%d", srvc.port))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer srvc.listener.Close()
|
||||
|
||||
log.Printf("%s service hosted on %s:%d\n", srvc.Name, config.GetAnnounceIP(), srvc.port)
|
||||
|
||||
close(srvc.started) // signal that the service has started
|
||||
for {
|
||||
conn, err := srvc.listener.Accept()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
// we expect this to happen when the service is stopped
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// create a new peer and pass it to the event loop
|
||||
peer := NewPeer(srvc.ctx, conn)
|
||||
eRecv := make(chan *PacketEvent)
|
||||
peerConnections <- newPeerConnection{channel: eRecv, peer: peer}
|
||||
go peer.Handler(eRecv)
|
||||
}
|
||||
}
|
||||
|
||||
func (srvc *Service) getPeer(channel chan *PacketEvent) *Peer {
|
||||
return srvc.peers[channel]
|
||||
}
|
||||
|
||||
func (srvc *Service) setPeer(channel chan *PacketEvent, peer *Peer) {
|
||||
srvc.peers[channel] = peer
|
||||
}
|
||||
|
||||
func (srvc *Service) removePeer(channel chan *PacketEvent) {
|
||||
delete(srvc.peers, channel)
|
||||
}
|
||||
|
||||
// returns a channel that is closed when the service has started.
|
||||
// this is useful if you need to wait until after the service has started.
|
||||
func (srvc *Service) Started() <-chan struct{} {
|
||||
return srvc.started
|
||||
}
|
||||
|
||||
// returns a channel that is closed when the service has stopped.
|
||||
// this is useful if you need wait until after the service has stopped.
|
||||
func (srvc *Service) Stopped() <-chan struct{} {
|
||||
return srvc.stopped
|
||||
}
|
||||
|
||||
// calls f for each peer in the service passing the peer and the stored uData.
|
||||
// if f returns false, the iteration is stopped.
|
||||
// NOTE: the peer map is not locked while iterating, if you're calling this
|
||||
// outside of the service's event loop, you'll need to lock the peer map yourself.
|
||||
func (srvc *Service) RangePeers(f func(peer *Peer) bool) {
|
||||
for _, peer := range srvc.peers {
|
||||
if !f(peer) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// locks the peer map.
|
||||
func (srvc *Service) Lock() {
|
||||
srvc.stateLock.Lock()
|
||||
}
|
||||
|
||||
// unlocks the peer map.
|
||||
func (srvc *Service) Unlock() {
|
||||
srvc.stateLock.Unlock()
|
||||
}
|
||||
|
||||
func (srvc *Service) stop() {
|
||||
// OnDisconnect handler might need to do something important
|
||||
srvc.RangePeers(func(peer *Peer) bool {
|
||||
peer.Kill()
|
||||
if srvc.OnDisconnect != nil {
|
||||
srvc.OnDisconnect(peer)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
log.Printf("%s service stopped\n", srvc.Name)
|
||||
close(srvc.stopped)
|
||||
}
|
||||
|
||||
// handleEvents is the main event loop for the service.
|
||||
// it handles all events from the peers and calls the appropriate handlers.
|
||||
func (srvc *Service) handleEvents(peerPipe <-chan newPeerConnection) {
|
||||
defer srvc.stop()
|
||||
|
||||
poll := make([]reflect.SelectCase, 0, 4)
|
||||
|
||||
// add the stop channel and the peer connection channel to our poll queue
|
||||
poll = append(poll, reflect.SelectCase{
|
||||
Dir: reflect.SelectRecv,
|
||||
Chan: reflect.ValueOf(srvc.ctx.Done()),
|
||||
})
|
||||
poll = append(poll, reflect.SelectCase{
|
||||
Dir: reflect.SelectRecv,
|
||||
Chan: reflect.ValueOf(peerPipe),
|
||||
})
|
||||
|
||||
addPoll := func(channel chan *PacketEvent) {
|
||||
poll = append(poll, reflect.SelectCase{
|
||||
Dir: reflect.SelectRecv,
|
||||
Chan: reflect.ValueOf(channel),
|
||||
})
|
||||
}
|
||||
|
||||
removePoll := func(index int) {
|
||||
poll = append(poll[:index], poll[index+1:]...)
|
||||
}
|
||||
|
||||
for {
|
||||
chosen, value, recvOK := reflect.Select(poll)
|
||||
switch chosen {
|
||||
case 0: // cancel signal received, stop the service
|
||||
return
|
||||
case 1: // new peer, add it to our poll queue
|
||||
if !recvOK {
|
||||
return
|
||||
}
|
||||
|
||||
evnt := value.Interface().(newPeerConnection)
|
||||
addPoll(evnt.channel)
|
||||
srvc.connect(evnt.channel, evnt.peer)
|
||||
default: // peer event
|
||||
channel := poll[chosen].Chan.Interface().(chan *PacketEvent)
|
||||
peer := srvc.getPeer(channel)
|
||||
if peer == nil {
|
||||
log.Printf("Unknown peer event: %v", value)
|
||||
removePoll(chosen)
|
||||
continue
|
||||
}
|
||||
|
||||
evnt, ok := value.Interface().(*PacketEvent)
|
||||
if !recvOK || !ok || evnt == nil {
|
||||
// peer disconnected, remove it from our poll queue
|
||||
removePoll(chosen)
|
||||
srvc.disconnect(channel, peer)
|
||||
continue
|
||||
}
|
||||
|
||||
srvc.Lock()
|
||||
if err := srvc.handlePacket(peer, evnt.PktID, protocol.NewPacket(evnt.Pkt)); err != nil {
|
||||
log.Printf("Error handling packet: %v", err)
|
||||
peer.Kill()
|
||||
}
|
||||
srvc.Unlock()
|
||||
|
||||
// the packet buffer is given to us by the event, so we'll need to make sure to return it to the pool
|
||||
protocol.PutBuffer(evnt.Pkt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (srvc *Service) handlePacket(peer *Peer, typeID uint32, pkt protocol.Packet) error {
|
||||
if hndlr, ok := srvc.packetHandlers[typeID]; ok {
|
||||
// fmt.Printf("Handling packet %x\n", typeID)
|
||||
if err := hndlr(peer, pkt); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
log.Printf("[WARN] unknown packet ID: %x\n", typeID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (srvc *Service) disconnect(channel chan *PacketEvent, peer *Peer) {
|
||||
log.Printf("Peer %p disconnected from %s\n", peer, srvc.Name)
|
||||
if srvc.OnDisconnect != nil {
|
||||
srvc.OnDisconnect(peer)
|
||||
}
|
||||
|
||||
srvc.removePeer(channel)
|
||||
}
|
||||
|
||||
func (srvc *Service) connect(channel chan *PacketEvent, peer *Peer) {
|
||||
log.Printf("New peer %p connected to %s\n", peer, srvc.Name)
|
||||
if srvc.OnConnect != nil {
|
||||
srvc.OnConnect(peer)
|
||||
}
|
||||
|
||||
srvc.setPeer(channel, peer)
|
||||
}
|
||||
98
cnet/service_test.go
Normal file
98
cnet/service_test.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package cnet_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/CPunch/gopenfusion/cnet"
|
||||
"github.com/CPunch/gopenfusion/cnet/protocol"
|
||||
"github.com/CPunch/gopenfusion/internal/testutil"
|
||||
"github.com/matryer/is"
|
||||
)
|
||||
|
||||
var (
|
||||
srvcPort int
|
||||
)
|
||||
|
||||
const (
|
||||
timeout = 2 * time.Second
|
||||
maxDummyPeers = 5
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
var err error
|
||||
srvcPort, err = cnet.RandomPort()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// this is fine since we don't defer anything
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestService(t *testing.T) {
|
||||
is := is.New(t)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
srvc := cnet.NewService(ctx, "TEST", srvcPort)
|
||||
wg := sync.WaitGroup{}
|
||||
|
||||
// shutdown service when test is done
|
||||
defer func() {
|
||||
cancel()
|
||||
is.True(testutil.SelectWithTimeout(srvc.Stopped(), timeout)) // wait for service to stop with timeout
|
||||
}()
|
||||
|
||||
// our dummy packet handler
|
||||
wg.Add(maxDummyPeers)
|
||||
srvc.AddPacketHandler(0x1234, func(peer *cnet.Peer, pkt protocol.Packet) error {
|
||||
log.Printf("Received packet %#v", pkt)
|
||||
wg.Done()
|
||||
return nil
|
||||
})
|
||||
|
||||
// wait for all dummy peers to connect and disconnect
|
||||
wg.Add(maxDummyPeers)
|
||||
srvc.OnConnect = func(peer *cnet.Peer) {
|
||||
wg.Done()
|
||||
}
|
||||
|
||||
wg.Add(maxDummyPeers)
|
||||
srvc.OnDisconnect = func(peer *cnet.Peer) {
|
||||
wg.Done()
|
||||
}
|
||||
|
||||
// run service
|
||||
go func() { is.NoErr(srvc.Start()) }() // srvc.Start error
|
||||
is.True(testutil.SelectWithTimeout(srvc.Started(), timeout)) // wait for service to start with timeout
|
||||
|
||||
wg.Add(maxDummyPeers * 2) // 2 wg.Done() per peer for receiving packets
|
||||
for i := 0; i < maxDummyPeers; i++ {
|
||||
go func() {
|
||||
// make dummy client
|
||||
conn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", srvcPort))
|
||||
is.NoErr(err) // net.Dial error
|
||||
|
||||
peer := cnet.NewPeer(ctx, conn)
|
||||
go func() {
|
||||
defer peer.Kill()
|
||||
|
||||
// send dummy packets
|
||||
for i := 0; i < 2; i++ {
|
||||
is.NoErr(peer.Send(0x1234)) // peer.Send error
|
||||
}
|
||||
}()
|
||||
|
||||
// we wait until Handler gracefully exits (peer was killed)
|
||||
peer.Handler(make(chan *cnet.PacketEvent))
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
is.True(testutil.WaitWithTimeout(&wg, timeout)) // wait for all dummy peers to be done with timeout
|
||||
}
|
||||
45
compose.yaml
Normal file
45
compose.yaml
Normal file
@@ -0,0 +1,45 @@
|
||||
|
||||
services:
|
||||
postgresql:
|
||||
image: 'postgres:15'
|
||||
environment:
|
||||
- POSTGRES_USER=gopenfusion
|
||||
- POSTGRES_DB=gopenfusion
|
||||
- POSTGRES_PASSWORD=gopenfusion
|
||||
redis:
|
||||
image: 'redis:7-alpine'
|
||||
login:
|
||||
restart: on-failure
|
||||
build: .
|
||||
hostname: login
|
||||
command: login
|
||||
environment:
|
||||
- DB_ADDR=postgresql:5432
|
||||
- DB_USER=gopenfusion
|
||||
- DB_PASS=gopenfusion
|
||||
- DB_NAME=gopenfusion
|
||||
- REDIS_ADDR=redis:6379
|
||||
ports:
|
||||
- '23000:23000'
|
||||
links:
|
||||
- postgresql
|
||||
- redis
|
||||
shard:
|
||||
restart: on-failure
|
||||
build: .
|
||||
hostname: shard
|
||||
command: shard
|
||||
environment:
|
||||
- DB_ADDR=postgresql:5432
|
||||
- DB_USER=gopenfusion
|
||||
- DB_PASS=gopenfusion
|
||||
- DB_NAME=gopenfusion
|
||||
- REDIS_ADDR=redis:6379
|
||||
- TDATA_PATH=/tdata
|
||||
volumes:
|
||||
- './tdata:/tdata'
|
||||
ports:
|
||||
- '23001:23001'
|
||||
links:
|
||||
- postgresql
|
||||
- redis
|
||||
@@ -1,15 +0,0 @@
|
||||
package config
|
||||
|
||||
var (
|
||||
SPAWN_X = 632032
|
||||
SPAWN_Y = 187177
|
||||
SPAWN_Z = -5500
|
||||
|
||||
AEQUIP_COUNT = 9
|
||||
AINVEN_COUNT = 50
|
||||
ABANK_COUNT = 119
|
||||
)
|
||||
|
||||
func GetMaxHP(level int) int {
|
||||
return (925 + 75*(level))
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"github.com/CPunch/gopenfusion/protocol"
|
||||
"github.com/blockloop/scan"
|
||||
)
|
||||
|
||||
type Account struct {
|
||||
AccountID int
|
||||
Login string
|
||||
Password string
|
||||
Selected int
|
||||
AccountLevel int
|
||||
Created int
|
||||
LastLogin int
|
||||
BannedUntil int
|
||||
BannedSince int
|
||||
BanReason string
|
||||
}
|
||||
|
||||
func (db *DBHandler) NewAccount(Login, Password string) (*Account, error) {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(Password), 12)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
row, err := db.Query("INSERT INTO Accounts (Login, Password, AccountLevel) VALUES(?, ?, ?) RETURNING *", Login, hash, protocol.CN_ACCOUNT_LEVEL__USER)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var account Account
|
||||
if err := scan.Row(&account, row); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &account, nil
|
||||
}
|
||||
|
||||
var (
|
||||
LoginErrorInvalidID = fmt.Errorf("Invalid Login ID!")
|
||||
LoginErrorInvalidPassword = fmt.Errorf("Invalid ID && Password combo!")
|
||||
)
|
||||
|
||||
func (db *DBHandler) TryLogin(Login, Password string) (*Account, error) {
|
||||
row, err := db.Query("SELECT * FROM Accounts WHERE Login=?", Login)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var account Account
|
||||
if err := scan.Row(&account, row); err != nil {
|
||||
return nil, LoginErrorInvalidID
|
||||
}
|
||||
|
||||
if bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(Password)) != nil {
|
||||
return nil, LoginErrorInvalidPassword
|
||||
}
|
||||
|
||||
// else, login was a success
|
||||
return &account, nil
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/CPunch/gopenfusion/protocol"
|
||||
)
|
||||
|
||||
type Inventory struct {
|
||||
PlayerID int
|
||||
Slot int
|
||||
ID int
|
||||
Type int
|
||||
Opt int
|
||||
TimeLimit int
|
||||
}
|
||||
|
||||
// start && end are both inclusive
|
||||
func (db *DBHandler) GetPlayerInventorySlots(PlayerID int, start int, end int) ([]protocol.SItemBase, error) {
|
||||
rows, err := db.Query("SELECT PlayerID, Slot, ID, Type, Opt, TimeLimit FROM Inventory WHERE Slot BETWEEN ? AND ? AND PlayerID = ?", start, end, PlayerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var inven []Inventory
|
||||
for rows.Next() {
|
||||
item := Inventory{}
|
||||
|
||||
if err := rows.Scan(
|
||||
&item.PlayerID, &item.Slot, &item.ID, &item.Type, &item.Opt, &item.TimeLimit); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
inven = append(inven, item)
|
||||
}
|
||||
|
||||
// populate an SItemBase array
|
||||
items := make([]protocol.SItemBase, end-start)
|
||||
for _, item := range inven {
|
||||
items[item.Slot-start] = protocol.SItemBase{
|
||||
IID: int16(item.ID),
|
||||
IType: int16(item.Type),
|
||||
IOpt: int32(item.Opt),
|
||||
ITimeLimit: int32(item.TimeLimit),
|
||||
}
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// start is inclusive
|
||||
func (db *DBHandler) SetPlayerInventorySlots(PlayerID int, start int, items []protocol.SItemBase) error {
|
||||
return db.Transaction(func(tx *sql.Tx) error {
|
||||
// delete inventory slots
|
||||
_, err := db.Query("DELETE FROM Inventory WHERE Slot BETWEEN ? AND ? AND PlayerID = ?", start, start+len(items)-1, PlayerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// insert inventory
|
||||
for i, item := range items {
|
||||
if item.IID != 0 {
|
||||
_, err := db.Query("INSERT INTO Inventory (PlayerID, Slot, ID, Type, Opt, TimeLimit) VALUES (?, ?, ?, ?, ?, ?)", PlayerID, start+i, item.IID, item.IType, item.IOpt, item.ITimeLimit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
227
db/players.go
227
db/players.go
@@ -1,227 +0,0 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/CPunch/gopenfusion/config"
|
||||
"github.com/CPunch/gopenfusion/protocol"
|
||||
"github.com/blockloop/scan"
|
||||
)
|
||||
|
||||
type Player struct {
|
||||
PlayerID int
|
||||
AccountID int
|
||||
FirstName string
|
||||
LastName string
|
||||
NameCheck int
|
||||
Slot int
|
||||
Created int
|
||||
LastLogin int
|
||||
Level int
|
||||
Nano1 int
|
||||
Nano2 int
|
||||
Nano3 int
|
||||
AppearanceFlag int
|
||||
TutorialFlag int
|
||||
PayZoneFlag int
|
||||
XCoordinate int
|
||||
YCoordinate int
|
||||
ZCoordinate int
|
||||
Angle int
|
||||
HP int
|
||||
FusionMatter int
|
||||
Taros int
|
||||
BatteryW int
|
||||
BatteryN int
|
||||
Mentor int
|
||||
CurrentMissionID int
|
||||
WarpLocationFlag int
|
||||
SkywayLocationFlag []byte
|
||||
FirstUseFlag []byte
|
||||
Quests []byte
|
||||
/* Appearance tbl */
|
||||
Body int `db:"Body"`
|
||||
EyeColor int `db:"EyeColor"`
|
||||
FaceStyle int `db:"FaceStyle"`
|
||||
Gender int `db:"Gender"`
|
||||
HairColor int `db:"HairColor"`
|
||||
HairStyle int `db:"HairStyle"`
|
||||
Height int `db:"Height"`
|
||||
SkinColor int `db:"SkinColor"`
|
||||
}
|
||||
|
||||
// returns PlayerID, error
|
||||
func (db *DBHandler) NewPlayer(AccountID int, FirstName, LastName string, slot int) (int, error) {
|
||||
nameCheck := 1 // for now, we approve all names
|
||||
QuestFlag := make([]byte, 128)
|
||||
SkywayLocationFlag := make([]byte, 16)
|
||||
FirstUseFlag := make([]byte, 16)
|
||||
|
||||
var PlayerID int
|
||||
if err := db.Transaction(func(tx *sql.Tx) error {
|
||||
// create player
|
||||
row, err := tx.Query(
|
||||
"INSERT INTO Players (AccountID, Slot, FirstName, LastName, XCoordinate, YCoordinate, ZCoordinate, Angle, HP, NameCheck, Quests, SkywayLocationFlag, FirstUseFlag) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING PlayerID",
|
||||
AccountID, slot, FirstName, LastName, config.SPAWN_X, config.SPAWN_Y, config.SPAWN_Z, 0, config.GetMaxHP(1), nameCheck, QuestFlag, SkywayLocationFlag, FirstUseFlag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := scan.Row(&PlayerID, row); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create appearance
|
||||
if _, err := tx.Exec("INSERT INTO Appearances (PlayerID) VALUES (?)", PlayerID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return -1, nil
|
||||
}
|
||||
|
||||
return PlayerID, nil
|
||||
}
|
||||
|
||||
// TODO: should this operate on the raw packet? should we do validation here or prior?
|
||||
func (db *DBHandler) FinishPlayer(character *protocol.SP_CL2LS_REQ_CHAR_CREATE, AccountId int) error {
|
||||
return db.Transaction(func(tx *sql.Tx) error {
|
||||
// update AppearanceFlag
|
||||
_, err := tx.Exec("UPDATE Players SET AppearanceFlag = 1 WHERE PlayerID = ? AND AccountID = ? AND AppearanceFlag = 0", character.PCStyle.IPC_UID, AccountId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update Appearance
|
||||
_, err = tx.Exec("UPDATE Appearances SET Body = ?, EyeColor = ?, FaceStyle = ?, Gender = ?, HairColor = ?, HairStyle = ?, Height = ?, SkinColor = ? WHERE PlayerID = ?",
|
||||
character.PCStyle.IBody,
|
||||
character.PCStyle.IEyeColor,
|
||||
character.PCStyle.IFaceStyle,
|
||||
character.PCStyle.IGender,
|
||||
character.PCStyle.IHairColor,
|
||||
character.PCStyle.IHairStyle,
|
||||
character.PCStyle.IHeight,
|
||||
character.PCStyle.ISkinColor,
|
||||
character.PCStyle.IPC_UID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update Inventory
|
||||
items := [3]int16{character.SOn_Item.IEquipUBID, character.SOn_Item.IEquipLBID, character.SOn_Item.IEquipFootID}
|
||||
for i := 0; i < len(items); i++ {
|
||||
_, err = tx.Exec("INSERT INTO Inventory (PlayerID, Slot, ID, Type, Opt) VALUES (?, ?, ?, ?, 1)", character.PCStyle.IPC_UID, i, items[i], i+1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (db *DBHandler) FinishTutorial(PlayerID, AccountID int) error {
|
||||
_, err := db.Exec("UPDATE Players SET TutorialFlag = 1 WHERE PlayerID = ? AND AccountID = ? AND TutorialFlag = 0", PlayerID, AccountID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: reference openfusion's finishTutorial for their academy specific patches
|
||||
return nil
|
||||
}
|
||||
|
||||
// returns the deleted Slot number
|
||||
func (db *DBHandler) DeletePlayer(PlayerID, AccountID int) (int, error) {
|
||||
row, err := db.Query("DELETE FROM Players WHERE AccountID = ? AND PlayerID = ? RETURNING Slot")
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
var slot int
|
||||
for row.Next() {
|
||||
if err := row.Scan(&slot); err != nil {
|
||||
return -1, err
|
||||
}
|
||||
}
|
||||
|
||||
return slot, nil
|
||||
}
|
||||
|
||||
func (db *DBHandler) GetPlayer(PlayerID int) (*Player, error) {
|
||||
rows, err := db.Query(`SELECT
|
||||
p.PlayerID, p.AccountID, p.Slot, p.FirstName, p.LastName,
|
||||
p.Level, p.Nano1, p.Nano2, p.Nano3,
|
||||
p.AppearanceFlag, p.TutorialFlag, p.PayZoneFlag,
|
||||
p.XCoordinate, p.YCoordinate, p.ZCoordinate, p.NameCheck,
|
||||
p.Angle, p.HP, p.FusionMatter, p.Taros, p.Quests,
|
||||
p.BatteryW, p.BatteryN, p.Mentor, p.WarpLocationFlag,
|
||||
p.SkywayLocationFlag, p.CurrentMissionID, p.FirstUseFlag,
|
||||
a.Body, a.EyeColor, a.FaceStyle, a.Gender, a.HairColor, a.HairStyle,
|
||||
a.Height, a.SkinColor
|
||||
FROM Players as p
|
||||
INNER JOIN Appearances as a ON p.PlayerID = a.PlayerID
|
||||
WHERE p.PlayerID = ?`, PlayerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var plr Player
|
||||
for rows.Next() {
|
||||
if err := rows.Scan(
|
||||
&plr.PlayerID, &plr.AccountID, &plr.Slot, &plr.FirstName, &plr.LastName,
|
||||
&plr.Level, &plr.Nano1, &plr.Nano2, &plr.Nano3,
|
||||
&plr.AppearanceFlag, &plr.TutorialFlag, &plr.PayZoneFlag,
|
||||
&plr.XCoordinate, &plr.YCoordinate, &plr.ZCoordinate, &plr.NameCheck,
|
||||
&plr.Angle, &plr.HP, &plr.FusionMatter, &plr.Taros, &plr.Quests,
|
||||
&plr.BatteryW, &plr.BatteryN, &plr.Mentor, &plr.WarpLocationFlag,
|
||||
&plr.SkywayLocationFlag, &plr.CurrentMissionID, &plr.FirstUseFlag,
|
||||
&plr.Body, &plr.EyeColor, &plr.FaceStyle, &plr.Gender, &plr.HairColor, &plr.HairStyle,
|
||||
&plr.Height, &plr.SkinColor); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &plr, nil
|
||||
}
|
||||
|
||||
func (db *DBHandler) GetPlayers(AccountID int) ([]Player, error) {
|
||||
rows, err := db.Query(`SELECT
|
||||
p.PlayerID, p.AccountID, p.Slot, p.FirstName, p.LastName,
|
||||
p.Level, p.Nano1, p.Nano2, p.Nano3,
|
||||
p.AppearanceFlag, p.TutorialFlag, p.PayZoneFlag,
|
||||
p.XCoordinate, p.YCoordinate, p.ZCoordinate, p.NameCheck,
|
||||
p.Angle, p.HP, p.FusionMatter, p.Taros, p.Quests,
|
||||
p.BatteryW, p.BatteryN, p.Mentor, p.WarpLocationFlag,
|
||||
p.SkywayLocationFlag, p.CurrentMissionID, p.FirstUseFlag,
|
||||
a.Body, a.EyeColor, a.FaceStyle, a.Gender, a.HairColor, a.HairStyle,
|
||||
a.Height, a.SkinColor
|
||||
FROM Players as p
|
||||
INNER JOIN Appearances as a ON p.PlayerID = a.PlayerID
|
||||
WHERE p.AccountID = ?`, AccountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var plrs []Player
|
||||
for rows.Next() {
|
||||
plr := Player{}
|
||||
|
||||
if err := rows.Scan(
|
||||
&plr.PlayerID, &plr.AccountID, &plr.Slot, &plr.FirstName, &plr.LastName,
|
||||
&plr.Level, &plr.Nano1, &plr.Nano2, &plr.Nano3,
|
||||
&plr.AppearanceFlag, &plr.TutorialFlag, &plr.PayZoneFlag,
|
||||
&plr.XCoordinate, &plr.YCoordinate, &plr.ZCoordinate, &plr.NameCheck,
|
||||
&plr.Angle, &plr.HP, &plr.FusionMatter, &plr.Taros, &plr.Quests,
|
||||
&plr.BatteryW, &plr.BatteryN, &plr.Mentor, &plr.WarpLocationFlag,
|
||||
&plr.SkywayLocationFlag, &plr.CurrentMissionID, &plr.FirstUseFlag,
|
||||
&plr.Body, &plr.EyeColor, &plr.FaceStyle, &plr.Gender, &plr.HairColor, &plr.HairStyle,
|
||||
&plr.Height, &plr.SkinColor); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
plrs = append(plrs, plr)
|
||||
}
|
||||
|
||||
return plrs, nil
|
||||
}
|
||||
40
go.mod
40
go.mod
@@ -3,19 +3,41 @@ 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/glebarez/go-sqlite v1.21.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
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230126093431-47fa9a501578 // indirect
|
||||
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
|
||||
github.com/docker/distribution v2.8.1+incompatible // indirect
|
||||
github.com/docker/docker v20.10.16+incompatible // indirect
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
github.com/docker/go-units v0.4.0 // indirect
|
||||
github.com/go-yaml/yaml v2.1.0+incompatible // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/jmoiron/sqlx v1.3.5 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.15 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.0.2 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
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
|
||||
modernc.org/libc v1.22.2 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.5.0 // indirect
|
||||
modernc.org/sqlite v1.20.4 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
146
go.sum
146
go.sum
@@ -1,36 +1,136 @@
|
||||
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=
|
||||
github.com/blockloop/scan v1.3.0/go.mod h1:qd+3w68+o7m5Xhj9X5SlJH2rbFyK8w0WT47Rkuer010=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/glebarez/go-sqlite v1.21.0 h1:b8MHPtBagkSD2gntImZPsG3o3QEXgMDxguW/GLUonHQ=
|
||||
github.com/glebarez/go-sqlite v1.21.0/go.mod h1:GodsA6yGSa3eKbvpr7dS+JaqazzVfMcjIXvx6KHhW/c=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68=
|
||||
github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v20.10.16+incompatible h1:2Db6ZR/+FUR3hqPMwnogOPHFn405crbpxvWzKovETOQ=
|
||||
github.com/docker/docker v20.10.16+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
|
||||
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
|
||||
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/georgysavva/scany/v2 v2.0.0 h1:RGXqxDv4row7/FYoK8MRXAZXqoWF/NM+NP0q50k3DKU=
|
||||
github.com/georgysavva/scany/v2 v2.0.0/go.mod h1:sigOdh+0qb/+aOs3TVhehVT10p8qJL7K/Zhyz8vWo38=
|
||||
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwnTLB6vQiq+o=
|
||||
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
|
||||
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=
|
||||
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
|
||||
github.com/jackc/pgx/v5 v5.0.0 h1:3UdmB3yUeTnJtZ+nDv3Mxzd4GHHvHkl9XN3oboIbOrY=
|
||||
github.com/jackc/puddle/v2 v2.0.0 h1:Kwk/AlLigcnZsDssc3Zun1dk1tAtQNPaBBxBHWn0Mjc=
|
||||
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
|
||||
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
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=
|
||||
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM=
|
||||
github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230126093431-47fa9a501578 h1:VstopitMQi3hZP0fzvnsLmzXZdQGc4bEcgu24cp+d4M=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230126093431-47fa9a501578/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/redis/go-redis/v9 v9.0.5 h1:CuQcn5HIEeK7BgElubPP8CGtE0KakrnbBSTLjathl5o=
|
||||
github.com/redis/go-redis/v9 v9.0.5/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
|
||||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||
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=
|
||||
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
||||
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=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
modernc.org/libc v1.22.2 h1:4U7v51GyhlWqQmwCHj28Rdq2Yzwk55ovjFrdPjs8Hb0=
|
||||
modernc.org/libc v1.22.2/go.mod h1:uvQavJ1pZ0hIoC/jfqNoMLURIMhKzINIWypNM17puug=
|
||||
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
||||
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
|
||||
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||
modernc.org/sqlite v1.20.4 h1:J8+m2trkN+KKoE7jglyHYYYiaq5xmz2HoHJIiBlRzbE=
|
||||
modernc.org/sqlite v1.20.4/go.mod h1:zKcGyrICaxNTMEHSr1HQ2GUraP0j+845GYw37+EyT6A=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
|
||||
golang.org/x/time v0.0.0-20220411224347-583f2d630306 h1:+gHMid33q6pen7kv9xvT+JRinntgeXO2AeZVd0AWD3w=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.2.0 h1:I0DwBVMGAx26dttAj1BtJLAkVGncrkkUXfJLC4Flt/I=
|
||||
|
||||
85
internal/config/config.go
Normal file
85
internal/config/config.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
/*
|
||||
Available environment variables:
|
||||
REDIS_ADDR
|
||||
REDIS_USER
|
||||
REDIS_PASS
|
||||
DB_ADDR
|
||||
DB_USER
|
||||
DB_PASS
|
||||
ANNOUNCE_IP
|
||||
TDATA_PATH
|
||||
*/
|
||||
|
||||
const (
|
||||
AEQUIP_COUNT = 9
|
||||
AINVEN_COUNT = 50
|
||||
ABANK_COUNT = 119
|
||||
|
||||
NANO_COUNT = 37
|
||||
)
|
||||
|
||||
var (
|
||||
SPAWN_X = 632032
|
||||
SPAWN_Y = 187177
|
||||
SPAWN_Z = -5500
|
||||
|
||||
LOGIN_PORT = 23000
|
||||
SHARD_PORT = 23001
|
||||
|
||||
LOGIN_TIMEOUT = time.Second * 30
|
||||
VIEW_DISTANCE = 25600
|
||||
)
|
||||
|
||||
func getEnv(key string, fallback string) string {
|
||||
if value, ok := os.LookupEnv(key); ok {
|
||||
return value
|
||||
}
|
||||
|
||||
return fallback
|
||||
}
|
||||
|
||||
func GetMaxHP(level int) int {
|
||||
return (925 + 75*(level))
|
||||
}
|
||||
|
||||
func GetRedisAddr() string {
|
||||
return getEnv("REDIS_ADDR", "localhost:6379")
|
||||
}
|
||||
|
||||
func GetRedisCredentials() func() (string, string) {
|
||||
return func() (string, string) {
|
||||
return getEnv("REDIS_USER", ""), getEnv("REDIS_PASS", "")
|
||||
}
|
||||
}
|
||||
|
||||
func GetDBName() string {
|
||||
return getEnv("DB_NAME", "postgres")
|
||||
}
|
||||
|
||||
func GetDBAddr() string {
|
||||
return getEnv("DB_ADDR", "localhost:5432")
|
||||
}
|
||||
|
||||
func GetDBUser() string {
|
||||
return getEnv("DB_USER", "postgres")
|
||||
}
|
||||
|
||||
func GetDBPass() string {
|
||||
return getEnv("DB_PASS", "")
|
||||
}
|
||||
|
||||
// needed for shard
|
||||
func GetAnnounceIP() string {
|
||||
return getEnv("ANNOUNCE_IP", "127.0.0.1")
|
||||
}
|
||||
|
||||
func GetTDataPath() string {
|
||||
return getEnv("TDATA_PATH", "./tdata")
|
||||
}
|
||||
73
internal/db/account.go
Normal file
73
internal/db/account.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"github.com/CPunch/gopenfusion/cnet/protocol"
|
||||
"github.com/georgysavva/scany/v2/sqlscan"
|
||||
)
|
||||
|
||||
type Account struct {
|
||||
AccountID int `db:"accountid"`
|
||||
Login string `db:"login"`
|
||||
Password string `db:"password"`
|
||||
Selected int `db:"selected"`
|
||||
AccountLevel int `db:"accountlevel"`
|
||||
Created int `db:"created"`
|
||||
LastLogin int `db:"lastlogin"`
|
||||
BannedUntil int `db:"banneduntil"`
|
||||
BannedSince int `db:"bannedsince"`
|
||||
BanReason string `db:"banreason"`
|
||||
}
|
||||
|
||||
func (db *DBHandler) NewAccount(Login, Password string) (*Account, error) {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(Password), 12)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
row, err := db.Query("INSERT INTO Accounts (Login, Password, AccountLevel) VALUES($1, $2, $3) RETURNING *", Login, hash, protocol.CN_ACCOUNT_LEVEL__USER)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var account Account
|
||||
row.Next()
|
||||
if err := sqlscan.ScanRow(&account, row); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &account, nil
|
||||
}
|
||||
|
||||
var (
|
||||
ErrLoginInvalidID = errors.New("invalid Login ID")
|
||||
ErrLoginInvalidPassword = errors.New("invalid ID && Password combo")
|
||||
)
|
||||
|
||||
func (db *DBHandler) TryLogin(Login, Password string) (*Account, error) {
|
||||
row, err := db.Query("SELECT * FROM Accounts WHERE Login=$1", Login)
|
||||
if err != nil {
|
||||
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 {
|
||||
return nil, ErrLoginInvalidID
|
||||
}
|
||||
|
||||
if bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(Password)) != nil {
|
||||
return nil, ErrLoginInvalidPassword
|
||||
}
|
||||
|
||||
// else, login was a success
|
||||
return &account, nil
|
||||
}
|
||||
124
internal/db/db_test.go
Normal file
124
internal/db/db_test.go
Normal file
@@ -0,0 +1,124 @@
|
||||
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/bitcomplete/sqltestutil"
|
||||
)
|
||||
|
||||
var (
|
||||
testDB *db.DBHandler
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
ret := 1
|
||||
defer func() {
|
||||
os.Exit(ret)
|
||||
}()
|
||||
|
||||
ctx := context.Background()
|
||||
psql, err := sqltestutil.StartPostgresContainer(ctx, "15")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer psql.Shutdown(ctx)
|
||||
|
||||
testDB, err = db.OpenFromConnectionString("postgres", psql.ConnectionString()+"?sslmode=disable")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer testDB.Close()
|
||||
|
||||
if err := testDB.Setup(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
ret = m.Run()
|
||||
}
|
||||
|
||||
func TestDBAccount(t *testing.T) {
|
||||
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")
|
||||
is.NoErr(err)
|
||||
|
||||
_, err = testDB.TryLogin("test", "wrongpassword")
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
test data has been collected by running the following commands in a postgresql shell started with:
|
||||
docker exec -it gofusion-postgresql-1 psql -U gopenfusion -W gopenfusion
|
||||
|
||||
gopenfusion=# SELECT * FROM Players;
|
||||
playerid | accountid | firstname | lastname | namecheck | slot | created | lastlogin | level | nano1 | nano2 | nano3 | appearanceflag | tutorialflag | payzoneflag | xcoordinate | ycoordinate | zcoordinate | angle | hp | fusionmatter | taros | batteryw | batteryn | mentor | currentmissionid | warplocationflag | skywaylocationflag | firstuseflag | quests
|
||||
----------+-----------+-----------+----------+-----------+------+---------+-----------+-------+-------+-------+-------+----------------+--------------+-------------+-------------+-------------+-------------+-------+------+--------------+-------+----------+----------+--------+------------------+------------------+------------------------------------+------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
1 | 1 | Neil | Mcscout | 1 | 1 | 76476 | 76476 | 1 | 0 | 0 | 0 | 1 | 1 | 0 | 632032 | 187177 | -5500 | 0 | 1000 | 0 | 0 | 0 | 0 | 5 | 0 | 0 | \x00000000000000000000000000000000 | \x00000000000000000000000000000000 | \x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||
|
||||
gopenfusion=# SELECT * FROM Appearances;
|
||||
playerid | body | eyecolor | facestyle | gender | haircolor | hairstyle | height | skincolor
|
||||
----------+------+----------+-----------+--------+-----------+-----------+--------+-----------
|
||||
1 | 2 | 3 | 5 | 1 | 11 | 3 | 0 | 9
|
||||
|
||||
gopenfusion=# SELECT * FROM Inventory;
|
||||
playerid | slot | id | type | opt | timelimit
|
||||
----------+------+-----+------+-----+-----------
|
||||
1 | 1 | 328 | 1 | 1 | 0
|
||||
1 | 2 | 359 | 2 | 1 | 0
|
||||
1 | 3 | 194 | 3 | 1 | 0
|
||||
|
||||
*/
|
||||
|
||||
func TestDBPlayer(t *testing.T) {
|
||||
is := is.New(t)
|
||||
_, err := testDB.NewAccount("testplayer", "test")
|
||||
is.NoErr(err)
|
||||
|
||||
// now try to retrieve account data
|
||||
acc, err := testDB.TryLogin("testplayer", "test")
|
||||
is.NoErr(err)
|
||||
|
||||
plrID, err := testDB.NewPlayer(acc.AccountID, "Neil", "Mcscout", 1)
|
||||
is.NoErr(err)
|
||||
|
||||
err = testDB.FinishPlayer(&protocol.SP_CL2LS_REQ_CHAR_CREATE{
|
||||
PCStyle: protocol.SPCStyle{
|
||||
IPC_UID: int64(plrID),
|
||||
INameCheck: 1,
|
||||
SzFirstName: "Neil",
|
||||
SzLastName: "Mcscout",
|
||||
IGender: 1,
|
||||
IFaceStyle: 5,
|
||||
IHairColor: 11,
|
||||
ISkinColor: 9,
|
||||
IEyeColor: 3,
|
||||
IHeight: 0,
|
||||
IBody: 2,
|
||||
IHairStyle: 3,
|
||||
},
|
||||
SOn_Item: protocol.SOnItem{
|
||||
IEquipUBID: 328,
|
||||
IEquipLBID: 359,
|
||||
IEquipFootID: 194,
|
||||
},
|
||||
}, acc.AccountID)
|
||||
is.NoErr(err)
|
||||
|
||||
err = testDB.FinishTutorial(plrID, acc.AccountID)
|
||||
is.NoErr(err)
|
||||
}
|
||||
62
internal/db/inventory.go
Normal file
62
internal/db/inventory.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/CPunch/gopenfusion/cnet/protocol"
|
||||
)
|
||||
|
||||
type Inventory struct {
|
||||
PlayerID int `db:"playerid"`
|
||||
Slot int `db:"slot"`
|
||||
ID int `db:"id"`
|
||||
Type int `db:"type"`
|
||||
Opt int `db:"opt"`
|
||||
TimeLimit int `db:"timelimit"`
|
||||
}
|
||||
|
||||
// start && end are both inclusive
|
||||
func (db *DBHandler) GetPlayerInventorySlots(PlayerID int, start int, end int) ([]protocol.SItemBase, error) {
|
||||
rows, err := db.Query("SELECT Slot, Type, ID, Opt, TimeLimit FROM Inventory WHERE Slot BETWEEN $1 AND $2 AND PlayerID = $3", start, end, PlayerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
items := make([]protocol.SItemBase, end-start)
|
||||
for rows.Next() {
|
||||
var slot int
|
||||
item := protocol.SItemBase{}
|
||||
|
||||
if err := rows.Scan(
|
||||
&slot, &item.IType, &item.IID, &item.IOpt, &item.ITimeLimit); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
items[slot-start] = item
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// start is inclusive
|
||||
func (db *DBHandler) SetPlayerInventorySlots(PlayerID int, start int, items []protocol.SItemBase) error {
|
||||
return db.Transaction(func(tx *sql.Tx) error {
|
||||
// delete inventory slots
|
||||
_, err := tx.Exec("DELETE FROM Inventory WHERE Slot BETWEEN $1 AND $2 AND PlayerID = $3", start, start+len(items)-1, PlayerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// insert inventory
|
||||
for i, item := range items {
|
||||
if item.IID != 0 {
|
||||
_, err := tx.Exec("INSERT INTO Inventory (PlayerID, Slot, ID, Type, Opt, TimeLimit) VALUES ($1, $2, $3, $4, $5, $6)", PlayerID, start+i, item.IID, item.IType, item.IOpt, item.ITimeLimit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -1,29 +1,31 @@
|
||||
-- this file has been lifted from https://github.com/OpenFusionProject/OpenFusion/blob/master/sql/tables.sql
|
||||
-- this file has been lifted from https://github.com/OpenFusionProject/OpenFusion/bytea/master/sql/tables.sql
|
||||
-- all credit to original contributors!
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS citext;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS Accounts (
|
||||
AccountID INTEGER NOT NULL,
|
||||
Login TEXT NOT NULL UNIQUE COLLATE NOCASE,
|
||||
AccountID SERIAL NOT NULL,
|
||||
Login CITEXT NOT NULL UNIQUE,
|
||||
Password TEXT NOT NULL,
|
||||
Selected INTEGER DEFAULT 1 NOT NULL,
|
||||
AccountLevel INTEGER NOT NULL,
|
||||
Created INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
|
||||
LastLogin INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
|
||||
Created INTEGER DEFAULT (extract(EPOCH FROM current_time)) NOT NULL,
|
||||
LastLogin INTEGER DEFAULT (extract(EPOCH FROM current_time)) NOT NULL,
|
||||
BannedUntil INTEGER DEFAULT 0 NOT NULL,
|
||||
BannedSince INTEGER DEFAULT 0 NOT NULL,
|
||||
BanReason TEXT DEFAULT '' NOT NULL,
|
||||
PRIMARY KEY(AccountID AUTOINCREMENT)
|
||||
PRIMARY KEY(AccountID)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS Players (
|
||||
PlayerID INTEGER NOT NULL,
|
||||
PlayerID SERIAL NOT NULL,
|
||||
AccountID INTEGER NOT NULL,
|
||||
FirstName TEXT NOT NULL COLLATE NOCASE,
|
||||
LastName TEXT NOT NULL COLLATE NOCASE,
|
||||
FirstName CITEXT NOT NULL,
|
||||
LastName CITEXT NOT NULL,
|
||||
NameCheck INTEGER NOT NULL,
|
||||
Slot INTEGER NOT NULL,
|
||||
Created INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
|
||||
LastLogin INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
|
||||
Created INTEGER DEFAULT (extract(EPOCH FROM current_time)) NOT NULL,
|
||||
LastLogin INTEGER DEFAULT (extract(EPOCH FROM current_time)) NOT NULL,
|
||||
Level INTEGER DEFAULT 1 NOT NULL,
|
||||
Nano1 INTEGER DEFAULT 0 NOT NULL,
|
||||
Nano2 INTEGER DEFAULT 0 NOT NULL,
|
||||
@@ -43,10 +45,10 @@ CREATE TABLE IF NOT EXISTS Players (
|
||||
Mentor INTEGER DEFAULT 5 NOT NULL,
|
||||
CurrentMissionID INTEGER DEFAULT 0 NOT NULL,
|
||||
WarpLocationFlag INTEGER DEFAULT 0 NOT NULL,
|
||||
SkywayLocationFlag BLOB NOT NULL,
|
||||
FirstUseFlag BLOB NOT NULL,
|
||||
Quests BLOB NOT NULL,
|
||||
PRIMARY KEY(PlayerID AUTOINCREMENT),
|
||||
SkywayLocationFlag bytea NOT NULL,
|
||||
FirstUseFlag bytea NOT NULL,
|
||||
Quests bytea NOT NULL,
|
||||
PRIMARY KEY(PlayerID),
|
||||
FOREIGN KEY(AccountID) REFERENCES Accounts(AccountID) ON DELETE CASCADE,
|
||||
UNIQUE (AccountID, Slot),
|
||||
UNIQUE (FirstName, LastName)
|
||||
@@ -123,8 +125,8 @@ CREATE TABLE IF NOT EXISTS EmailData (
|
||||
ReadFlag INTEGER NOT NULL,
|
||||
ItemFlag INTEGER NOT NULL,
|
||||
SenderID INTEGER NOT NULL,
|
||||
SenderFirstName TEXT NOT NULL COLLATE NOCASE,
|
||||
SenderLastName TEXT NOT NULL COLLATE NOCASE,
|
||||
SenderFirstName CITEXT NOT NULL,
|
||||
SenderLastName CITEXT NOT NULL,
|
||||
SubjectLine TEXT NOT NULL,
|
||||
MsgBody TEXT NOT NULL,
|
||||
Taros INTEGER NOT NULL,
|
||||
229
internal/db/players.go
Normal file
229
internal/db/players.go
Normal file
@@ -0,0 +1,229 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"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
|
||||
QuestFlag := make([]byte, 128)
|
||||
SkywayLocationFlag := make([]byte, 16)
|
||||
FirstUseFlag := make([]byte, 16)
|
||||
|
||||
var PlayerID int
|
||||
if err := db.Transaction(func(tx *sql.Tx) error {
|
||||
// create player
|
||||
rows, err := tx.Query(
|
||||
"INSERT INTO Players (AccountID, Slot, FirstName, LastName, XCoordinate, YCoordinate, ZCoordinate, Angle, HP, NameCheck, Quests, SkywayLocationFlag, FirstUseFlag) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING PlayerID",
|
||||
AccountID, slot, FirstName, LastName, config.SPAWN_X, config.SPAWN_Y, config.SPAWN_Z, 0, config.GetMaxHP(1), nameCheck, QuestFlag, SkywayLocationFlag, FirstUseFlag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := scan.Row(&PlayerID, rows); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create appearance
|
||||
if _, err := tx.Exec("INSERT INTO Appearances (PlayerID) VALUES ($1)", PlayerID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return -1, nil
|
||||
}
|
||||
|
||||
return PlayerID, nil
|
||||
}
|
||||
|
||||
// TODO: should this operate on the raw packet? should we do validation here or prior?
|
||||
func (db *DBHandler) FinishPlayer(character *protocol.SP_CL2LS_REQ_CHAR_CREATE, AccountId int) error {
|
||||
return db.Transaction(func(tx *sql.Tx) error {
|
||||
// update AppearanceFlag
|
||||
_, err := tx.Exec("UPDATE Players SET AppearanceFlag = 1 WHERE PlayerID = $1 AND AccountID = $2 AND AppearanceFlag = 0", character.PCStyle.IPC_UID, AccountId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update Appearance
|
||||
_, err = tx.Exec("UPDATE Appearances SET Body = $1, EyeColor = $2, FaceStyle = $3, Gender = $4, HairColor = $5, HairStyle = $6, Height = $7, SkinColor = $8 WHERE PlayerID = $9",
|
||||
character.PCStyle.IBody,
|
||||
character.PCStyle.IEyeColor,
|
||||
character.PCStyle.IFaceStyle,
|
||||
character.PCStyle.IGender,
|
||||
character.PCStyle.IHairColor,
|
||||
character.PCStyle.IHairStyle,
|
||||
character.PCStyle.IHeight,
|
||||
character.PCStyle.ISkinColor,
|
||||
character.PCStyle.IPC_UID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update Inventory
|
||||
items := [3]int16{character.SOn_Item.IEquipUBID, character.SOn_Item.IEquipLBID, character.SOn_Item.IEquipFootID}
|
||||
for i := 0; i < len(items); i++ {
|
||||
_, err = tx.Exec("INSERT INTO Inventory (PlayerID, Slot, ID, Type, Opt) VALUES ($1, $2, $3, $4, 1)", character.PCStyle.IPC_UID, i+1, items[i], i+1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (db *DBHandler) FinishTutorial(PlayerID, AccountID int) error {
|
||||
_, err := db.Exec("UPDATE Players SET TutorialFlag = 1 WHERE PlayerID = $1 AND AccountID = $2 AND TutorialFlag = 0", PlayerID, AccountID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: reference openfusion's finishTutorial for their academy specific patches
|
||||
return nil
|
||||
}
|
||||
|
||||
// returns the deleted Slot number
|
||||
func (db *DBHandler) DeletePlayer(PlayerID, AccountID int) (int, error) {
|
||||
row, err := db.Query("DELETE FROM Players WHERE AccountID = $1 AND PlayerID = $2 RETURNING Slot")
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
var slot int
|
||||
if err := row.Scan(&slot); err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
return slot, nil
|
||||
}
|
||||
|
||||
const (
|
||||
QUERY_PLAYERS = `SELECT
|
||||
p.PlayerID, p.AccountID, p.Slot, p.FirstName, p.LastName,
|
||||
p.Level, p.Nano1, p.Nano2, p.Nano3,
|
||||
p.AppearanceFlag, p.TutorialFlag, p.PayZoneFlag,
|
||||
p.XCoordinate, p.YCoordinate, p.ZCoordinate, p.NameCheck,
|
||||
p.Angle, p.HP, p.FusionMatter, p.Taros, p.Quests,
|
||||
p.BatteryW, p.BatteryN, p.Mentor, p.WarpLocationFlag,
|
||||
p.SkywayLocationFlag, p.CurrentMissionID, p.FirstUseFlag,
|
||||
a.Body, a.EyeColor, a.FaceStyle, a.Gender, a.HairColor, a.HairStyle,
|
||||
a.Height, a.SkinColor, acc.AccountLevel
|
||||
FROM Players as p
|
||||
INNER JOIN Appearances as a ON p.PlayerID = a.PlayerID
|
||||
INNER JOIN Accounts as acc ON p.AccountID = acc.AccountID `
|
||||
)
|
||||
|
||||
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,
|
||||
&plr.Level, &plr.EquippedNanos[0], &plr.EquippedNanos[1], &plr.EquippedNanos[2],
|
||||
&plr.PCStyle2.IAppearanceFlag, &plr.PCStyle2.ITutorialFlag, &plr.PCStyle2.IPayzoneFlag,
|
||||
&plr.X, &plr.Y, &plr.Z, &plr.PCStyle.INameCheck,
|
||||
&plr.Angle, &plr.HP, &plr.FusionMatter, &plr.Taros, &plr.Quests,
|
||||
&plr.BatteryW, &plr.BatteryN, &plr.Mentor, &plr.WarpLocationFlag,
|
||||
&plr.SkywayLocationFlag, &plr.CurrentMissionID, &plr.FirstUseFlag,
|
||||
&plr.PCStyle.IBody, &plr.PCStyle.IEyeColor, &plr.PCStyle.IFaceStyle, &plr.PCStyle.IGender, &plr.PCStyle.IHairColor, &plr.PCStyle.IHairStyle,
|
||||
&plr.PCStyle.IHeight, &plr.PCStyle.ISkinColor, &plr.AccountLevel); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
plr.PCStyle.IPC_UID = int64(plr.PlayerID)
|
||||
|
||||
inven, err := db.GetPlayerInventorySlots(plr.PlayerID, 0, config.AEQUIP_COUNT+config.AINVEN_COUNT+config.ABANK_COUNT)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// populate player inven
|
||||
for i, item := range inven {
|
||||
if item.IID == 0 { // skip empty slots
|
||||
continue
|
||||
}
|
||||
|
||||
switch {
|
||||
case i < config.AEQUIP_COUNT:
|
||||
plr.Equip[i] = item
|
||||
case i < config.AEQUIP_COUNT+config.AINVEN_COUNT:
|
||||
plr.Inven[i-config.AEQUIP_COUNT] = item
|
||||
default:
|
||||
plr.Bank[i-config.AEQUIP_COUNT-config.AINVEN_COUNT] = item
|
||||
}
|
||||
}
|
||||
|
||||
return &plr, nil
|
||||
}
|
||||
|
||||
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 *Player
|
||||
for rows.Next() {
|
||||
plr, err = db.readPlayer(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return plr, nil
|
||||
}
|
||||
|
||||
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 []Player
|
||||
for rows.Next() {
|
||||
plr, err := db.readPlayer(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
plrs = append(plrs, *plr)
|
||||
}
|
||||
|
||||
return plrs, nil
|
||||
}
|
||||
@@ -10,29 +10,19 @@ import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
|
||||
_ "github.com/glebarez/go-sqlite"
|
||||
"github.com/CPunch/gopenfusion/internal/config"
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
|
||||
type DBHandler struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
type DBQuery interface {
|
||||
Query(query string, args ...any) (*sql.Rows, error)
|
||||
Exec(query string, args ...any) (sql.Result, error)
|
||||
}
|
||||
|
||||
//go:embed migrations/new.sql
|
||||
var createDBQuery string
|
||||
|
||||
var (
|
||||
DefaultDB *DBHandler
|
||||
)
|
||||
|
||||
func OpenLiteDB(dbPath string) (*DBHandler, error) {
|
||||
sqliteFmt := fmt.Sprintf("%s", dbPath)
|
||||
|
||||
db, err := sql.Open("sqlite", sqliteFmt)
|
||||
func OpenFromConnectionString(driverName, connectionString string) (*DBHandler, error) {
|
||||
db, err := sql.Open(driverName, connectionString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -40,6 +30,11 @@ func OpenLiteDB(dbPath string) (*DBHandler, error) {
|
||||
return &DBHandler{db}, nil
|
||||
}
|
||||
|
||||
func OpenPostgresDB(dbAddr string) (*DBHandler, error) {
|
||||
fmt := fmt.Sprintf("postgresql://%s:%s@%s/%s?sslmode=disable", config.GetDBUser(), config.GetDBPass(), dbAddr, config.GetDBName())
|
||||
return OpenFromConnectionString("postgres", fmt)
|
||||
}
|
||||
|
||||
func (db *DBHandler) Query(query string, args ...any) (*sql.Rows, error) {
|
||||
return db.db.Query(query, args...)
|
||||
}
|
||||
52
internal/redis/login.go
Normal file
52
internal/redis/login.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/CPunch/gopenfusion/internal/config"
|
||||
)
|
||||
|
||||
type LoginMetadata struct {
|
||||
FEKey []byte `json:",omitempty"`
|
||||
PlayerID int32 `json:",omitempty"`
|
||||
AccountID int `json:",omitempty"`
|
||||
}
|
||||
|
||||
// we store login queues into redis with the name "loginMetadata_<serialKey>"
|
||||
// set to expire after config.LOGIN_TIMEOUT duration. this way we can easily
|
||||
// have a shared pool of active serial keys & player login data which any
|
||||
// shard can pull from
|
||||
|
||||
func makeLoginMetadataKey(serialKey int64) string {
|
||||
return fmt.Sprintf("loginMetadata_%s", strconv.Itoa(int(serialKey)))
|
||||
}
|
||||
|
||||
func (r *RedisHandler) QueueLogin(serialKey int64, data LoginMetadata) error {
|
||||
value, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add to table
|
||||
return r.client.Set(r.ctx, makeLoginMetadataKey(serialKey), value, config.LOGIN_TIMEOUT).Err()
|
||||
}
|
||||
|
||||
func (r *RedisHandler) GetLogin(serialKey int64) (LoginMetadata, error) {
|
||||
value, err := r.client.Get(r.ctx, makeLoginMetadataKey(serialKey)).Result()
|
||||
if err != nil {
|
||||
return LoginMetadata{}, err
|
||||
}
|
||||
|
||||
var data LoginMetadata
|
||||
if err := json.Unmarshal([]byte(value), &data); err != nil {
|
||||
return LoginMetadata{}, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (r *RedisHandler) RemoveLogin(serialKey int64) error {
|
||||
return r.client.Del(r.ctx, makeLoginMetadataKey(serialKey)).Err()
|
||||
}
|
||||
32
internal/redis/redis.go
Normal file
32
internal/redis/redis.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package redis
|
||||
|
||||
/*
|
||||
used for state management between shard and login servers
|
||||
*/
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/CPunch/gopenfusion/internal/config"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
type RedisHandler struct {
|
||||
client *redis.Client
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func OpenRedis(addr string) (*RedisHandler, error) {
|
||||
client := redis.NewClient(&redis.Options{
|
||||
Addr: addr,
|
||||
CredentialsProvider: config.GetRedisCredentials(),
|
||||
})
|
||||
|
||||
_, err := client.Ping(context.Background()).Result()
|
||||
return &RedisHandler{client: client, ctx: context.Background()}, err
|
||||
}
|
||||
|
||||
func (r *RedisHandler) Close() error {
|
||||
return r.client.Close()
|
||||
}
|
||||
82
internal/redis/redis_test.go
Normal file
82
internal/redis/redis_test.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package redis_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/CPunch/gopenfusion/internal/redis"
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
"github.com/matryer/is"
|
||||
)
|
||||
|
||||
var (
|
||||
rh *redis.RedisHandler
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
ret := 1
|
||||
defer func() {
|
||||
os.Exit(ret)
|
||||
}()
|
||||
|
||||
r, err := miniredis.Run()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
rh, err = redis.OpenRedis(r.Addr())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer rh.Close()
|
||||
|
||||
ret = m.Run()
|
||||
}
|
||||
|
||||
func TestRedisLogin(t *testing.T) {
|
||||
is := is.New(t)
|
||||
|
||||
// test data
|
||||
serialKey := int64(1234)
|
||||
data := redis.LoginMetadata{
|
||||
FEKey: []byte("test"),
|
||||
PlayerID: 1,
|
||||
AccountID: 2,
|
||||
}
|
||||
|
||||
// queue login
|
||||
is.NoErr(rh.QueueLogin(serialKey, data))
|
||||
|
||||
// get login
|
||||
loginData, err := rh.GetLogin(serialKey)
|
||||
is.NoErr(err)
|
||||
|
||||
// compare
|
||||
is.Equal(loginData, data) // received data should be the same as sent data
|
||||
|
||||
// delete login
|
||||
is.NoErr(rh.RemoveLogin(serialKey))
|
||||
|
||||
// get login
|
||||
_, err = rh.GetLogin(serialKey)
|
||||
is.True(err != nil) // should fail to get removed login
|
||||
}
|
||||
|
||||
func TestRedisShard(t *testing.T) {
|
||||
is := is.New(t)
|
||||
|
||||
// test data
|
||||
shard := redis.ShardMetadata{
|
||||
IP: "0.0.0.0",
|
||||
Port: 1234,
|
||||
}
|
||||
|
||||
// register shard
|
||||
is.NoErr(rh.RegisterShard(shard))
|
||||
|
||||
// get shards
|
||||
shards := rh.GetShards()
|
||||
is.True(len(shards) == 1) // should only be 1 shard
|
||||
is.Equal(shards[0], shard) // received data should be the same as sent data
|
||||
}
|
||||
38
internal/redis/shard.go
Normal file
38
internal/redis/shard.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package redis
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type ShardMetadata struct {
|
||||
IP string
|
||||
Port int
|
||||
}
|
||||
|
||||
const (
|
||||
SHARD_SET = "shards"
|
||||
)
|
||||
|
||||
func (r *RedisHandler) RegisterShard(shard ShardMetadata) error {
|
||||
value, err := json.Marshal(shard)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return r.client.SAdd(r.ctx, SHARD_SET, value).Err()
|
||||
}
|
||||
|
||||
func (r *RedisHandler) GetShards() []ShardMetadata {
|
||||
shardData := r.client.SMembers(r.ctx, SHARD_SET).Val()
|
||||
|
||||
// unmarshal all shards
|
||||
shards := make([]ShardMetadata, 0, len(shardData))
|
||||
for _, data := range shardData {
|
||||
var shard ShardMetadata
|
||||
if err := json.Unmarshal([]byte(data), &shard); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
shards = append(shards, shard)
|
||||
}
|
||||
|
||||
return shards
|
||||
}
|
||||
68
internal/testutil/account.go
Normal file
68
internal/testutil/account.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"github.com/CPunch/gopenfusion/cnet/protocol"
|
||||
"github.com/CPunch/gopenfusion/internal/db"
|
||||
"github.com/CPunch/gopenfusion/internal/redis"
|
||||
)
|
||||
|
||||
var (
|
||||
TestCharCreate = protocol.SP_CL2LS_REQ_CHAR_CREATE{
|
||||
PCStyle: protocol.SPCStyle{
|
||||
INameCheck: 1, SzFirstName: "Hector",
|
||||
SzLastName: "Bannonvenom", IGender: 1, IFaceStyle: 1,
|
||||
IHairStyle: 17, IHairColor: 11, ISkinColor: 10, IEyeColor: 2,
|
||||
IHeight: 1, IBody: 0, IClass: 0,
|
||||
},
|
||||
SOn_Item: protocol.SOnItem{
|
||||
IEquipHandID: 0, IEquipUBID: 53, IEquipLBID: 17, IEquipFootID: 58,
|
||||
IEquipHeadID: 0, IEquipFaceID: 0, IEquipBackID: 0,
|
||||
},
|
||||
SOn_Item_Index: protocol.SOnItem_Index{
|
||||
IEquipUBID_index: 15, IEquipLBID_index: 12, IEquipFootID_index: 17,
|
||||
IFaceStyle: 2, IHairStyle: 18,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// creates a new account and player in the database
|
||||
func MakeTestPlayer(db *db.DBHandler, id string, password string) (acc *db.Account, plr *db.Player, err error) {
|
||||
acc, err = db.NewAccount(id, password)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var plrID int
|
||||
plrID, err = db.NewPlayer(acc.AccountID, TestCharCreate.PCStyle.SzFirstName, TestCharCreate.PCStyle.SzLastName, 1)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
charCreate := TestCharCreate
|
||||
charCreate.PCStyle.IPC_UID = int64(plrID)
|
||||
err = db.FinishPlayer(&charCreate, acc.AccountID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = db.FinishTutorial(plrID, acc.AccountID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
plr, err = db.GetPlayer(plrID)
|
||||
return
|
||||
}
|
||||
|
||||
func QueueLogin(redisHndlr *redis.RedisHandler, FEKey []byte, plrID, accID int) (int64, error) {
|
||||
key, err := protocol.GenSerialKey()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return key, redisHndlr.QueueLogin(key, redis.LoginMetadata{
|
||||
FEKey: FEKey,
|
||||
PlayerID: int32(plrID),
|
||||
AccountID: accID,
|
||||
})
|
||||
}
|
||||
50
internal/testutil/dummy.go
Normal file
50
internal/testutil/dummy.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/CPunch/gopenfusion/cnet"
|
||||
"github.com/CPunch/gopenfusion/cnet/protocol"
|
||||
"github.com/matryer/is"
|
||||
)
|
||||
|
||||
type DummyPeer struct {
|
||||
Recv chan *cnet.PacketEvent
|
||||
Peer *cnet.Peer
|
||||
is *is.I
|
||||
}
|
||||
|
||||
// MakeDummyPeer creates a new dummy peer and returns it
|
||||
func MakeDummyPeer(ctx context.Context, is *is.I, port int) *DummyPeer {
|
||||
conn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port))
|
||||
is.NoErr(err)
|
||||
|
||||
recv := make(chan *cnet.PacketEvent)
|
||||
peer := cnet.NewPeer(ctx, conn)
|
||||
go func() {
|
||||
peer.Handler(recv)
|
||||
}()
|
||||
|
||||
return &DummyPeer{Recv: recv, Peer: peer, is: is}
|
||||
}
|
||||
|
||||
// SendAndRecv sends a packet (sID & out), waits for the expected response (rID) and decodes it into in
|
||||
func (dp *DummyPeer) SendAndRecv(sID, rID uint32, out, in interface{}) {
|
||||
// send out packet
|
||||
err := dp.Peer.Send(sID, out)
|
||||
dp.is.NoErr(err) // peer.Send() should not return an error
|
||||
|
||||
// receive response
|
||||
evnt := <-dp.Recv
|
||||
defer protocol.PutBuffer(evnt.Pkt)
|
||||
|
||||
dp.is.Equal(evnt.PktID, rID) // should receive expected type
|
||||
dp.is.NoErr(protocol.NewPacket(evnt.Pkt).Decode(in)) // packet.Decode() should not return an error
|
||||
}
|
||||
|
||||
// Kill closes the peer's connection
|
||||
func (dp *DummyPeer) Kill() {
|
||||
dp.Peer.Kill()
|
||||
}
|
||||
52
internal/testutil/env.go
Normal file
52
internal/testutil/env.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/CPunch/gopenfusion/internal/db"
|
||||
"github.com/CPunch/gopenfusion/internal/redis"
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
"github.com/bitcomplete/sqltestutil"
|
||||
)
|
||||
|
||||
// SetupEnvironment spawns a postgres container and returns a db and redis handler
|
||||
// along with a cleanup function
|
||||
func SetupEnvironment(ctx context.Context) (*db.DBHandler, *redis.RedisHandler, func()) {
|
||||
// spawn postgres container
|
||||
psql, err := sqltestutil.StartPostgresContainer(ctx, "15")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// open db handler
|
||||
testDB, err := db.OpenFromConnectionString("postgres", psql.ConnectionString()+"?sslmode=disable")
|
||||
if err != nil {
|
||||
psql.Shutdown(ctx)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err = testDB.Setup(); err != nil {
|
||||
psql.Shutdown(ctx)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// start miniredis
|
||||
r, err := miniredis.Run()
|
||||
if err != nil {
|
||||
psql.Shutdown(ctx)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// open redis handler
|
||||
rh, err := redis.OpenRedis(r.Addr())
|
||||
if err != nil {
|
||||
psql.Shutdown(ctx)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return testDB, rh, func() {
|
||||
psql.Shutdown(ctx)
|
||||
rh.Close()
|
||||
r.Close()
|
||||
}
|
||||
}
|
||||
25
internal/testutil/helpers.go
Normal file
25
internal/testutil/helpers.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
func SelectWithTimeout(ch <-chan struct{}, timeout time.Duration) bool {
|
||||
select {
|
||||
case <-ch:
|
||||
return true
|
||||
case <-time.After(timeout):
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func WaitWithTimeout(wg *sync.WaitGroup, timeout time.Duration) bool {
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
wg.Wait()
|
||||
}()
|
||||
|
||||
return SelectWithTimeout(done, timeout)
|
||||
}
|
||||
35
internal/testutil/testutil_test.go
Normal file
35
internal/testutil/testutil_test.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package testutil_test
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/CPunch/gopenfusion/internal/testutil"
|
||||
"github.com/matryer/is"
|
||||
)
|
||||
|
||||
func TestWaitWithTimeout(t *testing.T) {
|
||||
is := is.New(t)
|
||||
wg := &sync.WaitGroup{}
|
||||
go func() {
|
||||
time.Sleep(1 * time.Second)
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
wg.Add(1)
|
||||
is.True(!testutil.WaitWithTimeout(wg, 500*time.Millisecond)) // timeout should occur
|
||||
is.True(testutil.WaitWithTimeout(wg, 750*time.Millisecond)) // timeout shouldn't occur
|
||||
}
|
||||
|
||||
func TestSelectWithTimeout(t *testing.T) {
|
||||
is := is.New(t)
|
||||
ch := make(chan struct{})
|
||||
go func() {
|
||||
time.Sleep(1 * time.Second)
|
||||
close(ch)
|
||||
}()
|
||||
|
||||
is.True(!testutil.SelectWithTimeout(ch, 500*time.Millisecond)) // timeout should occur
|
||||
is.True(testutil.SelectWithTimeout(ch, 750*time.Millisecond)) // timeout shouldn't occur
|
||||
}
|
||||
@@ -1,14 +1,17 @@
|
||||
package server
|
||||
package login
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
"log"
|
||||
"math/rand"
|
||||
|
||||
"github.com/CPunch/gopenfusion/config"
|
||||
"github.com/CPunch/gopenfusion/db"
|
||||
"github.com/CPunch/gopenfusion/protocol"
|
||||
"github.com/CPunch/gopenfusion/util"
|
||||
"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/redis"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -23,16 +26,14 @@ const (
|
||||
LOGIN_UPDATED_EUALA_REQUIRED = 9
|
||||
)
|
||||
|
||||
func (server *LoginServer) AcceptLogin(peer *Peer, 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)),
|
||||
ISlotNum: ISlotNum,
|
||||
IPaymentFlag: 1,
|
||||
IOpenBetaFlag: 0,
|
||||
UiSvrTime: uint64(time.Now().Unix()),
|
||||
UiSvrTime: protocol.GetTime(),
|
||||
}
|
||||
|
||||
if err := peer.Send(protocol.P_LS2CL_REP_LOGIN_SUCC, resp); err != nil {
|
||||
@@ -61,7 +62,7 @@ func (server *LoginServer) AcceptLogin(peer *Peer, SzID string, IClientVerC int3
|
||||
return nil
|
||||
}
|
||||
|
||||
func (server *LoginServer) Login(peer *Peer, 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)
|
||||
|
||||
@@ -73,22 +74,22 @@ func (server *LoginServer) Login(peer *Peer, pkt protocol.Packet) error {
|
||||
}
|
||||
|
||||
// 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 := db.DefaultDB.TryLogin(loginPkt.SzID, loginPkt.SzPassword)
|
||||
if err == db.LoginErrorInvalidID {
|
||||
account, err := server.dbHndlr.TryLogin(loginPkt.SzID, loginPkt.SzPassword)
|
||||
if errors.Is(err, db.ErrLoginInvalidID) {
|
||||
// this is the default behavior, auto create the account if the ID isn't in use
|
||||
account, err = db.DefaultDB.NewAccount(loginPkt.SzID, loginPkt.SzPassword)
|
||||
account, err = server.dbHndlr.NewAccount(loginPkt.SzID, loginPkt.SzPassword)
|
||||
if err != nil {
|
||||
// respond with a dummy login error packet so the client doesn't get softlocked
|
||||
SendError(LOGIN_DATABASE_ERROR)
|
||||
return err
|
||||
}
|
||||
} else if err == db.LoginErrorInvalidPassword {
|
||||
} else if errors.Is(err, db.ErrLoginInvalidPassword) {
|
||||
// respond with invalid password
|
||||
SendError(LOGIN_ID_AND_PASSWORD_DO_NOT_MATCH)
|
||||
return nil
|
||||
@@ -98,41 +99,46 @@ func (server *LoginServer) Login(peer *Peer, pkt protocol.Packet) error {
|
||||
}
|
||||
|
||||
// grab player data
|
||||
peer.AccountID = account.AccountID
|
||||
plrs, err := db.DefaultDB.GetPlayers(account.AccountID)
|
||||
peer.SetUserData(account)
|
||||
plrs, err := server.dbHndlr.GetPlayers(account.AccountID)
|
||||
if err != nil {
|
||||
SendError(LOGIN_DATABASE_ERROR)
|
||||
return err
|
||||
}
|
||||
log.Printf("Account (%d) %s has logged in", account.AccountID, account.Login)
|
||||
|
||||
// truncate plrs
|
||||
if len(plrs) > 3 {
|
||||
plrs = plrs[:4]
|
||||
}
|
||||
|
||||
// build character list
|
||||
charInfo := make([]protocol.SP_LS2CL_REP_CHAR_INFO, 0, 4)
|
||||
for _, plr := range plrs {
|
||||
PCStyle, PCStyle2 := util.Player2PCStyle(&plr)
|
||||
charInfo := [4]protocol.SP_LS2CL_REP_CHAR_INFO{}
|
||||
for i, plr := range plrs {
|
||||
info := protocol.SP_LS2CL_REP_CHAR_INFO{
|
||||
ISlot: int8(plr.Slot),
|
||||
ILevel: int16(plr.Level),
|
||||
SPC_Style: PCStyle,
|
||||
SPC_Style2: PCStyle2,
|
||||
IX: int32(plr.XCoordinate),
|
||||
IY: int32(plr.YCoordinate),
|
||||
IZ: int32(plr.ZCoordinate),
|
||||
SPC_Style: plr.PCStyle,
|
||||
SPC_Style2: plr.PCStyle2,
|
||||
IX: int32(plr.X),
|
||||
IY: int32(plr.Y),
|
||||
IZ: int32(plr.Z),
|
||||
}
|
||||
|
||||
AEquip, err := db.DefaultDB.GetPlayerInventorySlots(plr.PlayerID, 0, config.AEQUIP_COUNT-1)
|
||||
AEquip, err := server.dbHndlr.GetPlayerInventorySlots(plr.PlayerID, 0, config.AEQUIP_COUNT-1)
|
||||
if err != nil {
|
||||
SendError(LOGIN_DATABASE_ERROR)
|
||||
return err
|
||||
}
|
||||
|
||||
copy(info.AEquip[:], AEquip)
|
||||
charInfo = append(charInfo, info)
|
||||
charInfo[i] = info
|
||||
}
|
||||
|
||||
return server.AcceptLogin(peer, loginPkt.SzID, loginPkt.IClientVerC, 1, charInfo)
|
||||
return server.AcceptLogin(peer, loginPkt.SzID, loginPkt.IClientVerC, 1, charInfo[:len(plrs)])
|
||||
}
|
||||
|
||||
func (server *LoginServer) CheckCharacterName(peer *Peer, 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)
|
||||
|
||||
@@ -143,17 +149,18 @@ func (server *LoginServer) CheckCharacterName(peer *Peer, pkt protocol.Packet) e
|
||||
})
|
||||
}
|
||||
|
||||
func (server *LoginServer) SaveCharacterName(peer *Peer, 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!")
|
||||
return fmt.Errorf("out of order P_LS2CL_REP_SAVE_CHAR_NAME_FAIL")
|
||||
}
|
||||
|
||||
// TODO: sanity check SzFirstName && SzLastName
|
||||
PlayerID, err := db.DefaultDB.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
|
||||
@@ -191,10 +198,11 @@ func validateCharacterCreation(character *protocol.SP_CL2LS_REQ_CHAR_CREATE) boo
|
||||
return false
|
||||
}
|
||||
|
||||
// TODO: sanity check items in SOn_Item; see db.FinishPlayer()
|
||||
return true
|
||||
}
|
||||
|
||||
func SendFail(peer *Peer) 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 {
|
||||
@@ -204,37 +212,49 @@ func SendFail(peer *Peer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (server *LoginServer) CharacterCreate(peer *Peer, 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 := db.DefaultDB.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)
|
||||
}
|
||||
|
||||
plr, err := db.DefaultDB.GetPlayer(int(charPkt.PCStyle.IPC_UID))
|
||||
plr, err := server.dbHndlr.GetPlayer(int(charPkt.PCStyle.IPC_UID))
|
||||
if err != nil {
|
||||
log.Printf("Error getting player: %v", err)
|
||||
return SendFail(peer)
|
||||
}
|
||||
|
||||
PCStyle, PCStyle2 := util.Player2PCStyle(plr)
|
||||
return peer.Send(protocol.P_LS2CL_REP_CHAR_CREATE_SUCC, protocol.SP_LS2CL_REP_CHAR_CREATE_SUCC{
|
||||
ILevel: int16(plr.Level),
|
||||
SPC_Style: PCStyle,
|
||||
SPC_Style2: PCStyle2,
|
||||
SPC_Style: plr.PCStyle,
|
||||
SPC_Style2: plr.PCStyle2,
|
||||
SOn_Item: charPkt.SOn_Item, // if items were faked, we don't really care since the db only stores the sanitized fields
|
||||
})
|
||||
}
|
||||
|
||||
func (server *LoginServer) CharacterDelete(peer *Peer, 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 := db.DefaultDB.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)
|
||||
}
|
||||
@@ -244,11 +264,72 @@ func (server *LoginServer) CharacterDelete(peer *Peer, pkt protocol.Packet) erro
|
||||
})
|
||||
}
|
||||
|
||||
func (server *LoginServer) FinishTutorial(peer *Peer, 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")
|
||||
}
|
||||
|
||||
// TODO: better shard selection logic pls
|
||||
// for now, pick random shard
|
||||
shard := shards[rand.Intn(len(shards))]
|
||||
|
||||
// make sure the player is owned by the account
|
||||
plr, err := server.dbHndlr.GetPlayer(int(selection.IPC_UID))
|
||||
if err != nil {
|
||||
log.Printf("Error getting player: %v", err)
|
||||
return SendFail(peer)
|
||||
}
|
||||
accountID := account.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: accountID,
|
||||
})
|
||||
|
||||
// craft response
|
||||
resp := protocol.SP_LS2CL_REP_SHARD_SELECT_SUCC{
|
||||
G_FE_ServerPort: int32(shard.Port),
|
||||
IEnterSerialKey: key,
|
||||
}
|
||||
|
||||
// the rest of the bytes in G_FE_ServerIP will be zero'd, so there's no need to write the NULL byte
|
||||
copy(resp.G_FE_ServerIP[:], []byte(shard.IP))
|
||||
|
||||
return peer.Send(protocol.P_LS2CL_REP_SHARD_SELECT_SUCC, resp)
|
||||
}
|
||||
|
||||
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 := db.DefaultDB.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
179
login/login_test.go
Normal file
@@ -0,0 +1,179 @@
|
||||
package login_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/CPunch/gopenfusion/cnet"
|
||||
"github.com/CPunch/gopenfusion/cnet/protocol"
|
||||
"github.com/CPunch/gopenfusion/internal/db"
|
||||
"github.com/CPunch/gopenfusion/internal/redis"
|
||||
"github.com/CPunch/gopenfusion/internal/testutil"
|
||||
"github.com/CPunch/gopenfusion/login"
|
||||
"github.com/matryer/is"
|
||||
)
|
||||
|
||||
var (
|
||||
loginSrv *login.LoginServer
|
||||
loginPort int
|
||||
testDB *db.DBHandler
|
||||
rh *redis.RedisHandler
|
||||
)
|
||||
|
||||
/*
|
||||
test data was scraped by dumping packets, just adding a println to the LoginService
|
||||
to print the packet data
|
||||
*/
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
ret := 1
|
||||
defer func() {
|
||||
os.Exit(ret)
|
||||
}()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// setup environment
|
||||
var closer func()
|
||||
testDB, rh, closer = testutil.SetupEnvironment(ctx)
|
||||
defer closer()
|
||||
|
||||
var err error
|
||||
loginPort, err = cnet.RandomPort()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// start login server
|
||||
loginSrv, err = login.NewLoginServer(ctx, testDB, rh, loginPort)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := loginSrv.Start(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
// wait for login server to start, then start tests
|
||||
<-loginSrv.Service().Started()
|
||||
ret = m.Run()
|
||||
cancel()
|
||||
<-loginSrv.Service().Stopped()
|
||||
}
|
||||
|
||||
// This test tries a typical login sequence.
|
||||
func TestLoginSuccSequence(t *testing.T) {
|
||||
is := is.New(t)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
dummy := testutil.MakeDummyPeer(ctx, is, loginPort)
|
||||
defer dummy.Kill()
|
||||
|
||||
// send login request (this should create an account)
|
||||
var resp protocol.SP_LS2CL_REP_LOGIN_SUCC
|
||||
dummy.SendAndRecv(protocol.P_CL2LS_REQ_LOGIN, protocol.P_LS2CL_REP_LOGIN_SUCC,
|
||||
protocol.SP_CL2LS_REQ_LOGIN{
|
||||
SzID: "testLoginSequence",
|
||||
SzPassword: "test",
|
||||
}, &resp)
|
||||
|
||||
// verify response
|
||||
is.Equal(resp.SzID, "testLoginSequence") // should have the same ID
|
||||
is.Equal(resp.ICharCount, int8(0)) // should have 0 characters
|
||||
|
||||
// verify account was created
|
||||
_, err := testDB.TryLogin("testLoginSequence", "test")
|
||||
is.NoErr(err) // TryLogin() should not return an error
|
||||
}
|
||||
|
||||
// This test tries a typical login sequence, but with an invalid password.
|
||||
func TestLoginFailSequence(t *testing.T) {
|
||||
is := is.New(t)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
dummy := testutil.MakeDummyPeer(ctx, is, loginPort)
|
||||
defer dummy.Kill()
|
||||
|
||||
// send login request (this should not create an account)
|
||||
var resp protocol.SP_LS2CL_REP_LOGIN_FAIL
|
||||
dummy.SendAndRecv(protocol.P_CL2LS_REQ_LOGIN, protocol.P_LS2CL_REP_LOGIN_FAIL,
|
||||
protocol.SP_CL2LS_REQ_LOGIN{
|
||||
SzID: "",
|
||||
SzPassword: "",
|
||||
}, &resp)
|
||||
|
||||
// verify response
|
||||
is.Equal(resp.SzID, "") // should have the same ID
|
||||
is.Equal(resp.IErrorCode, int32(login.LOGIN_ID_AND_PASSWORD_DO_NOT_MATCH)) // should respond with LOGIN_ID_AND_PASSWORD_DO_NOT_MATCH
|
||||
}
|
||||
|
||||
// This test tries a typical login sequence, and creates a character
|
||||
func TestCharacterSequence(t *testing.T) {
|
||||
is := is.New(t)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
dummy := testutil.MakeDummyPeer(ctx, is, loginPort)
|
||||
defer dummy.Kill()
|
||||
|
||||
// send login request (this should create an account)
|
||||
var resp protocol.SP_LS2CL_REP_LOGIN_SUCC
|
||||
dummy.SendAndRecv(protocol.P_CL2LS_REQ_LOGIN, protocol.P_LS2CL_REP_LOGIN_SUCC,
|
||||
protocol.SP_CL2LS_REQ_LOGIN{
|
||||
SzID: "testCharacterSequence",
|
||||
SzPassword: "test",
|
||||
}, &resp)
|
||||
|
||||
// verify response
|
||||
is.Equal(resp.SzID, "testCharacterSequence") // should have the same ID
|
||||
is.Equal(resp.ICharCount, int8(0)) // should have 0 characters
|
||||
|
||||
// perform key swap
|
||||
dummy.Peer.E_key = protocol.CreateNewKey(
|
||||
resp.UiSvrTime,
|
||||
uint64(resp.ICharCount+1),
|
||||
uint64(resp.ISlotNum+1),
|
||||
)
|
||||
dummy.Peer.FE_key = protocol.CreateNewKey(
|
||||
binary.LittleEndian.Uint64([]byte(protocol.DEFAULT_KEY)),
|
||||
0,
|
||||
1,
|
||||
)
|
||||
|
||||
// send character name check request
|
||||
var charResp protocol.SP_LS2CL_REP_SAVE_CHAR_NAME_SUCC
|
||||
dummy.SendAndRecv(protocol.P_CL2LS_REQ_SAVE_CHAR_NAME, protocol.P_LS2CL_REP_SAVE_CHAR_NAME_SUCC,
|
||||
protocol.SP_CL2LS_REQ_SAVE_CHAR_NAME{
|
||||
ISlotNum: 1,
|
||||
IGender: 1,
|
||||
IFNCode: 260,
|
||||
ILNCode: 551,
|
||||
IMNCode: 33,
|
||||
SzFirstName: testutil.TestCharCreate.PCStyle.SzFirstName,
|
||||
SzLastName: testutil.TestCharCreate.PCStyle.SzLastName,
|
||||
}, &charResp)
|
||||
|
||||
// verify response
|
||||
is.Equal(charResp.ISlotNum, int8(1)) // should have the same slot number
|
||||
is.Equal(charResp.IGender, int8(1)) // should have the same gender
|
||||
is.Equal(charResp.SzFirstName, testutil.TestCharCreate.PCStyle.SzFirstName) // should have the same first name
|
||||
is.Equal(charResp.SzLastName, testutil.TestCharCreate.PCStyle.SzLastName) // should have the same last name
|
||||
|
||||
// send character create request
|
||||
charCreate := testutil.TestCharCreate
|
||||
charCreate.PCStyle.IPC_UID = charResp.IPC_UID
|
||||
var charCreateResp protocol.SP_LS2CL_REP_CHAR_CREATE_SUCC
|
||||
dummy.SendAndRecv(protocol.P_CL2LS_REQ_CHAR_CREATE, protocol.P_LS2CL_REP_CHAR_CREATE_SUCC,
|
||||
charCreate, &charCreateResp)
|
||||
|
||||
// verify response
|
||||
is.Equal(charCreate.PCStyle, charCreateResp.SPC_Style) // should have the same PCStyle
|
||||
is.Equal(charCreate.SOn_Item, charCreateResp.SOn_Item) // should have the same SOn_Item
|
||||
}
|
||||
51
login/loginserver.go
Normal file
51
login/loginserver.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package login
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/CPunch/gopenfusion/cnet"
|
||||
"github.com/CPunch/gopenfusion/cnet/protocol"
|
||||
"github.com/CPunch/gopenfusion/internal/db"
|
||||
"github.com/CPunch/gopenfusion/internal/redis"
|
||||
)
|
||||
|
||||
type LoginServer struct {
|
||||
service *cnet.Service
|
||||
dbHndlr *db.DBHandler
|
||||
redisHndlr *redis.RedisHandler
|
||||
}
|
||||
|
||||
func NewLoginServer(ctx context.Context, dbHndlr *db.DBHandler, redisHndlr *redis.RedisHandler, port int) (*LoginServer, error) {
|
||||
srvc := cnet.NewService(ctx, "LOGIN", port)
|
||||
|
||||
server := &LoginServer{
|
||||
service: srvc,
|
||||
dbHndlr: dbHndlr,
|
||||
redisHndlr: redisHndlr,
|
||||
}
|
||||
|
||||
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) Service() *cnet.Service {
|
||||
return server.service
|
||||
}
|
||||
|
||||
func (server *LoginServer) Start() error {
|
||||
return server.service.Start()
|
||||
}
|
||||
14
main.go
14
main.go
@@ -1,14 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/CPunch/gopenfusion/db"
|
||||
"github.com/CPunch/gopenfusion/server"
|
||||
)
|
||||
|
||||
func main() {
|
||||
db.DefaultDB, _ = db.OpenLiteDB("test.db")
|
||||
db.DefaultDB.Setup()
|
||||
|
||||
server := server.NewLoginServer()
|
||||
server.Start()
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package pool
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var allocator = sync.Pool{
|
||||
New: func() any { return new(bytes.Buffer) },
|
||||
}
|
||||
|
||||
func Get() *bytes.Buffer {
|
||||
return allocator.Get().(*bytes.Buffer)
|
||||
}
|
||||
|
||||
func Put(buf *bytes.Buffer) {
|
||||
buf.Reset()
|
||||
allocator.Put(buf)
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/CPunch/gopenfusion/protocol"
|
||||
)
|
||||
|
||||
type PacketHandler func(peer *Peer, pkt protocol.Packet) error
|
||||
|
||||
type LoginServer struct {
|
||||
listener net.Listener
|
||||
packetHandlers map[uint32]PacketHandler
|
||||
peers map[*Peer]bool
|
||||
peersLock sync.Mutex
|
||||
}
|
||||
|
||||
func NewLoginServer() *LoginServer {
|
||||
listener, err := net.Listen("tcp", ":23000")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
loginServer := &LoginServer{
|
||||
listener: listener,
|
||||
packetHandlers: make(map[uint32]PacketHandler),
|
||||
peers: make(map[*Peer]bool),
|
||||
}
|
||||
|
||||
loginServer.RegisterPacketHandler(protocol.P_CL2LS_REQ_LOGIN, loginServer.Login)
|
||||
loginServer.RegisterPacketHandler(protocol.P_CL2LS_REQ_CHECK_CHAR_NAME, loginServer.CheckCharacterName)
|
||||
loginServer.RegisterPacketHandler(protocol.P_CL2LS_REQ_SAVE_CHAR_NAME, loginServer.SaveCharacterName)
|
||||
loginServer.RegisterPacketHandler(protocol.P_CL2LS_REQ_CHAR_CREATE, loginServer.CharacterCreate)
|
||||
// loginServer.RegisterPacketHandler(protocol.P_CL2LS_REQ_CHAR_SELECT, func(_ *Peer, _ protocol.Packet) error { /* stubbed */ return nil })
|
||||
loginServer.RegisterPacketHandler(protocol.P_CL2LS_REQ_CHAR_DELETE, loginServer.CharacterDelete)
|
||||
// loginServer.RegisterPacketHandler(protocol.P_CL2LS_REQ_SHARD_SELECT, func(_ *Peer, _ protocol.Packet) error { /* stubbed */ return nil })
|
||||
// loginServer.RegisterPacketHandler(protocol.P_CL2LS_REQ_SHARD_LIST_INFO, func(_ *Peer, _ protocol.Packet) error { /* stubbed */ return nil })
|
||||
// loginServer.RegisterPacketHandler(protocol.P_CL2LS_CHECK_NAME_LIST, func(_ *Peer, _ protocol.Packet) error { /* stubbed */ return nil })
|
||||
loginServer.RegisterPacketHandler(protocol.P_CL2LS_REQ_SAVE_CHAR_TUTOR, loginServer.FinishTutorial)
|
||||
// loginServer.RegisterPacketHandler(protocol.P_CL2LS_REQ_PC_EXIT_DUPLICATE, func(_ *Peer, _ protocol.Packet) error { /* stubbed */ return nil })
|
||||
// loginServer.RegisterPacketHandler(protocol.P_CL2LS_REP_LIVE_CHECK, func(_ *Peer, _ protocol.Packet) error { /* stubbed */ return nil })
|
||||
// loginServer.RegisterPacketHandler(protocol.P_CL2LS_REQ_CHANGE_CHAR_NAME, func(_ *Peer, _ protocol.Packet) error { /* stubbed */ return nil })
|
||||
// loginServer.RegisterPacketHandler(protocol.P_CL2LS_REQ_SERVER_SELECT, func(_ *Peer, _ protocol.Packet) error { /* stubbed */ return nil })
|
||||
|
||||
return loginServer
|
||||
}
|
||||
|
||||
func (server *LoginServer) RegisterPacketHandler(typeID uint32, hndlr PacketHandler) {
|
||||
server.packetHandlers[typeID] = hndlr
|
||||
}
|
||||
|
||||
func (server *LoginServer) Start() {
|
||||
log.Print("Server hosted on 127.0.0.1:23000")
|
||||
|
||||
for {
|
||||
conn, err := server.listener.Accept()
|
||||
if err != nil {
|
||||
log.Println("Connection error: ", err)
|
||||
return
|
||||
}
|
||||
|
||||
client := NewPeer(server, conn)
|
||||
server.Connect(client)
|
||||
go client.Handler()
|
||||
}
|
||||
}
|
||||
|
||||
func (server *LoginServer) HandlePacket(peer *Peer, 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] unsupported packet ID: %x\n", typeID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (server *LoginServer) Disconnect(peer *Peer) {
|
||||
server.peersLock.Lock()
|
||||
delete(server.peers, peer)
|
||||
server.peersLock.Unlock()
|
||||
}
|
||||
|
||||
func (server *LoginServer) Connect(peer *Peer) {
|
||||
server.peersLock.Lock()
|
||||
server.peers[peer] = true
|
||||
server.peersLock.Unlock()
|
||||
}
|
||||
148
server/peer.go
148
server/peer.go
@@ -1,148 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
|
||||
"github.com/CPunch/gopenfusion/db"
|
||||
"github.com/CPunch/gopenfusion/protocol"
|
||||
"github.com/CPunch/gopenfusion/protocol/pool"
|
||||
)
|
||||
|
||||
const (
|
||||
USE_E = iota
|
||||
USE_FE
|
||||
)
|
||||
|
||||
type PeerHandler interface {
|
||||
HandlePacket(client *Peer, typeID uint32, pkt protocol.Packet) error
|
||||
Connect(client *Peer)
|
||||
Disconnect(client *Peer)
|
||||
}
|
||||
|
||||
type Peer struct {
|
||||
E_key []byte
|
||||
FE_key []byte
|
||||
SzID string
|
||||
AccountID int
|
||||
Player *db.Player
|
||||
handler PeerHandler
|
||||
conn net.Conn
|
||||
alive bool
|
||||
whichKey int
|
||||
}
|
||||
|
||||
func NewPeer(handler PeerHandler, conn net.Conn) *Peer {
|
||||
return &Peer{
|
||||
E_key: []byte(protocol.DEFAULT_KEY),
|
||||
FE_key: nil,
|
||||
SzID: "",
|
||||
AccountID: -1,
|
||||
Player: nil,
|
||||
handler: handler,
|
||||
conn: conn,
|
||||
alive: true,
|
||||
whichKey: USE_E,
|
||||
}
|
||||
}
|
||||
|
||||
func (peer *Peer) Send(typeID uint32, data ...interface{}) error {
|
||||
buf := pool.Get()
|
||||
defer pool.Put(buf) // always return the buffer to the pool
|
||||
|
||||
// body start
|
||||
pkt := protocol.NewPacket(buf)
|
||||
|
||||
// encode type id
|
||||
if err := pkt.Encode(uint32(typeID)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// encode data
|
||||
for _, trailer := range data {
|
||||
if err := pkt.Encode(trailer); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// encrypt body
|
||||
switch peer.whichKey {
|
||||
case USE_E:
|
||||
protocol.EncryptData(buf.Bytes(), peer.E_key)
|
||||
case USE_FE:
|
||||
protocol.EncryptData(buf.Bytes(), peer.FE_key)
|
||||
}
|
||||
|
||||
// write packet size
|
||||
if err := binary.Write(peer.conn, binary.LittleEndian, uint32(buf.Len())); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// write packet body
|
||||
log.Printf("Sending %#v, sizeof: %d", data, buf.Len())
|
||||
if _, err := peer.conn.Write(buf.Bytes()); err != nil {
|
||||
return fmt.Errorf("[FATAL] failed to write packet body! %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (peer *Peer) Kill() {
|
||||
if !peer.alive {
|
||||
return
|
||||
}
|
||||
|
||||
peer.alive = false
|
||||
peer.conn.Close()
|
||||
peer.handler.Disconnect(peer)
|
||||
}
|
||||
|
||||
func (peer *Peer) 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 > protocol.CN_PACKET_BUFFER_SIZE || sz < 4 {
|
||||
log.Printf("[FATAL] malicious packet size received! %d", sz)
|
||||
return
|
||||
}
|
||||
|
||||
// read packet body
|
||||
buf := pool.Get()
|
||||
if _, err := buf.ReadFrom(io.LimitReader(peer.conn, int64(sz))); err != nil {
|
||||
log.Printf("[FATAL] failed to read packet body! %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// decrypt
|
||||
protocol.DecryptData(buf.Bytes(), peer.E_key)
|
||||
pkt := protocol.NewPacket(buf)
|
||||
|
||||
// create packet && read typeID
|
||||
var typeID uint32
|
||||
if err := pkt.Decode(&typeID); err != nil {
|
||||
log.Printf("[FATAL] failed to read packet type! %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// dispatch packet
|
||||
log.Printf("Got packet ID: %x, with a sizeof: %d\n", typeID, sz)
|
||||
if err := peer.handler.HandlePacket(peer, typeID, pkt); err != nil {
|
||||
log.Printf("[FATAL] %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// restore buffer to pool
|
||||
pool.Put(buf)
|
||||
}
|
||||
}
|
||||
59
shard/chat.go
Normal file
59
shard/chat.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package shard
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"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)
|
||||
|
||||
plr, ok := peer.UserData().(*entity.Player)
|
||||
if !ok || plr == nil {
|
||||
return fmt.Errorf("freeChat: plr is nil")
|
||||
}
|
||||
|
||||
// spread message
|
||||
return server.sendAllPacket(plr, protocol.P_FE2CL_REP_SEND_FREECHAT_MESSAGE_SUCC, protocol.SP_FE2CL_REP_SEND_FREECHAT_MESSAGE_SUCC{
|
||||
IPC_ID: int32(plr.PlayerID),
|
||||
SzFreeChat: chat.SzFreeChat,
|
||||
IEmoteCode: chat.IEmoteCode,
|
||||
})
|
||||
}
|
||||
|
||||
func (server *ShardServer) menuChat(peer *cnet.Peer, pkt protocol.Packet) error {
|
||||
var chat protocol.SP_CL2FE_REQ_SEND_MENUCHAT_MESSAGE
|
||||
pkt.Decode(&chat)
|
||||
|
||||
plr, ok := peer.UserData().(*entity.Player)
|
||||
if !ok || plr == nil {
|
||||
return fmt.Errorf("menuChat: plr is nil")
|
||||
}
|
||||
|
||||
// spread message
|
||||
return server.sendAllPacket(plr, protocol.P_FE2CL_REP_SEND_MENUCHAT_MESSAGE_SUCC, protocol.SP_FE2CL_REP_SEND_MENUCHAT_MESSAGE_SUCC{
|
||||
IPC_ID: int32(plr.PlayerID),
|
||||
SzFreeChat: chat.SzFreeChat,
|
||||
IEmoteCode: chat.IEmoteCode,
|
||||
})
|
||||
}
|
||||
|
||||
func (server *ShardServer) emoteChat(peer *cnet.Peer, pkt protocol.Packet) error {
|
||||
var chat protocol.SP_CL2FE_REQ_PC_AVATAR_EMOTES_CHAT
|
||||
pkt.Decode(&chat)
|
||||
|
||||
plr, ok := peer.UserData().(*entity.Player)
|
||||
if !ok || plr == nil {
|
||||
return fmt.Errorf("emoteChat: plr is nil")
|
||||
}
|
||||
|
||||
// spread message
|
||||
return server.sendAllPacket(plr, protocol.P_FE2CL_REP_PC_AVATAR_EMOTES_CHAT, protocol.SP_FE2CL_REP_PC_AVATAR_EMOTES_CHAT{
|
||||
IID_From: int32(plr.PlayerID),
|
||||
IEmoteCode: chat.IEmoteCode,
|
||||
})
|
||||
}
|
||||
115
shard/entity/chunk.go
Normal file
115
shard/entity/chunk.go
Normal file
@@ -0,0 +1,115 @@
|
||||
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{}
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
func NewChunk(position ChunkPosition) *Chunk {
|
||||
return &Chunk{
|
||||
Position: position,
|
||||
entities: make(map[Entity]struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Chunk) AddEntity(entity Entity) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
c.entities[entity] = struct{}{}
|
||||
}
|
||||
|
||||
func (c *Chunk) RemoveEntity(entity Entity) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
delete(c.entities, entity)
|
||||
}
|
||||
|
||||
// send packet to all peers in this chunk and kill each peer if error
|
||||
func (c *Chunk) SendPacket(typeID uint32, pkt ...interface{}) {
|
||||
c.SendPacketExclude(nil, typeID, pkt...)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
for entity := range c.entities {
|
||||
if f(entity) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Chunk) SendPacketExclude(exclude Entity, typeID uint32, pkt ...interface{}) {
|
||||
c.ForEachEntity(func(entity Entity) bool {
|
||||
// only send to players, and exclude the player that sent the packet
|
||||
if entity.GetKind() != ENTITY_KIND_PLAYER || entity == exclude {
|
||||
return false
|
||||
}
|
||||
|
||||
plr, ok := entity.(*Player)
|
||||
if !ok {
|
||||
log.Panic("Chunk.SendPacket: entity kind was player, but is not a *Player")
|
||||
}
|
||||
peer := plr.Peer
|
||||
|
||||
if err := peer.Send(typeID, pkt...); err != nil {
|
||||
log.Printf("Error sending packet to peer %p: %v", peer, err)
|
||||
peer.Kill()
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Chunk) GetAdjacentPositions() []ChunkPosition {
|
||||
return []ChunkPosition{
|
||||
{c.Position.X - 1, c.Position.Y - 1},
|
||||
{c.Position.X - 1, c.Position.Y},
|
||||
{c.Position.X - 1, c.Position.Y + 1},
|
||||
{c.Position.X, c.Position.Y - 1},
|
||||
{c.Position.X, c.Position.Y},
|
||||
{c.Position.X, c.Position.Y + 1},
|
||||
{c.Position.X + 1, c.Position.Y - 1},
|
||||
{c.Position.X + 1, c.Position.Y},
|
||||
{c.Position.X + 1, c.Position.Y + 1},
|
||||
}
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/45428032 lol
|
||||
func ChunkSliceDifference(a, b []*Chunk) []*Chunk {
|
||||
m := make(map[*Chunk]struct{})
|
||||
for _, item := range b {
|
||||
m[item] = struct{}{}
|
||||
}
|
||||
|
||||
var diff []*Chunk
|
||||
for _, item := range a {
|
||||
if _, ok := m[item]; !ok {
|
||||
diff = append(diff, item)
|
||||
}
|
||||
}
|
||||
|
||||
return diff
|
||||
}
|
||||
25
shard/entity/entity.go
Normal file
25
shard/entity/entity.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package entity
|
||||
|
||||
import "github.com/CPunch/gopenfusion/cnet"
|
||||
|
||||
type EntityKind int
|
||||
|
||||
const (
|
||||
ENTITY_KIND_PLAYER EntityKind = iota
|
||||
ENTITY_KIND_NPC
|
||||
)
|
||||
|
||||
type Entity interface {
|
||||
GetKind() EntityKind
|
||||
|
||||
GetChunkPos() ChunkPosition
|
||||
GetPosition() (x int, y int, z int)
|
||||
GetAngle() int
|
||||
|
||||
SetChunkPos(chunk ChunkPosition)
|
||||
SetPosition(x, y, z int)
|
||||
SetAngle(angle int)
|
||||
|
||||
DisappearFromViewOf(peer *cnet.Peer)
|
||||
EnterIntoViewOf(peer *cnet.Peer)
|
||||
}
|
||||
36
shard/entity/entity_test.go
Normal file
36
shard/entity/entity_test.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package entity_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"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)),
|
||||
entity.NewChunk(entity.MakeChunkPosition(1, 0)),
|
||||
entity.NewChunk(entity.MakeChunkPosition(1, 1)),
|
||||
}
|
||||
|
||||
c1 := []*entity.Chunk{
|
||||
chunks[0],
|
||||
chunks[1],
|
||||
chunks[2],
|
||||
chunks[3],
|
||||
}
|
||||
|
||||
c2 := []*entity.Chunk{
|
||||
chunks[0],
|
||||
chunks[1],
|
||||
chunks[2],
|
||||
}
|
||||
|
||||
diff := entity.ChunkSliceDifference(c1, c2)
|
||||
|
||||
is.True(len(diff) == 1) // should be 1 chunk in difference
|
||||
is.True(diff[0] == chunks[3]) // should be chunks[3] in difference
|
||||
}
|
||||
88
shard/entity/npc.go
Normal file
88
shard/entity/npc.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/CPunch/gopenfusion/cnet"
|
||||
"github.com/CPunch/gopenfusion/cnet/protocol"
|
||||
)
|
||||
|
||||
type NPC struct {
|
||||
ID int
|
||||
X int `json:"iX"`
|
||||
Y int `json:"iY"`
|
||||
Z int `json:"iZ"`
|
||||
Angle int `json:"iAngle"`
|
||||
NPCType int `json:"iNPCType"`
|
||||
Chunk ChunkPosition
|
||||
}
|
||||
|
||||
var nextNPCID = &atomic.Int32{}
|
||||
|
||||
func NewNPC(X, Y, Z, Angle int, npcType int) *NPC {
|
||||
return &NPC{
|
||||
ID: int(nextNPCID.Add(1)),
|
||||
X: X,
|
||||
Y: Y,
|
||||
Z: Z,
|
||||
Angle: Angle,
|
||||
NPCType: npcType,
|
||||
Chunk: MakeChunkPosition(X, Y),
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Entity interface ====================
|
||||
|
||||
func (npc *NPC) GetKind() EntityKind {
|
||||
return ENTITY_KIND_NPC
|
||||
}
|
||||
|
||||
func (npc *NPC) GetChunkPos() ChunkPosition {
|
||||
return npc.Chunk
|
||||
}
|
||||
|
||||
func (npc *NPC) GetPosition() (x int, y int, z int) {
|
||||
return npc.X, npc.Y, npc.Z
|
||||
}
|
||||
|
||||
func (npc *NPC) GetAngle() int {
|
||||
return npc.Angle
|
||||
}
|
||||
|
||||
func (npc *NPC) SetChunkPos(chunk ChunkPosition) {
|
||||
npc.Chunk = chunk
|
||||
}
|
||||
|
||||
func (npc *NPC) SetPosition(x, y, z int) {
|
||||
npc.X = x
|
||||
npc.Y = y
|
||||
npc.Z = z
|
||||
}
|
||||
|
||||
func (npc *NPC) SetAngle(angle int) {
|
||||
npc.Angle = angle
|
||||
}
|
||||
|
||||
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 *cnet.Peer) {
|
||||
peer.Send(protocol.P_FE2CL_NPC_NEW, protocol.SP_FE2CL_NPC_NEW{
|
||||
NPCAppearanceData: npc.GetAppearanceData(),
|
||||
})
|
||||
}
|
||||
|
||||
func (npc *NPC) GetAppearanceData() protocol.SNPCAppearanceData {
|
||||
return protocol.SNPCAppearanceData{
|
||||
INPC_ID: int32(npc.ID),
|
||||
INPCType: int32(npc.NPCType),
|
||||
IHP: 100,
|
||||
IX: int32(npc.X),
|
||||
IY: int32(npc.Y),
|
||||
IZ: int32(npc.Z),
|
||||
IAngle: int32(npc.Angle),
|
||||
}
|
||||
}
|
||||
109
shard/entity/player.go
Normal file
109
shard/entity/player.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"github.com/CPunch/gopenfusion/cnet"
|
||||
"github.com/CPunch/gopenfusion/cnet/protocol"
|
||||
"github.com/CPunch/gopenfusion/internal/db"
|
||||
)
|
||||
|
||||
type Player struct {
|
||||
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 ====================
|
||||
|
||||
func (plr *Player) GetKind() EntityKind {
|
||||
return ENTITY_KIND_PLAYER
|
||||
}
|
||||
|
||||
func (plr *Player) GetChunkPos() ChunkPosition {
|
||||
return plr.Chunk
|
||||
}
|
||||
|
||||
func (plr *Player) GetPosition() (x int, y int, z int) {
|
||||
return plr.X, plr.Y, plr.Z
|
||||
}
|
||||
|
||||
func (plr *Player) GetAngle() int {
|
||||
return plr.Angle
|
||||
}
|
||||
|
||||
func (plr *Player) SetChunkPos(chunk ChunkPosition) {
|
||||
plr.Chunk = chunk
|
||||
}
|
||||
|
||||
func (plr *Player) SetPosition(x, y, z int) {
|
||||
plr.X = x
|
||||
plr.Y = y
|
||||
plr.Z = z
|
||||
}
|
||||
|
||||
func (plr *Player) SetAngle(angle int) {
|
||||
plr.Angle = angle
|
||||
}
|
||||
|
||||
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 *cnet.Peer) {
|
||||
peer.Send(protocol.P_FE2CL_PC_NEW, protocol.SP_FE2CL_PC_NEW{
|
||||
PCAppearanceData: plr.GetAppearanceData(),
|
||||
})
|
||||
}
|
||||
|
||||
func (plr *Player) ToPCLoadData2CL() protocol.SPCLoadData2CL {
|
||||
return protocol.SPCLoadData2CL{
|
||||
IUserLevel: int16(plr.AccountLevel),
|
||||
PCStyle: plr.PCStyle,
|
||||
PCStyle2: plr.PCStyle2,
|
||||
IMentor: int16(plr.Mentor),
|
||||
IMentorCount: 1,
|
||||
IHP: int32(plr.HP),
|
||||
IBatteryW: int32(plr.BatteryW),
|
||||
IBatteryN: int32(plr.BatteryN),
|
||||
ICandy: int32(plr.Taros),
|
||||
IFusionMatter: int32(plr.FusionMatter),
|
||||
ISpecialState: 0,
|
||||
IMapNum: 0,
|
||||
IX: int32(plr.X),
|
||||
IY: int32(plr.Y),
|
||||
IZ: int32(plr.Z),
|
||||
IAngle: int32(plr.Angle),
|
||||
AEquip: plr.Equip,
|
||||
AInven: plr.Inven,
|
||||
ANanoSlots: [3]int16{int16(plr.EquippedNanos[0]), int16(plr.EquippedNanos[1]), int16(plr.EquippedNanos[2])},
|
||||
IActiveNanoSlotNum: int16(plr.ActiveNanoSlotNum),
|
||||
IWarpLocationFlag: int32(plr.WarpLocationFlag),
|
||||
IBuddyWarpTime: 60,
|
||||
IFatigue: 50,
|
||||
}
|
||||
}
|
||||
|
||||
func (plr *Player) GetAppearanceData() protocol.SPCAppearanceData {
|
||||
return protocol.SPCAppearanceData{
|
||||
IID: int32(plr.PlayerID),
|
||||
IHP: int32(plr.HP),
|
||||
ILv: int16(plr.Level),
|
||||
IX: int32(plr.X),
|
||||
IY: int32(plr.Y),
|
||||
IZ: int32(plr.Z),
|
||||
IAngle: int32(plr.Angle),
|
||||
PCStyle: plr.PCStyle,
|
||||
IPCState: plr.IPCState,
|
||||
ItemEquip: plr.Equip,
|
||||
Nano: plr.Nanos[plr.ActiveNanoSlotNum],
|
||||
}
|
||||
}
|
||||
126
shard/entitymanager.go
Normal file
126
shard/entitymanager.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package shard
|
||||
|
||||
import (
|
||||
"github.com/CPunch/gopenfusion/shard/entity"
|
||||
)
|
||||
|
||||
func (server *ShardServer) addEntity(e entity.Entity) {
|
||||
pos := e.GetChunkPos()
|
||||
server.addEntityToChunks(e, server.getViewableChunks(pos))
|
||||
server.getChunk(pos).AddEntity(e)
|
||||
}
|
||||
|
||||
func (server *ShardServer) removeEntity(e entity.Entity) {
|
||||
// TODO: chunk cleanup
|
||||
pos := e.GetChunkPos()
|
||||
server.removeEntityFromChunks(e, server.getViewableChunks(pos))
|
||||
server.getChunk(pos).RemoveEntity(e)
|
||||
}
|
||||
|
||||
func (server *ShardServer) getChunk(pos entity.ChunkPosition) *entity.Chunk {
|
||||
chunk, ok := server.chunks[pos]
|
||||
if !ok {
|
||||
chunk = entity.NewChunk(pos)
|
||||
server.chunks[pos] = chunk
|
||||
}
|
||||
|
||||
return chunk
|
||||
}
|
||||
|
||||
func (server *ShardServer) getViewableChunks(pos entity.ChunkPosition) []*entity.Chunk {
|
||||
chunks := make([]*entity.Chunk, 0, 9)
|
||||
for _, pos := range server.getChunk(pos).GetAdjacentPositions() {
|
||||
chunks = append(chunks, server.getChunk(pos))
|
||||
}
|
||||
|
||||
return chunks
|
||||
}
|
||||
|
||||
// sends a packet to all peers in the given chunks, excluding the given peer
|
||||
func (server *ShardServer) sendOthersPacket(plr *entity.Player, typeID uint32, pkt ...interface{}) error {
|
||||
chunks := server.getViewableChunks(plr.Chunk)
|
||||
for _, chunk := range chunks {
|
||||
chunk.SendPacketExclude(plr, typeID, pkt...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// sends a packet to all peers in the given chunks
|
||||
func (server *ShardServer) sendAllPacket(plr *entity.Player, typeID uint32, pkt ...interface{}) error {
|
||||
chunks := server.getViewableChunks(plr.Chunk)
|
||||
for _, chunk := range chunks {
|
||||
chunk.SendPacket(typeID, pkt...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (server *ShardServer) removeEntityFromChunks(this entity.Entity, chunks []*entity.Chunk) {
|
||||
for _, chunk := range chunks {
|
||||
chunk.ForEachEntity(func(e entity.Entity) bool {
|
||||
if e == this {
|
||||
return false
|
||||
}
|
||||
|
||||
// notify other players we're leaving
|
||||
if e.GetKind() == entity.ENTITY_KIND_PLAYER {
|
||||
otherPlr := e.(*entity.Player)
|
||||
this.DisappearFromViewOf(otherPlr.Peer)
|
||||
}
|
||||
|
||||
// notify us they're leaving
|
||||
if this.GetKind() == entity.ENTITY_KIND_PLAYER {
|
||||
thisPlr := this.(*entity.Player)
|
||||
e.DisappearFromViewOf(thisPlr.Peer)
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (server *ShardServer) addEntityToChunks(this entity.Entity, chunks []*entity.Chunk) {
|
||||
for _, chunk := range chunks {
|
||||
chunk.ForEachEntity(func(e entity.Entity) bool {
|
||||
if e == this {
|
||||
return false
|
||||
}
|
||||
|
||||
// notify other players we're entering
|
||||
if e.GetKind() == entity.ENTITY_KIND_PLAYER {
|
||||
otherPlr := e.(*entity.Player)
|
||||
this.EnterIntoViewOf(otherPlr.Peer)
|
||||
}
|
||||
|
||||
// notify us they're entering
|
||||
if this.GetKind() == entity.ENTITY_KIND_PLAYER {
|
||||
thisPlr := this.(*entity.Player)
|
||||
e.EnterIntoViewOf(thisPlr.Peer)
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (server *ShardServer) updateEntityChunk(e entity.Entity, from entity.ChunkPosition, to entity.ChunkPosition) {
|
||||
// no change needed
|
||||
if from == to {
|
||||
return
|
||||
}
|
||||
|
||||
oldViewables := server.getViewableChunks(from)
|
||||
newViewables := server.getViewableChunks(to)
|
||||
|
||||
// compute differences
|
||||
toExit := entity.ChunkSliceDifference(oldViewables, newViewables)
|
||||
toEnter := entity.ChunkSliceDifference(newViewables, oldViewables)
|
||||
|
||||
// update chunks
|
||||
server.removeEntityFromChunks(e, toExit)
|
||||
server.addEntityToChunks(e, toEnter)
|
||||
server.getChunk(from).RemoveEntity(e)
|
||||
server.getChunk(to).AddEntity(e)
|
||||
e.SetChunkPos(to)
|
||||
}
|
||||
92
shard/join.go
Normal file
92
shard/join.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package shard
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"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 *cnet.Peer, meta redis.LoginMetadata) (*entity.Player, error) {
|
||||
dbPlr, err := server.dbHndlr.GetPlayer(int(meta.PlayerID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
plr := entity.NewPlayer(peer, dbPlr)
|
||||
|
||||
// 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 *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 :(
|
||||
peer.Send(protocol.P_FE2CL_REP_PC_ENTER_FAIL, protocol.SP_FE2CL_REP_PC_ENTER_FAIL{})
|
||||
return err
|
||||
}
|
||||
|
||||
plr, err := server.attachPlayer(peer, loginData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp := &protocol.SP_FE2CL_REP_PC_ENTER_SUCC{
|
||||
IID: int32(plr.PlayerID),
|
||||
PCLoadData2CL: plr.ToPCLoadData2CL(),
|
||||
UiSvrTime: protocol.GetTime(),
|
||||
}
|
||||
|
||||
// setup peer
|
||||
peer.E_key = protocol.CreateNewKey(resp.UiSvrTime, uint64(resp.IID+1), uint64(resp.PCLoadData2CL.IFusionMatter+1))
|
||||
peer.FE_key = loginData.FEKey
|
||||
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
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (server *ShardServer) LoadingComplete(peer *cnet.Peer, pkt protocol.Packet) error {
|
||||
var loadComplete protocol.SP_CL2FE_REQ_PC_LOADING_COMPLETE
|
||||
pkt.Decode(&loadComplete)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
95
shard/movement.go
Normal file
95
shard/movement.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package shard
|
||||
|
||||
import (
|
||||
"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) {
|
||||
plr.X = X
|
||||
plr.Y = Y
|
||||
plr.Z = Z
|
||||
plr.Angle = Angle
|
||||
server.updateEntityChunk(plr, plr.GetChunkPos(), entity.MakeChunkPosition(X, Y))
|
||||
}
|
||||
|
||||
func (server *ShardServer) playerMove(peer *cnet.Peer, pkt protocol.Packet) error {
|
||||
var move protocol.SP_CL2FE_REQ_PC_MOVE
|
||||
pkt.Decode(&move)
|
||||
|
||||
plr, ok := peer.UserData().(*entity.Player)
|
||||
if !ok || plr == nil {
|
||||
return fmt.Errorf("playerMove: plr is nil")
|
||||
}
|
||||
|
||||
// update chunking
|
||||
server.updatePlayerPosition(plr, int(move.IX), int(move.IY), int(move.IZ), int(move.IAngle))
|
||||
|
||||
return server.sendOthersPacket(plr, protocol.P_FE2CL_PC_MOVE, protocol.SP_FE2CL_PC_MOVE{
|
||||
ICliTime: move.ICliTime,
|
||||
IX: move.IX,
|
||||
IY: move.IY,
|
||||
IZ: move.IZ,
|
||||
FVX: move.FVX,
|
||||
FVY: move.FVY,
|
||||
FVZ: move.FVZ,
|
||||
IAngle: move.IAngle,
|
||||
CKeyValue: move.CKeyValue,
|
||||
ISpeed: move.ISpeed,
|
||||
IID: int32(plr.PlayerID),
|
||||
ISvrTime: protocol.GetTime(),
|
||||
})
|
||||
}
|
||||
|
||||
func (server *ShardServer) playerStop(peer *cnet.Peer, pkt protocol.Packet) error {
|
||||
var stop protocol.SP_CL2FE_REQ_PC_STOP
|
||||
pkt.Decode(&stop)
|
||||
|
||||
plr, ok := peer.UserData().(*entity.Player)
|
||||
if !ok || plr == nil {
|
||||
return fmt.Errorf("playerStop: plr is nil")
|
||||
}
|
||||
|
||||
// update chunking
|
||||
server.updatePlayerPosition(plr, int(stop.IX), int(stop.IY), int(stop.IZ), plr.Angle)
|
||||
|
||||
return server.sendOthersPacket(plr, protocol.P_FE2CL_PC_STOP, protocol.SP_FE2CL_PC_STOP{
|
||||
ICliTime: stop.ICliTime,
|
||||
IX: stop.IX,
|
||||
IY: stop.IY,
|
||||
IZ: stop.IZ,
|
||||
IID: int32(plr.PlayerID),
|
||||
ISvrTime: protocol.GetTime(),
|
||||
})
|
||||
}
|
||||
|
||||
func (server *ShardServer) playerJump(peer *cnet.Peer, pkt protocol.Packet) error {
|
||||
var jump protocol.SP_CL2FE_REQ_PC_JUMP
|
||||
pkt.Decode(&jump)
|
||||
|
||||
plr, ok := peer.UserData().(*entity.Player)
|
||||
if !ok || plr == nil {
|
||||
return fmt.Errorf("playerJump: _plr is nil")
|
||||
}
|
||||
|
||||
// update chunking
|
||||
server.updatePlayerPosition(plr, int(jump.IX), int(jump.IY), int(jump.IZ), plr.Angle)
|
||||
|
||||
return server.sendOthersPacket(plr, protocol.P_FE2CL_PC_JUMP, protocol.SP_FE2CL_PC_JUMP{
|
||||
ICliTime: jump.ICliTime,
|
||||
IX: jump.IX,
|
||||
IY: jump.IY,
|
||||
IZ: jump.IZ,
|
||||
IVX: jump.IVX,
|
||||
IVY: jump.IVY,
|
||||
IVZ: jump.IVZ,
|
||||
IAngle: jump.IAngle,
|
||||
CKeyValue: jump.CKeyValue,
|
||||
ISpeed: jump.ISpeed,
|
||||
IID: int32(plr.PlayerID),
|
||||
ISvrTime: protocol.GetTime(),
|
||||
})
|
||||
}
|
||||
33
shard/npcloader.go
Normal file
33
shard/npcloader.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package shard
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/CPunch/gopenfusion/internal/config"
|
||||
"github.com/CPunch/gopenfusion/shard/entity"
|
||||
)
|
||||
|
||||
type NPCData struct {
|
||||
NPCs map[string]entity.NPC `json:"NPCs"`
|
||||
}
|
||||
|
||||
func (server *ShardServer) LoadNPCs() {
|
||||
log.Print("Loading NPCs...")
|
||||
|
||||
data, err := os.ReadFile(config.GetTDataPath() + "/NPCs.json")
|
||||
if err != nil {
|
||||
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
|
||||
var NPCs NPCData
|
||||
json.Unmarshal(data, &NPCs)
|
||||
for _, npc := range NPCs.NPCs {
|
||||
server.addEntity(entity.NewNPC(npc.X, npc.Y, npc.Z, npc.Angle, npc.NPCType))
|
||||
}
|
||||
|
||||
log.Printf("Loaded %d NPCs!", len(NPCs.NPCs))
|
||||
}
|
||||
72
shard/shardserver.go
Normal file
72
shard/shardserver.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package shard
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"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/redis"
|
||||
"github.com/CPunch/gopenfusion/shard/entity"
|
||||
)
|
||||
|
||||
type PacketHandler func(peer *cnet.Peer, pkt protocol.Packet) error
|
||||
|
||||
type ShardServer struct {
|
||||
service *cnet.Service
|
||||
dbHndlr *db.DBHandler
|
||||
redisHndlr *redis.RedisHandler
|
||||
chunks map[entity.ChunkPosition]*entity.Chunk
|
||||
}
|
||||
|
||||
func NewShardServer(ctx context.Context, dbHndlr *db.DBHandler, redisHndlr *redis.RedisHandler, port int) (*ShardServer, error) {
|
||||
srvc := cnet.NewService(ctx, "SHARD", port)
|
||||
|
||||
server := &ShardServer{
|
||||
service: srvc,
|
||||
dbHndlr: dbHndlr,
|
||||
redisHndlr: redisHndlr,
|
||||
chunks: make(map[entity.ChunkPosition]*entity.Chunk),
|
||||
}
|
||||
|
||||
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(),
|
||||
Port: port,
|
||||
})
|
||||
|
||||
return server, nil
|
||||
}
|
||||
|
||||
func (server *ShardServer) Start() error {
|
||||
server.LoadNPCs()
|
||||
return server.service.Start()
|
||||
}
|
||||
|
||||
func (server *ShardServer) onDisconnect(peer *cnet.Peer) {
|
||||
// remove from chunks
|
||||
plr, ok := peer.UserData().(*entity.Player)
|
||||
if ok && plr != nil {
|
||||
server.removeEntity(plr)
|
||||
}
|
||||
}
|
||||
|
||||
func (server *ShardServer) onConnect(peer *cnet.Peer) {
|
||||
|
||||
}
|
||||
|
||||
func (server *ShardServer) Service() *cnet.Service {
|
||||
return server.service
|
||||
}
|
||||
1
tdata
Submodule
1
tdata
Submodule
Submodule tdata added at cc65dbb402
@@ -32,10 +32,10 @@ WARN_INVALID = False
|
||||
|
||||
class StructTranspiler:
|
||||
class StructField:
|
||||
def __init__(self, name: str, type: str, marshal: str) -> None:
|
||||
def __init__(self, name: str, csType: str, marshal: str) -> None:
|
||||
self.marshal = marshal
|
||||
self.type = type
|
||||
self.ctype = type # for transpilation to c
|
||||
self.type = csType
|
||||
self.ctype = csType # for transpilation to c
|
||||
self.tags = ""
|
||||
self.size = 0
|
||||
self.padding = 0
|
||||
@@ -43,87 +43,88 @@ class StructTranspiler:
|
||||
self.cname = self.name
|
||||
self.needsPatching = False
|
||||
|
||||
if type == "byte":
|
||||
self.type = "uint8"
|
||||
self.ctype = "char"
|
||||
self.size = 1
|
||||
elif type == "sbyte":
|
||||
self.type = "int8"
|
||||
self.ctype = "char"
|
||||
self.size = 1
|
||||
elif type == "short":
|
||||
self.type = "int16"
|
||||
self.ctype = "short"
|
||||
self.size = 2
|
||||
elif type == "int":
|
||||
self.type = "int32"
|
||||
self.ctype = "int"
|
||||
self.size = 4
|
||||
elif type == "uint":
|
||||
self.type = "uint32"
|
||||
self.ctype = "int"
|
||||
self.size = 4
|
||||
elif type == "float":
|
||||
self.type = "float32"
|
||||
self.ctype = "float"
|
||||
self.size = 4
|
||||
elif type == "long":
|
||||
self.type = "int64"
|
||||
self.ctype = "long"
|
||||
self.size = 8
|
||||
elif type == "ulong":
|
||||
self.type = "uint64"
|
||||
self.ctype = "long"
|
||||
self.size = 8
|
||||
elif type == "byte[]":
|
||||
self.size = int(marshal[(marshal.find("SizeConst = ") + len("SizeConst = ")):marshal.find(")]")])
|
||||
self.type = "[%d]byte" % self.size
|
||||
self.ctype = "char"
|
||||
self.cname += "[%d]" % self.size
|
||||
elif type == "short[]":
|
||||
self.size = int(marshal[(marshal.find("SizeConst = ") + len("SizeConst = ")):marshal.find(")]")])
|
||||
self.type = "[%d]int16" % self.size
|
||||
self.ctype = "short"
|
||||
self.cname += "[%d]" % self.size
|
||||
self.size *= 2
|
||||
elif type == "string":
|
||||
# all strings in fusionfall are utf16, in a uint16 array
|
||||
self.size = int(marshal[(marshal.find("SizeConst = ") + len("SizeConst = ")):marshal.find(")]")])
|
||||
self.type = "string"
|
||||
self.addTag("size:\"%d\"" % self.size) # special tag to tell our decoder/encoder the size of the uint16[] array
|
||||
self.ctype = "short"
|
||||
self.cname += "[%d]" % self.size
|
||||
self.size *= 2
|
||||
elif type == "int[]":
|
||||
self.size = int(marshal[(marshal.find("SizeConst = ") + len("SizeConst = ")):marshal.find(")]")])
|
||||
self.type = "[%d]int32" % self.size
|
||||
self.ctype = "int"
|
||||
self.cname += "[%d]" % self.size
|
||||
self.size *= 4
|
||||
elif type == "float[]":
|
||||
self.size = int(marshal[(marshal.find("SizeConst = ") + len("SizeConst = ")):marshal.find(")]")])
|
||||
self.type = "[%d]float32" % self.size
|
||||
self.ctype = "float"
|
||||
self.cname += "[%d]" % self.size
|
||||
self.size *= 4
|
||||
elif type == "long[]":
|
||||
self.size = int(marshal[(marshal.find("SizeConst = ") + len("SizeConst = ")):marshal.find(")]")])
|
||||
self.type = "[%d]int64" % self.size
|
||||
self.ctype = "long"
|
||||
self.cname += "[%d]" % self.size
|
||||
self.size *= 8
|
||||
else:
|
||||
# assume it's a structure that will be defined later
|
||||
if type.find("[]") != -1: # it's an array!
|
||||
type = type.replace("[]", "")
|
||||
self.size = int(marshal[(marshal.find("SizeConst = ") + len("SizeConst = ")):marshal.find(")]")])
|
||||
self.cname = self.name + "[%d]" % self.size
|
||||
else:
|
||||
self.cname = self.name
|
||||
match csType:
|
||||
case "byte":
|
||||
self.type = "uint8"
|
||||
self.ctype = "char"
|
||||
self.size = 1
|
||||
self.type = sanitizeName(type)
|
||||
self.ctype = sanitizeName(type)
|
||||
self.needsPatching = True
|
||||
case "sbyte":
|
||||
self.type = "int8"
|
||||
self.ctype = "char"
|
||||
self.size = 1
|
||||
case "short":
|
||||
self.type = "int16"
|
||||
self.ctype = "short"
|
||||
self.size = 2
|
||||
case "int":
|
||||
self.type = "int32"
|
||||
self.ctype = "int"
|
||||
self.size = 4
|
||||
case "uint":
|
||||
self.type = "uint32"
|
||||
self.ctype = "int"
|
||||
self.size = 4
|
||||
case "float":
|
||||
self.type = "float32"
|
||||
self.ctype = "float"
|
||||
self.size = 4
|
||||
case "long":
|
||||
self.type = "int64"
|
||||
self.ctype = "long"
|
||||
self.size = 8
|
||||
case "ulong":
|
||||
self.type = "uint64"
|
||||
self.ctype = "long"
|
||||
self.size = 8
|
||||
case "byte[]":
|
||||
self.size = int(marshal[(marshal.find("SizeConst = ") + len("SizeConst = ")):marshal.find(")]")])
|
||||
self.type = "[%d]byte" % self.size
|
||||
self.ctype = "char"
|
||||
self.cname += "[%d]" % self.size
|
||||
case "short[]":
|
||||
self.size = int(marshal[(marshal.find("SizeConst = ") + len("SizeConst = ")):marshal.find(")]")])
|
||||
self.type = "[%d]int16" % self.size
|
||||
self.ctype = "short"
|
||||
self.cname += "[%d]" % self.size
|
||||
self.size *= 2
|
||||
case "string":
|
||||
# all strings in fusionfall are utf16, in a uint16 array
|
||||
self.size = int(marshal[(marshal.find("SizeConst = ") + len("SizeConst = ")):marshal.find(")]")])
|
||||
self.type = "string"
|
||||
self.addTag("size:\"%d\"" % self.size) # special tag to tell our decoder/encoder the size of the uint16[] array
|
||||
self.ctype = "short"
|
||||
self.cname += "[%d]" % self.size
|
||||
self.size *= 2
|
||||
case "int[]":
|
||||
self.size = int(marshal[(marshal.find("SizeConst = ") + len("SizeConst = ")):marshal.find(")]")])
|
||||
self.type = "[%d]int32" % self.size
|
||||
self.ctype = "int"
|
||||
self.cname += "[%d]" % self.size
|
||||
self.size *= 4
|
||||
case "float[]":
|
||||
self.size = int(marshal[(marshal.find("SizeConst = ") + len("SizeConst = ")):marshal.find(")]")])
|
||||
self.type = "[%d]float32" % self.size
|
||||
self.ctype = "float"
|
||||
self.cname += "[%d]" % self.size
|
||||
self.size *= 4
|
||||
case "long[]":
|
||||
self.size = int(marshal[(marshal.find("SizeConst = ") + len("SizeConst = ")):marshal.find(")]")])
|
||||
self.type = "[%d]int64" % self.size
|
||||
self.ctype = "long"
|
||||
self.cname += "[%d]" % self.size
|
||||
self.size *= 8
|
||||
case _:
|
||||
# assume it's a structure that will be defined later
|
||||
if csType.find("[]") != -1: # it's an array!
|
||||
csType = csType.replace("[]", "")
|
||||
self.size = int(marshal[(marshal.find("SizeConst = ") + len("SizeConst = ")):marshal.find(")]")])
|
||||
self.cname = self.name + "[%d]" % self.size
|
||||
else:
|
||||
self.cname = self.name
|
||||
self.size = 1
|
||||
self.type = sanitizeName(csType)
|
||||
self.ctype = sanitizeName(csType)
|
||||
self.needsPatching = True
|
||||
|
||||
def addTag(self, tag: str) -> None:
|
||||
if len(self.tags) > 0: # if there's already a tag defined, make sure there's a space separating them
|
||||
@@ -368,6 +369,13 @@ if __name__ == '__main__':
|
||||
source += "\t%s = 0x%x\n" % (const.name, const.value)
|
||||
source += ")\n\n"
|
||||
|
||||
source += "func PacketIDToString(id uint32) string {\n"
|
||||
source += "\tswitch id {\n"
|
||||
for const in consts:
|
||||
if const.name.startswith("P_"):
|
||||
source += "\tcase %s:\n\t\treturn \"%s\"\n" % (const.name, const.name)
|
||||
source += "\t}\n\treturn \"UNKNOWN\"\n}\n\n"
|
||||
|
||||
for struct in structs:
|
||||
source += struct.toGoStyle() + "\n"
|
||||
print(source)
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"github.com/CPunch/gopenfusion/db"
|
||||
"github.com/CPunch/gopenfusion/protocol"
|
||||
)
|
||||
|
||||
func Player2PCStyle(plr *db.Player) (protocol.SPCStyle, protocol.SPCStyle2) {
|
||||
return protocol.SPCStyle{
|
||||
IPC_UID: int64(plr.PlayerID),
|
||||
INameCheck: int8(plr.NameCheck),
|
||||
SzFirstName: plr.FirstName,
|
||||
SzLastName: plr.LastName,
|
||||
IGender: int8(plr.Gender),
|
||||
IFaceStyle: int8(plr.FaceStyle),
|
||||
IHairStyle: int8(plr.HairStyle),
|
||||
IHairColor: int8(plr.HairColor),
|
||||
ISkinColor: int8(plr.SkinColor),
|
||||
IEyeColor: int8(plr.EyeColor),
|
||||
IHeight: int8(plr.Height),
|
||||
IBody: int8(plr.Body),
|
||||
},
|
||||
protocol.SPCStyle2{
|
||||
IAppearanceFlag: int8(plr.AppearanceFlag),
|
||||
ITutorialFlag: int8(plr.TutorialFlag),
|
||||
IPayzoneFlag: int8(plr.PayZoneFlag),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user