Compare commits

...

6 Commits

Author SHA1 Message Date
a78dedcb89 better readme 2023-08-23 18:38:14 -05:00
dcb86e2518 more minor refactoring 2023-08-23 18:37:57 -05:00
458e907c99 moved 'core' to 'internal' 2023-08-23 18:16:24 -05:00
74b68863b1 updated readme 2023-08-23 18:05:03 -05:00
83b664da93 minor refactoring, fixed go-staticcheck warnings 2023-08-23 18:03:14 -05:00
670d4a514c more better CNPeer.Send()
this fixes a race condition where if 2 goroutines try to send a packet at the same time, they could end up being
malformed due to the 2 separate calls to peer.conn.Write().

instead of writing the packet size to peer.conn.Write() directly, we make space in buf for the packet size,
and patch it in place. this lets us get away with only having 1 call to peer.conn.Write() which will ensure that
the full packet is written properly and be goroutine safe :3
2023-08-23 17:38:10 -05:00
29 changed files with 74 additions and 77 deletions

View File

@ -1,10 +1,10 @@
# gopenfusion # gopenfusion
A toy implementation of the [Fusionfall Packet Protocol](https://openpunk.com/pages/fusionfall-openfusion/) written in Go. A toy implementation of the [Fusionfall Packet Protocol](https://openpunk.com/pages/fusionfall-openfusion/) and accompanying services, written in Go.
## Landwalker demo ## 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). The DB implementation in `core/db/` matches the OpenFusion 1.4 SQLite tables, which the login server uses. There's no support for NPCs nor other players, and is liable to softlock the client. 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 Startup the environment using
@ -13,6 +13,8 @@ $ chmod +x ./build.sh && ./build.sh
$ docker compose up $ 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) 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 ## Generating structures

View File

@ -7,8 +7,8 @@ import (
"os" "os"
"github.com/CPunch/gopenfusion/config" "github.com/CPunch/gopenfusion/config"
"github.com/CPunch/gopenfusion/core/db" "github.com/CPunch/gopenfusion/internal/db"
"github.com/CPunch/gopenfusion/core/redis" "github.com/CPunch/gopenfusion/internal/redis"
"github.com/google/subcommands" "github.com/google/subcommands"
) )

View File

@ -6,7 +6,7 @@ import (
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"github.com/CPunch/gopenfusion/core/protocol" "github.com/CPunch/gopenfusion/internal/protocol"
"github.com/georgysavva/scany/v2/sqlscan" "github.com/georgysavva/scany/v2/sqlscan"
) )

View File

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

View File

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

View File

@ -1,6 +1,6 @@
package entity package entity
import "github.com/CPunch/gopenfusion/core/protocol" import "github.com/CPunch/gopenfusion/internal/protocol"
type EntityKind int type EntityKind int

View File

@ -3,7 +3,7 @@ package entity
import ( import (
"sync/atomic" "sync/atomic"
"github.com/CPunch/gopenfusion/core/protocol" "github.com/CPunch/gopenfusion/internal/protocol"
) )
type NPC struct { type NPC struct {

View File

@ -2,7 +2,7 @@ package entity
import ( import (
"github.com/CPunch/gopenfusion/config" "github.com/CPunch/gopenfusion/config"
"github.com/CPunch/gopenfusion/core/protocol" "github.com/CPunch/gopenfusion/internal/protocol"
) )
type Player struct { type Player struct {

View File

@ -8,7 +8,7 @@ import (
"net" "net"
"time" "time"
"github.com/CPunch/gopenfusion/core/protocol/pool" "github.com/CPunch/gopenfusion/internal/protocol/pool"
) )
const ( const (
@ -51,6 +51,9 @@ func (peer *CNPeer) Send(typeID uint32, data ...interface{}) error {
buf := pool.Get() buf := pool.Get()
defer pool.Put(buf) defer pool.Put(buf)
// allocate space for packet size
buf.Write(make([]byte, 4))
// body start // body start
pkt := NewPacket(buf) pkt := NewPacket(buf)
@ -66,25 +69,22 @@ func (peer *CNPeer) Send(typeID uint32, data ...interface{}) error {
} }
} }
// prepend the packet size
binary.LittleEndian.PutUint32(buf.Bytes()[:4], uint32(buf.Len()-4))
// encrypt body // encrypt body
switch peer.whichKey { switch peer.whichKey {
case USE_E: case USE_E:
EncryptData(buf.Bytes(), peer.E_key) EncryptData(buf.Bytes()[4:], peer.E_key)
case USE_FE: case USE_FE:
EncryptData(buf.Bytes(), peer.FE_key) EncryptData(buf.Bytes()[4:], peer.FE_key)
} }
// write packet size // send full packet
if err := binary.Write(peer.conn, binary.LittleEndian, uint32(buf.Len())); err != nil { log.Printf("Sending %#v, sizeof: %d, buffer: %v", data, buf.Len(), buf.Bytes())
return err
}
// write packet body
log.Printf("Sending %#v, sizeof: %d", data, buf.Len())
if _, err := peer.conn.Write(buf.Bytes()); err != nil { if _, err := peer.conn.Write(buf.Bytes()); err != nil {
return fmt.Errorf("[FATAL] failed to write packet body! %v", err) return fmt.Errorf("failed to write packet body! %v", err)
} }
return nil return nil
} }

View File

@ -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 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")) sz, err := strconv.Atoi(field.Tag.Get("size"))
if err != nil { 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())) buf16 := utf16.Encode([]rune(value.String()))
@ -100,7 +100,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 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")) sz, err := strconv.Atoi(field.Tag.Get("size"))
if err != nil { 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) buf16 := make([]uint16, sz)

View File

@ -7,9 +7,9 @@ import (
"math/rand" "math/rand"
"github.com/CPunch/gopenfusion/config" "github.com/CPunch/gopenfusion/config"
"github.com/CPunch/gopenfusion/core/db" "github.com/CPunch/gopenfusion/internal/db"
"github.com/CPunch/gopenfusion/core/protocol" "github.com/CPunch/gopenfusion/internal/protocol"
"github.com/CPunch/gopenfusion/core/redis" "github.com/CPunch/gopenfusion/internal/redis"
) )
const ( const (
@ -76,7 +76,7 @@ func (server *LoginServer) Login(peer *protocol.CNPeer, pkt protocol.Packet) err
// client is resending a login packet?? // client is resending a login packet??
if peer.AccountID != -1 { if peer.AccountID != -1 {
SendError(LOGIN_ERROR) SendError(LOGIN_ERROR)
return fmt.Errorf("Out of order P_CL2LS_REQ_LOGIN!") return fmt.Errorf("out of order P_CL2LS_REQ_LOGIN")
} }
// attempt login // attempt login
@ -155,7 +155,7 @@ func (server *LoginServer) SaveCharacterName(peer *protocol.CNPeer, pkt protocol
if peer.AccountID == -1 { if peer.AccountID == -1 {
peer.Send(protocol.P_LS2CL_REP_SAVE_CHAR_NAME_FAIL, protocol.SP_LS2CL_REP_SAVE_CHAR_NAME_FAIL{}) peer.Send(protocol.P_LS2CL_REP_SAVE_CHAR_NAME_FAIL, protocol.SP_LS2CL_REP_SAVE_CHAR_NAME_FAIL{})
return fmt.Errorf("Out of order P_LS2CL_REP_SAVE_CHAR_NAME_FAIL!") return fmt.Errorf("out of order P_LS2CL_REP_SAVE_CHAR_NAME_FAIL")
} }
// TODO: sanity check SzFirstName && SzLastName // TODO: sanity check SzFirstName && SzLastName
@ -260,7 +260,7 @@ func (server *LoginServer) ShardSelect(peer *protocol.CNPeer, pkt protocol.Packe
shards := server.redisHndlr.GetShards() shards := server.redisHndlr.GetShards()
if len(shards) == 0 { if len(shards) == 0 {
SendFail(peer) SendFail(peer)
return fmt.Errorf("LoginServer has found no linked shards!") return fmt.Errorf("loginServer has found no linked shards")
} }
key, err := protocol.GenSerialKey() key, err := protocol.GenSerialKey()

View File

@ -7,10 +7,10 @@ import (
"sync" "sync"
"github.com/CPunch/gopenfusion/config" "github.com/CPunch/gopenfusion/config"
"github.com/CPunch/gopenfusion/core/db" "github.com/CPunch/gopenfusion/internal/db"
"github.com/CPunch/gopenfusion/core/protocol" "github.com/CPunch/gopenfusion/internal/protocol"
"github.com/CPunch/gopenfusion/core/protocol/pool" "github.com/CPunch/gopenfusion/internal/protocol/pool"
"github.com/CPunch/gopenfusion/core/redis" "github.com/CPunch/gopenfusion/internal/redis"
) )
type PacketHandler func(peer *protocol.CNPeer, pkt protocol.Packet) error type PacketHandler func(peer *protocol.CNPeer, pkt protocol.Packet) error
@ -81,20 +81,18 @@ func (server *LoginServer) Start() {
} }
func (server *LoginServer) handleEvents() { func (server *LoginServer) handleEvents() {
for { for event := range server.eRecv {
select { switch event.Type {
case event := <-server.eRecv: case protocol.EVENT_CLIENT_DISCONNECT:
switch event.Type { server.disconnect(event.Peer)
case protocol.EVENT_CLIENT_DISCONNECT: case protocol.EVENT_CLIENT_PACKET:
server.disconnect(event.Peer) if err := server.handlePacket(event.Peer, event.PktID, protocol.NewPacket(event.Pkt)); err != nil {
case protocol.EVENT_CLIENT_PACKET: log.Printf("Error handling packet: %v", err)
defer pool.Put(event.Pkt) event.Peer.Kill()
if err := server.handlePacket(event.Peer, event.PktID, protocol.NewPacket(event.Pkt)); err != nil {
log.Printf("Error handling packet: %v", err)
event.Peer.Kill()
}
} }
// the packet is given to us by the event, so we'll need to make sure to return it to the pool
pool.Put(event.Pkt)
} }
} }
} }

View File

@ -1,6 +1,6 @@
package shard package shard
import "github.com/CPunch/gopenfusion/core/protocol" import "github.com/CPunch/gopenfusion/internal/protocol"
func (server *ShardServer) freeChat(peer *protocol.CNPeer, pkt protocol.Packet) error { func (server *ShardServer) freeChat(peer *protocol.CNPeer, pkt protocol.Packet) error {
var chat protocol.SP_CL2FE_REQ_SEND_FREECHAT_MESSAGE var chat protocol.SP_CL2FE_REQ_SEND_FREECHAT_MESSAGE

View File

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

View File

@ -4,16 +4,16 @@ import (
"fmt" "fmt"
"log" "log"
"github.com/CPunch/gopenfusion/core/entity" "github.com/CPunch/gopenfusion/internal/entity"
"github.com/CPunch/gopenfusion/core/protocol" "github.com/CPunch/gopenfusion/internal/protocol"
"github.com/CPunch/gopenfusion/core/redis" "github.com/CPunch/gopenfusion/internal/redis"
) )
func (server *ShardServer) attachPlayer(peer *protocol.CNPeer, meta redis.LoginMetadata) (*entity.Player, error) { func (server *ShardServer) attachPlayer(peer *protocol.CNPeer, meta redis.LoginMetadata) (*entity.Player, error) {
// resending a shard enter packet? // resending a shard enter packet?
old, err := server.getPlayer(peer) old, _ := server.getPlayer(peer)
if old != nil { if old != nil {
return nil, fmt.Errorf("resent enter packet!") return nil, fmt.Errorf("resent enter packet")
} }
// attach player // attach player

View File

@ -1,8 +1,8 @@
package shard package shard
import ( import (
"github.com/CPunch/gopenfusion/core/entity" "github.com/CPunch/gopenfusion/internal/entity"
"github.com/CPunch/gopenfusion/core/protocol" "github.com/CPunch/gopenfusion/internal/protocol"
) )
func (server *ShardServer) updatePlayerPosition(plr *entity.Player, X, Y, Z, Angle int) { func (server *ShardServer) updatePlayerPosition(plr *entity.Player, X, Y, Z, Angle int) {

View File

@ -6,7 +6,7 @@ import (
"os" "os"
"github.com/CPunch/gopenfusion/config" "github.com/CPunch/gopenfusion/config"
"github.com/CPunch/gopenfusion/core/entity" "github.com/CPunch/gopenfusion/internal/entity"
) )
type NPCData struct { type NPCData struct {

View File

@ -7,17 +7,15 @@ import (
"sync" "sync"
"github.com/CPunch/gopenfusion/config" "github.com/CPunch/gopenfusion/config"
"github.com/CPunch/gopenfusion/core/db" "github.com/CPunch/gopenfusion/internal/db"
"github.com/CPunch/gopenfusion/core/entity" "github.com/CPunch/gopenfusion/internal/entity"
"github.com/CPunch/gopenfusion/core/protocol" "github.com/CPunch/gopenfusion/internal/protocol"
"github.com/CPunch/gopenfusion/core/protocol/pool" "github.com/CPunch/gopenfusion/internal/protocol/pool"
"github.com/CPunch/gopenfusion/core/redis" "github.com/CPunch/gopenfusion/internal/redis"
) )
type PacketHandler func(peer *protocol.CNPeer, pkt protocol.Packet) error type PacketHandler func(peer *protocol.CNPeer, pkt protocol.Packet) error
func stubbedPacket(_ *protocol.CNPeer, _ protocol.Packet) error { /* stubbed */ return nil }
type ShardServer struct { type ShardServer struct {
listener net.Listener listener net.Listener
port int port int
@ -67,18 +65,17 @@ func NewShardServer(dbHndlr *db.DBHandler, redisHndlr *redis.RedisHandler, port
} }
func (server *ShardServer) handleEvents() { func (server *ShardServer) handleEvents() {
for { for event := range server.eRecv {
select { switch event.Type {
case event := <-server.eRecv: case protocol.EVENT_CLIENT_DISCONNECT:
switch event.Type { server.disconnect(event.Peer)
case protocol.EVENT_CLIENT_DISCONNECT: case protocol.EVENT_CLIENT_PACKET:
server.disconnect(event.Peer) if err := server.handlePacket(event.Peer, event.PktID, protocol.NewPacket(event.Pkt)); err != nil {
case protocol.EVENT_CLIENT_PACKET: event.Peer.Kill()
defer pool.Put(event.Pkt)
if err := server.handlePacket(event.Peer, event.PktID, protocol.NewPacket(event.Pkt)); err != nil {
event.Peer.Kill()
}
} }
// the packet is given to us by the event, so we'll need to make sure to return it to the pool
pool.Put(event.Pkt)
} }
} }
} }
@ -143,7 +140,7 @@ func (server *ShardServer) connect(peer *protocol.CNPeer) {
func (server *ShardServer) getPlayer(peer *protocol.CNPeer) (*entity.Player, error) { func (server *ShardServer) getPlayer(peer *protocol.CNPeer) (*entity.Player, error) {
plr, ok := server.peers[peer] plr, ok := server.peers[peer]
if !ok { if !ok {
return nil, fmt.Errorf("Player not found") return nil, fmt.Errorf("player not found")
} }
return plr, nil return plr, nil