Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions lang/en.toml
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,12 @@ other = "§aGave {{.Count}}x {{.Item}} to {{.Name}}§r"
[cmd.give.received]
other = "§aYou received {{.Count}}x {{.Item}}§r"

[cmd.spawnpoint.usage]
other = "Usage: /spawnpoint [player: target] [spawnPos: x y z]"

[cmd.spawnpoint.success]
other = "Set {{.Name}}'s spawnpoint to {{.X}}, {{.Y}}, {{.Z}}"

[cmd.difficulty.usage]
other = "§cUsage: /difficulty <peaceful|easy|normal|hard>§r"

Expand Down
57 changes: 55 additions & 2 deletions server/cmd/default/command_test.v
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ mut:
pos_z f32
cleared bool
given_id string
spawn_x f32
spawn_y f32
spawn_z f32
spawn_set bool
given_count int
given_ok bool = true
whitelisted []string
Expand Down Expand Up @@ -123,6 +127,13 @@ fn (mut s RecordingSender) clear_inventory() {
s.cleared = true
}

fn (mut s RecordingSender) set_spawn_point(x f32, y f32, z f32) {
s.spawn_x = x
s.spawn_y = y
s.spawn_z = z
s.spawn_set = true
}

fn (mut s RecordingSender) give_item(id string, count int) bool {
if !s.given_ok {
return false
Expand Down Expand Up @@ -390,15 +401,15 @@ fn test_available_commands_roundtrip() {
mut sender := RecordingSender{}
sender.perm.set_op(true)
pkt := r.available_commands(sender)
assert pkt.commands.len == 14
assert pkt.commands.len == 15
encoded := protocol.encode_packet_to_bytes(pkt)
mut pool := proto.new_packet_pool()
mut reader := serializer.new_reader(encoded)
decoded := pool.decode(mut reader)!
assert decoded.name() == 'AvailableCommandsPacket'
mut available := proto.AvailableCommandsPacket{}
decode_into(pkt, mut available)!
assert available.commands.len == 14
assert available.commands.len == 15
assert available.commands[0].alias_enum == -1
assert available.commands[0].overloads.len == 1
}
Expand Down Expand Up @@ -541,3 +552,45 @@ fn decode_into[T](p protocol.Packet, mut out T) ! {
protocol.read_packet_header(mut r)!
out.decode_payload(mut r)!
}

fn test_spawnpoint_defaults_to_the_senders_position() {
r := full_registry()
mut sender := RecordingSender{
pos_x: 10
pos_y: 64
pos_z: -3
}
sender.perm.set_op(true)
r.dispatch('/spawnpoint', mut sender, base_ctx())!
assert sender.spawn_set
assert sender.spawn_x == 10
assert sender.spawn_y == 64
assert sender.spawn_z == -3
}

fn test_spawnpoint_takes_explicit_coordinates() {
r := full_registry()
mut target := RecordingSender{
sender_name: 'Alex'
}
mut sender := RecordingSender{
peers: {
'alex': cmd.Sender(&target)
}
}
sender.perm.set_op(true)
r.dispatch('/spawnpoint Alex 1 2 3', mut sender, base_ctx())!
assert target.spawn_set
assert target.spawn_x == 1
assert target.spawn_y == 2
assert target.spawn_z == 3
assert !sender.spawn_set
}

fn test_spawnpoint_denied_without_op() {
r := full_registry()
mut sender := RecordingSender{}
r.dispatch('/spawnpoint', mut sender, base_ctx())!
assert !sender.spawn_set
assert sender.messages[0].contains('permission')
}
1 change: 1 addition & 0 deletions server/cmd/default/register.v
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub fn register_all(mut r cmd.Registry) {
r.register(TeleportCommand{})
r.register(ClearCommand{})
r.register(GiveCommand{})
r.register(SpawnpointCommand{})
r.register(DifficultyCommand{})
r.register(SayCommand{})
r.register(TitleCommand{})
Expand Down
87 changes: 87 additions & 0 deletions server/cmd/default/spawnpoint.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
module default

import strconv
import server.permission
import server.cmd

pub struct SpawnpointCommand {}

pub fn (c SpawnpointCommand) name() string {
return 'spawnpoint'
}

pub fn (c SpawnpointCommand) description() string {
return 'Sets where a player respawns'
}

pub fn (c SpawnpointCommand) aliases() []string {
return []
}

pub fn (c SpawnpointCommand) permission() string {
return permission.command_spawnpoint
}

pub fn (c SpawnpointCommand) arguments() []cmd.Argument {
return [
cmd.StringArgument{
arg_name: 'player'
arg_optional: true
},
cmd.IntArgument{
arg_name: 'x'
arg_optional: true
},
cmd.IntArgument{
arg_name: 'y'
arg_optional: true
},
cmd.IntArgument{
arg_name: 'z'
arg_optional: true
},
]
}

// execute takes the target's current position when no coordinates are given,
// which is what the command is for most of the time.
pub fn (c SpawnpointCommand) execute(mut sender cmd.Sender, ctx cmd.Context) ! {
mut target := sender
if ctx.args.len > 0 {
target_name := ctx.args[0]
target = sender.find_player(target_name) or {
sender.send_message(ctx.lang.tf('cmd.player_not_found', {
'Name': target_name
}))!
return
}
} else if !sender.is_player() {
sender.send_message(ctx.lang.t('cmd.player_only'))!
return
}
mut pos := target.position()
if ctx.args.len >= 4 {
x := strconv.atoi(ctx.args[1]) or {
sender.send_message(ctx.lang.t('cmd.spawnpoint.usage'))!
return
}
y := strconv.atoi(ctx.args[2]) or {
sender.send_message(ctx.lang.t('cmd.spawnpoint.usage'))!
return
}
z := strconv.atoi(ctx.args[3]) or {
sender.send_message(ctx.lang.t('cmd.spawnpoint.usage'))!
return
}
pos.x = f32(x)
pos.y = f32(y)
pos.z = f32(z)
}
target.set_spawn_point(pos.x, pos.y, pos.z)
sender.send_message(ctx.lang.tf('cmd.spawnpoint.success', {
'Name': target.name()
'X': int(pos.x).str()
'Y': int(pos.y).str()
'Z': int(pos.z).str()
}))!
}
6 changes: 6 additions & 0 deletions server/permission/permission.v
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ pub const command_teleport_other = 'vedrock.cmd.teleport.other'
pub const command_clear_self = 'vedrock.cmd.clear.self'
pub const command_clear_other = 'vedrock.cmd.clear.other'
pub const command_give = 'vedrock.cmd.give'
pub const command_spawnpoint = 'vedrock.cmd.spawnpoint'
pub const command_difficulty = 'vedrock.cmd.difficulty'
pub const command_say = 'vedrock.cmd.say'
pub const command_title = 'vedrock.cmd.title'
Expand Down Expand Up @@ -120,6 +121,11 @@ fn new_registry() &Registry {
description: 'Allows giving items to players'
default: .op
})
r.register(Permission{
name: command_spawnpoint
description: 'Allows setting where a player respawns'
default: .op
})
r.register(Permission{
name: command_difficulty
description: "Allows changing the world's difficulty"
Expand Down
4 changes: 4 additions & 0 deletions server/player/player.v
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ mut:
effects effect.Manager
has_last_death bool
last_death_pos types.Vector3
// spawn_point is where this player respawns. Unset until they sleep in a
// bed or an operator places it, and the world spawn answers until then.
has_spawn_point bool
spawn_point types.Vector3
air_supply_ticks i64 = max_air_supply_ticks
fire_ticks i64
// give_next_slot round robins give_item across the hotbar. Only the
Expand Down
26 changes: 15 additions & 11 deletions server/player/playerdb/player.v
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,21 @@ pub mut:

pub struct PlayerData {
pub mut:
x f32
y f32
z f32
yaw f32
pitch f32
gamemode int
items []InvItem
has_last_death bool
last_death_x f32
last_death_y f32
last_death_z f32
x f32
y f32
z f32
yaw f32
pitch f32
gamemode int
items []InvItem
has_spawn_point bool
spawn_x f32
spawn_y f32
spawn_z f32
has_last_death bool
last_death_x f32
last_death_y f32
last_death_z f32
}

// safe_key strips anything that could let a key (which may come from an
Expand Down
33 changes: 33 additions & 0 deletions server/player/spawn_point.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
module player

import bedrock_v.protocol.types

// spawn_point is where this player comes back after dying, or none when they
// have never set one and the world's own spawn still decides.
pub fn (p &Player) spawn_point() ?types.Vector3 {
mut m := p.state_mutex
m.lock()
defer {
m.unlock()
}
if !p.has_spawn_point {
return none
}
return p.spawn_point
}

pub fn (mut p Player) set_spawn_point(pos types.Vector3) {
p.state_mutex.lock()
p.spawn_point = pos
p.has_spawn_point = true
p.state_mutex.unlock()
}

// clear_spawn_point sends the player back to the world spawn, as a bed that no
// longer exists does.
pub fn (mut p Player) clear_spawn_point() {
p.state_mutex.lock()
p.has_spawn_point = false
p.spawn_point = types.Vector3{}
p.state_mutex.unlock()
}
27 changes: 27 additions & 0 deletions server/player/spawn_point_test.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
module player

import bedrock_v.protocol.types

fn test_a_player_starts_without_a_spawn_point() {
p := new_player()
if _ := p.spawn_point() {
assert false, 'a fresh player already had a spawn point'
}
}

fn test_setting_and_clearing_a_spawn_point() {
mut p := new_player()
p.set_spawn_point(types.Vector3{1.5, 64.0, -2.5})
point := p.spawn_point() or {
assert false, 'spawn point was not recorded'
return
}
assert point.x == 1.5
assert point.y == 64.0
assert point.z == -2.5

p.clear_spawn_point()
if _ := p.spawn_point() {
assert false, 'spawn point survived being cleared'
}
}
2 changes: 2 additions & 0 deletions server/player/view.v
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ mut:
teleport(x f32, y f32, z f32)
clear_inventory()
give_item(id string, count int) bool
// set_spawn_point moves where this player comes back after dying.
set_spawn_point(x f32, y f32, z f32)
send_form(f form.Form) !
send_scoreboard(board &scoreboard.Scoreboard)
remove_scoreboard()
Expand Down
8 changes: 4 additions & 4 deletions server/session/combat.v
Original file line number Diff line number Diff line change
Expand Up @@ -285,12 +285,12 @@ fn (mut s NetworkSession) apply_respawn(mut tx worldrt.WorldTx) {
}
s.player.set_dead(false)
s.player.set_health(20.0)
spawn_y := s.generator.spawn_y()
spawn_pos := s.respawn_position()
mut ctx := event.new_context(player.RespawnData{
player: s.player
x: 0.0
y: f32(spawn_y) + player_eye_height
z: 0.0
x: spawn_pos.x
y: spawn_pos.y
z: spawn_pos.z
})
s.player.handler.on_player_respawn(mut ctx)
s.player.reset_position(types.Vector3{ctx.val.x, ctx.val.y, ctx.val.z})
Expand Down
2 changes: 2 additions & 0 deletions server/session/console.v
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ fn (mut c ConsoleSender) give_item(_ string, _ int) bool {
return false
}

fn (mut c ConsoleSender) set_spawn_point(_ f32, _ f32, _ f32) {}

fn (mut c ConsoleSender) send_form(_ form.Form) ! {
return error('the console cannot display forms')
}
Expand Down
28 changes: 17 additions & 11 deletions server/session/items.v
Original file line number Diff line number Diff line change
Expand Up @@ -255,18 +255,24 @@ fn (mut s NetworkSession) save_player_data() {
}
}
current := s.player.movement()
spawn_point := s.player.spawn_point()
saved_spawn := spawn_point or { types.Vector3{} }
mut provider := s.hub.player_data_provider
provider.save(s.player_key(), playerdb.PlayerData{
x: current.position.x
y: current.position.y
z: current.position.z
yaw: current.yaw
pitch: current.pitch
gamemode: player.gamemode_to_wire(s.player.game_mode())
items: items
has_last_death: s.player.has_last_death()
last_death_x: s.player.last_death_pos().x
last_death_y: s.player.last_death_pos().y
last_death_z: s.player.last_death_pos().z
x: current.position.x
y: current.position.y
z: current.position.z
yaw: current.yaw
pitch: current.pitch
gamemode: player.gamemode_to_wire(s.player.game_mode())
items: items
has_spawn_point: spawn_point != none
spawn_x: saved_spawn.x
spawn_y: saved_spawn.y
spawn_z: saved_spawn.z
has_last_death: s.player.has_last_death()
last_death_x: s.player.last_death_pos().x
last_death_y: s.player.last_death_pos().y
last_death_z: s.player.last_death_pos().z
}) or { s.log.warn('Failed to save player ${s.player_key()}: ${err}') }
}
Loading
Loading