Veloren is an open source game written entirely in Rust. The open-world, voxel game takes inspiration from Zelda, Dwarf Fortress, and Minecraft. It started in 2018 and is still being actively developed. I wanted to explore the codebase and get a feel for how it was structured. To do that, I traced the code from a user’s mouse click in the client all the way to damage being dealt to an enemy creature on the server.
The best way to read this blog post would be to clone
the repository for Veloren yourself and
follow along with the hunt. This will give you a much better idea of how the
project is structured than if you simply read filename after filename without
any context for where they really fall in the directory hierarchy. Exact file
paths and code snippets will be from commit 05a6f6a0e
.
Frontend (voxygen
crate)
Voxygen is the default frontend for Valoren, although not the only one. To
start, we need to find where the input is accepted in the voxygen
crate. There
are only 437 Rust files in this crate, so finding the right one should be
trivial. With a little bit of effort, we can find the function default_binding
in voxygen/src/settings/control.rs
.
pub fn default_binding(game_input: GameInput) -> Option<KeyMouse> {
// If a new GameInput is added, be sure to update GameInput::iterator() too!
match game_input {
GameInput::Primary => Some(KeyMouse::Mouse(MouseButton::Left)),
GameInput::Secondary => Some(KeyMouse::Mouse(MouseButton::Right)),
// l33l33: And then about 70 more bindings...
The struct KeyMouse
is provided elsewhere in the voxygen
crate, and
MouseButton::Left
is taken from the winit
library. winit
is a
cross-platform window creation and management library designed to handle window
sizing and user input. This tells us what sort of functions we should be on the
lookout for, but it is not where the input itself is actually accepted. The
winit
library operates using an “event loop”. This means that when an input is
detected, it gets added to a queue. The event loop pulls from this queue and
process each input. The next important file that sticks out is
voxygen/src/window.rs
. In this file, there is a function titled
handle_window_event
. Bingo! The majority of this function is one large match
statement on a WindowEvent
. Obviously, the branch we care about is
WindowEvent::MouseInput
.
// l33l33: Inside the "event" match statement...
WindowEvent::MouseInput { button, state, .. } => {
if let (true, Some(game_inputs)) =
// Mouse input not mapped to input if it is not grabbed
(
self.cursor_grabbed,
Window::map_input(
KeyMouse::Mouse(button), // l33l33: button is either left, right, or middle.
controls, // l33l33: controls is a struct with the default_binding method attached.
&mut self.remapping_keybindings,
),
)
{
for game_input in game_inputs {
self.events.push(Event::InputUpdate(
*game_input,
state == winit::event::ElementState::Pressed,
));
}
}
self.events.push(Event::MouseButton(button, state)); // l33l33: state is pressed or released.
}
// l33l33: More cases...
So we got our mouse input, we interpreted it, and now we pushed it onto the
event queue attached to self
. What next? We need to hunt down which event loop
handles the queue, and figure out what functions that event loop calls before
the mouse click is sent from our client to the server. Above the
handle_window_event
method is a method called fetch_events
. fetch_events
is a method on Window
that returns a list of events that need to be processed.
Following where this method is called in the code will lead us to our event
loop. In voxygen/src/run.rs
we find the function handle_main_events_cleared
.
Let’s take a look at the first few lines of this function.
fn handle_main_events_cleared(
states: &mut Vec<Box<dyn PlayState>>,
control_flow: &mut winit::event_loop::ControlFlow,
global_state: &mut GlobalState,
) {
// l33l33: Some setup code here...
// Run tick here
// What's going on here?
// ---------------------
// The state system used by Voxygen allows for the easy development of
// stack-based menus. For example, you may want a "title" state
// that can push a "main menu" state on top of it, which can in
// turn push a "settings" state or a "game session" state on top of it.
// The code below manages the state transfer logic automatically so that we
// don't have to re-engineer it for each menu we decide to add
// to the game.
while let Some(state_result) = states.last_mut().map(|last| {
let events = global_state.window.fetch_events();
last.tick(global_state, events)
}) {
// Implement state transfer logic.
What is this doing? We have these “states”, and a comment hinting at their use.
After looking into what implements the PlayState
trait, things become more
clear. Some of the structs that implement this train include MainMenuState
,
ServerInfoState
, CharSelectionState
, and SessionState
. Knowing that we
could be performing a game “tick” on any of these states to progress the game
state, we need to choose the most likely one of these to give us a state of “hit
enemy”. SessionState
seems promising. That brings us to the file
voxygen/src/session/mod.rs
in which the struct SessionState
has a method
tick
to comply with the implementation of the trait PlayState
. Our events
once more have a match statement.
match event {
// l33l33: Skipping some events we don't care about to get to...
Event::InputUpdate(input, state)
if state != self.inputs_state.contains(&input) =>
{
if !self.inputs_state.insert(input) {
self.inputs_state.remove(&input);
}
match input {
GameInput::Primary => {
self.walking_speed = false;
let mut client = self.client.borrow_mut();
// Mine and build targets can be the same block. make building
// take precedence.
// Order of precedence: build, then mining, then attack.
if let Some(build_target) = build_target.filter(|bt| {
state && can_build && nearest_block_dist == Some(bt.distance)
}) {
client.remove_block(build_target.position_int());
} else {
client.handle_input( // l33l33: We have escaped the voxygen frontend, and are off to the client crate!
InputKind::Primary,
state,
default_select_pos,
self.target_entity,
);
}
}
// l33l33: Other input cases like Secondary, Block, Roll, etc...
}
},
// l33l33: Other event cases like AnalogGameInput, Zoom, etc...
}
The march continues! We have a code comment explicitly mentioning the attack
action, so we must be hot on the trail! More over, we are finally out of the
frontend crate. After the code determines that the player is not attempting to
build, it calls client.handle_input
, which is in the client
crate.
Client Backend (client
crate)
Over in the client
crate, the file client/src/lib.rs
contains the method
handle_input
that the voxygen
frontend called. handle_input
immediately
calls control_action
, which is in the same file. The control_action
method
is incredibly interesting, so let’s break down what it does.
fn control_action(&mut self, control_action: ControlAction) {
if let Some(controller) = self
.state
.ecs()
.write_storage::<Controller>()
.get_mut(self.entity())
{
controller.push_action(control_action);
}
self.send_msg(ClientGeneral::ControlAction(control_action));
}
So, the first line of this file is a rather large if statement. self
here
refers to the Client
struct. The state field on this struct is a struct from
the crate common
. It can be found in common/state/src/state.rs
. As Anakin
would say, this is where the fun begins. The ecs
method on this State
struct
is a specs::World
. specs
“provides an ECS [entity component system] variant
designed for parallel execution and convenient usage.” (Crate specs) What is an
entity component system? I’m so glad you asked! I am by no means an expert on
the topic, but I did read the Wikipedia page, so I’m pretty close to one now.
(Here is the Wiki page
for those who want a better explanation then what I’m about to give.) You can
think of an ECS as a collection of arrays with each array containing the values
for components that are common across many different game objects. A component
is an attribute of a game object. Rather than having each game object be stored
as a struct, we store each field of the would-be struct in its respective array.
There may be an array for health, an array for world position, etc. Each
“entity” (player character, monster, etc.) is then given a unique ID and its
components placed in these arrays. With this crude understanding of how ECS are
usually implemented, let’s look at specs::World
.
pub struct World {
resources: HashMap<ResourceId, AtomicRefCell<Box<dyn Resource>>>,
}
Fear not! This last step is as deep as I want to go into these libraries! Notice
how each ResourceId
is mapped to an AtomicRefCell
? These AtomicRefCell
s
are part of the atomic_refcell
crate. This crate is particularly cool because
it is a reimplementation of the RwLock
from the standard library. The
difference though, is that if a consumer guarantees that they will never mutably
borrow the contents within the immutable borrows, the crate makes use of extra
performance benefits (atomic_refcel). The Resource
these AtomicRefCell
s
contain is a trait that may, or may not, have the Send
and Sync
traits
depending on the cfg flags. The rest of how this library implements their ECS is
a topic for another post, but we have enough understand to go back to
control_action
now.
The last two lines in the if-let expression’s assignment tell the ECS to try and
get the player entity (self.entity()
) from the world. If we successfully
obtain the player entity, we push the control_action (our attack) onto the
entity’s vector of ControlAction
s. (Looking at the ControlAction
enum in
common/src/comp/controller.rs
, the most likely value would be a
ControlAction::StartInput{ input: InputKind::Primary, target_entity: Option<Uid>, select_pos: Option<Vec3<f32>> }
.)
The last thing the function always does, regardless of if it was able to obtain
the player’s entity, is call send_msg
on the ControlAction
. That almost
sounds like an attempt to send our attack to the server, and indeed, if we look
it common/net/src/msg/client.rs
we will find the ClientGeneral
struct that
we are wrapping our attack in. The comment above this struct reads, “Messages
sent from the client to the server”.
The function send_msg
(still in client/src/lib.rs
) is rather boring, and its
only purpose is as a wrapper to its twin, send_msg_err
. If a connection fails,
the send_msg
function prints a warning. The send_msg_err
function by
comparison, is much more interesting. The first decision that is made by
send_msg_err
is which network stream to send the message across. The main
network streams appear to be register_stream
, character_screen_stream
,
in_game_stream
, terrain_stream
, and ping_stream
. The one we will focus on
is the in_game_stream
, since that is the stream that attacks are sent over.
One thing that surprised me is that most of the streams aren’t implemented
differently. Most of them use the same Stream
object from
network/src/api.rs
. At any rate, the ‘Stream::send’ method calls
send_raw_move
on the serialized attack message.
fn send_raw_move(&self, message: Message) -> Result<(), StreamError> {
if self.send_closed.load(Ordering::Relaxed) {
return Err(StreamError::StreamClosed);
}
#[cfg(debug_assertions)]
message.verify(self.params());
self.a2b_msg_s.send((self.sid, message.data))?;
Ok(())
}
The a2b_msg_s
is of type crossbeam_channel::Sender
. Similar to how Veloren
uses AtomicRefCell
instead of the standard library’s implementations,
crossbeam
is, “an alternative to std::sync::mpsc
with more features and
better performance.” (crossbeam_channel) Now there is just one issue. Who has
the receiver, and which function is trying to read from it?
Server Backend (server
crate)
The in_game_stream
attached to the Client
was created in Client::new
. The
value of in_game_stream
was set with
let in_game_stream = participant.opened().await?;
. Here, the participant
is
a connection to the server made earlier in the constructor, and it is set to one
of the following from network/src/api.rs
.
pub enum ConnectAddr {
Tcp(SocketAddr),
Udp(SocketAddr),
#[cfg(feature = "quic")]
Quic(SocketAddr, quinn::ClientConfig, String),
Mpsc(u64),
}
To figure out which one, let’s look at the server
crate. After poking around
for a while, you will find the file server/src/client.rs
. In there, a struct
named Client
has a in_game_stream
field. It’s nice to know what struct owns
the receive side, but recall the second question we asked at the end of the
client logic. We don’t just need to know who has the receiver, but also which
function is trying to read from the receiver. If we look in the file
server/src/sys/msg/in_game.rs
we will find a function titled run
. Hey, look!
run
calls a function titled try_recv_all
!
let _ = super::try_recv_all(client, 2, |client, msg| {
Self::handle_client_in_game_msg(
// l33l33: Tons of arguments get passed in here.
)
});
The try_recv_all
is in server/src/sys/msg/mod.rs
, but I’ll leave it to the
reader if they want to look at it. To continue chasing down our attack logic, we
need to look at the function handle_client_in_game_msg
(still in the same
in_game.rs
file) called in the closure. The handling of the client in-game
message is, yet again, another large match statement on the message type. In
client’s control_action
function, we wrapped our message in a ControlAction
,
so we need to find the branch matching on that.
// l33l33: Within the match statement we find the case:
ClientGeneral::ControlAction(event) => {
if presence.kind.controlling_char() {
if let Some(controller) = controller {
controller.push_action(event);
}
}
},
How unfortunate. This should look familiar. This handle function appears to be
pushing our attack event onto another queue. This means we need to hunt down
whatever event loop handles actions on the server side. I know this is starting
to sound a bit like a broken record, but we’re almost finished! The controller
that we are pushing the attack action to is in common/src/comp/controller.rs
.
pub struct Controller {
pub inputs: ControllerInputs,
pub queued_inputs: BTreeMap<InputKind, InputAttr>,
// TODO: consider SmallVec
pub events: Vec<ControlEvent>,
pub actions: Vec<ControlAction>,
}
Next, because we are looking for an event-loop type structure that uses the
actions
field from a Controller
, we should be thinking “tick”. At this
point, we are not digging through the code. We are excavating. Deep in the
common
crate, we can find common/systems/src/character_behavior.rs
, which
has another function named run
. This run
however, calls
let actions = std::mem::take(&mut controller.actions);
. This gets passed
around to, you guessed it, another file! The following is an abbreviated version
of the function handle_event
in common/src/comp/character_state.rs
.
pub fn handle_event(
&self,
j: &JoinData,
output_events: &mut OutputEvents,
action: ControlAction,
) -> StateUpdate {
match &self {
CharacterState::Idle(data) => data.handle_event(j, output_events, action),
// l33l33: Skipping a few...
CharacterState::BasicMelee(data) => data.handle_event(j, output_events, action),
CharacterState::BasicRanged(data) => data.handle_event(j, output_events, action),
// l33l33: Skip the rest...
}
}
This code checks what the player is “currently” doing, and hands off the action
to data’s handle_event
to decide what to do with it. Continuing forward, we
end up in common/src/states/behavior.rs
.
fn handle_event(
&self,
data: &JoinData,
output_events: &mut OutputEvents,
event: ControlAction,
) -> StateUpdate {
match event {
ControlAction::SwapEquippedWeapons => self.swap_equipped_weapons(data, output_events),
// l33l33: Skipping a few cases...
ControlAction::Talk => self.talk(data, output_events),
ControlAction::StartInput {
input,
target_entity,
select_pos,
} => self.start_input(data, input, target_entity, select_pos), // l33l33: Follow this!
ControlAction::CancelInput(input) => self.cancel_input(data, input),
}
}
Okay, I didn’t think this would trail would be this long. Luckily, the call to
start_input
in handle_event
is a function in the same file.
fn start_input(
&self,
data: &JoinData,
input: InputKind,
target_entity: Option<Uid>,
select_pos: Option<Vec3<f32>>,
) -> StateUpdate {
let mut update = StateUpdate::from(data);
update.queued_inputs.insert(input, InputAttr {
select_pos,
target_entity,
}); // l33l33: Guess what? ANOTHER QUEUE!
update
}
Trying to cut to the chase, this shoves the attack into yet another queue,
meaning there must be another function that handles the updates to the
StateUpdate
. I’m beginning to question my sanity, because the trail leads back
to the common/systems/src/character_behavior.rs
file. This time, the function
publish_state_update
handles the state update queue with a for loop.
for (input, attr) in state_update.queued_inputs {
join.controller.queued_inputs.insert(input, attr);
}
Why perform any actions on game objects when we can just continue passing this
attack around different queues? This queued_inputs
is another field in the
Controller
struct from earlier. Thus, we are back to hunting for loops or
ticks in the game logic to execute this queue of tasks. Back in
common/systems/src/character_behavior.rs
the same run function calls
let state_update = j.character.behavior(&j, &mut output_events);
. Following
this call to behavior
, we get to common/src/comp/character_state.rs
again.
pub fn behavior(&self, j: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
match &self {
CharacterState::Idle(data) => data.behavior(j, output_events),
CharacterState::Talk => talk::Data.behavior(j, output_events),
CharacterState::Climb(data) => data.behavior(j, output_events),
// l33l33: And lots more...
}
}
Okay, this is the part I am unsure of, because looking at this, I think we can
only perform an attack from the Wielding
state. This goes down another rabbit
hole, so if the reader would like to explore the code behind the behavior
function linked to Wielding
, I would highly encourage it. For the sake of
shortening this, let’s assume that the server ticks forward and changes from the
Wielding
state to the BasicMelee
state. In this hypothetical, our call to
data.behavior
FINALLY brings us to common/src/states/basic_melee.rs
. The
behavior
method attached to BasicMelee
is called every tick stepping through
the attack on our enemy until the attack is completed. After the attack is
finished, end_melee_ability
from common/src/states/utils.rs
is called and
the state is returned to Wielding
.
I originally meant to keep the execution flow short and focus more on cool uses of libraries (like the alternative libraries for mutexes and channels), but following the attack execution took up more of the post than I originally thought. I hope this was still interesting, even if it got a little dry towards the end. Thanks for reading!
Fun Facts
- The Valoren Discord has a channel titled “counting” which is at 21896 at the time of writing.
References
- https://gitlab.com/veloren/veloren
- https://docs.rs/specs/latest/specs/
- https://en.wikipedia.org/wiki/Entity_component_system
- https://docs.rs/atomic_refcell/latest/atomic_refcell/index.html
- https://docs.rs/crossbeam-channel/latest/crossbeam_channel/