Compare commits
23 Commits
2a0f9348e9
...
v1.1.1
Author | SHA1 | Date | |
---|---|---|---|
944c33ce3b | |||
d5bb464d9f | |||
4549ee7517 | |||
31ae9c525f | |||
06913a5217 | |||
49663c9094 | |||
a843680b09 | |||
7183df4a8b | |||
33e355200d | |||
e45066b2a8 | |||
bb01dccf2b | |||
0f56916295 | |||
a1ddbadea0 | |||
e4d0b98945 | |||
509bc8b20b | |||
c40e4ae7ac | |||
863f5a939c | |||
cd68581429 | |||
b9d0d46bd6 | |||
b96c7ada7a | |||
d86cbe15a3 | |||
fb018e2a7d | |||
5ca973fdf1 |
13
.gitignore
vendored
Normal file
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
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*
|
30
Dockerfile.build
Normal file
30
Dockerfile.build
Normal file
@ -0,0 +1,30 @@
|
||||
FROM golang:1.23
|
||||
|
||||
# Install build dependencies
|
||||
RUN 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 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
# Copy build scripts
|
||||
COPY . /build/
|
||||
|
||||
# Set execute permissions
|
||||
RUN chmod +x /build/scripts/build.sh
|
||||
|
||||
# Build command
|
||||
CMD ["make", "all"]
|
24
LICENSE
24
LICENSE
@ -1,11 +1,21 @@
|
||||
“Commons Clause” License Condition v1.0
|
||||
MIT License
|
||||
|
||||
The Software is provided to you by the Licensor under the License, as defined below, subject to the following condition.
|
||||
Copyright (c) 2025 bdnugget
|
||||
|
||||
Without limiting other conditions in the License, the grant of rights under the License will not include, and the License does not grant to you, right to Sell the Software.
|
||||
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:
|
||||
|
||||
For purposes of the foregoing, “Sell” means practicing any or all of the rights granted to you under the License to provide to third parties, for a fee or other consideration (including without limitation fees for hosting or consulting/ support services related to the Software), a product or service whose value derives, entirely or substantially, from the functionality of the Software. Any license notice or attribution required by the License must also include this Commons Cause License Condition notice.
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
Software: GoonScape
|
||||
License: Commons Clause v1.0
|
||||
Licensor: bdnugget
|
||||
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.
|
||||
|
30
Makefile
Normal file
30
Makefile
Normal file
@ -0,0 +1,30 @@
|
||||
.PHONY: all clean windows linux darwin
|
||||
|
||||
include scripts/platforms.mk
|
||||
|
||||
BINARY_NAME=goonscape
|
||||
VERSION=1.1.0
|
||||
BUILD_DIR=build
|
||||
ASSETS_DIR=resources
|
||||
|
||||
all: clean $(PLATFORMS)
|
||||
|
||||
$(PLATFORMS):
|
||||
@echo "Building for $@..."
|
||||
@mkdir -p $(BUILD_DIR)/$@
|
||||
@scripts/build.sh $(word 1,$(subst /, ,$@)) $(word 2,$(subst /, ,$@)) \
|
||||
$(BUILD_DIR)/$@/$(BINARY_NAME)$(if $(findstring windows,$@),.exe,)
|
||||
@cp -r $(ASSETS_DIR) $(BUILD_DIR)/$@/
|
||||
@cd $(BUILD_DIR) && zip -r $(BINARY_NAME)-$(word 1,$(subst /, ,$@))-$(word 2,$(subst /, ,$@))-v$(VERSION).zip $@
|
||||
@echo "Done building for $@"
|
||||
|
||||
clean:
|
||||
rm -rf $(BUILD_DIR)
|
||||
|
||||
# Development build for current platform
|
||||
dev:
|
||||
go build -o $(BINARY_NAME)
|
||||
|
||||
# Run tests
|
||||
test:
|
||||
go test ./...
|
77
README.md
77
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
|
||||
@ -41,7 +74,7 @@ go run main.go
|
||||
- **Mouse Click**: Move to location
|
||||
- **T**: Open chat
|
||||
- **Enter**: Send chat message
|
||||
- **Escape**: Cancel chat/Close game
|
||||
- **Escape**: Cancel chat/Close game (it does both of these at the same time so gg)
|
||||
- **Arrow Keys**: Rotate camera
|
||||
- **Mouse Wheel**: Zoom in/out
|
||||
|
||||
@ -63,6 +96,26 @@ go run main.go -addr somehost:6970 # Uses somehost:6970
|
||||
|
||||
Note: The `-local` flag is a shorthand for `-addr localhost:6969` and cannot be used together with `-addr`.
|
||||
|
||||
## Building Release Binaries
|
||||
|
||||
The project uses Docker to create consistent builds across platforms. To build release binaries:
|
||||
|
||||
1. Build the Docker image (only needed once):
|
||||
```bash
|
||||
sudo docker build -t goonscape-builder -f Dockerfile.build .
|
||||
```
|
||||
|
||||
2. Create release builds:
|
||||
```bash
|
||||
sudo docker run -v $(pwd):/build goonscape-builder
|
||||
```
|
||||
|
||||
This will create zip files in the `build` directory for:
|
||||
- Windows (64-bit): `goonscape-windows-amd64-v1.0.0.zip`
|
||||
- Linux (64-bit): `goonscape-linux-amd64-v1.0.0.zip`
|
||||
|
||||
Each zip contains the binary and all required assets.
|
||||
|
||||
## Development
|
||||
|
||||
The project uses Protocol Buffers for network communication. If you modify the `.proto` files, regenerate the Go code with:
|
||||
|
14
game/chat.go
14
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
|
||||
}
|
||||
|
||||
@ -168,13 +175,12 @@ func (c *Chat) Update() (string, bool) {
|
||||
c.inputBuffer = c.inputBuffer[:0]
|
||||
c.cursorPos = 0
|
||||
c.isTyping = false
|
||||
|
||||
return message, true
|
||||
}
|
||||
c.isTyping = false
|
||||
}
|
||||
|
||||
if rl.IsKeyPressed(rl.KeyEscape) {
|
||||
if rl.IsKeyPressed(rl.KeyEscape) && c.isTyping {
|
||||
c.inputBuffer = c.inputBuffer[:0]
|
||||
c.cursorPos = 0
|
||||
c.isTyping = false
|
||||
|
157
game/game.go
157
game/game.go
@ -1,9 +1,11 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
"os"
|
||||
"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"
|
||||
@ -16,18 +18,15 @@ type Game struct {
|
||||
Models []types.ModelAsset
|
||||
Music rl.Music
|
||||
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,
|
||||
},
|
||||
OtherPlayers: make(map[int32]*types.Player),
|
||||
Camera: rl.Camera3D{
|
||||
Position: rl.NewVector3(0, 10, 10),
|
||||
@ -36,9 +35,10 @@ func New() *Game {
|
||||
Fovy: 45.0,
|
||||
Projection: rl.CameraPerspective,
|
||||
},
|
||||
Chat: NewChat(),
|
||||
Chat: NewChat(),
|
||||
QuitChan: make(chan struct{}),
|
||||
loginScreen: NewLoginScreen(),
|
||||
}
|
||||
game.Player.UserData = game
|
||||
game.Chat.userData = game
|
||||
return game
|
||||
}
|
||||
@ -59,6 +59,46 @@ 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
|
||||
return
|
||||
}
|
||||
|
||||
// Don't process other inputs if menu is open
|
||||
if g.MenuOpen {
|
||||
return
|
||||
}
|
||||
|
||||
if message, sent := g.Chat.Update(); sent {
|
||||
g.Player.Lock()
|
||||
g.Player.ActionQueue = append(g.Player.ActionQueue, &pb.Action{
|
||||
@ -125,7 +165,7 @@ func (g *Game) DrawPlayer(player *types.Player, model rl.Model) {
|
||||
|
||||
rl.DrawModel(model, playerPos, 16, rl.White)
|
||||
|
||||
if player.FloatingMessage != nil && time.Now().Before(player.FloatingMessage.ExpireTime) {
|
||||
if player.FloatingMessage != nil {
|
||||
screenPos := rl.GetWorldToScreen(rl.Vector3{
|
||||
X: playerPos.X,
|
||||
Y: playerPos.Y + 24.0,
|
||||
@ -133,8 +173,6 @@ func (g *Game) DrawPlayer(player *types.Player, model rl.Model) {
|
||||
}, g.Camera)
|
||||
|
||||
player.FloatingMessage.ScreenPos = screenPos
|
||||
} else if player.FloatingMessage != nil {
|
||||
player.FloatingMessage = nil
|
||||
}
|
||||
|
||||
if len(player.TargetPath) > 0 {
|
||||
@ -160,14 +198,27 @@ 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()
|
||||
|
||||
// Draw floating messages
|
||||
drawFloatingMessage := func(msg *types.FloatingMessage) {
|
||||
if msg == nil || time.Now().After(msg.ExpireTime) {
|
||||
return
|
||||
@ -196,8 +247,17 @@ func (g *Game) Render() {
|
||||
drawFloatingMessage(other.FloatingMessage)
|
||||
}
|
||||
|
||||
// Draw menu if open
|
||||
if g.MenuOpen {
|
||||
g.DrawMenu()
|
||||
}
|
||||
|
||||
// Only draw chat if menu is not open
|
||||
if !g.MenuOpen {
|
||||
g.Chat.Draw(int32(rl.GetScreenWidth()), int32(rl.GetScreenHeight()))
|
||||
}
|
||||
|
||||
rl.DrawFPS(10, 10)
|
||||
g.Chat.Draw(int32(rl.GetScreenWidth()), int32(rl.GetScreenHeight()))
|
||||
rl.EndDrawing()
|
||||
}
|
||||
|
||||
@ -223,3 +283,72 @@ func (g *Game) HandleInput() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Game) DrawMenu() {
|
||||
screenWidth := float32(rl.GetScreenWidth())
|
||||
screenHeight := float32(rl.GetScreenHeight())
|
||||
|
||||
// Semi-transparent background
|
||||
rl.DrawRectangle(0, 0, int32(screenWidth), int32(screenHeight), rl.ColorAlpha(rl.Black, 0.7))
|
||||
|
||||
// Menu title
|
||||
title := "Menu"
|
||||
titleSize := int32(40)
|
||||
titleWidth := rl.MeasureText(title, titleSize)
|
||||
rl.DrawText(title, int32(screenWidth/2)-titleWidth/2, 100, titleSize, rl.White)
|
||||
|
||||
// Menu buttons
|
||||
buttonWidth := float32(200)
|
||||
buttonHeight := float32(40)
|
||||
buttonY := float32(200)
|
||||
buttonSpacing := float32(60)
|
||||
|
||||
menuItems := []string{"Resume", "Settings", "Exit Game"}
|
||||
for _, item := range menuItems {
|
||||
buttonRect := rl.Rectangle{
|
||||
X: screenWidth/2 - buttonWidth/2,
|
||||
Y: buttonY,
|
||||
Width: buttonWidth,
|
||||
Height: buttonHeight,
|
||||
}
|
||||
|
||||
// Check mouse hover
|
||||
mousePoint := rl.GetMousePosition()
|
||||
mouseHover := rl.CheckCollisionPointRec(mousePoint, buttonRect)
|
||||
|
||||
// Draw button
|
||||
if mouseHover {
|
||||
rl.DrawRectangleRec(buttonRect, rl.ColorAlpha(rl.White, 0.3))
|
||||
if rl.IsMouseButtonPressed(rl.MouseLeftButton) {
|
||||
switch item {
|
||||
case "Resume":
|
||||
g.MenuOpen = false
|
||||
case "Settings":
|
||||
// TODO: Implement settings
|
||||
case "Exit Game":
|
||||
g.Shutdown()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw button text
|
||||
textSize := int32(20)
|
||||
textWidth := rl.MeasureText(item, textSize)
|
||||
textX := int32(buttonRect.X+buttonRect.Width/2) - textWidth/2
|
||||
textY := int32(buttonRect.Y + buttonRect.Height/2 - float32(textSize)/2)
|
||||
rl.DrawText(item, textX, textY, textSize, rl.White)
|
||||
|
||||
buttonY += buttonSpacing
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Game) Shutdown() {
|
||||
close(g.QuitChan)
|
||||
<-g.Player.QuitDone
|
||||
rl.CloseWindow()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func (g *Game) HandleServerMessages(messages []*pb.ChatMessage) {
|
||||
g.Chat.HandleServerMessages(messages)
|
||||
}
|
||||
|
185
game/login.go
Normal file
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
|
||||
}
|
6
go.mod
6
go.mod
@ -3,9 +3,9 @@ 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.2
|
||||
google.golang.org/protobuf v1.36.3
|
||||
)
|
||||
|
||||
require (
|
||||
@ -14,4 +14,4 @@ require (
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
)
|
||||
|
||||
// replace gitea.boner.be/bdnugget/goonserver => ./goonserver
|
||||
replace gitea.boner.be/bdnugget/goonserver => ./goonserver
|
||||
|
16
go.sum
16
go.sum
@ -1,24 +1,12 @@
|
||||
gitea.boner.be/bdnugget/goonserver v0.0.0-20250113131525-49e23114973c h1:TO14y5QeQXn6sLCv6vORVdjnMn5hP/Vd+60UjqcrtFA=
|
||||
gitea.boner.be/bdnugget/goonserver v0.0.0-20250113131525-49e23114973c/go.mod h1:inR1bKrr/vcTba+G1KzmmY6vssMq9oGNOk836VwPa4c=
|
||||
github.com/ebitengine/purego v0.8.0 h1:JbqvnEzRvPpxhCJzJJ2y0RbiZ8nyjccVUrSM3q+GvvE=
|
||||
github.com/ebitengine/purego v0.8.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
|
||||
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/gen2brain/raylib-go/raylib v0.0.0-20240930075631-c66f9e2942fe h1:mInjrbJkUglTM7tBmXG+epnPCE744aj15J7vjJwM4gs=
|
||||
github.com/gen2brain/raylib-go/raylib v0.0.0-20240930075631-c66f9e2942fe/go.mod h1:BaY76bZk7nw1/kVOSQObPY1v1iwVE1KHAGMfvI6oK1Q=
|
||||
github.com/gen2brain/raylib-go/raylib v0.0.0-20250109172833-6dbba4f81a9b h1:JJfspevP3YOXcSKVABizYOv++yMpTJIdPUtoDzF/RWw=
|
||||
github.com/gen2brain/raylib-go/raylib v0.0.0-20250109172833-6dbba4f81a9b/go.mod h1:BaY76bZk7nw1/kVOSQObPY1v1iwVE1KHAGMfvI6oK1Q=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
|
||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
|
||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
|
||||
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU=
|
||||
google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
|
||||
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
|
Submodule goonserver updated: b73d8de851...f9ec811b10
41
main.go
41
main.go
@ -11,57 +11,50 @@ 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)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
// Wait for clean shutdown
|
||||
<-game.QuitChan
|
||||
}
|
||||
|
@ -3,142 +3,233 @@ 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
|
||||
}
|
||||
|
||||
func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Player, otherPlayers map[int32]*types.Player) {
|
||||
// Create a buffered reader for the connection
|
||||
func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Player, otherPlayers map[int32]*types.Player, quitChan <-chan struct{}) {
|
||||
reader := bufio.NewReader(conn)
|
||||
|
||||
actionTicker := time.NewTicker(types.ClientTickRate)
|
||||
defer actionTicker.Stop()
|
||||
defer conn.Close()
|
||||
defer close(player.QuitDone)
|
||||
|
||||
// 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 range actionTicker.C {
|
||||
player.Lock()
|
||||
if len(player.ActionQueue) > 0 {
|
||||
actions := make([]*pb.Action, len(player.ActionQueue))
|
||||
copy(actions, player.ActionQueue)
|
||||
|
||||
batch := &pb.ActionBatch{
|
||||
for {
|
||||
select {
|
||||
case <-quitChan:
|
||||
// Send disconnect message to server
|
||||
disconnectMsg := &pb.ActionBatch{
|
||||
PlayerId: playerID,
|
||||
Actions: actions,
|
||||
Tick: player.CurrentTick,
|
||||
Actions: []*pb.Action{{
|
||||
Type: pb.Action_DISCONNECT,
|
||||
PlayerId: playerID,
|
||||
}},
|
||||
}
|
||||
writeMessage(conn, disconnectMsg)
|
||||
done <- struct{}{}
|
||||
return
|
||||
case <-actionTicker.C:
|
||||
player.Lock()
|
||||
if len(player.ActionQueue) > 0 {
|
||||
actions := make([]*pb.Action, len(player.ActionQueue))
|
||||
copy(actions, player.ActionQueue)
|
||||
|
||||
player.ActionQueue = player.ActionQueue[:0]
|
||||
player.Unlock()
|
||||
batch := &pb.ActionBatch{
|
||||
PlayerId: playerID,
|
||||
Actions: actions,
|
||||
Tick: player.CurrentTick,
|
||||
}
|
||||
|
||||
if err := writeMessage(conn, batch); err != nil {
|
||||
log.Printf("Failed to send actions to server: %v", err)
|
||||
return
|
||||
player.ActionQueue = player.ActionQueue[:0]
|
||||
player.Unlock()
|
||||
|
||||
if err := writeMessage(conn, batch); err != nil {
|
||||
log.Printf("Failed to send actions to server: %v", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
player.Unlock()
|
||||
}
|
||||
} else {
|
||||
player.Unlock()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
// Read message length (4 bytes)
|
||||
lengthBuf := make([]byte, 4)
|
||||
if _, err := io.ReadFull(reader, lengthBuf); err != nil {
|
||||
log.Printf("Failed to read message length: %v", err)
|
||||
return
|
||||
}
|
||||
messageLength := binary.BigEndian.Uint32(lengthBuf)
|
||||
select {
|
||||
case <-quitChan:
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
<-done
|
||||
close(player.QuitDone)
|
||||
}()
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
var serverMessage pb.ServerMessage
|
||||
if err := proto.Unmarshal(messageBuf, &serverMessage); err != nil {
|
||||
log.Printf("Failed to unmarshal server message: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
player.Lock()
|
||||
player.CurrentTick = serverMessage.CurrentTick
|
||||
|
||||
tickDiff := serverMessage.CurrentTick - player.CurrentTick
|
||||
if tickDiff > types.MaxTickDesync {
|
||||
for _, state := range serverMessage.Players {
|
||||
if state.PlayerId == playerID {
|
||||
player.ForceResync(state)
|
||||
break
|
||||
}
|
||||
select {
|
||||
case <-done:
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
case <-time.After(1 * time.Second):
|
||||
log.Println("Shutdown timed out")
|
||||
}
|
||||
}
|
||||
player.Unlock()
|
||||
return
|
||||
default:
|
||||
// Read message length (4 bytes)
|
||||
lengthBuf := make([]byte, 4)
|
||||
if _, err := io.ReadFull(reader, lengthBuf); err != nil {
|
||||
log.Printf("Failed to read message length: %v", err)
|
||||
return
|
||||
}
|
||||
messageLength := binary.BigEndian.Uint32(lengthBuf)
|
||||
|
||||
for _, state := range serverMessage.Players {
|
||||
if state.PlayerId == playerID {
|
||||
// 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
|
||||
}
|
||||
|
||||
var serverMessage pb.ServerMessage
|
||||
if err := proto.Unmarshal(messageBuf, &serverMessage); err != nil {
|
||||
log.Printf("Failed to unmarshal server message: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if otherPlayer, exists := otherPlayers[state.PlayerId]; exists {
|
||||
otherPlayer.UpdatePosition(state, types.ServerTickRate)
|
||||
} else {
|
||||
otherPlayers[state.PlayerId] = types.NewPlayer(state)
|
||||
}
|
||||
}
|
||||
player.Lock()
|
||||
player.CurrentTick = serverMessage.CurrentTick
|
||||
|
||||
if g, ok := player.UserData.(*game.Game); ok && len(serverMessage.ChatMessages) > 0 {
|
||||
g.Chat.HandleServerMessages(serverMessage.ChatMessages)
|
||||
tickDiff := serverMessage.CurrentTick - player.CurrentTick
|
||||
if tickDiff > types.MaxTickDesync {
|
||||
for _, state := range serverMessage.Players {
|
||||
if state.PlayerId == playerID {
|
||||
player.ForceResync(state)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
if otherPlayer, exists := otherPlayers[state.PlayerId]; exists {
|
||||
otherPlayer.UpdatePosition(state, types.ServerTickRate)
|
||||
} else {
|
||||
otherPlayers[state.PlayerId] = types.NewPlayer(state)
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
27
scripts/build.sh
Executable file
27
scripts/build.sh
Executable file
@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Main build process
|
||||
build() {
|
||||
local os=$1
|
||||
local arch=$2
|
||||
local output=$3
|
||||
|
||||
# Set GOOS and GOARCH for cross-compilation
|
||||
export GOOS=$os
|
||||
export GOARCH=$arch
|
||||
|
||||
# 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++
|
||||
fi
|
||||
|
||||
go build -buildvcs=false -ldflags="-s -w" -o $output
|
||||
}
|
||||
|
||||
# Call build with provided arguments
|
||||
build "$1" "$2" "$3"
|
1
scripts/platforms.mk
Normal file
1
scripts/platforms.mk
Normal file
@ -0,0 +1 @@
|
||||
PLATFORMS=windows/amd64 linux/amd64
|
@ -27,8 +27,9 @@ type Player struct {
|
||||
CurrentTick int64
|
||||
LastUpdateTime time.Time
|
||||
InterpolationProgress float32
|
||||
UserData interface{} // Used to store reference to game
|
||||
UserData interface{}
|
||||
FloatingMessage *FloatingMessage
|
||||
QuitDone chan struct{}
|
||||
}
|
||||
|
||||
type ModelAsset struct {
|
||||
@ -38,6 +39,7 @@ type ModelAsset struct {
|
||||
|
||||
type ChatMessage struct {
|
||||
PlayerID int32
|
||||
Username string
|
||||
Content string
|
||||
Time time.Time
|
||||
}
|
||||
@ -48,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
|
||||
|
Reference in New Issue
Block a user