mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
first implemetations
sync, somehow we miht wanna put the whole clock inside maybe ?, move files sync, disable Control and replace it by RemoteControl
This commit is contained in:
parent
1e93648081
commit
aa5111c265
@ -28,9 +28,9 @@ use common::{
|
||||
invite::{InviteKind, InviteResponse},
|
||||
skills::Skill,
|
||||
slot::{EquipSlot, InvSlotId, Slot},
|
||||
CharacterState, ChatMode, ControlAction, ControlEvent, Controller, ControllerInputs,
|
||||
GroupManip, InputKind, InventoryAction, InventoryEvent, InventoryUpdateEvent,
|
||||
MapMarkerChange, UtteranceKind,
|
||||
CharacterState, ChatMode, CommandGenerator, ControlAction, ControlEvent, Controller,
|
||||
ControllerInputs, GroupManip, InputKind, InventoryAction, InventoryEvent,
|
||||
InventoryUpdateEvent, MapMarkerChange, RemoteController, UtteranceKind,
|
||||
},
|
||||
event::{EventBus, LocalEvent, UpdateCharacterMetadata},
|
||||
grid::Grid,
|
||||
@ -73,7 +73,7 @@ use num::traits::FloatConst;
|
||||
use rayon::prelude::*;
|
||||
use specs::Component;
|
||||
use std::{
|
||||
collections::{BTreeMap, VecDeque},
|
||||
collections::VecDeque,
|
||||
mem,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
@ -235,6 +235,9 @@ pub struct Client {
|
||||
// The pending trade the client is involved in, and it's id
|
||||
pending_trade: Option<(TradeId, PendingTrade, Option<SitePrices>)>,
|
||||
|
||||
local_command_gen: CommandGenerator,
|
||||
next_control: Controller,
|
||||
|
||||
network: Option<Network>,
|
||||
participant: Option<Participant>,
|
||||
general_stream: Stream,
|
||||
@ -716,6 +719,9 @@ impl Client {
|
||||
pending_invites: HashSet::new(),
|
||||
pending_trade: None,
|
||||
|
||||
local_command_gen: CommandGenerator::default(),
|
||||
next_control: Controller::default(),
|
||||
|
||||
network: Some(network),
|
||||
participant: Some(participant),
|
||||
general_stream: stream,
|
||||
@ -841,9 +847,7 @@ impl Client {
|
||||
| ClientGeneral::Character(_, _)
|
||||
| ClientGeneral::Spectate(_) => &mut self.character_screen_stream,
|
||||
//Only in game
|
||||
ClientGeneral::ControllerInputs(_)
|
||||
| ClientGeneral::ControlEvent(_)
|
||||
| ClientGeneral::ControlAction(_)
|
||||
ClientGeneral::Control(_)
|
||||
| ClientGeneral::SetViewDistance(_)
|
||||
| ClientGeneral::BreakBlock(_)
|
||||
| ClientGeneral::PlaceBlock(_, _)
|
||||
@ -1032,9 +1036,11 @@ impl Client {
|
||||
ControlAction::InventoryAction(InventoryAction::Swap(equip, slot)),
|
||||
),
|
||||
(Slot::Inventory(inv1), Slot::Inventory(inv2)) => {
|
||||
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InventoryEvent(
|
||||
InventoryEvent::Swap(inv1, inv2),
|
||||
)))
|
||||
self.next_control
|
||||
.events
|
||||
.push(ControlEvent::InventoryEvent(InventoryEvent::Swap(
|
||||
inv1, inv2,
|
||||
)));
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -1044,9 +1050,10 @@ impl Client {
|
||||
Slot::Equip(equip) => {
|
||||
self.control_action(ControlAction::InventoryAction(InventoryAction::Drop(equip)))
|
||||
},
|
||||
Slot::Inventory(inv) => self.send_msg(ClientGeneral::ControlEvent(
|
||||
ControlEvent::InventoryEvent(InventoryEvent::Drop(inv)),
|
||||
)),
|
||||
Slot::Inventory(inv) => self
|
||||
.next_control
|
||||
.events
|
||||
.push(ControlEvent::InventoryEvent(InventoryEvent::Drop(inv))),
|
||||
}
|
||||
}
|
||||
|
||||
@ -1059,9 +1066,9 @@ impl Client {
|
||||
if let TradeAction::Decline = action {
|
||||
self.pending_trade.take();
|
||||
}
|
||||
self.send_msg(ClientGeneral::ControlEvent(
|
||||
ControlEvent::PerformTradeAction(id, action),
|
||||
));
|
||||
self.next_control
|
||||
.events
|
||||
.push(ControlEvent::PerformTradeAction(id, action));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1077,11 +1084,9 @@ impl Client {
|
||||
(Slot::Equip(equip), slot) | (slot, Slot::Equip(equip)) => self.control_action(
|
||||
ControlAction::InventoryAction(InventoryAction::Swap(equip, slot)),
|
||||
),
|
||||
(Slot::Inventory(inv1), Slot::Inventory(inv2)) => {
|
||||
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InventoryEvent(
|
||||
InventoryEvent::SplitSwap(inv1, inv2),
|
||||
)))
|
||||
},
|
||||
(Slot::Inventory(inv1), Slot::Inventory(inv2)) => self.next_control.events.push(
|
||||
ControlEvent::InventoryEvent(InventoryEvent::SplitSwap(inv1, inv2)),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@ -1090,9 +1095,10 @@ impl Client {
|
||||
Slot::Equip(equip) => {
|
||||
self.control_action(ControlAction::InventoryAction(InventoryAction::Drop(equip)))
|
||||
},
|
||||
Slot::Inventory(inv) => self.send_msg(ClientGeneral::ControlEvent(
|
||||
ControlEvent::InventoryEvent(InventoryEvent::SplitDrop(inv)),
|
||||
)),
|
||||
Slot::Inventory(inv) => self
|
||||
.next_control
|
||||
.events
|
||||
.push(ControlEvent::InventoryEvent(InventoryEvent::SplitDrop(inv))),
|
||||
}
|
||||
}
|
||||
|
||||
@ -1104,10 +1110,9 @@ impl Client {
|
||||
if self.is_dead() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InventoryEvent(
|
||||
InventoryEvent::Pickup(uid),
|
||||
)));
|
||||
self.next_control
|
||||
.events
|
||||
.push(ControlEvent::InventoryEvent(InventoryEvent::Pickup(uid)));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1118,7 +1123,7 @@ impl Client {
|
||||
}
|
||||
|
||||
if let Some(uid) = self.state.read_component_copied(npc_entity) {
|
||||
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::Interact(uid)));
|
||||
self.next_control.events.push(ControlEvent::Interact(uid));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1165,7 +1170,7 @@ impl Client {
|
||||
let (can_craft, required_sprite) = self.can_craft_recipe(recipe, amount);
|
||||
let has_sprite = required_sprite.map_or(true, |s| Some(s) == craft_sprite.map(|(_, s)| s));
|
||||
if can_craft && has_sprite {
|
||||
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InventoryEvent(
|
||||
self.next_control.events.push(ControlEvent::InventoryEvent(
|
||||
InventoryEvent::CraftRecipe {
|
||||
craft_event: CraftEvent::Simple {
|
||||
recipe: recipe.to_string(),
|
||||
@ -1174,7 +1179,7 @@ impl Client {
|
||||
},
|
||||
craft_sprite: craft_sprite.map(|(pos, _)| pos),
|
||||
},
|
||||
)));
|
||||
));
|
||||
true
|
||||
} else {
|
||||
false
|
||||
@ -1194,12 +1199,12 @@ impl Client {
|
||||
pub fn salvage_item(&mut self, slot: InvSlotId, salvage_pos: Vec3<i32>) -> bool {
|
||||
let is_salvageable = self.can_salvage_item(slot);
|
||||
if is_salvageable {
|
||||
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InventoryEvent(
|
||||
self.next_control.events.push(ControlEvent::InventoryEvent(
|
||||
InventoryEvent::CraftRecipe {
|
||||
craft_event: CraftEvent::Salvage(slot),
|
||||
craft_sprite: Some(salvage_pos),
|
||||
},
|
||||
)));
|
||||
));
|
||||
}
|
||||
is_salvageable
|
||||
}
|
||||
@ -1241,7 +1246,7 @@ impl Client {
|
||||
(mod_kind(primary_component), mod_kind(secondary_component))
|
||||
{
|
||||
drop(inventories);
|
||||
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InventoryEvent(
|
||||
self.next_control.events.push(ControlEvent::InventoryEvent(
|
||||
InventoryEvent::CraftRecipe {
|
||||
craft_event: CraftEvent::ModularWeapon {
|
||||
primary_component,
|
||||
@ -1249,7 +1254,7 @@ impl Client {
|
||||
},
|
||||
craft_sprite: sprite_pos,
|
||||
},
|
||||
)));
|
||||
));
|
||||
true
|
||||
} else {
|
||||
false
|
||||
@ -1264,8 +1269,9 @@ impl Client {
|
||||
slots: Vec<(u32, InvSlotId)>,
|
||||
sprite_pos: Option<Vec3<i32>>,
|
||||
) {
|
||||
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InventoryEvent(
|
||||
InventoryEvent::CraftRecipe {
|
||||
self.next_control
|
||||
.events
|
||||
.push(ControlEvent::InventoryEvent(InventoryEvent::CraftRecipe {
|
||||
craft_event: CraftEvent::ModularWeaponPrimaryComponent {
|
||||
toolkind,
|
||||
material,
|
||||
@ -1273,8 +1279,7 @@ impl Client {
|
||||
slots,
|
||||
},
|
||||
craft_sprite: sprite_pos,
|
||||
},
|
||||
)));
|
||||
}));
|
||||
}
|
||||
|
||||
fn update_available_recipes(&mut self) {
|
||||
@ -1301,18 +1306,16 @@ impl Client {
|
||||
|
||||
pub fn sites_mut(&mut self) -> &mut HashMap<SiteId, SiteInfoRich> { &mut self.sites }
|
||||
|
||||
pub fn enable_lantern(&mut self) {
|
||||
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::EnableLantern));
|
||||
}
|
||||
pub fn enable_lantern(&mut self) { self.next_control.events.push(ControlEvent::EnableLantern); }
|
||||
|
||||
pub fn disable_lantern(&mut self) {
|
||||
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::DisableLantern));
|
||||
self.next_control.events.push(ControlEvent::DisableLantern);
|
||||
}
|
||||
|
||||
pub fn remove_buff(&mut self, buff_id: BuffKind) {
|
||||
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::RemoveBuff(
|
||||
buff_id,
|
||||
)));
|
||||
self.next_control
|
||||
.events
|
||||
.push(ControlEvent::RemoveBuff(buff_id));
|
||||
}
|
||||
|
||||
pub fn unlock_skill(&mut self, skill: Skill) {
|
||||
@ -1338,43 +1341,43 @@ impl Client {
|
||||
pub fn is_trading(&self) -> bool { self.pending_trade.is_some() }
|
||||
|
||||
pub fn send_invite(&mut self, invitee: Uid, kind: InviteKind) {
|
||||
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InitiateInvite(
|
||||
invitee, kind,
|
||||
)))
|
||||
self.next_control
|
||||
.events
|
||||
.push(ControlEvent::InitiateInvite(invitee, kind));
|
||||
}
|
||||
|
||||
pub fn accept_invite(&mut self) {
|
||||
// Clear invite
|
||||
self.invite.take();
|
||||
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InviteResponse(
|
||||
InviteResponse::Accept,
|
||||
)));
|
||||
self.next_control
|
||||
.events
|
||||
.push(ControlEvent::InviteResponse(InviteResponse::Accept));
|
||||
}
|
||||
|
||||
pub fn decline_invite(&mut self) {
|
||||
// Clear invite
|
||||
self.invite.take();
|
||||
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InviteResponse(
|
||||
InviteResponse::Decline,
|
||||
)));
|
||||
self.next_control
|
||||
.events
|
||||
.push(ControlEvent::InviteResponse(InviteResponse::Decline));
|
||||
}
|
||||
|
||||
pub fn leave_group(&mut self) {
|
||||
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::GroupManip(
|
||||
GroupManip::Leave,
|
||||
)));
|
||||
self.next_control
|
||||
.events
|
||||
.push(ControlEvent::GroupManip(GroupManip::Leave));
|
||||
}
|
||||
|
||||
pub fn kick_from_group(&mut self, uid: Uid) {
|
||||
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::GroupManip(
|
||||
GroupManip::Kick(uid),
|
||||
)));
|
||||
self.next_control
|
||||
.events
|
||||
.push(ControlEvent::GroupManip(GroupManip::Kick(uid)));
|
||||
}
|
||||
|
||||
pub fn assign_group_leader(&mut self, uid: Uid) {
|
||||
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::GroupManip(
|
||||
GroupManip::AssignLeader(uid),
|
||||
)));
|
||||
self.next_control
|
||||
.events
|
||||
.push(ControlEvent::GroupManip(GroupManip::AssignLeader(uid)));
|
||||
}
|
||||
|
||||
pub fn is_riding(&self) -> bool {
|
||||
@ -1395,11 +1398,11 @@ impl Client {
|
||||
|
||||
pub fn mount(&mut self, entity: EcsEntity) {
|
||||
if let Some(uid) = self.state.read_component_copied(entity) {
|
||||
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::Mount(uid)));
|
||||
self.next_control.events.push(ControlEvent::Mount(uid));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unmount(&mut self) { self.send_msg(ClientGeneral::ControlEvent(ControlEvent::Unmount)); }
|
||||
pub fn unmount(&mut self) { self.next_control.events.push(ControlEvent::Unmount); }
|
||||
|
||||
pub fn respawn(&mut self) {
|
||||
if self
|
||||
@ -1409,7 +1412,7 @@ impl Client {
|
||||
.get(self.entity())
|
||||
.map_or(false, |h| h.is_dead)
|
||||
{
|
||||
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::Respawn));
|
||||
self.next_control.events.push(ControlEvent::Respawn);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1490,7 +1493,7 @@ impl Client {
|
||||
}
|
||||
|
||||
pub fn utter(&mut self, kind: UtteranceKind) {
|
||||
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::Utterance(kind)));
|
||||
self.next_control.events.push(ControlEvent::Utterance(kind));
|
||||
}
|
||||
|
||||
pub fn toggle_sneak(&mut self) {
|
||||
@ -1542,15 +1545,7 @@ impl Client {
|
||||
}
|
||||
|
||||
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));
|
||||
self.next_control.actions.push(control_action);
|
||||
}
|
||||
|
||||
pub fn view_distance(&self) -> Option<u32> { self.view_distance }
|
||||
@ -1675,11 +1670,11 @@ impl Client {
|
||||
)
|
||||
});
|
||||
|
||||
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::ChangeAbility {
|
||||
self.next_control.events.push(ControlEvent::ChangeAbility {
|
||||
slot,
|
||||
auxiliary_key,
|
||||
new_ability,
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
/// Execute a single client tick, handle input and update the game state by
|
||||
@ -1688,6 +1683,7 @@ impl Client {
|
||||
&mut self,
|
||||
inputs: ControllerInputs,
|
||||
dt: Duration,
|
||||
total_tick_time: Duration,
|
||||
add_foreign_systems: impl Fn(&mut DispatcherBuilder),
|
||||
) -> Result<Vec<Event>, Error> {
|
||||
span!(_guard, "tick", "Client::tick");
|
||||
@ -1714,30 +1710,25 @@ impl Client {
|
||||
// Pass character actions from frontend input to the player's entity.
|
||||
if self.presence.is_some() {
|
||||
prof_span!("handle and send inputs");
|
||||
if let Err(e) = self
|
||||
self.next_control.inputs = inputs;
|
||||
let con = std::mem::take(&mut self.next_control);
|
||||
let rcon = self.local_command_gen.gen(total_tick_time, con);
|
||||
let commands = self
|
||||
.state
|
||||
.ecs()
|
||||
.write_storage::<Controller>()
|
||||
.write_storage::<RemoteController>()
|
||||
.entry(self.entity())
|
||||
.map(|entry| {
|
||||
entry
|
||||
.or_insert_with(|| Controller {
|
||||
inputs: inputs.clone(),
|
||||
queued_inputs: BTreeMap::new(),
|
||||
events: Vec::new(),
|
||||
actions: Vec::new(),
|
||||
})
|
||||
.inputs = inputs.clone();
|
||||
})
|
||||
{
|
||||
let entry = self.entity();
|
||||
error!(
|
||||
?e,
|
||||
?entry,
|
||||
"Couldn't access controller component on client entity"
|
||||
);
|
||||
}
|
||||
self.send_msg_err(ClientGeneral::ControllerInputs(Box::new(inputs)))?;
|
||||
.map(|rc| {
|
||||
let rc = rc.or_insert_with(RemoteController::default);
|
||||
rc.push(rcon);
|
||||
rc.commands().clone()
|
||||
});
|
||||
match commands {
|
||||
Ok(commands) => self.send_msg_err(ClientGeneral::Control(commands))?,
|
||||
Err(e) => {
|
||||
error!(?e, "couldn't create RemoteController for own entity");
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 2) Build up a list of events for this frame, to be passed to the frontend.
|
||||
@ -2187,6 +2178,17 @@ impl Client {
|
||||
) -> Result<(), Error> {
|
||||
prof_span!("handle_server_in_game_msg");
|
||||
match msg {
|
||||
ServerGeneral::AckControl(acked_ids) => {
|
||||
if let Some(remote_controller) = self
|
||||
.state
|
||||
.ecs()
|
||||
.write_storage::<RemoteController>()
|
||||
.get_mut(self.entity())
|
||||
{
|
||||
remote_controller.acked(acked_ids);
|
||||
remote_controller.maintain();
|
||||
}
|
||||
},
|
||||
ServerGeneral::GroupUpdate(change_notification) => {
|
||||
use comp::group::ChangeNotification::*;
|
||||
// Note: we use a hashmap since this would not work with entities outside
|
||||
@ -2868,8 +2870,12 @@ mod tests {
|
||||
let mut clock = Clock::new(Duration::from_secs_f64(SPT));
|
||||
|
||||
//tick
|
||||
let events_result: Result<Vec<Event>, Error> =
|
||||
client.tick(ControllerInputs::default(), clock.dt(), |_| {});
|
||||
let events_result: Result<Vec<Event>, Error> = client.tick(
|
||||
comp::ControllerInputs::default(),
|
||||
clock.dt(),
|
||||
clock.total_tick_time(),
|
||||
|_| {},
|
||||
);
|
||||
|
||||
//chat functionality
|
||||
client.send_chat("foobar".to_string());
|
||||
|
@ -58,9 +58,7 @@ pub enum ClientGeneral {
|
||||
Character(CharacterId, ViewDistances),
|
||||
Spectate(ViewDistances),
|
||||
//Only in game
|
||||
ControllerInputs(Box<comp::ControllerInputs>),
|
||||
ControlEvent(comp::ControlEvent),
|
||||
ControlAction(comp::ControlAction),
|
||||
Control(comp::ControlCommands),
|
||||
SetViewDistance(ViewDistances),
|
||||
BreakBlock(Vec3<i32>),
|
||||
PlaceBlock(Vec3<i32>, Block),
|
||||
@ -118,9 +116,7 @@ impl ClientMsg {
|
||||
c_type == ClientType::Game && presence.is_none()
|
||||
},
|
||||
//Only in game
|
||||
ClientGeneral::ControllerInputs(_)
|
||||
| ClientGeneral::ControlEvent(_)
|
||||
| ClientGeneral::ControlAction(_)
|
||||
ClientGeneral::Control(_)
|
||||
| ClientGeneral::SetViewDistance(_)
|
||||
| ClientGeneral::BreakBlock(_)
|
||||
| ClientGeneral::PlaceBlock(_, _)
|
||||
|
@ -18,7 +18,7 @@ use common::{
|
||||
uuid::Uuid,
|
||||
weather::WeatherGrid,
|
||||
};
|
||||
use hashbrown::HashMap;
|
||||
use hashbrown::{HashMap, HashSet};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
use tracing::warn;
|
||||
@ -142,6 +142,7 @@ pub enum ServerGeneral {
|
||||
CharacterSuccess,
|
||||
SpectatorSuccess(Vec3<f32>),
|
||||
//Ingame related
|
||||
AckControl(HashSet<u64>),
|
||||
GroupUpdate(comp::group::ChangeNotification<Uid>),
|
||||
/// Indicate to the client that they are invited to join a group
|
||||
Invite {
|
||||
@ -315,6 +316,7 @@ impl ServerMsg {
|
||||
},
|
||||
//Ingame related
|
||||
ServerGeneral::GroupUpdate(_)
|
||||
| ServerGeneral::AckControl(_)
|
||||
| ServerGeneral::Invite { .. }
|
||||
| ServerGeneral::InvitePending(_)
|
||||
| ServerGeneral::InviteComplete { .. }
|
||||
|
@ -103,6 +103,8 @@ impl Clock {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn total_tick_time(&self) -> Duration { self.total_tick_time }
|
||||
|
||||
/// Do not modify without asking @xMAC94x first!
|
||||
pub fn tick(&mut self) {
|
||||
span!(_guard, "tick", "Clock::tick");
|
||||
|
@ -127,6 +127,7 @@ pub enum UtteranceKind {
|
||||
* sounds */
|
||||
}
|
||||
|
||||
/// instantaneous Events that can happen anytime and are
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ControlEvent {
|
||||
//ToggleLantern,
|
||||
@ -150,6 +151,8 @@ pub enum ControlEvent {
|
||||
},
|
||||
}
|
||||
|
||||
/// in contrast to events, action move a entity between 2 states, we want to
|
||||
/// limit when they can happen
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub enum ControlAction {
|
||||
SwapEquippedWeapons,
|
||||
|
@ -40,6 +40,8 @@ pub mod loot_owner;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod projectile;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod remote_controller;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod shockwave;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod skillset;
|
||||
@ -108,6 +110,7 @@ pub use self::{
|
||||
player::{AliasError, Player, MAX_ALIAS_LEN},
|
||||
poise::{Poise, PoiseChange, PoiseState},
|
||||
projectile::{Projectile, ProjectileConstructor},
|
||||
remote_controller::{CommandGenerator, ControlCommands, RemoteController},
|
||||
shockwave::{Shockwave, ShockwaveHitEntities},
|
||||
skillset::{
|
||||
skills::{self, Skill},
|
||||
|
240
common/src/comp/remote_controller.rs
Normal file
240
common/src/comp/remote_controller.rs
Normal file
@ -0,0 +1,240 @@
|
||||
use crate::comp::Controller;
|
||||
use hashbrown::HashSet;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specs::{Component, DenseVecStorage};
|
||||
use std::{collections::VecDeque, time::Duration};
|
||||
|
||||
pub type ControlCommands = VecDeque<ControlCommand>;
|
||||
|
||||
/// Controller messages are real-time-combat relevant.
|
||||
/// Controller contains all kind of inputs a entity can change
|
||||
/// Remote controlled entities (e.g. clients) have a latency
|
||||
/// They can predict the future locally and send us their TIMED controls
|
||||
/// Other things, like chunk requests or build a weapon are done one other
|
||||
/// channel via other methods.
|
||||
#[derive(Debug)]
|
||||
pub struct RemoteController {
|
||||
///sorted, deduplicated
|
||||
commands: ControlCommands,
|
||||
existing_commands: HashSet<u64>,
|
||||
max_hold: Duration,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct ControlCommand {
|
||||
id: u64,
|
||||
source_time: Duration,
|
||||
msg: Controller,
|
||||
}
|
||||
|
||||
impl RemoteController {
|
||||
/// delte old commands
|
||||
pub fn maintain(&mut self) {
|
||||
let min_allowed_age = self.commands.back().unwrap().source_time;
|
||||
|
||||
while let Some(first) = self.commands.front() {
|
||||
if first.source_time + self.max_hold < min_allowed_age {
|
||||
self.commands
|
||||
.pop_front()
|
||||
.map(|c| self.existing_commands.remove(&c.id));
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push(&mut self, command: ControlCommand) -> Option<u64> {
|
||||
let id = command.id;
|
||||
//check if command fits on the end.
|
||||
if self.existing_commands.contains(&id) {
|
||||
return None; // element already existed
|
||||
}
|
||||
self.existing_commands.insert(id);
|
||||
self.commands.push_back(command);
|
||||
|
||||
Some(id)
|
||||
}
|
||||
|
||||
pub fn append(&mut self, commands: ControlCommands) -> HashSet<u64> {
|
||||
let mut result = HashSet::new();
|
||||
for command in commands {
|
||||
let id = command.id;
|
||||
if !self.existing_commands.contains(&id) {
|
||||
result.insert(id);
|
||||
self.existing_commands.insert(id);
|
||||
self.commands.push_back(command);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub fn current(&self, time: Duration) -> Option<&ControlCommand> {
|
||||
//TODO: actually sort it!
|
||||
let mut lowest = None;
|
||||
let mut lowest_cmd = None;
|
||||
for c in &self.commands {
|
||||
if c.source_time >= time {
|
||||
let diff = c.source_time - time;
|
||||
if match lowest {
|
||||
None => true,
|
||||
Some(lowest) => diff < lowest,
|
||||
} {
|
||||
lowest = Some(diff);
|
||||
lowest_cmd = Some(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
lowest_cmd
|
||||
}
|
||||
|
||||
/// arrived at remote and no longer need to be hold locally
|
||||
pub fn acked(&mut self, ids: HashSet<u64>) { self.commands.retain(|c| !ids.contains(&c.id)); }
|
||||
|
||||
pub fn commands(&self) -> &ControlCommands { &self.commands }
|
||||
}
|
||||
|
||||
impl Default for RemoteController {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
commands: VecDeque::new(),
|
||||
existing_commands: HashSet::new(),
|
||||
max_hold: Duration::from_secs(1),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for RemoteController {
|
||||
type Storage = DenseVecStorage<Self>;
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct CommandGenerator {
|
||||
id: u64,
|
||||
}
|
||||
|
||||
impl CommandGenerator {
|
||||
pub fn gen(&mut self, time: Duration, msg: Controller) -> ControlCommand {
|
||||
self.id += 1;
|
||||
ControlCommand {
|
||||
source_time: time,
|
||||
id: self.id,
|
||||
msg,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ControlCommand {
|
||||
pub fn msg(&self) -> &Controller { &self.msg }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn generate_control_cmds(count: usize) -> ControlCommands {
|
||||
let mut result = VecDeque::new();
|
||||
let mut generator = CommandGenerator::default();
|
||||
let mut time = Duration::new(0, 0);
|
||||
const INCREASE: Duration = Duration::from_millis(33);
|
||||
for _ in 0..count {
|
||||
let msg = Controller::default();
|
||||
let cc = generator.gen(time, msg);
|
||||
result.push_back(cc);
|
||||
time += INCREASE;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resend_data() {
|
||||
let data = generate_control_cmds(5);
|
||||
let mut list = RemoteController::default();
|
||||
assert_eq!(list.push(data[0].clone()), Some(1));
|
||||
assert_eq!(list.commands.len(), 1);
|
||||
|
||||
assert_eq!(list.push(data[0].clone()), None);
|
||||
assert_eq!(list.push(data[1].clone()), Some(2));
|
||||
assert_eq!(list.commands.len(), 2);
|
||||
|
||||
assert_eq!(list.push(data[0].clone()), None);
|
||||
assert_eq!(list.push(data[1].clone()), None);
|
||||
assert_eq!(list.push(data[2].clone()), Some(3));
|
||||
assert_eq!(list.commands.len(), 3);
|
||||
|
||||
assert_eq!(list.push(data[1].clone()), None);
|
||||
assert_eq!(list.push(data[2].clone()), None);
|
||||
assert_eq!(list.push(data[3].clone()), Some(4));
|
||||
assert_eq!(list.commands.len(), 4);
|
||||
|
||||
assert_eq!(list.push(data[2].clone()), None);
|
||||
assert_eq!(list.push(data[3].clone()), None);
|
||||
assert_eq!(list.push(data[4].clone()), Some(5));
|
||||
assert_eq!(list.commands.len(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_auto_evict() {
|
||||
let data = generate_control_cmds(6);
|
||||
let mut list = RemoteController::default();
|
||||
list.max_hold = Duration::from_millis(100);
|
||||
assert_eq!(list.push(data[0].clone()), Some(1));
|
||||
assert_eq!(list.push(data[1].clone()), Some(2));
|
||||
assert_eq!(list.push(data[2].clone()), Some(3));
|
||||
assert_eq!(list.commands.len(), 3);
|
||||
assert_eq!(list.push(data[3].clone()), Some(4));
|
||||
assert_eq!(list.commands.len(), 4);
|
||||
assert_eq!(list.commands[0].id, 1);
|
||||
assert_eq!(list.push(data[4].clone()), Some(5));
|
||||
assert_eq!(list.commands.len(), 5);
|
||||
list.maintain();
|
||||
assert_eq!(list.commands.len(), 4);
|
||||
assert_eq!(list.commands[0].id, 2);
|
||||
assert_eq!(list.push(data[5].clone()), Some(6));
|
||||
assert_eq!(list.commands.len(), 5);
|
||||
list.maintain();
|
||||
assert_eq!(list.commands.len(), 4);
|
||||
assert_eq!(list.commands[0].id, 3);
|
||||
assert_eq!(list.commands[1].id, 4);
|
||||
assert_eq!(list.commands[2].id, 5);
|
||||
assert_eq!(list.commands[3].id, 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_acked() {
|
||||
let data = generate_control_cmds(7);
|
||||
let mut list = RemoteController::default();
|
||||
assert_eq!(list.push(data[0].clone()), Some(1));
|
||||
assert_eq!(list.push(data[1].clone()), Some(2));
|
||||
assert_eq!(list.push(data[2].clone()), Some(3));
|
||||
assert_eq!(list.push(data[3].clone()), Some(4));
|
||||
assert_eq!(list.push(data[4].clone()), Some(5));
|
||||
let mut to_export = list.commands().iter().map(|e| e.id).collect::<HashSet<_>>();
|
||||
// damange one entry
|
||||
to_export.remove(&3);
|
||||
list.acked(to_export);
|
||||
assert_eq!(list.push(data[5].clone()), Some(6));
|
||||
assert_eq!(list.push(data[6].clone()), Some(7));
|
||||
println!("asd{:?}", &list);
|
||||
|
||||
let to_export = list.commands().clone();
|
||||
assert_eq!(to_export.len(), 3);
|
||||
assert_eq!(to_export[0].id, 3);
|
||||
assert_eq!(to_export[1].id, 6);
|
||||
assert_eq!(to_export[2].id, 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_append() {
|
||||
let data = generate_control_cmds(5);
|
||||
let mut list = RemoteController::default();
|
||||
let set = list.append(data);
|
||||
assert_eq!(set.len(), 5);
|
||||
assert!(!set.contains(&0));
|
||||
assert!(set.contains(&1));
|
||||
assert!(set.contains(&2));
|
||||
assert!(set.contains(&3));
|
||||
assert!(set.contains(&4));
|
||||
assert!(set.contains(&5));
|
||||
assert!(!set.contains(&6));
|
||||
}
|
||||
}
|
@ -207,7 +207,7 @@ impl State {
|
||||
ecs.register::<comp::Admin>();
|
||||
|
||||
// Register components send from clients -> server
|
||||
ecs.register::<comp::Controller>();
|
||||
ecs.register::<comp::RemoteController>();
|
||||
|
||||
// Register components send directly from server -> all but one client
|
||||
ecs.register::<comp::PhysicsState>();
|
||||
@ -224,6 +224,7 @@ impl State {
|
||||
|
||||
// Register client-local components
|
||||
// TODO: only register on the client
|
||||
ecs.register::<comp::Controller>();
|
||||
ecs.register::<comp::LightAnimation>();
|
||||
ecs.register::<sync_interp::InterpBuffer<comp::Pos>>();
|
||||
ecs.register::<sync_interp::InterpBuffer<comp::Vel>>();
|
||||
|
@ -10,6 +10,7 @@ mod interpolation;
|
||||
pub mod melee;
|
||||
mod mount;
|
||||
pub mod phys;
|
||||
pub mod predict_controller;
|
||||
pub mod projectile;
|
||||
mod shockwave;
|
||||
mod stats;
|
||||
@ -19,6 +20,7 @@ use common_ecs::{dispatch, System};
|
||||
use specs::DispatcherBuilder;
|
||||
|
||||
pub fn add_local_systems(dispatch_builder: &mut DispatcherBuilder) {
|
||||
dispatch::<predict_controller::Sys>(dispatch_builder, &[]);
|
||||
//TODO: don't run interpolation on server
|
||||
dispatch::<interpolation::Sys>(dispatch_builder, &[]);
|
||||
dispatch::<mount::Sys>(dispatch_builder, &[]);
|
||||
|
47
common/systems/src/predict_controller.rs
Normal file
47
common/systems/src/predict_controller.rs
Normal file
@ -0,0 +1,47 @@
|
||||
use common::comp::{Controller, RemoteController};
|
||||
use common_ecs::{Job, Origin, Phase, System};
|
||||
use specs::{shred::ResourceId, Entities, Join, ReadStorage, SystemData, World, WriteStorage};
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(SystemData)]
|
||||
pub struct ReadData<'a> {
|
||||
entities: Entities<'a>,
|
||||
remote_controllers: ReadStorage<'a, RemoteController>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Sys;
|
||||
|
||||
impl<'a> System<'a> for Sys {
|
||||
type SystemData = (ReadData<'a>, WriteStorage<'a, Controller>);
|
||||
|
||||
const NAME: &'static str = "predict_controller";
|
||||
const ORIGIN: Origin = Origin::Common;
|
||||
const PHASE: Phase = Phase::Create;
|
||||
|
||||
fn run(_job: &mut Job<Self>, (read_data, mut controllers): Self::SystemData) {
|
||||
for (entity, _a) in (&read_data.entities, &read_data.remote_controllers).join() {
|
||||
let i = _a.commands().len();
|
||||
tracing::warn!(?i, "foo");
|
||||
let _ = controllers
|
||||
.entry(entity)
|
||||
.map(|e| e.or_insert_with(Default::default));
|
||||
}
|
||||
|
||||
for (_, remote_controller, controller) in (
|
||||
&read_data.entities,
|
||||
&read_data.remote_controllers,
|
||||
&mut controllers,
|
||||
)
|
||||
.join()
|
||||
{
|
||||
// grab the most fitting entry
|
||||
let time = Duration::from_millis(1000);
|
||||
let r = remote_controller.current(time).map(|c| c.msg());
|
||||
// Do nothing when already populated and we dont have a new value
|
||||
if let Some(r) = r {
|
||||
*controller = r.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -103,6 +103,7 @@ impl Client {
|
||||
},
|
||||
//In-game related
|
||||
ServerGeneral::GroupUpdate(_)
|
||||
| ServerGeneral::AckControl(_)
|
||||
| ServerGeneral::Invite { .. }
|
||||
| ServerGeneral::InvitePending(_)
|
||||
| ServerGeneral::InviteComplete { .. }
|
||||
@ -175,6 +176,7 @@ impl Client {
|
||||
},
|
||||
//In-game related
|
||||
ServerGeneral::GroupUpdate(_)
|
||||
| ServerGeneral::AckControl(_)
|
||||
| ServerGeneral::Invite { .. }
|
||||
| ServerGeneral::InvitePending(_)
|
||||
| ServerGeneral::InviteComplete { .. }
|
||||
|
@ -3,8 +3,8 @@ use crate::TerrainPersistence;
|
||||
use crate::{client::Client, presence::Presence, Settings};
|
||||
use common::{
|
||||
comp::{
|
||||
Admin, AdminRole, CanBuild, ControlEvent, Controller, ForceUpdate, Health, Ori, Player,
|
||||
Pos, SkillSet, Vel,
|
||||
Admin, AdminRole, CanBuild, ForceUpdate, Health, Ori, Player, Pos, RemoteController,
|
||||
SkillSet, Vel,
|
||||
},
|
||||
event::{EventBus, ServerEvent},
|
||||
link::Is,
|
||||
@ -59,7 +59,7 @@ impl Sys {
|
||||
position: Option<&mut Pos>,
|
||||
velocity: Option<&mut Vel>,
|
||||
orientation: Option<&mut Ori>,
|
||||
controller: Option<&mut Controller>,
|
||||
remote_controller: &mut RemoteController,
|
||||
settings: &Read<'_, Settings>,
|
||||
build_areas: &Read<'_, BuildAreas>,
|
||||
player_physics_setting: Option<&mut PlayerPhysicsSetting>,
|
||||
@ -93,32 +93,32 @@ impl Sys {
|
||||
client.send(ServerGeneral::SetViewDistance(clamped_vds.terrain))?;
|
||||
}
|
||||
},
|
||||
ClientGeneral::ControllerInputs(inputs) => {
|
||||
ClientGeneral::Control(rc) => {
|
||||
if presence.kind.controlling_char() {
|
||||
if let Some(controller) = controller {
|
||||
controller.inputs.update_with_new(*inputs);
|
||||
}
|
||||
}
|
||||
},
|
||||
ClientGeneral::ControlEvent(event) => {
|
||||
if presence.kind.controlling_char() {
|
||||
// Skip respawn if client entity is alive
|
||||
if let ControlEvent::Respawn = event {
|
||||
if healths.get(entity).map_or(true, |h| !h.is_dead) {
|
||||
//Todo: comment why return!
|
||||
return Ok(());
|
||||
let ids = remote_controller.append(rc);
|
||||
remote_controller.maintain();
|
||||
// confirm controls
|
||||
client.send(ServerGeneral::AckControl(ids))?;
|
||||
//Todo: FIXME!!!
|
||||
/*
|
||||
// Skip respawn if client entity is alive
|
||||
if let ControlEvent::Respawn = event {
|
||||
if healths.get(entity).map_or(true, |h| !h.is_dead) {
|
||||
//Todo: comment why return!
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(controller) = controllers.get_mut(entity) {
|
||||
controller.push_event(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(controller) = controller {
|
||||
controller.push_event(event);
|
||||
}
|
||||
}
|
||||
},
|
||||
ClientGeneral::ControlAction(event) => {
|
||||
if presence.kind.controlling_char() {
|
||||
if let Some(controller) = controller {
|
||||
controller.push_action(event);
|
||||
}
|
||||
},
|
||||
ClientGeneral::ControlAction(event) => {
|
||||
if presence.kind.controlling_char() {
|
||||
if let Some(controller) = controllers.get_mut(entity) {
|
||||
controller.push_action(event);
|
||||
*/
|
||||
}
|
||||
},
|
||||
ClientGeneral::PlayerPhysics { pos, vel, ori, force_counter } => {
|
||||
@ -330,7 +330,7 @@ impl<'a> System<'a> for Sys {
|
||||
WriteStorage<'a, Ori>,
|
||||
WriteStorage<'a, Presence>,
|
||||
WriteStorage<'a, Client>,
|
||||
WriteStorage<'a, Controller>,
|
||||
WriteStorage<'a, RemoteController>,
|
||||
Read<'a, Settings>,
|
||||
Read<'a, BuildAreas>,
|
||||
Write<'a, PlayerPhysicsSettings>,
|
||||
@ -361,7 +361,7 @@ impl<'a> System<'a> for Sys {
|
||||
mut orientations,
|
||||
mut presences,
|
||||
mut clients,
|
||||
mut controllers,
|
||||
mut remote_controllers,
|
||||
settings,
|
||||
build_areas,
|
||||
mut player_physics_settings_,
|
||||
@ -390,7 +390,7 @@ impl<'a> System<'a> for Sys {
|
||||
(&mut positions).maybe(),
|
||||
(&mut velocities).maybe(),
|
||||
(&mut orientations).maybe(),
|
||||
(&mut controllers).maybe(),
|
||||
(&mut remote_controllers).maybe(),
|
||||
)
|
||||
.join()
|
||||
// NOTE: Required because Specs has very poor work splitting for sparse joins.
|
||||
@ -407,7 +407,7 @@ impl<'a> System<'a> for Sys {
|
||||
ref mut pos,
|
||||
ref mut vel,
|
||||
ref mut ori,
|
||||
ref mut controller,
|
||||
remote_controller,
|
||||
)| {
|
||||
let old_player_physics_setting = maybe_player.map(|p| {
|
||||
player_physics_settings
|
||||
@ -421,6 +421,14 @@ impl<'a> System<'a> for Sys {
|
||||
// ingame messages to be ignored.
|
||||
let mut clearable_maybe_presence = maybe_presence.as_deref_mut();
|
||||
let mut skill_set = skill_set.map(Cow::Borrowed);
|
||||
let mut new_remote_controller = None;
|
||||
let remote_controller = match remote_controller {
|
||||
Some(rc) => rc,
|
||||
None => {
|
||||
new_remote_controller = Some(RemoteController::default());
|
||||
new_remote_controller.as_mut().unwrap()
|
||||
}
|
||||
};
|
||||
let _ = super::try_recv_all(client, 2, |client, msg| {
|
||||
Self::handle_client_in_game_msg(
|
||||
server_emitter,
|
||||
@ -437,7 +445,7 @@ impl<'a> System<'a> for Sys {
|
||||
pos.as_deref_mut(),
|
||||
vel.as_deref_mut(),
|
||||
ori.as_deref_mut(),
|
||||
controller.as_deref_mut(),
|
||||
remote_controller,
|
||||
&settings,
|
||||
&build_areas,
|
||||
new_player_physics_setting.as_mut(),
|
||||
@ -467,27 +475,26 @@ impl<'a> System<'a> for Sys {
|
||||
let physics_update = maybe_player.map(|p| p.uuid())
|
||||
.zip(new_player_physics_setting
|
||||
.filter(|_| old_player_physics_setting != new_player_physics_setting));
|
||||
(skill_set_update, physics_update)
|
||||
(skill_set_update, physics_update, new_remote_controller.map(|c| (entity, c)))
|
||||
},
|
||||
)
|
||||
// NOTE: Would be nice to combine this with the map_init somehow, but I'm not sure if
|
||||
// that's possible.
|
||||
.filter(|(x, y)| x.is_some() || y.is_some())
|
||||
.filter(|(x, y, z)| x.is_some() || y.is_some() || z.is_some())
|
||||
// NOTE: I feel like we shouldn't actually need to allocate here, but hopefully this
|
||||
// doesn't turn out to be important as there shouldn't be that many connected clients.
|
||||
// The reason we can't just use unzip is that the two sides might be different lengths.
|
||||
.collect::<Vec<_>>();
|
||||
let player_physics_settings = &mut *player_physics_settings_;
|
||||
// Deferred updates to skillsets and player physics.
|
||||
// Deferred updates to skillsets and player physics and new_remote_controller.
|
||||
//
|
||||
// NOTE: It is an invariant that there is at most one client entry per player
|
||||
// uuid; since we joined on clients, it follows that there's just one update
|
||||
// per uuid, so the physics update is sound and doesn't depend on evaluation
|
||||
// order, even though we're not updating directly by entity or uid (note that
|
||||
// for a given entity, we process messages serially).
|
||||
deferred_updates
|
||||
.iter_mut()
|
||||
.for_each(|(skill_set_update, physics_update)| {
|
||||
deferred_updates.iter_mut().for_each(
|
||||
|(skill_set_update, physics_update, new_remote_controller)| {
|
||||
if let Some((entity, new_skill_set)) = skill_set_update {
|
||||
// We know this exists, because we already iterated over it with the skillset
|
||||
// lock taken, so we can ignore the error.
|
||||
@ -506,7 +513,11 @@ impl<'a> System<'a> for Sys {
|
||||
.settings
|
||||
.insert(uuid, player_physics_setting);
|
||||
}
|
||||
});
|
||||
if let Some((uuid, new_remote_controller)) = new_remote_controller.take() {
|
||||
let _ = remote_controllers.insert(uuid, new_remote_controller);
|
||||
}
|
||||
},
|
||||
);
|
||||
// Finally, drop the deferred updates in another thread.
|
||||
slow_jobs.spawn("CHUNK_DROP", move || {
|
||||
drop(deferred_updates);
|
||||
|
@ -208,6 +208,7 @@ impl PlayState for CharSelectionState {
|
||||
let res = self.client.borrow_mut().tick(
|
||||
comp::ControllerInputs::default(),
|
||||
global_state.clock.dt(),
|
||||
global_state.clock.total_tick_time(),
|
||||
|_| {},
|
||||
);
|
||||
match res {
|
||||
|
@ -226,6 +226,7 @@ impl PlayState for MainMenuState {
|
||||
match client.tick(
|
||||
comp::ControllerInputs::default(),
|
||||
global_state.clock.dt(),
|
||||
global_state.clock.total_tick_time(),
|
||||
|_| {},
|
||||
) {
|
||||
Ok(events) => {
|
||||
|
@ -270,7 +270,12 @@ impl SessionState {
|
||||
self.mumble_link.update(player_pos, player_pos);
|
||||
}
|
||||
|
||||
for event in client.tick(self.inputs.clone(), dt, crate::ecs::sys::add_local_systems)? {
|
||||
for event in client.tick(
|
||||
self.inputs.clone(),
|
||||
dt,
|
||||
global_state.clock.total_tick_time(),
|
||||
crate::ecs::sys::add_local_systems,
|
||||
)? {
|
||||
match event {
|
||||
client::Event::Chat(m) => {
|
||||
self.hud.new_message(m);
|
||||
|
Loading…
Reference in New Issue
Block a user