Compare commits

...

6 Commits

Author SHA1 Message Date
CPunch a78dedcb89 better readme 2023-08-23 18:38:14 -05:00
CPunch dcb86e2518 more minor refactoring 2023-08-23 18:37:57 -05:00
CPunch 458e907c99 moved 'core' to 'internal' 2023-08-23 18:16:24 -05:00
CPunch 74b68863b1 updated readme 2023-08-23 18:05:03 -05:00
CPunch 83b664da93 minor refactoring, fixed go-staticcheck warnings 2023-08-23 18:03:14 -05:00
CPunch 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
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
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
@ -13,6 +13,8 @@ $ 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,7 +8,7 @@ import (
"net"
"time"
"github.com/CPunch/gopenfusion/core/protocol/pool"
"github.com/CPunch/gopenfusion/internal/protocol/pool"
)
const (
@ -51,6 +51,9 @@ func (peer *CNPeer) Send(typeID uint32, data ...interface{}) error {
buf := pool.Get()
defer pool.Put(buf)
// allocate space for packet size
buf.Write(make([]byte, 4))
// body start
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
switch peer.whichKey {
case USE_E:
EncryptData(buf.Bytes(), peer.E_key)
EncryptData(buf.Bytes()[4:], peer.E_key)
case USE_FE:
EncryptData(buf.Bytes(), peer.FE_key)
EncryptData(buf.Bytes()[4:], 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())
// 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("[FATAL] failed to write packet body! %v", err)
return fmt.Errorf("failed to write packet body! %v", err)
}
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
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()))
@ -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
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)

View File

@ -7,9 +7,9 @@ import (
"math/rand"
"github.com/CPunch/gopenfusion/config"
"github.com/CPunch/gopenfusion/core/db"
"github.com/CPunch/gopenfusion/core/protocol"
"github.com/CPunch/gopenfusion/core/redis"
"github.com/CPunch/gopenfusion/internal/db"
"github.com/CPunch/gopenfusion/internal/protocol"
"github.com/CPunch/gopenfusion/internal/redis"
)
const (
@ -76,7 +76,7 @@ func (server *LoginServer) Login(peer *protocol.CNPeer, pkt protocol.Packet) err
// client is resending a login packet??
if peer.AccountID != -1 {
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
@ -155,7 +155,7 @@ func (server *LoginServer) SaveCharacterName(peer *protocol.CNPeer, pkt protocol
if peer.AccountID == -1 {
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
@ -260,7 +260,7 @@ func (server *LoginServer) ShardSelect(peer *protocol.CNPeer, pkt protocol.Packe
shards := server.redisHndlr.GetShards()
if len(shards) == 0 {
SendFail(peer)
return fmt.Errorf("LoginServer has found no linked shards!")
return fmt.Errorf("loginServer has found no linked shards")
}
key, err := protocol.GenSerialKey()

View File

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

View File

@ -1,6 +1,6 @@
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 {
var chat protocol.SP_CL2FE_REQ_SEND_FREECHAT_MESSAGE

View File

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

View File

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

View File

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

View File

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

View File

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