Tracing Veloren's Input Logic

- 14 mins read


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 AtomicRefCells 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 AtomicRefCells 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 ControlActions. (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.

We got em!

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