Spieler und Befehle
Ein Spieler hat eine dauerhafte Kennung, einen Namen, eine Werkzeugstufe und ein Inventar. Befehle (Beitreten, Verlassen, Graben) sind der einzige Weg, die Welt zu ändern, und lassen sich kodieren. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
//! Everything a player can ask the world to do.
|
||||
|
||||
use crate::codec::{DecodeError, Reader, Writer};
|
||||
use crate::map::Pos;
|
||||
use crate::player::{clamp_name, PlayerId, PlayerKey, MAX_NAME_BYTES};
|
||||
|
||||
/// A request that changes the world. Commands are the only way to change it.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Command {
|
||||
/// A player enters the world, new or returning.
|
||||
Join { key: PlayerKey, name: String },
|
||||
/// A player leaves the world. Their inventory stays.
|
||||
Leave { player: PlayerId },
|
||||
/// A player hits the tile at `pos` with their pickaxe.
|
||||
Dig { player: PlayerId, pos: Pos },
|
||||
}
|
||||
|
||||
const TAG_JOIN: u8 = 1;
|
||||
const TAG_LEAVE: u8 = 2;
|
||||
const TAG_DIG: u8 = 3;
|
||||
|
||||
impl Command {
|
||||
pub fn encode(&self, w: &mut Writer) {
|
||||
match self {
|
||||
Command::Join { key, name } => {
|
||||
w.u8(TAG_JOIN);
|
||||
w.raw(key);
|
||||
w.str(&clamp_name(name));
|
||||
}
|
||||
Command::Leave { player } => {
|
||||
w.u8(TAG_LEAVE);
|
||||
w.u8(player.0);
|
||||
}
|
||||
Command::Dig { player, pos } => {
|
||||
w.u8(TAG_DIG);
|
||||
w.u8(player.0);
|
||||
w.i32(pos.x);
|
||||
w.i32(pos.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decode(r: &mut Reader<'_>) -> Result<Self, DecodeError> {
|
||||
match r.u8()? {
|
||||
TAG_JOIN => {
|
||||
let mut key = [0u8; 16];
|
||||
key.copy_from_slice(r.raw(16)?);
|
||||
let name = r.string()?;
|
||||
if name.len() > MAX_NAME_BYTES {
|
||||
return Err(DecodeError::InvalidValue("player name"));
|
||||
}
|
||||
Ok(Command::Join { key, name })
|
||||
}
|
||||
TAG_LEAVE => Ok(Command::Leave {
|
||||
player: PlayerId(r.u8()?),
|
||||
}),
|
||||
TAG_DIG => {
|
||||
let player = PlayerId(r.u8()?);
|
||||
let x = r.i32()?;
|
||||
let y = r.i32()?;
|
||||
Ok(Command::Dig {
|
||||
player,
|
||||
pos: Pos::new(x, y),
|
||||
})
|
||||
}
|
||||
_ => Err(DecodeError::InvalidValue("command tag")),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,10 @@
|
||||
//! world and exchange nothing but commands.
|
||||
|
||||
pub mod codec;
|
||||
pub mod command;
|
||||
pub mod hash;
|
||||
pub mod inventory;
|
||||
pub mod map;
|
||||
pub mod mapgen;
|
||||
pub mod player;
|
||||
pub mod rng;
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
//! Players known to a world.
|
||||
|
||||
use crate::codec::{DecodeError, Reader, Writer};
|
||||
use crate::inventory::Inventory;
|
||||
|
||||
/// Stable identity of a player across sessions, chosen by the player's game.
|
||||
pub type PlayerKey = [u8; 16];
|
||||
|
||||
/// Index of a player in the world's player list.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct PlayerId(pub u8);
|
||||
|
||||
/// Most players that can be online at the same time.
|
||||
pub const MAX_ONLINE_PLAYERS: usize = 4;
|
||||
/// Most players a world remembers.
|
||||
pub const MAX_KNOWN_PLAYERS: usize = 16;
|
||||
/// Longest stored player name, in bytes of UTF-8.
|
||||
pub const MAX_NAME_BYTES: usize = 32;
|
||||
/// Pickaxe tier every player starts with: stone.
|
||||
pub const STARTING_TOOL_TIER: u8 = 1;
|
||||
|
||||
/// A player and everything that belongs to them personally.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Player {
|
||||
pub key: PlayerKey,
|
||||
pub name: String,
|
||||
pub online: bool,
|
||||
pub tool_tier: u8,
|
||||
/// First tick at which the player may act again.
|
||||
pub next_action_tick: u64,
|
||||
pub inventory: Inventory,
|
||||
}
|
||||
|
||||
impl Player {
|
||||
/// Creates an online player with a stone pickaxe and an empty inventory.
|
||||
pub fn new(key: PlayerKey, name: &str) -> Self {
|
||||
Self {
|
||||
key,
|
||||
name: clamp_name(name),
|
||||
online: true,
|
||||
tool_tier: STARTING_TOOL_TIER,
|
||||
next_action_tick: 0,
|
||||
inventory: Inventory::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encode(&self, w: &mut Writer) {
|
||||
w.raw(&self.key);
|
||||
w.str(&self.name);
|
||||
w.bool(self.online);
|
||||
w.u8(self.tool_tier);
|
||||
w.u64(self.next_action_tick);
|
||||
self.inventory.encode(w);
|
||||
}
|
||||
|
||||
pub fn decode(r: &mut Reader<'_>) -> Result<Self, DecodeError> {
|
||||
let mut key = [0u8; 16];
|
||||
key.copy_from_slice(r.raw(16)?);
|
||||
let name = r.string()?;
|
||||
if name.len() > MAX_NAME_BYTES {
|
||||
return Err(DecodeError::InvalidValue("player name"));
|
||||
}
|
||||
let online = r.bool()?;
|
||||
let tool_tier = r.u8()?;
|
||||
let next_action_tick = r.u64()?;
|
||||
let inventory = Inventory::decode(r)?;
|
||||
Ok(Self {
|
||||
key,
|
||||
name,
|
||||
online,
|
||||
tool_tier,
|
||||
next_action_tick,
|
||||
inventory,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Shortens `name` to at most [`MAX_NAME_BYTES`] bytes without splitting a
|
||||
/// character.
|
||||
pub fn clamp_name(name: &str) -> String {
|
||||
let mut end = name.len().min(MAX_NAME_BYTES);
|
||||
while !name.is_char_boundary(end) {
|
||||
end -= 1;
|
||||
}
|
||||
name[..end].to_owned()
|
||||
}
|
||||
Reference in New Issue
Block a user