Improved server

This commit is contained in:
Eduard Urbach 2024-01-26 00:29:05 +01:00
parent 816337ede5
commit 0172a81bdf
Signed by: akyoto
GPG Key ID: C874F672B1AF20C0
17 changed files with 286 additions and 181 deletions

View File

@ -1,8 +1,11 @@
extends PacketHandler extends PacketHandler
var auth_token: String var auth_token: String
var instance_id := OS.get_process_id() % 4
var username := "user%d" % instance_id
func _ready(): func _ready():
DisplayServer.window_set_title(username)
send_login() send_login()
func handle_packet(data: PackedByteArray, _peer: PacketPeer): func handle_packet(data: PackedByteArray, _peer: PacketPeer):
@ -17,10 +20,11 @@ func handle_packet(data: PackedByteArray, _peer: PacketPeer):
func send_login(): func send_login():
if is_logged_in(): if is_logged_in():
return return
var password := "password"
var buffer := StreamPeerBuffer.new() var buffer := StreamPeerBuffer.new()
buffer.put_8(Packet.LOGIN) buffer.put_8(Packet.LOGIN)
buffer.put_data(JSON.stringify(["user1", "password"]).to_utf8_buffer()) buffer.put_data(JSON.stringify([username, password]).to_utf8_buffer())
%Client.socket.put_packet(buffer.data_array) %Client.socket.put_packet(buffer.data_array)
print("[Client] Connecting...") print("[Client] Connecting...")

View File

@ -17,9 +17,14 @@ func handle_packet(data: PackedByteArray, _peer: PacketPeer):
print(server_position) print(server_position)
var player := spawn_player() var player := spawn_player()
player.name = player_name
player.position = server_position player.position = server_position
Global.player = player player.set_character_name(player_name)
if false:
Global.player = player
var controller := PlayerController.new()
controller.character = Global.player
Global.player.add_child(controller)
func spawn_player() -> Player: func spawn_player() -> Player:
var player = player_scene.instantiate() var player = player_scene.instantiate()

View File

@ -1,7 +1,6 @@
class_name Player class_name Player
extends Character extends Character
func _ready(): func set_character_name(new_name: String):
var controller := PlayerController.new() name = new_name
controller.character = self get_node("Label").text = name
add_child(controller)

View File

@ -1,8 +1,7 @@
[gd_scene load_steps=6 format=3 uid="uid://2lcnu3dy54lx"] [gd_scene load_steps=5 format=3 uid="uid://2lcnu3dy54lx"]
[ext_resource type="Script" path="res://player/Player.gd" id="1_8gebs"] [ext_resource type="Script" path="res://player/Player.gd" id="1_8gebs"]
[ext_resource type="PackedScene" uid="uid://2bbycjulf00g" path="res://character/health/HealthComponent.tscn" id="2_np5ag"] [ext_resource type="PackedScene" uid="uid://2bbycjulf00g" path="res://character/health/HealthComponent.tscn" id="2_np5ag"]
[ext_resource type="Script" path="res://player/controller/PlayerController.gd" id="3_oox5k"]
[sub_resource type="PrismMesh" id="PrismMesh_y7abh"] [sub_resource type="PrismMesh" id="PrismMesh_y7abh"]
size = Vector3(0.5, 1.6, 0.5) size = Vector3(0.5, 1.6, 0.5)
@ -29,6 +28,7 @@ shape = SubResource("CapsuleShape3D_2f50n")
[node name="Health" parent="." instance=ExtResource("2_np5ag")] [node name="Health" parent="." instance=ExtResource("2_np5ag")]
[node name="Controller" type="Node" parent="." node_paths=PackedStringArray("character")] [node name="Label" type="Label3D" parent="."]
script = ExtResource("3_oox5k") transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.8, 0)
character = NodePath("..") billboard = 1
text = "ABC"

View File

@ -68,10 +68,10 @@ camera_attributes = ExtResource("9_w4cdu")
[node name="Trees" type="Node3D" parent="World"] [node name="Trees" type="Node3D" parent="World"]
[node name="Tree" parent="World/Trees" instance=ExtResource("11_wlyv1")] [node name="Tree" parent="World/Trees" instance=ExtResource("11_wlyv1")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 5.11323, 0, -4.64839) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 5.11323, 0, -7.57074)
[node name="Tree2" parent="World/Trees" instance=ExtResource("11_wlyv1")] [node name="Tree2" parent="World/Trees" instance=ExtResource("11_wlyv1")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 5.11323, 0, 5.35161) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 5.11323, 0, 7.80548)
[node name="Enemies" type="Node3D" parent="World"] [node name="Enemies" type="Node3D" parent="World"]
@ -118,6 +118,7 @@ follow_speed = 5.0
[node name="PostProcessing" type="MeshInstance3D" parent="Viewport/SubViewport/CameraPivot/Camera"] [node name="PostProcessing" type="MeshInstance3D" parent="Viewport/SubViewport/CameraPivot/Camera"]
unique_name_in_owner = true unique_name_in_owner = true
visible = false
extra_cull_margin = 16384.0 extra_cull_margin = 16384.0
mesh = SubResource("QuadMesh_7yiqd") mesh = SubResource("QuadMesh_7yiqd")

View File

@ -1,31 +0,0 @@
package game
import (
"net"
"time"
)
// Client represents a logged in client.
type Client struct {
address *net.UDPAddr
lastPacket time.Time
authToken string
player Player
}
// NewClient creates a new client.
func NewClient(address *net.UDPAddr) *Client {
return &Client{
address: address,
}
}
// String shows the client address.
func (c *Client) String() string {
return c.address.String()
}
// KeepAlive sets the last packet time to now.
func (c *Client) KeepAlive() {
c.lastPacket = time.Now()
}

67
server/game/Game.go Normal file
View File

@ -0,0 +1,67 @@
package game
import (
"fmt"
"os"
"os/signal"
"server/game/packet"
"time"
)
// Game represents the entire state of the game server.
type Game struct {
server *Server
players *PlayerManager
}
// New creates a new game.
func New() *Game {
return &Game{
server: NewServer(),
players: NewPlayerManager(),
}
}
// Run starts all game systems.
func (game *Game) Run() {
game.start()
close := make(chan os.Signal, 1)
signal.Notify(close, os.Interrupt)
<-close
}
// start starts all game systems.
func (game *Game) start() {
go game.network()
go game.physics()
go game.statistics()
}
// network will listen for new packets and process them.
func (game *Game) network() {
game.server.SetHandler(packet.Ping, game.Ping)
game.server.SetHandler(packet.Login, game.Login)
game.server.Run(4242)
}
// physics periodically runs the Tick function for each player.
func (game *Game) physics() {
updater := time.NewTicker(20 * time.Millisecond)
for range updater.C {
game.players.Each(func(c *Player) bool {
c.Tick()
return true
})
}
}
// statistics periodically shows server statistics on the command line.
func (game *Game) statistics() {
ticker := time.NewTicker(time.Second)
for range ticker.C {
fmt.Printf("%d players | %d packets\n", game.players.Count(), game.server.PacketCount())
game.server.ResetPacketCount()
}
}

View File

@ -1,4 +1,4 @@
package core package game
import "net" import "net"

View File

@ -3,70 +3,78 @@ package game
import ( import (
"crypto/rand" "crypto/rand"
"encoding/base64" "encoding/base64"
"encoding/binary"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"math"
"net" "net"
"server/core" "server/game/packet"
"server/packet"
) )
var ( var (
Clients = NewManager()
LoginSuccess = []byte{0} LoginSuccess = []byte{0}
LoginFailure = []byte{1} LoginFailure = []byte{1}
) )
// Login checks the account credentials and gives a network peer access to an account. // Login checks the account credentials and gives a network peer access to an account.
func Login(data []byte, address *net.UDPAddr, server *core.Server) error { func (game *Game) Login(data []byte, address *net.UDPAddr, server *Server) error {
var loginRequest [2]string username, password, err := getLoginData(data)
err := json.Unmarshal(data, &loginRequest)
if err != nil { if err != nil {
server.Send(packet.Login, LoginFailure, address) server.Send(packet.Login, LoginFailure, address)
return err return err
} }
username := loginRequest[0]
password := loginRequest[1]
if password != "password" { if password != "password" {
server.Send(packet.Login, LoginFailure, address) server.Send(packet.Login, LoginFailure, address)
return errors.New("login failure") return errors.New("login failure")
} }
randomBytes := make([]byte, 32) player := game.players.Get(address)
_, err = rand.Read(randomBytes) player.AuthToken = createAuthToken()
player.Name = username
player.KeepAlive()
if err != nil { if username == "user0" {
server.Send(packet.Login, LoginFailure, address) player.Position.X = 5.0
return err
} }
client := Clients.Get(address) if username == "user1" {
client.KeepAlive() player.Position.X = -5.0
client.authToken = base64.StdEncoding.EncodeToString(randomBytes) }
client.player.Name = username
client.player.Position.X = 5.0
playerState := []byte(client.player.Name + "\u0000")
playerState = appendVector3(playerState, client.player.Position)
server.Send(packet.Login, append(LoginSuccess, []byte(client.authToken)...), address) if username == "user2" {
server.Send(packet.PlayerState, playerState, address) player.Position.Z = -5.0
}
if username == "user3" {
player.Position.Z = 5.0
}
server.Send(packet.Login, append(LoginSuccess, []byte(player.AuthToken)...), address)
player.OnConnect()
game.players.Each(func(other *Player) bool {
server.Send(packet.PlayerState, other.State(), address)
return true
})
fmt.Printf("%s logged in.\n", username)
return nil return nil
} }
func appendVector3(data []byte, vector Vector3) []byte { func getLoginData(data []byte) (string, string, error) {
bits := math.Float32bits(vector.X) loginRequest := [2]string{}
data = binary.LittleEndian.AppendUint32(data, bits) err := json.Unmarshal(data, &loginRequest)
bits = math.Float32bits(vector.Y) if err != nil {
data = binary.LittleEndian.AppendUint32(data, bits) return "", "", err
}
bits = math.Float32bits(vector.Z) username := loginRequest[0]
return binary.LittleEndian.AppendUint32(data, bits) password := loginRequest[1]
return username, password, nil
}
func createAuthToken() string {
randomBytes := make([]byte, 32)
rand.Read(randomBytes)
return base64.StdEncoding.EncodeToString(randomBytes)
} }

View File

@ -1,68 +0,0 @@
package game
import (
"net"
"sync"
"sync/atomic"
"time"
)
// Manager keeps tracks of all player connections.
type Manager struct {
clients sync.Map
count atomic.Int64
}
// NewManager creates a new manager.
func NewManager() *Manager {
manager := &Manager{}
timeout := 5 * time.Second
interval := time.Second
go func() {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for range ticker.C {
now := time.Now()
manager.clients.Range(func(key, value interface{}) bool {
item := value.(*Client)
if !item.lastPacket.IsZero() && now.After(item.lastPacket.Add(timeout)) {
manager.clients.Delete(key)
manager.count.Add(-1)
}
return true
})
}
}()
return manager
}
// Get either returns a new or existing client for the requested address.
func (m *Manager) Get(addr *net.UDPAddr) *Client {
obj, exists := m.clients.Load(addr.String())
if exists {
return obj.(*Client)
}
client := NewClient(addr)
m.clients.Store(addr.String(), client)
m.count.Add(1)
return client
}
// Contains tells you whether the address is already a registered client.
func (m *Manager) Contains(addr *net.UDPAddr) bool {
_, exists := m.clients.Load(addr.String())
return exists
}
// Count returns the number of clients.
func (m *Manager) Count() int {
return int(m.count.Load())
}

View File

@ -2,16 +2,15 @@ package game
import ( import (
"net" "net"
"server/core" "server/game/packet"
"server/packet"
) )
// Ping is used as a heartbeat and latency check. // Ping is used as a heartbeat and latency check.
func Ping(data []byte, address *net.UDPAddr, server *core.Server) error { func (game *Game) Ping(data []byte, address *net.UDPAddr, server *Server) error {
server.Send(packet.Ping, data, address) server.Send(packet.Ping, data, address)
if Clients.Contains(address) { if game.players.Contains(address) {
Clients.Get(address).KeepAlive() game.players.Get(address).KeepAlive()
} }
return nil return nil

View File

@ -1,6 +1,53 @@
package game package game
import (
"fmt"
"net"
"time"
)
// Player represents a logged in client.
type Player struct { type Player struct {
Name string `json:"name"` Name string `json:"name"`
Position Vector3 `json:"position"` Position Vector3 `json:"position"`
AuthToken string
address *net.UDPAddr
lastPacket time.Time
}
// NewClient creates a new client.
func NewClient(address *net.UDPAddr) *Player {
return &Player{
address: address,
}
}
// String shows the client address.
func (c *Player) String() string {
return c.address.String()
}
// KeepAlive sets the last packet time to now.
func (c *Player) KeepAlive() {
c.lastPacket = time.Now()
}
// Tick is run on every tick.
func (c *Player) Tick() {
// ...
}
// State returns the player state (name and position).
func (player *Player) State() []byte {
state := []byte(player.Name + "\u0000")
state = appendVector3(state, player.Position)
return state
}
func (player *Player) OnConnect() {
fmt.Printf("%s connected.\n", player.Name)
}
func (player *Player) OnDisconnect() {
fmt.Printf("%s disconnected.\n", player.Name)
} }

View File

@ -0,0 +1,76 @@
package game
import (
"net"
"sync"
"sync/atomic"
"time"
)
// PlayerManager keeps tracks of all players.
type PlayerManager struct {
players sync.Map
count atomic.Int64
}
// NewPlayerManager creates a new player manager.
func NewPlayerManager() *PlayerManager {
m := &PlayerManager{}
timeout := 5 * time.Second
interval := time.Second
go func() {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for range ticker.C {
now := time.Now()
m.players.Range(func(key, value interface{}) bool {
player := value.(*Player)
if !player.lastPacket.IsZero() && now.After(player.lastPacket.Add(timeout)) {
player.OnDisconnect()
m.players.Delete(key)
m.count.Add(-1)
}
return true
})
}
}()
return m
}
// Contains tells you whether the address is already a registered client.
func (m *PlayerManager) Contains(addr *net.UDPAddr) bool {
_, exists := m.players.Load(addr.String())
return exists
}
// Count returns the number of clients.
func (m *PlayerManager) Count() int {
return int(m.count.Load())
}
// Each calls the callback function for each client.
func (m *PlayerManager) Each(callback func(*Player) bool) {
m.players.Range(func(key, value any) bool {
return callback(value.(*Player))
})
}
// Get either returns a new or existing client for the requested address.
func (m *PlayerManager) Get(addr *net.UDPAddr) *Player {
obj, exists := m.players.Load(addr.String())
if exists {
return obj.(*Player)
}
client := NewClient(addr)
m.players.Store(addr.String(), client)
m.count.Add(1)
return client
}

View File

@ -1,4 +1,4 @@
package core package game
import ( import (
"fmt" "fmt"
@ -12,8 +12,8 @@ type Server struct {
packetCount int packetCount int
} }
// New creates a new server. // NewServer creates a new server.
func New() *Server { func NewServer() *Server {
return &Server{} return &Server{}
} }
@ -66,7 +66,7 @@ func listen(port int) *net.UDPConn {
// read is a blocking call which will read incoming packets and handle them. // read is a blocking call which will read incoming packets and handle them.
func (s *Server) read() { func (s *Server) read() {
buffer := make([]byte, 4096) buffer := make([]byte, 16384)
for { for {
n, addr, err := s.socket.ReadFromUDP(buffer) n, addr, err := s.socket.ReadFromUDP(buffer)

View File

@ -1,7 +1,25 @@
package game package game
import (
"encoding/binary"
"math"
)
// Vector3 represents a 3-dimensional vector using 32-bit float precision.
type Vector3 struct { type Vector3 struct {
X float32 `json:"x"` X float32 `json:"x"`
Y float32 `json:"y"` Y float32 `json:"y"`
Z float32 `json:"z"` Z float32 `json:"z"`
} }
// appendVector3 adds the raw bits of the vector to the given byte slice in XYZ order.
func appendVector3(data []byte, vector Vector3) []byte {
bits := math.Float32bits(vector.X)
data = binary.LittleEndian.AppendUint32(data, bits)
bits = math.Float32bits(vector.Y)
data = binary.LittleEndian.AppendUint32(data, bits)
bits = math.Float32bits(vector.Z)
return binary.LittleEndian.AppendUint32(data, bits)
}

View File

@ -1,29 +1,9 @@
package main package main
import ( import (
"fmt"
"server/core"
"server/game" "server/game"
"server/packet"
"time"
) )
func main() { func main() {
// Init server game.New().Run()
server := core.New()
server.SetHandler(packet.Ping, game.Ping)
server.SetHandler(packet.Login, game.Login)
// Show statistics
ticker := time.NewTicker(time.Second)
go func() {
for range ticker.C {
fmt.Printf("%d packets per second, %d clients\n", server.PacketCount(), game.Clients.Count())
server.ResetPacketCount()
}
}()
// Start listening
server.Run(4242)
} }