Compare commits
10 Commits
testbuild-
...
feature/ma
Author | SHA1 | Date | |
---|---|---|---|
d5bb464d9f | |||
4549ee7517 | |||
31ae9c525f | |||
06913a5217 | |||
49663c9094 | |||
a843680b09 | |||
7183df4a8b | |||
33e355200d | |||
e45066b2a8 | |||
bb01dccf2b |
13
.gitignore
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
# Build artifacts
|
||||
build/
|
||||
goonscape
|
||||
goonscape.exe
|
||||
|
||||
# IDE files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
25
.woodpecker.yml
Normal file
@ -0,0 +1,25 @@
|
||||
pipeline:
|
||||
build:
|
||||
image: golang:1.23
|
||||
commands:
|
||||
# Install build dependencies
|
||||
- apt-get update && apt-get install -y gcc-mingw-w64 cmake zip libasound2-dev mesa-common-dev libx11-dev libxrandr-dev libxi-dev xorg-dev libgl1-mesa-dev libglu1-mesa-dev libwayland-dev wayland-protocols libxkbcommon-dev
|
||||
|
||||
# Build for all platforms
|
||||
- make all
|
||||
|
||||
when:
|
||||
event: tag
|
||||
tag: v*
|
||||
|
||||
# Optional: Create Gitea release with built artifacts
|
||||
release:
|
||||
image: plugins/gitea-release
|
||||
settings:
|
||||
api_key:
|
||||
from_secret: gitea_token
|
||||
base_url: https://gitea.boner.be
|
||||
files: build/*.zip
|
||||
when:
|
||||
event: tag
|
||||
tag: v*
|
2
Makefile
@ -3,7 +3,7 @@
|
||||
include scripts/platforms.mk
|
||||
|
||||
BINARY_NAME=goonscape
|
||||
VERSION=1.0.0
|
||||
VERSION=1.1.0
|
||||
BUILD_DIR=build
|
||||
ASSETS_DIR=resources
|
||||
|
||||
|
55
README.md
@ -16,24 +16,57 @@ A multiplayer isometric game inspired by Oldschool RuneScape, built with Go and
|
||||
## Prerequisites
|
||||
|
||||
- Go 1.23 or higher
|
||||
- GCC (for CGO/SQLite support)
|
||||
- OpenGL development libraries
|
||||
- Raylib dependencies (see [raylib-go](https://github.com/gen2brain/raylib-go#requirements))
|
||||
|
||||
## Installation
|
||||
|
||||
1. Clone the repository:
|
||||
### Pre-built Binaries
|
||||
The easiest way to get started is to download the latest release from:
|
||||
```
|
||||
https://gitea.boner.be/bdnugget/goonscape/releases
|
||||
```
|
||||
Choose the appropriate zip file for your platform:
|
||||
- Windows: `goonscape-windows-amd64-v1.1.0.zip`
|
||||
- Linux: `goonscape-linux-amd64-v1.1.0.zip`
|
||||
|
||||
Extract the zip and run the executable.
|
||||
|
||||
### Quick Start
|
||||
For development:
|
||||
```bash
|
||||
# Run directly (recommended for development)
|
||||
go run main.go
|
||||
|
||||
# Run with local server
|
||||
go run main.go -local
|
||||
```
|
||||
|
||||
### Server Setup
|
||||
The server requires CGO for SQLite support:
|
||||
```bash
|
||||
# Enable CGO
|
||||
go env -w CGO_ENABLED=1
|
||||
|
||||
# Clone and build server
|
||||
git clone https://gitea.boner.be/bdnugget/goonserver.git
|
||||
cd goonserver
|
||||
go build
|
||||
```
|
||||
|
||||
### Client Installation
|
||||
Then install or build:
|
||||
```bash
|
||||
# Install the client
|
||||
go install gitea.boner.be/bdnugget/goonscape@latest
|
||||
```
|
||||
|
||||
Or build from source:
|
||||
```bash
|
||||
git clone https://gitea.boner.be/bdnugget/goonscape.git
|
||||
cd goonscape
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
go mod tidy
|
||||
```
|
||||
|
||||
3. Build and run:
|
||||
```bash
|
||||
go run main.go
|
||||
go build
|
||||
```
|
||||
|
||||
## Controls
|
||||
|
@ -1,12 +0,0 @@
|
||||
# Blender 3.6.0 MTL File: 'None'
|
||||
# www.blender.org
|
||||
|
||||
newmtl Material.001
|
||||
Ns 250.000000
|
||||
Ka 1.000000 1.000000 1.000000
|
||||
Ks 0.500000 0.500000 0.500000
|
||||
Ke 0.000000 0.000000 0.000000
|
||||
Ni 1.450000
|
||||
d 1.000000
|
||||
illum 2
|
||||
map_Kd coomer.png
|
Before Width: | Height: | Size: 2.2 MiB |
@ -1,12 +0,0 @@
|
||||
# Blender 3.6.0 MTL File: 'None'
|
||||
# www.blender.org
|
||||
|
||||
newmtl Material.001
|
||||
Ns 250.000000
|
||||
Ka 1.000000 1.000000 1.000000
|
||||
Ks 0.500000 0.500000 0.500000
|
||||
Ke 0.000000 0.000000 0.000000
|
||||
Ni 1.450000
|
||||
d 1.000000
|
||||
illum 2
|
||||
map_Kd goonion.png
|
Before Width: | Height: | Size: 2.5 MiB |
@ -1,12 +0,0 @@
|
||||
# Blender 3.6.0 MTL File: 'None'
|
||||
# www.blender.org
|
||||
|
||||
newmtl Material.001
|
||||
Ns 250.000000
|
||||
Ka 1.000000 1.000000 1.000000
|
||||
Ks 0.500000 0.500000 0.500000
|
||||
Ke 0.000000 0.000000 0.000000
|
||||
Ni 1.450000
|
||||
d 1.000000
|
||||
illum 2
|
||||
map_Kd shreke.png
|
Before Width: | Height: | Size: 4.8 MiB |
Before Width: | Height: | Size: 104 KiB |
@ -1,12 +0,0 @@
|
||||
# Blender 3.6.0 MTL File: 'None'
|
||||
# www.blender.org
|
||||
|
||||
newmtl Material.001
|
||||
Ns 250.000000
|
||||
Ka 1.000000 1.000000 1.000000
|
||||
Ks 0.500000 0.500000 0.500000
|
||||
Ke 0.000000 0.000000 0.000000
|
||||
Ni 1.450000
|
||||
d 1.000000
|
||||
illum 2
|
||||
map_Kd coomer.png
|
Before Width: | Height: | Size: 2.2 MiB |
@ -1,12 +0,0 @@
|
||||
# Blender 3.6.0 MTL File: 'None'
|
||||
# www.blender.org
|
||||
|
||||
newmtl Material.001
|
||||
Ns 250.000000
|
||||
Ka 1.000000 1.000000 1.000000
|
||||
Ks 0.500000 0.500000 0.500000
|
||||
Ke 0.000000 0.000000 0.000000
|
||||
Ni 1.450000
|
||||
d 1.000000
|
||||
illum 2
|
||||
map_Kd goonion.png
|
Before Width: | Height: | Size: 2.5 MiB |
@ -1,12 +0,0 @@
|
||||
# Blender 3.6.0 MTL File: 'None'
|
||||
# www.blender.org
|
||||
|
||||
newmtl Material.001
|
||||
Ns 250.000000
|
||||
Ka 1.000000 1.000000 1.000000
|
||||
Ks 0.500000 0.500000 0.500000
|
||||
Ke 0.000000 0.000000 0.000000
|
||||
Ni 1.450000
|
||||
d 1.000000
|
||||
illum 2
|
||||
map_Kd shreke.png
|
Before Width: | Height: | Size: 4.8 MiB |
Before Width: | Height: | Size: 104 KiB |
11
game/chat.go
@ -53,6 +53,7 @@ func (c *Chat) HandleServerMessages(messages []*pb.ChatMessage) {
|
||||
for _, msg := range messages {
|
||||
localMsg := types.ChatMessage{
|
||||
PlayerID: msg.PlayerId,
|
||||
Username: msg.Username,
|
||||
Content: msg.Content,
|
||||
Time: time.Unix(0, msg.Timestamp),
|
||||
}
|
||||
@ -117,8 +118,14 @@ func (c *Chat) Draw(screenWidth, screenHeight int32) {
|
||||
|
||||
for i := startIdx; i < endIdx; i++ {
|
||||
msg := c.messages[i]
|
||||
text := fmt.Sprintf("[%d]: %s", msg.PlayerID, msg.Content)
|
||||
rl.DrawText(text, int32(chatX)+5, int32(messageY), 20, rl.White)
|
||||
var color rl.Color
|
||||
if msg.PlayerID == 0 { // System message
|
||||
color = rl.Gold
|
||||
} else {
|
||||
color = rl.White
|
||||
}
|
||||
text := fmt.Sprintf("%s: %s", msg.Username, msg.Content)
|
||||
rl.DrawText(text, int32(chatX)+5, int32(messageY), 20, color)
|
||||
messageY += messageHeight
|
||||
}
|
||||
|
||||
|
64
game/game.go
@ -5,6 +5,7 @@ import (
|
||||
"time"
|
||||
|
||||
"gitea.boner.be/bdnugget/goonscape/assets"
|
||||
"gitea.boner.be/bdnugget/goonscape/network"
|
||||
"gitea.boner.be/bdnugget/goonscape/types"
|
||||
pb "gitea.boner.be/bdnugget/goonserver/actions"
|
||||
rl "github.com/gen2brain/raylib-go/raylib"
|
||||
@ -19,19 +20,13 @@ type Game struct {
|
||||
Chat *Chat
|
||||
MenuOpen bool
|
||||
QuitChan chan struct{} // Channel to signal shutdown
|
||||
loginScreen *LoginScreen
|
||||
isLoggedIn bool
|
||||
}
|
||||
|
||||
func New() *Game {
|
||||
InitWorld()
|
||||
game := &Game{
|
||||
Player: &types.Player{
|
||||
PosActual: rl.NewVector3(5*types.TileSize, 0, 5*types.TileSize),
|
||||
PosTile: GetTile(5, 5),
|
||||
Speed: 50.0,
|
||||
TargetPath: []types.Tile{},
|
||||
UserData: nil,
|
||||
QuitDone: make(chan struct{}),
|
||||
},
|
||||
OtherPlayers: make(map[int32]*types.Player),
|
||||
Camera: rl.Camera3D{
|
||||
Position: rl.NewVector3(0, 10, 10),
|
||||
@ -40,10 +35,10 @@ func New() *Game {
|
||||
Fovy: 45.0,
|
||||
Projection: rl.CameraPerspective,
|
||||
},
|
||||
Chat: NewChat(),
|
||||
QuitChan: make(chan struct{}),
|
||||
Chat: NewChat(),
|
||||
QuitChan: make(chan struct{}),
|
||||
loginScreen: NewLoginScreen(),
|
||||
}
|
||||
game.Player.UserData = game
|
||||
game.Chat.userData = game
|
||||
return game
|
||||
}
|
||||
@ -64,6 +59,35 @@ func (g *Game) LoadAssets() error {
|
||||
}
|
||||
|
||||
func (g *Game) Update(deltaTime float32) {
|
||||
if !g.isLoggedIn {
|
||||
username, password, isRegistering, submitted := g.loginScreen.Update()
|
||||
if submitted {
|
||||
conn, playerID, err := network.ConnectToServer(username, password, isRegistering)
|
||||
if err != nil {
|
||||
g.loginScreen.SetError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Assign model based on player ID
|
||||
modelIndex := int(playerID) % len(g.Models)
|
||||
g.Player = &types.Player{
|
||||
Speed: 50.0,
|
||||
TargetPath: []types.Tile{},
|
||||
UserData: g,
|
||||
QuitDone: make(chan struct{}),
|
||||
ID: playerID,
|
||||
Model: g.Models[modelIndex].Model,
|
||||
Texture: g.Models[modelIndex].Texture,
|
||||
}
|
||||
|
||||
go network.HandleServerCommunication(conn, playerID, g.Player, g.OtherPlayers, g.QuitChan)
|
||||
g.isLoggedIn = true
|
||||
return
|
||||
}
|
||||
g.loginScreen.Draw()
|
||||
return
|
||||
}
|
||||
|
||||
// Handle ESC for menu
|
||||
if rl.IsKeyPressed(rl.KeyEscape) {
|
||||
g.MenuOpen = !g.MenuOpen
|
||||
@ -174,11 +198,23 @@ func (g *Game) Render() {
|
||||
rl.BeginDrawing()
|
||||
rl.ClearBackground(rl.RayWhite)
|
||||
|
||||
if !g.isLoggedIn {
|
||||
g.loginScreen.Draw()
|
||||
rl.EndDrawing()
|
||||
return
|
||||
}
|
||||
|
||||
rl.BeginMode3D(g.Camera)
|
||||
g.DrawMap()
|
||||
g.DrawPlayer(g.Player, g.Player.Model)
|
||||
for id, other := range g.OtherPlayers {
|
||||
g.DrawPlayer(other, g.Models[int(id)%len(g.Models)].Model)
|
||||
if other.Model.Meshes == nil {
|
||||
// Assign model based on player ID for consistency
|
||||
modelIndex := int(id) % len(g.Models)
|
||||
other.Model = g.Models[modelIndex].Model
|
||||
other.Texture = g.Models[modelIndex].Texture
|
||||
}
|
||||
g.DrawPlayer(other, other.Model)
|
||||
}
|
||||
rl.EndMode3D()
|
||||
|
||||
@ -312,3 +348,7 @@ func (g *Game) Shutdown() {
|
||||
rl.CloseWindow()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func (g *Game) HandleServerMessages(messages []*pb.ChatMessage) {
|
||||
g.Chat.HandleServerMessages(messages)
|
||||
}
|
||||
|
185
game/login.go
Normal file
@ -0,0 +1,185 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
rl "github.com/gen2brain/raylib-go/raylib"
|
||||
)
|
||||
|
||||
type LoginScreen struct {
|
||||
username string
|
||||
password string
|
||||
errorMessage string
|
||||
isRegistering bool
|
||||
focusedField int // 0 = username, 1 = password
|
||||
}
|
||||
|
||||
func NewLoginScreen() *LoginScreen {
|
||||
return &LoginScreen{}
|
||||
}
|
||||
|
||||
func (l *LoginScreen) Draw() {
|
||||
screenWidth := float32(rl.GetScreenWidth())
|
||||
screenHeight := float32(rl.GetScreenHeight())
|
||||
|
||||
// Draw background
|
||||
rl.DrawRectangle(0, 0, int32(screenWidth), int32(screenHeight), rl.RayWhite)
|
||||
|
||||
// Draw title
|
||||
title := "GoonScape"
|
||||
if l.isRegistering {
|
||||
title += " - Register"
|
||||
} else {
|
||||
title += " - Login"
|
||||
}
|
||||
titleSize := int32(40)
|
||||
titleWidth := rl.MeasureText(title, titleSize)
|
||||
rl.DrawText(title, int32(screenWidth/2)-titleWidth/2, 100, titleSize, rl.Black)
|
||||
|
||||
// Draw input fields
|
||||
inputWidth := float32(200)
|
||||
inputHeight := float32(30)
|
||||
inputX := screenWidth/2 - inputWidth/2
|
||||
|
||||
// Username field
|
||||
rl.DrawRectangleRec(rl.Rectangle{
|
||||
X: inputX, Y: 200,
|
||||
Width: inputWidth, Height: inputHeight,
|
||||
}, rl.LightGray)
|
||||
rl.DrawText(l.username, int32(inputX)+5, 205, 20, rl.Black)
|
||||
if l.focusedField == 0 {
|
||||
rl.DrawRectangleLinesEx(rl.Rectangle{
|
||||
X: inputX - 2, Y: 198,
|
||||
Width: inputWidth + 4, Height: inputHeight + 4,
|
||||
}, 2, rl.Blue)
|
||||
}
|
||||
|
||||
// Password field
|
||||
rl.DrawRectangleRec(rl.Rectangle{
|
||||
X: inputX, Y: 250,
|
||||
Width: inputWidth, Height: inputHeight,
|
||||
}, rl.LightGray)
|
||||
masked := ""
|
||||
for range l.password {
|
||||
masked += "*"
|
||||
}
|
||||
rl.DrawText(masked, int32(inputX)+5, 255, 20, rl.Black)
|
||||
if l.focusedField == 1 {
|
||||
rl.DrawRectangleLinesEx(rl.Rectangle{
|
||||
X: inputX - 2, Y: 248,
|
||||
Width: inputWidth + 4, Height: inputHeight + 4,
|
||||
}, 2, rl.Blue)
|
||||
}
|
||||
|
||||
// Draw error message
|
||||
if l.errorMessage != "" {
|
||||
msgWidth := rl.MeasureText(l.errorMessage, 20)
|
||||
rl.DrawText(l.errorMessage, int32(screenWidth/2)-msgWidth/2, 300, 20, rl.Red)
|
||||
}
|
||||
|
||||
// Draw buttons
|
||||
buttonWidth := float32(100)
|
||||
buttonHeight := float32(30)
|
||||
buttonY := float32(350)
|
||||
|
||||
// Login/Register button
|
||||
actionBtn := rl.Rectangle{
|
||||
X: screenWidth/2 - buttonWidth - 10,
|
||||
Y: buttonY,
|
||||
Width: buttonWidth,
|
||||
Height: buttonHeight,
|
||||
}
|
||||
rl.DrawRectangleRec(actionBtn, rl.Blue)
|
||||
actionText := "Login"
|
||||
if l.isRegistering {
|
||||
actionText = "Register"
|
||||
}
|
||||
actionWidth := rl.MeasureText(actionText, 20)
|
||||
rl.DrawText(actionText,
|
||||
int32(actionBtn.X+actionBtn.Width/2)-actionWidth/2,
|
||||
int32(actionBtn.Y+5),
|
||||
20, rl.White)
|
||||
|
||||
// Switch mode button
|
||||
switchBtn := rl.Rectangle{
|
||||
X: screenWidth/2 + 10,
|
||||
Y: buttonY,
|
||||
Width: buttonWidth,
|
||||
Height: buttonHeight,
|
||||
}
|
||||
rl.DrawRectangleRec(switchBtn, rl.DarkGray)
|
||||
switchText := "Register"
|
||||
if l.isRegistering {
|
||||
switchText = "Login"
|
||||
}
|
||||
switchWidth := rl.MeasureText(switchText, 20)
|
||||
rl.DrawText(switchText,
|
||||
int32(switchBtn.X+switchBtn.Width/2)-switchWidth/2,
|
||||
int32(switchBtn.Y+5),
|
||||
20, rl.White)
|
||||
}
|
||||
|
||||
func (l *LoginScreen) Update() (string, string, bool, bool) {
|
||||
// Handle input field focus
|
||||
if rl.IsMouseButtonPressed(rl.MouseLeftButton) {
|
||||
mousePos := rl.GetMousePosition()
|
||||
screenWidth := float32(rl.GetScreenWidth())
|
||||
inputWidth := float32(200)
|
||||
inputX := screenWidth/2 - inputWidth/2
|
||||
|
||||
// Check username field
|
||||
if mousePos.X >= inputX && mousePos.X <= inputX+inputWidth &&
|
||||
mousePos.Y >= 200 && mousePos.Y <= 230 {
|
||||
l.focusedField = 0
|
||||
}
|
||||
// Check password field
|
||||
if mousePos.X >= inputX && mousePos.X <= inputX+inputWidth &&
|
||||
mousePos.Y >= 250 && mousePos.Y <= 280 {
|
||||
l.focusedField = 1
|
||||
}
|
||||
|
||||
// Check buttons
|
||||
buttonWidth := float32(100)
|
||||
if mousePos.Y >= 350 && mousePos.Y <= 380 {
|
||||
// Action button
|
||||
if mousePos.X >= screenWidth/2-buttonWidth-10 &&
|
||||
mousePos.X <= screenWidth/2-10 {
|
||||
return l.username, l.password, l.isRegistering, true
|
||||
}
|
||||
// Switch mode button
|
||||
if mousePos.X >= screenWidth/2+10 &&
|
||||
mousePos.X <= screenWidth/2+buttonWidth+10 {
|
||||
l.isRegistering = !l.isRegistering
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle text input
|
||||
key := rl.GetCharPressed()
|
||||
for key > 0 {
|
||||
if l.focusedField == 0 && len(l.username) < 12 {
|
||||
l.username += string(key)
|
||||
} else if l.focusedField == 1 && len(l.password) < 20 {
|
||||
l.password += string(key)
|
||||
}
|
||||
key = rl.GetCharPressed()
|
||||
}
|
||||
|
||||
// Handle backspace
|
||||
if rl.IsKeyPressed(rl.KeyBackspace) {
|
||||
if l.focusedField == 0 && len(l.username) > 0 {
|
||||
l.username = l.username[:len(l.username)-1]
|
||||
} else if l.focusedField == 1 && len(l.password) > 0 {
|
||||
l.password = l.password[:len(l.password)-1]
|
||||
}
|
||||
}
|
||||
|
||||
// Handle tab to switch fields
|
||||
if rl.IsKeyPressed(rl.KeyTab) {
|
||||
l.focusedField = (l.focusedField + 1) % 2
|
||||
}
|
||||
|
||||
return "", "", false, false
|
||||
}
|
||||
|
||||
func (l *LoginScreen) SetError(msg string) {
|
||||
l.errorMessage = msg
|
||||
}
|
2
go.mod
@ -3,7 +3,7 @@ module gitea.boner.be/bdnugget/goonscape
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
gitea.boner.be/bdnugget/goonserver v0.0.0-20250113131525-49e23114973c
|
||||
gitea.boner.be/bdnugget/goonserver v1.1.0
|
||||
github.com/gen2brain/raylib-go/raylib v0.0.0-20250109172833-6dbba4f81a9b
|
||||
google.golang.org/protobuf v1.36.3
|
||||
)
|
||||
|
37
main.go
@ -11,20 +11,21 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
local := flag.Bool("local", false, "Use local server instead of remote")
|
||||
addr := flag.String("addr", "boner.be:6969", "Server address (hostname:port or hostname)")
|
||||
// Parse command line flags
|
||||
local := flag.Bool("local", false, "Connect to local server")
|
||||
addr := flag.String("addr", "", "Server address (host or host:port)")
|
||||
flag.Parse()
|
||||
|
||||
if *local && *addr != "boner.be:6969" {
|
||||
log.Fatal("Cannot use both -local and -addr flags")
|
||||
}
|
||||
|
||||
// Set server address based on flags
|
||||
if *local {
|
||||
if *addr != "" {
|
||||
log.Fatal("Cannot use -local and -addr together")
|
||||
}
|
||||
network.SetServerAddr("localhost:6969")
|
||||
} else if *addr != "" {
|
||||
// If only hostname is provided, append default port
|
||||
// If port is not specified, append default port
|
||||
if !strings.Contains(*addr, ":") {
|
||||
*addr = *addr + ":6969"
|
||||
*addr += ":6969"
|
||||
}
|
||||
network.SetServerAddr(*addr)
|
||||
}
|
||||
@ -32,36 +33,24 @@ func main() {
|
||||
rl.InitWindow(1024, 768, "GoonScape")
|
||||
rl.SetExitKey(0)
|
||||
defer rl.CloseWindow()
|
||||
|
||||
rl.InitAudioDevice()
|
||||
defer rl.CloseAudioDevice()
|
||||
|
||||
rl.SetTargetFPS(60)
|
||||
|
||||
game := game.New()
|
||||
if err := game.LoadAssets(); err != nil {
|
||||
log.Fatalf("Failed to load assets: %v", err)
|
||||
}
|
||||
defer game.Cleanup()
|
||||
|
||||
conn, playerID, err := network.ConnectToServer()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to connect to server: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
game.Player.ID = playerID
|
||||
modelIndex := int(playerID) % len(game.Models)
|
||||
game.Player.Model = game.Models[modelIndex].Model
|
||||
game.Player.Texture = game.Models[modelIndex].Texture
|
||||
|
||||
go network.HandleServerCommunication(conn, playerID, game.Player, game.OtherPlayers, game.QuitChan)
|
||||
|
||||
rl.PlayMusicStream(game.Music)
|
||||
rl.SetMusicVolume(game.Music, 0.5)
|
||||
rl.SetTargetFPS(60)
|
||||
|
||||
for !rl.WindowShouldClose() {
|
||||
rl.UpdateMusicStream(game.Music)
|
||||
deltaTime := rl.GetFrameTime()
|
||||
|
||||
rl.UpdateMusicStream(game.Music)
|
||||
game.Update(deltaTime)
|
||||
game.Render()
|
||||
}
|
||||
|
@ -3,56 +3,89 @@ package network
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"gitea.boner.be/bdnugget/goonscape/game"
|
||||
"gitea.boner.be/bdnugget/goonscape/types"
|
||||
pb "gitea.boner.be/bdnugget/goonserver/actions"
|
||||
rl "github.com/gen2brain/raylib-go/raylib"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const protoVersion = 1
|
||||
|
||||
var serverAddr = "boner.be:6969"
|
||||
|
||||
func SetServerAddr(addr string) {
|
||||
serverAddr = addr
|
||||
}
|
||||
|
||||
func ConnectToServer() (net.Conn, int32, error) {
|
||||
func ConnectToServer(username, password string, isRegistering bool) (net.Conn, int32, error) {
|
||||
conn, err := net.Dial("tcp", serverAddr)
|
||||
if err != nil {
|
||||
log.Printf("Failed to dial server: %v", err)
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
log.Println("Connected to server. Waiting for player ID...")
|
||||
reader := bufio.NewReader(conn)
|
||||
log.Println("Connected to server. Authenticating...")
|
||||
|
||||
// Read message length (4 bytes)
|
||||
// Send auth message
|
||||
authAction := &pb.Action{
|
||||
Type: pb.Action_LOGIN,
|
||||
Username: username,
|
||||
Password: password,
|
||||
}
|
||||
if isRegistering {
|
||||
authAction.Type = pb.Action_REGISTER
|
||||
}
|
||||
|
||||
authBatch := &pb.ActionBatch{
|
||||
Actions: []*pb.Action{authAction},
|
||||
ProtocolVersion: protoVersion,
|
||||
}
|
||||
|
||||
if err := writeMessage(conn, authBatch); err != nil {
|
||||
conn.Close()
|
||||
return nil, 0, fmt.Errorf("failed to send auth: %v", err)
|
||||
}
|
||||
|
||||
// Read server response
|
||||
reader := bufio.NewReader(conn)
|
||||
lengthBuf := make([]byte, 4)
|
||||
if _, err := io.ReadFull(reader, lengthBuf); err != nil {
|
||||
log.Printf("Failed to read message length: %v", err)
|
||||
return nil, 0, err
|
||||
conn.Close()
|
||||
return nil, 0, fmt.Errorf("failed to read auth response: %v", err)
|
||||
}
|
||||
messageLength := binary.BigEndian.Uint32(lengthBuf)
|
||||
|
||||
// Read the full message
|
||||
messageBuf := make([]byte, messageLength)
|
||||
if _, err := io.ReadFull(reader, messageBuf); err != nil {
|
||||
log.Printf("Failed to read message body: %v", err)
|
||||
return nil, 0, err
|
||||
conn.Close()
|
||||
return nil, 0, fmt.Errorf("failed to read auth response body: %v", err)
|
||||
}
|
||||
|
||||
var response pb.ServerMessage
|
||||
if err := proto.Unmarshal(messageBuf, &response); err != nil {
|
||||
log.Printf("Failed to unmarshal server response: %v", err)
|
||||
return nil, 0, err
|
||||
conn.Close()
|
||||
return nil, 0, fmt.Errorf("failed to unmarshal auth response: %v", err)
|
||||
}
|
||||
|
||||
if response.ProtocolVersion > protoVersion {
|
||||
conn.Close()
|
||||
return nil, 0, fmt.Errorf("server requires newer protocol version (server: %d, client: %d)",
|
||||
response.ProtocolVersion, protoVersion)
|
||||
}
|
||||
|
||||
if !response.AuthSuccess {
|
||||
conn.Close()
|
||||
return nil, 0, fmt.Errorf(response.ErrorMessage)
|
||||
}
|
||||
|
||||
playerID := response.GetPlayerId()
|
||||
log.Printf("Successfully connected with player ID: %d", playerID)
|
||||
log.Printf("Successfully authenticated with player ID: %d", playerID)
|
||||
return conn, playerID, nil
|
||||
}
|
||||
|
||||
@ -67,6 +100,9 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play
|
||||
// Create a channel to signal when goroutines are done
|
||||
done := make(chan struct{})
|
||||
|
||||
// Create a set of current players to track disconnects
|
||||
currentPlayers := make(map[int32]bool)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
@ -161,7 +197,19 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play
|
||||
player.Unlock()
|
||||
|
||||
for _, state := range serverMessage.Players {
|
||||
currentPlayers[state.PlayerId] = true
|
||||
if state.PlayerId == playerID {
|
||||
player.Lock()
|
||||
// Update initial position if not set
|
||||
if player.PosActual.X == 0 && player.PosActual.Z == 0 {
|
||||
player.PosActual = rl.Vector3{
|
||||
X: float32(state.X * types.TileSize),
|
||||
Y: 0,
|
||||
Z: float32(state.Y * types.TileSize),
|
||||
}
|
||||
player.PosTile = types.Tile{X: int(state.X), Y: int(state.Y)}
|
||||
}
|
||||
player.Unlock()
|
||||
continue
|
||||
}
|
||||
|
||||
@ -172,8 +220,15 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play
|
||||
}
|
||||
}
|
||||
|
||||
if g, ok := player.UserData.(*game.Game); ok && len(serverMessage.ChatMessages) > 0 {
|
||||
g.Chat.HandleServerMessages(serverMessage.ChatMessages)
|
||||
// Remove players that are no longer in the server state
|
||||
for id := range otherPlayers {
|
||||
if !currentPlayers[id] {
|
||||
delete(otherPlayers, id)
|
||||
}
|
||||
}
|
||||
|
||||
if handler, ok := player.UserData.(types.ChatMessageHandler); ok && len(serverMessage.ChatMessages) > 0 {
|
||||
handler.HandleServerMessages(serverMessage.ChatMessages)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,12 +6,15 @@ build() {
|
||||
local arch=$2
|
||||
local output=$3
|
||||
|
||||
# Set CGO flags for static linking
|
||||
export CGO_ENABLED=1
|
||||
# Set GOOS and GOARCH for cross-compilation
|
||||
export GOOS=$os
|
||||
export GOARCH=$arch
|
||||
|
||||
# Platform specific flags
|
||||
|
||||
# Disable CGO only for cross-compilation
|
||||
if [ "$os" != "$(go env GOOS)" ] || [ "$arch" != "$(go env GOARCH)" ]; then
|
||||
export CGO_ENABLED=0
|
||||
fi
|
||||
|
||||
if [ "$os" = "windows" ]; then
|
||||
export CC=x86_64-w64-mingw32-gcc
|
||||
export CXX=x86_64-w64-mingw32-g++
|
||||
|
@ -39,6 +39,7 @@ type ModelAsset struct {
|
||||
|
||||
type ChatMessage struct {
|
||||
PlayerID int32
|
||||
Username string
|
||||
Content string
|
||||
Time time.Time
|
||||
}
|
||||
@ -49,6 +50,10 @@ type FloatingMessage struct {
|
||||
ScreenPos rl.Vector2 // Store the screen position for 2D rendering
|
||||
}
|
||||
|
||||
type ChatMessageHandler interface {
|
||||
HandleServerMessages([]*pb.ChatMessage)
|
||||
}
|
||||
|
||||
const (
|
||||
MapWidth = 50
|
||||
MapHeight = 50
|
||||
|