Skip to content

Commit 128b308

Browse files
committed
feat: supply another batch of small fixes and introduce vxlgen
1 parent 2e3f205 commit 128b308

22 files changed

Lines changed: 3265 additions & 26 deletions

File tree

config/config-laby.toml

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
[server]
2+
# The server's name shown on the master server list and in the game
3+
name = "Fosilo Labyrinth Server"
4+
5+
# Port for the server to listen on
6+
port = 32887
7+
8+
# Laby
9+
gamemode = 5
10+
11+
# Maximum players allowed
12+
max_players = 32
13+
14+
# Respawn time in seconds
15+
respawn_time = 5
16+
17+
# Enable master server listing
18+
# Set to true to appear on server lists
19+
master = false
20+
21+
# You can specify masterserver hosts if one of them dies
22+
# [[server.master_hosts]]
23+
# host = "master.buildandshoot.com"
24+
# port = 32886
25+
26+
# Map rotation
27+
maps = [
28+
"vxlgen"
29+
]
30+
31+
# Welcome messages sent when players join
32+
welcome_messages = [
33+
"Welcome to the Labyrinth!",
34+
"One flag, one tower. Capture the intel to win!",
35+
"Grenades teleport you to where they land",
36+
"You regenerate +3 HP per second",
37+
"Type /floor to see what floor you're on"
38+
]
39+
40+
# Periodic announcements broadcasted to all players
41+
periodic_messages = [
42+
"This server is powered by Fosilo"
43+
]
44+
45+
46+
# Team configuration
47+
[teams]
48+
team1 = { name = "Blue", color = [0, 0, 255] }
49+
team2 = { name = "Green", color = [0, 255, 0] }
50+
51+
52+
# Permission role passwords
53+
# Players use /login <role> <password> to gain permissions
54+
# IMPORTANT: Change these defaults for security!
55+
[passwords]
56+
manager = "changeme1" # Full control
57+
admin = "changeme2" # Can kick, ban, manage
58+
moderator = "changeme3" # Can kick, mute
59+
guard = "changeme4" # Can mute, warn
60+
trusted = "changeme5" # Trusted status
61+
62+
63+
# Voting system
64+
[voting]
65+
votekick_enabled = true
66+
votekick_percentage = 35 # 35% yes votes required
67+
votekick_ban_duration = 30 # 30 minute temporary ban
68+
vote_cooldown = 120 # 2 minute cooldown between votes
69+
vote_timeout = 120 # Vote expires after 2 minutes
70+
71+
votemap_enabled = true
72+
votemap_percentage = 80 # 80% yes votes required
73+
votemap_choices = 5 # Offer 5 random maps
74+
votemap_allow_extend = true # Allow extending current map
75+
76+
77+
# Laby Gamemode Settings
78+
[gamemode.laby]
79+
# Number of captures needed to win the match
80+
cap_limit = 1
81+
82+
# Time in seconds before an intel carrier is auto-killed
83+
# Prevents players from holding the intel indefinitely
84+
hog_timeout = 180
85+
86+
# HP regeneration interval in seconds
87+
# All players regenerate +3 HP every interval
88+
regen_rate = 1.0

internal/callbacks/callbacks.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type Callbacks interface {
2020
OnIntelDrop(p *player.Player, team uint8)
2121
OnWeaponFire(p *player.Player)
2222
OnGrenadeToss(p *player.Player)
23+
OnGrenadeExplode(thrower *player.Player, x, y, z float32)
2324
OnRestock(p *player.Player)
2425
}
2526

@@ -41,7 +42,9 @@ func (d *DefaultCallbacks) OnIntelCapture(p *player.Player, team uint8) bool
4142
func (d *DefaultCallbacks) OnIntelDrop(p *player.Player, team uint8) {}
4243
func (d *DefaultCallbacks) OnWeaponFire(p *player.Player) {}
4344
func (d *DefaultCallbacks) OnGrenadeToss(p *player.Player) {}
44-
func (d *DefaultCallbacks) OnRestock(p *player.Player) {}
45+
func (d *DefaultCallbacks) OnGrenadeExplode(thrower *player.Player, x, y, z float32) {
46+
}
47+
func (d *DefaultCallbacks) OnRestock(p *player.Player) {}
4548

4649
type CallbackChain struct {
4750
callbacks []Callbacks
@@ -156,6 +159,12 @@ func (c *CallbackChain) OnGrenadeToss(p *player.Player) {
156159
}
157160
}
158161

162+
func (c *CallbackChain) OnGrenadeExplode(thrower *player.Player, x, y, z float32) {
163+
for _, cb := range c.callbacks {
164+
cb.OnGrenadeExplode(thrower, x, y, z)
165+
}
166+
}
167+
159168
func (c *CallbackChain) OnRestock(p *player.Player) {
160169
for _, cb := range c.callbacks {
161170
cb.OnRestock(p)

internal/gamemode/lua_gamemode.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,23 @@ func (gm *LuaGameMode) OnGrenadeToss(p *player.Player) {
370370
}
371371
}
372372

373+
func (gm *LuaGameMode) OnGrenadeExplode(thrower *player.Player, x, y, z float32) {
374+
if !gm.vm.HasFunction("on_grenade_explode") {
375+
return
376+
}
377+
378+
gm.vm.State().Global("on_grenade_explode")
379+
lua.PushPlayer(gm.vm.State(), thrower)
380+
gm.vm.State().PushNumber(float64(x))
381+
gm.vm.State().PushNumber(float64(y))
382+
gm.vm.State().PushNumber(float64(z))
383+
if err := gm.vm.State().ProtectedCall(4, 0, 0); err != nil {
384+
if gm.logger != nil {
385+
gm.logger.Error("lua gamemode on_grenade_explode error", "error", err)
386+
}
387+
}
388+
}
389+
373390
func (gm *LuaGameMode) OnRestock(p *player.Player) {
374391
if !gm.vm.HasFunction("on_restock") {
375392
return

internal/server/server.go

Lines changed: 130 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
"github.com/siohaza/fosilo/pkg/config"
3131
"github.com/siohaza/fosilo/pkg/lua"
3232
"github.com/siohaza/fosilo/pkg/vxl"
33+
"github.com/siohaza/fosilo/pkg/vxlgen"
3334

3435
"github.com/codecat/go-enet"
3536
)
@@ -147,6 +148,7 @@ func (s *Server) Start() error {
147148
return fmt.Errorf("failed to load Lua gamemode: %w", err)
148149
}
149150
s.gameMode = luaMode
151+
s.callbacks.Register(luaMode)
150152
s.logger.Info("loaded Lua game mode", "path", luaGamemodePath, "mode", s.gameMode.Name())
151153

152154
if s.luaCommands != nil {
@@ -395,6 +397,47 @@ func (s *Server) prepareMapResources(mapSpec string) (*vxl.Map, *config.MapConfi
395397
return vxlMap, mapCfg, display, "classicgen", nil
396398
}
397399

400+
if strings.EqualFold(base, "vxlgen") {
401+
seed, display := s.resolveVxlgenSeed(param)
402+
result := vxlgen.Generate(vxlgen.Config{Seed: seed})
403+
vxlMap := result.Map
404+
405+
mapCfg := config.DefaultMapConfig()
406+
mapCfg.Map.Author = "VxlGen"
407+
mapCfg.Map.Description = fmt.Sprintf("Procedurally generated labyrinth (seed %d)", seed)
408+
409+
blueZ := 63 - result.BlueSpawnArea.Max.Z
410+
greenZ := 63 - result.GreenSpawnArea.Max.Z
411+
412+
mapCfg.Tents.Team1 = config.TentArea{
413+
Start: [3]int{result.BlueSpawnArea.Min.X, result.BlueSpawnArea.Min.Y, max(0, blueZ-5)},
414+
End: [3]int{result.BlueSpawnArea.Max.X, result.BlueSpawnArea.Max.Y, min(63, blueZ+10)},
415+
}
416+
mapCfg.Tents.Team2 = config.TentArea{
417+
Start: [3]int{result.GreenSpawnArea.Min.X, result.GreenSpawnArea.Min.Y, max(0, greenZ-5)},
418+
End: [3]int{result.GreenSpawnArea.Max.X, result.GreenSpawnArea.Max.Y, min(63, greenZ+10)},
419+
}
420+
421+
mapCfg.SpawnPoints.Team1 = config.SpawnArea{
422+
Start: [3]int{result.BlueSpawnArea.Min.X, result.BlueSpawnArea.Min.Y, max(0, blueZ-5)},
423+
End: [3]int{result.BlueSpawnArea.Max.X, result.BlueSpawnArea.Max.Y, min(63, blueZ+10)},
424+
}
425+
mapCfg.SpawnPoints.Team2 = config.SpawnArea{
426+
Start: [3]int{result.GreenSpawnArea.Min.X, result.GreenSpawnArea.Min.Y, max(0, greenZ-5)},
427+
End: [3]int{result.GreenSpawnArea.Max.X, result.GreenSpawnArea.Max.Y, min(63, greenZ+10)},
428+
}
429+
430+
setVxlgenIntelPositions(vxlMap, mapCfg, result)
431+
432+
s.config.Server.TowerPosX = result.TowerPos.X
433+
s.config.Server.TowerPosY = result.TowerPos.Y
434+
s.config.Server.TowerCellsX = result.NumCells.X
435+
s.config.Server.TowerCellsY = result.NumCells.Y
436+
s.config.Server.TowerCellsZ = result.NumCells.Z
437+
438+
return vxlMap, mapCfg, display, "vxlgen", nil
439+
}
440+
398441
mapPath := fmt.Sprintf("maps/%s.vxl", mapSpec)
399442

400443
mapData, err := os.ReadFile(mapPath)
@@ -449,8 +492,9 @@ func setClassicgenIntelPositions(vxlMap *vxl.Map, mapCfg *config.MapConfig) {
449492

450493
placeTent := func(area config.TentArea) tentPlacement {
451494
x, y, top, slope := findTentPlacement(vxlMap, area)
452-
flattenTentArea(vxlMap, x, y, top)
495+
flattenTentArea(vxlMap, x, y, top+1)
453496

497+
top = vxlMap.FindTopBlock(x, y)
454498
baseHeight := clampInt(top, 1, vxlMap.Depth()-2)
455499
heightOffset := 1 + min(slope, 2)
456500

@@ -610,7 +654,8 @@ func flattenTentArea(vxlMap *vxl.Map, centerX, centerY, centerZ int) {
610654
depth := vxlMap.Depth()
611655

612656
targetHeight := clampInt(centerZ, 1, depth-2)
613-
radius := 4
657+
radius := 6
658+
innerRadius := 3
614659

615660
for dx := -radius; dx <= radius; dx++ {
616661
for dy := -radius; dy <= radius; dy++ {
@@ -621,27 +666,32 @@ func flattenTentArea(vxlMap *vxl.Map, centerX, centerY, centerZ int) {
621666
cx := clampInt(centerX+dx, 0, width-1)
622667
cy := clampInt(centerY+dy, 0, height-1)
623668

669+
currentTop := vxlMap.FindTopBlock(cx, cy)
624670
distance := abs(dx) + abs(dy)
671+
625672
desiredHeight := targetHeight
626-
if distance >= radius-1 {
627-
desiredHeight = max(targetHeight-1, 1)
673+
if distance > innerRadius {
674+
steps := distance - innerRadius
675+
if currentTop > targetHeight {
676+
desiredHeight = min(targetHeight+steps, currentTop)
677+
} else if currentTop < targetHeight {
678+
desiredHeight = max(targetHeight-steps, currentTop)
679+
}
628680
}
629681
desiredHeight = clampInt(desiredHeight, 1, depth-2)
630682

631-
currentTop := vxlMap.FindTopBlock(cx, cy)
632-
if currentTop > desiredHeight {
633-
for z := desiredHeight + 1; z <= currentTop; z++ {
683+
if currentTop < desiredHeight {
684+
for z := currentTop; z < desiredHeight; z++ {
634685
vxlMap.SetAir(cx, cy, z)
635686
}
636-
} else if currentTop < desiredHeight {
687+
} else if currentTop > desiredHeight {
637688
color := fallbackTerrainColor
638689
if currentTop >= 0 {
639690
if c := vxlMap.Get(cx, cy, currentTop); c != 0 {
640691
color = c
641692
}
642693
}
643-
start := max(currentTop+1, 0)
644-
for z := start; z <= desiredHeight; z++ {
694+
for z := desiredHeight; z < currentTop; z++ {
645695
vxlMap.Set(cx, cy, z, color)
646696
}
647697
}
@@ -674,6 +724,52 @@ func (s *Server) resolveClassicgenSeed(seedHint string) (uint32, string) {
674724
return seed, displayName
675725
}
676726

727+
func (s *Server) resolveVxlgenSeed(seedHint string) (uint64, string) {
728+
var seed uint64
729+
if seedHint != "" {
730+
if v, err := strconv.ParseUint(seedHint, 0, 64); err == nil {
731+
seed = v
732+
} else {
733+
seed = uint64(crc32.ChecksumIEEE([]byte(seedHint)))
734+
}
735+
}
736+
if seed == 0 {
737+
seed = uint64(time.Now().UnixNano())
738+
}
739+
displayName := fmt.Sprintf("vxlgen (seed: %d)", seed)
740+
return seed, displayName
741+
}
742+
743+
func setVxlgenIntelPositions(vxlMap *vxl.Map, mapCfg *config.MapConfig, result *vxlgen.Result) {
744+
if vxlMap == nil || mapCfg == nil {
745+
return
746+
}
747+
748+
placeIntel := func(area vxlgen.Box3i) tentPlacement {
749+
cx := (area.Min.X + area.Max.X) / 2
750+
cy := (area.Min.Y + area.Max.Y) / 2
751+
gz := 63 - (area.Min.Z+area.Max.Z)/2
752+
return tentPlacement{
753+
intel: [3]float64{float64(cx) + 0.5, float64(cy) + 0.5, float64(gz) + 2.0},
754+
base: [3]float64{float64(cx) + 0.5, float64(cy) + 0.5, float64(gz)},
755+
x: cx,
756+
y: cy,
757+
z: gz,
758+
}
759+
}
760+
761+
team1 := placeIntel(result.BlueSpawnArea)
762+
team2 := placeIntel(result.GreenSpawnArea)
763+
764+
mapCfg.Intel.Team1Position = team1.intel
765+
mapCfg.Intel.Team1Base = team1.base
766+
mapCfg.Intel.Team2Position = team2.intel
767+
mapCfg.Intel.Team2Base = team2.base
768+
769+
mapCfg.SpawnPoints.Team1Points = team1.spawnPoints(vxlMap)
770+
mapCfg.SpawnPoints.Team2Points = team2.spawnPoints(vxlMap)
771+
}
772+
677773
func (s *Server) run() {
678774
ticker := time.NewTicker(s.tickRate)
679775
defer ticker.Stop()
@@ -2162,18 +2258,9 @@ func (s *Server) respawnPlayer(playerID uint8) {
21622258
s.gameMode.OnPlayerSpawn(p)
21632259
s.callbacks.OnPlayerSpawn(p)
21642260

2165-
p.RLock()
2166-
packet := protocol.PacketCreatePlayer{
2167-
PacketID: uint8(protocol.PacketTypeCreatePlayer),
2168-
PlayerID: p.ID,
2169-
Weapon: p.Weapon,
2170-
Team: toNetworkTeamID(p.Team),
2171-
X: spawnPos.X,
2172-
Y: spawnPos.Y,
2173-
Z: spawnPos.Z,
2174-
}
2175-
copy(packet.Name[:], p.Name)
2261+
s.BroadcastCreatePlayer(p)
21762262

2263+
p.RLock()
21772264
reloadPacket := protocol.PacketWeaponReload{
21782265
PacketID: uint8(protocol.PacketTypeWeaponReload),
21792266
PlayerID: p.ID,
@@ -2182,12 +2269,29 @@ func (s *Server) respawnPlayer(playerID uint8) {
21822269
}
21832270
p.RUnlock()
21842271

2185-
s.broadcastPacket(&packet, true)
21862272
s.sendPacket(p, &reloadPacket, true)
21872273
s.broadcastShortPlayerData(p)
21882274
s.sendPlayerProperties(p)
21892275
}
21902276

2277+
func (s *Server) BroadcastCreatePlayer(p *player.Player) {
2278+
p.RLock()
2279+
pos := p.Position
2280+
packet := protocol.PacketCreatePlayer{
2281+
PacketID: uint8(protocol.PacketTypeCreatePlayer),
2282+
PlayerID: p.ID,
2283+
Weapon: p.Weapon,
2284+
Team: toNetworkTeamID(p.Team),
2285+
X: pos.X,
2286+
Y: pos.Y,
2287+
Z: pos.Z,
2288+
}
2289+
copy(packet.Name[:], p.Name)
2290+
p.RUnlock()
2291+
2292+
s.broadcastPacket(&packet, true)
2293+
}
2294+
21912295
func (s *Server) damagePlayer(playerID uint8, damage uint8, source protocol.Vector3f, damageType protocol.HurtType) bool {
21922296
p, ok := s.gameState.Players.Get(playerID)
21932297
if !ok {
@@ -2537,6 +2641,10 @@ func (s *Server) explodeGrenade(grenade *gamestate.Grenade) {
25372641
}
25382642
s.broadcastPacket(&blockPacket, true)
25392643
}
2644+
2645+
if thrower, ok := s.gameState.Players.Get(grenade.PlayerID); ok {
2646+
s.callbacks.OnGrenadeExplode(thrower, grenade.Position.X, grenade.Position.Y, grenade.Position.Z)
2647+
}
25402648
}
25412649

25422650
func marshalPacket(packet interface{}) ([]byte, error) {

0 commit comments

Comments
 (0)