mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Merge branch 'aweinstock/trade-implementation' into 'master'
Aweinstock/trade implementation See merge request veloren/veloren!1775
This commit is contained in:
commit
0da363e420
@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Snow particles
|
||||
- Basic NPC interaction
|
||||
- Lights in dungeons
|
||||
- Trading system (bound to the `R` key by default, currently only works with players)
|
||||
|
||||
### Changed
|
||||
|
||||
|
@ -40,6 +40,7 @@
|
||||
"gameinput.escape": "Escape",
|
||||
"gameinput.map": "Map",
|
||||
"gameinput.bag": "Bag",
|
||||
"gameinput.trade": "Trade",
|
||||
"gameinput.social": "Social",
|
||||
"gameinput.sit": "Sit",
|
||||
"gameinput.spellbook": "Spells",
|
||||
|
@ -5,6 +5,7 @@
|
||||
string_map: {
|
||||
"hud.group": "Group",
|
||||
"hud.group.invite_to_join": "{name} invited you to their group!",
|
||||
"hud.group.invite_to_trade": "{name} would like to trade with you.",
|
||||
"hud.group.invite": "Invite",
|
||||
"hud.group.kick": "Kick",
|
||||
"hud.group.assign_leader": "Assign Leader",
|
||||
|
21
assets/voxygen/i18n/en/hud/trade.ron
Normal file
21
assets/voxygen/i18n/en/hud/trade.ron
Normal file
@ -0,0 +1,21 @@
|
||||
/// WARNING: Localization files shall be saved in UTF-8 format without BOM
|
||||
|
||||
/// Localization for "global" English
|
||||
(
|
||||
string_map: {
|
||||
"hud.trade.trade_window": "Trade window",
|
||||
"hud.trade.phase1_description": "Drag the items you want to trade\n into your area.",
|
||||
"hud.trade.phase2_description": "The trade is now locked to give you\n time to review it.",
|
||||
/// Phase3 should only be visible for a few milliseconds if everything is working properly, but is included for completeness
|
||||
"hud.trade.phase3_description": "Trade is being processed.",
|
||||
"hud.trade.persons_offer": "{playername}'s offer",
|
||||
"hud.trade.has_accepted": "{playername}\nhas accepted",
|
||||
"hud.trade.accept": "Accept",
|
||||
"hud.trade.decline": "Decline",
|
||||
},
|
||||
|
||||
|
||||
vector_map: {
|
||||
}
|
||||
)
|
||||
|
@ -21,6 +21,7 @@ use common::{
|
||||
self,
|
||||
chat::{KillSource, KillType},
|
||||
group,
|
||||
invite::{InviteKind, InviteResponse},
|
||||
skills::Skill,
|
||||
slot::Slot,
|
||||
ChatMode, ControlAction, ControlEvent, Controller, ControllerInputs, GroupManip,
|
||||
@ -32,6 +33,7 @@ use common::{
|
||||
recipe::RecipeBook,
|
||||
span,
|
||||
terrain::{block::Block, neighbors, BiomeKind, SitesKind, TerrainChunk, TerrainChunkSize},
|
||||
trade::{PendingTrade, TradeAction, TradeId, TradeResult},
|
||||
uid::{Uid, UidAllocator},
|
||||
vol::RectVolSize,
|
||||
};
|
||||
@ -69,6 +71,15 @@ const PING_ROLLING_AVERAGE_SECS: usize = 10;
|
||||
|
||||
pub enum Event {
|
||||
Chat(comp::ChatMsg),
|
||||
InviteComplete {
|
||||
target: Uid,
|
||||
answer: InviteAnswer,
|
||||
kind: InviteKind,
|
||||
},
|
||||
TradeComplete {
|
||||
result: TradeResult,
|
||||
trade: PendingTrade,
|
||||
},
|
||||
Disconnect,
|
||||
DisconnectionNotification(u64),
|
||||
InventoryUpdated(InventoryUpdateEvent),
|
||||
@ -130,12 +141,14 @@ pub struct Client {
|
||||
|
||||
max_group_size: u32,
|
||||
// Client has received an invite (inviter uid, time out instant)
|
||||
group_invite: Option<(Uid, std::time::Instant, std::time::Duration)>,
|
||||
invite: Option<(Uid, std::time::Instant, std::time::Duration, InviteKind)>,
|
||||
group_leader: Option<Uid>,
|
||||
// Note: potentially representable as a client only component
|
||||
group_members: HashMap<Uid, group::Role>,
|
||||
// Pending invites that this client has sent out
|
||||
pending_invites: HashSet<Uid>,
|
||||
// The pending trade the client is involved in, and it's id
|
||||
pending_trade: Option<(TradeId, PendingTrade)>,
|
||||
|
||||
_network: Network,
|
||||
participant: Option<Participant>,
|
||||
@ -420,10 +433,11 @@ impl Client {
|
||||
chat_mode: ChatMode::default(),
|
||||
|
||||
max_group_size,
|
||||
group_invite: None,
|
||||
invite: None,
|
||||
group_leader: None,
|
||||
group_members: HashMap::new(),
|
||||
pending_invites: HashSet::new(),
|
||||
pending_trade: None,
|
||||
|
||||
_network: network,
|
||||
participant: Some(participant),
|
||||
@ -636,18 +650,25 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn perform_trade_action(&mut self, action: TradeAction) {
|
||||
if let Some((id, _)) = self.pending_trade {
|
||||
if let TradeAction::Decline = action {
|
||||
self.pending_trade.take();
|
||||
}
|
||||
self.send_msg(ClientGeneral::ControlEvent(
|
||||
ControlEvent::PerformTradeAction(id, action),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_dead(&self) -> bool { self.current::<comp::Health>().map_or(false, |h| h.is_dead) }
|
||||
|
||||
pub fn pick_up(&mut self, entity: EcsEntity) {
|
||||
// Get the health component from the entity
|
||||
|
||||
if let Some(uid) = self.state.read_component_copied(entity) {
|
||||
// If we're dead, exit before sending the message
|
||||
if self
|
||||
.state
|
||||
.ecs()
|
||||
.read_storage::<comp::Health>()
|
||||
.get(self.entity)
|
||||
.map_or(false, |h| h.is_dead)
|
||||
{
|
||||
if self.is_dead() {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -659,13 +680,7 @@ impl Client {
|
||||
|
||||
pub fn npc_interact(&mut self, npc_entity: EcsEntity) {
|
||||
// If we're dead, exit before sending message
|
||||
if self
|
||||
.state
|
||||
.ecs()
|
||||
.read_storage::<comp::Health>()
|
||||
.get(self.entity)
|
||||
.map_or(false, |h| h.is_dead)
|
||||
{
|
||||
if self.is_dead() {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -737,8 +752,8 @@ impl Client {
|
||||
|
||||
pub fn max_group_size(&self) -> u32 { self.max_group_size }
|
||||
|
||||
pub fn group_invite(&self) -> Option<(Uid, std::time::Instant, std::time::Duration)> {
|
||||
self.group_invite
|
||||
pub fn invite(&self) -> Option<(Uid, std::time::Instant, std::time::Duration, InviteKind)> {
|
||||
self.invite
|
||||
}
|
||||
|
||||
pub fn group_info(&self) -> Option<(String, Uid)> {
|
||||
@ -749,25 +764,27 @@ impl Client {
|
||||
|
||||
pub fn pending_invites(&self) -> &HashSet<Uid> { &self.pending_invites }
|
||||
|
||||
pub fn send_group_invite(&mut self, invitee: Uid) {
|
||||
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::GroupManip(
|
||||
GroupManip::Invite(invitee),
|
||||
pub fn pending_trade(&self) -> &Option<(TradeId, PendingTrade)> { &self.pending_trade }
|
||||
|
||||
pub fn send_invite(&mut self, invitee: Uid, kind: InviteKind) {
|
||||
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InitiateInvite(
|
||||
invitee, kind,
|
||||
)))
|
||||
}
|
||||
|
||||
pub fn accept_group_invite(&mut self) {
|
||||
pub fn accept_invite(&mut self) {
|
||||
// Clear invite
|
||||
self.group_invite.take();
|
||||
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::GroupManip(
|
||||
GroupManip::Accept,
|
||||
self.invite.take();
|
||||
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InviteResponse(
|
||||
InviteResponse::Accept,
|
||||
)));
|
||||
}
|
||||
|
||||
pub fn decline_group_invite(&mut self) {
|
||||
pub fn decline_invite(&mut self) {
|
||||
// Clear invite
|
||||
self.group_invite.take();
|
||||
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::GroupManip(
|
||||
GroupManip::Decline,
|
||||
self.invite.take();
|
||||
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InviteResponse(
|
||||
InviteResponse::Decline,
|
||||
)));
|
||||
}
|
||||
|
||||
@ -1091,12 +1108,12 @@ impl Client {
|
||||
frontend_events.append(&mut self.handle_new_messages()?);
|
||||
|
||||
// 3) Update client local data
|
||||
// Check if the group invite has timed out and remove if so
|
||||
// Check if the invite has timed out and remove if so
|
||||
if self
|
||||
.group_invite
|
||||
.map_or(false, |(_, timeout, dur)| timeout.elapsed() > dur)
|
||||
.invite
|
||||
.map_or(false, |(_, timeout, dur, _)| timeout.elapsed() > dur)
|
||||
{
|
||||
self.group_invite = None;
|
||||
self.invite = None;
|
||||
}
|
||||
|
||||
// 4) Tick the client's LocalState
|
||||
@ -1468,30 +1485,34 @@ impl Client {
|
||||
},
|
||||
}
|
||||
},
|
||||
ServerGeneral::GroupInvite { inviter, timeout } => {
|
||||
self.group_invite = Some((inviter, std::time::Instant::now(), timeout));
|
||||
ServerGeneral::Invite {
|
||||
inviter,
|
||||
timeout,
|
||||
kind,
|
||||
} => {
|
||||
self.invite = Some((inviter, std::time::Instant::now(), timeout, kind));
|
||||
},
|
||||
ServerGeneral::InvitePending(uid) => {
|
||||
if !self.pending_invites.insert(uid) {
|
||||
warn!("Received message about pending invite that was already pending");
|
||||
}
|
||||
},
|
||||
ServerGeneral::InviteComplete { target, answer } => {
|
||||
ServerGeneral::InviteComplete {
|
||||
target,
|
||||
answer,
|
||||
kind,
|
||||
} => {
|
||||
if !self.pending_invites.remove(&target) {
|
||||
warn!(
|
||||
"Received completed invite message for invite that was not in the list of \
|
||||
pending invites"
|
||||
)
|
||||
}
|
||||
// TODO: expose this as a new event variant instead of going
|
||||
// through the chat
|
||||
let msg = match answer {
|
||||
// TODO: say who accepted/declined/timed out the invite
|
||||
InviteAnswer::Accepted => "Invite accepted",
|
||||
InviteAnswer::Declined => "Invite declined",
|
||||
InviteAnswer::TimedOut => "Invite timed out",
|
||||
};
|
||||
frontend_events.push(Event::Chat(comp::ChatType::Meta.chat_msg(msg)));
|
||||
frontend_events.push(Event::InviteComplete {
|
||||
target,
|
||||
answer,
|
||||
kind,
|
||||
});
|
||||
},
|
||||
// Cleanup for when the client goes back to the `presence = None`
|
||||
ServerGeneral::ExitInGameSuccess => {
|
||||
@ -1538,6 +1559,15 @@ impl Client {
|
||||
impulse,
|
||||
});
|
||||
},
|
||||
ServerGeneral::UpdatePendingTrade(id, trade) => {
|
||||
tracing::trace!("UpdatePendingTrade {:?} {:?}", id, trade);
|
||||
self.pending_trade = Some((id, trade));
|
||||
},
|
||||
ServerGeneral::FinishedTrade(result) => {
|
||||
if let Some((_, trade)) = self.pending_trade.take() {
|
||||
frontend_events.push(Event::TradeComplete { result, trade })
|
||||
}
|
||||
},
|
||||
_ => unreachable!("Not a in_game message"),
|
||||
}
|
||||
Ok(())
|
||||
|
@ -3,11 +3,12 @@ use crate::sync;
|
||||
use authc::AuthClientError;
|
||||
use common::{
|
||||
character::{self, CharacterItem},
|
||||
comp,
|
||||
comp::{self, invite::InviteKind},
|
||||
outcome::Outcome,
|
||||
recipe::RecipeBook,
|
||||
resources::TimeOfDay,
|
||||
terrain::{Block, TerrainChunk},
|
||||
trade::{PendingTrade, TradeId, TradeResult},
|
||||
uid::Uid,
|
||||
};
|
||||
use hashbrown::HashMap;
|
||||
@ -77,9 +78,10 @@ pub enum ServerGeneral {
|
||||
//Ingame related
|
||||
GroupUpdate(comp::group::ChangeNotification<sync::Uid>),
|
||||
/// Indicate to the client that they are invited to join a group
|
||||
GroupInvite {
|
||||
Invite {
|
||||
inviter: sync::Uid,
|
||||
timeout: std::time::Duration,
|
||||
kind: InviteKind,
|
||||
},
|
||||
/// Indicate to the client that their sent invite was not invalid and is
|
||||
/// currently pending
|
||||
@ -92,6 +94,7 @@ pub enum ServerGeneral {
|
||||
InviteComplete {
|
||||
target: sync::Uid,
|
||||
answer: InviteAnswer,
|
||||
kind: InviteKind,
|
||||
},
|
||||
/// Trigger cleanup for when the client goes back to the `Registered` state
|
||||
/// from an ingame state
|
||||
@ -120,6 +123,8 @@ pub enum ServerGeneral {
|
||||
Disconnect(DisconnectReason),
|
||||
/// Send a popup notification such as "Waypoint Saved"
|
||||
Notification(Notification),
|
||||
UpdatePendingTrade(TradeId, PendingTrade),
|
||||
FinishedTrade(TradeResult),
|
||||
}
|
||||
|
||||
impl ServerGeneral {
|
||||
@ -216,7 +221,7 @@ impl ServerMsg {
|
||||
},
|
||||
//Ingame related
|
||||
ServerGeneral::GroupUpdate(_)
|
||||
| ServerGeneral::GroupInvite { .. }
|
||||
| ServerGeneral::Invite { .. }
|
||||
| ServerGeneral::InvitePending(_)
|
||||
| ServerGeneral::InviteComplete { .. }
|
||||
| ServerGeneral::ExitInGameSuccess
|
||||
@ -225,7 +230,9 @@ impl ServerMsg {
|
||||
| ServerGeneral::TerrainBlockUpdates(_)
|
||||
| ServerGeneral::SetViewDistance(_)
|
||||
| ServerGeneral::Outcomes(_)
|
||||
| ServerGeneral::Knockback(_) => {
|
||||
| ServerGeneral::Knockback(_)
|
||||
| ServerGeneral::UpdatePendingTrade(_, _)
|
||||
| ServerGeneral::FinishedTrade(_) => {
|
||||
c_type == ClientType::Game && presence.is_some()
|
||||
},
|
||||
// Always possible
|
||||
|
@ -1,8 +1,10 @@
|
||||
use crate::{
|
||||
comp::{
|
||||
inventory::slot::{EquipSlot, InvSlotId, Slot},
|
||||
invite::{InviteKind, InviteResponse},
|
||||
BuffKind,
|
||||
},
|
||||
trade::{TradeAction, TradeId},
|
||||
uid::Uid,
|
||||
util::Dir,
|
||||
};
|
||||
@ -69,9 +71,6 @@ impl From<InventoryManip> for SlotManip {
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub enum GroupManip {
|
||||
Invite(Uid),
|
||||
Accept,
|
||||
Decline,
|
||||
Leave,
|
||||
Kick(Uid),
|
||||
AssignLeader(Uid),
|
||||
@ -83,6 +82,9 @@ pub enum ControlEvent {
|
||||
EnableLantern,
|
||||
DisableLantern,
|
||||
Interact(Uid),
|
||||
InitiateInvite(Uid, InviteKind),
|
||||
InviteResponse(InviteResponse),
|
||||
PerformTradeAction(TradeId, TradeAction),
|
||||
Mount(Uid),
|
||||
Unmount,
|
||||
InventoryManip(InventoryManip),
|
||||
|
@ -28,18 +28,6 @@ impl Component for Group {
|
||||
type Storage = DerefFlaggedStorage<Self, IdvStorage<Self>>;
|
||||
}
|
||||
|
||||
pub struct Invite(pub specs::Entity);
|
||||
impl Component for Invite {
|
||||
type Storage = IdvStorage<Self>;
|
||||
}
|
||||
|
||||
// Pending invites that an entity currently has sent out
|
||||
// (invited entity, instant when invite times out)
|
||||
pub struct PendingInvites(pub Vec<(specs::Entity, std::time::Instant)>);
|
||||
impl Component for PendingInvites {
|
||||
type Storage = IdvStorage<Self>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GroupInfo {
|
||||
// TODO: what about enemy groups, either the leader will constantly change because they have to
|
||||
|
31
common/src/comp/invite.rs
Normal file
31
common/src/comp/invite.rs
Normal file
@ -0,0 +1,31 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specs::Component;
|
||||
use specs_idvs::IdvStorage;
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub enum InviteKind {
|
||||
Group,
|
||||
Trade,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub enum InviteResponse {
|
||||
Accept,
|
||||
Decline,
|
||||
}
|
||||
|
||||
pub struct Invite {
|
||||
pub inviter: specs::Entity,
|
||||
pub kind: InviteKind,
|
||||
}
|
||||
|
||||
impl Component for Invite {
|
||||
type Storage = IdvStorage<Self>;
|
||||
}
|
||||
|
||||
/// Pending invites that an entity currently has sent out
|
||||
/// (invited entity, instant when invite times out)
|
||||
pub struct PendingInvites(pub Vec<(specs::Entity, InviteKind, std::time::Instant)>);
|
||||
impl Component for PendingInvites {
|
||||
type Storage = IdvStorage<Self>;
|
||||
}
|
@ -14,6 +14,7 @@ mod health;
|
||||
pub mod home_chunk;
|
||||
mod inputs;
|
||||
pub mod inventory;
|
||||
pub mod invite;
|
||||
mod last;
|
||||
mod location;
|
||||
mod misc;
|
||||
|
@ -1,5 +1,14 @@
|
||||
use crate::{character::CharacterId, comp, rtsim::RtSimEntity, uid::Uid, util::Dir, Explosion};
|
||||
use crate::{
|
||||
character::CharacterId,
|
||||
comp,
|
||||
rtsim::RtSimEntity,
|
||||
trade::{TradeAction, TradeId},
|
||||
uid::Uid,
|
||||
util::Dir,
|
||||
Explosion,
|
||||
};
|
||||
use comp::{
|
||||
invite::{InviteKind, InviteResponse},
|
||||
item::{Item, Reagent},
|
||||
Ori, Pos,
|
||||
};
|
||||
@ -79,6 +88,9 @@ pub enum ServerEvent {
|
||||
EnableLantern(EcsEntity),
|
||||
DisableLantern(EcsEntity),
|
||||
NpcInteract(EcsEntity, EcsEntity),
|
||||
InviteResponse(EcsEntity, InviteResponse),
|
||||
InitiateInvite(EcsEntity, Uid, InviteKind),
|
||||
ProcessTradeAction(EcsEntity, TradeId, TradeAction),
|
||||
Mount(EcsEntity, EcsEntity),
|
||||
Unmount(EcsEntity),
|
||||
Possess(Uid, Uid),
|
||||
|
@ -49,6 +49,7 @@ pub mod states;
|
||||
pub mod store;
|
||||
pub mod terrain;
|
||||
pub mod time;
|
||||
pub mod trade;
|
||||
pub mod typed;
|
||||
pub mod uid;
|
||||
pub mod util;
|
||||
|
270
common/src/trade.rs
Normal file
270
common/src/trade.rs
Normal file
@ -0,0 +1,270 @@
|
||||
use crate::{
|
||||
comp::inventory::{slot::InvSlotId, Inventory},
|
||||
uid::Uid,
|
||||
};
|
||||
use hashbrown::HashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::{trace, warn};
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub enum TradePhase {
|
||||
Mutate,
|
||||
Review,
|
||||
Complete,
|
||||
}
|
||||
|
||||
/// Clients submit `TradeAction` to the server, which adds the Uid of the
|
||||
/// player out-of-band (i.e. without trusting the client to say who it's
|
||||
/// accepting on behalf of)
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub enum TradeAction {
|
||||
AddItem {
|
||||
item: InvSlotId,
|
||||
quantity: u32,
|
||||
},
|
||||
RemoveItem {
|
||||
item: InvSlotId,
|
||||
quantity: u32,
|
||||
},
|
||||
/// Accept needs the phase indicator to avoid progressing too far in the
|
||||
/// trade if there's latency and a player presses the accept button
|
||||
/// multiple times
|
||||
Accept(TradePhase),
|
||||
Decline,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub enum TradeResult {
|
||||
Completed,
|
||||
Declined,
|
||||
NotEnoughSpace,
|
||||
}
|
||||
|
||||
/// Items are not removed from the inventory during a PendingTrade: all the
|
||||
/// items are moved atomically (if there's space and both parties agree) upon
|
||||
/// completion
|
||||
///
|
||||
/// Since this stores `InvSlotId`s (i.e. references into inventories) instead of
|
||||
/// items themselves, there aren't any duplication/loss risks from things like
|
||||
/// dropped connections or declines, since the server doesn't have to move items
|
||||
/// from a trade back into a player's inventory.
|
||||
///
|
||||
/// On the flip side, since they are references to *slots*, if a player could
|
||||
/// swap items in their inventory during a trade, they could mutate the trade,
|
||||
/// enabling them to remove an item from the trade even after receiving the
|
||||
/// counterparty's phase2 accept. To prevent this, we disallow all
|
||||
/// forms of inventory manipulation in `server::events::inventory_manip` if
|
||||
/// there's a pending trade that's past phase1 (in phase1, the trade should be
|
||||
/// mutable anyway).
|
||||
///
|
||||
/// Inventory manipulation in phase1 may be beneficial to trade (e.g. splitting
|
||||
/// a stack of items, once that's implemented), but should reset both phase1
|
||||
/// accept flags to make the changes more visible.
|
||||
///
|
||||
/// Another edge case prevented by using `InvSlotId`s is that it disallows
|
||||
/// trading currently-equipped items (since `EquipSlot`s are disjoint from
|
||||
/// `InvSlotId`s), which avoids the issues associated with trading equipped bags
|
||||
/// that may still have contents.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct PendingTrade {
|
||||
/// `parties[0]` is the entity that initiated the trade, parties[1] is the
|
||||
/// other entity that's being traded with
|
||||
pub parties: [Uid; 2],
|
||||
/// `offers[i]` represents the items and quantities of the party i's items
|
||||
/// being offered
|
||||
pub offers: [HashMap<InvSlotId, u32>; 2],
|
||||
/// The current phase of the trade
|
||||
pub phase: TradePhase,
|
||||
/// `accept_flags` indicate that which parties wish to proceed to the next
|
||||
/// phase of the trade
|
||||
pub accept_flags: [bool; 2],
|
||||
}
|
||||
|
||||
impl TradePhase {
|
||||
fn next(self) -> TradePhase {
|
||||
match self {
|
||||
TradePhase::Mutate => TradePhase::Review,
|
||||
TradePhase::Review => TradePhase::Complete,
|
||||
TradePhase::Complete => TradePhase::Complete,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PendingTrade {
|
||||
pub fn new(party: Uid, counterparty: Uid) -> PendingTrade {
|
||||
PendingTrade {
|
||||
parties: [party, counterparty],
|
||||
offers: [HashMap::new(), HashMap::new()],
|
||||
phase: TradePhase::Mutate,
|
||||
accept_flags: [false, false],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn phase(&self) -> TradePhase { self.phase }
|
||||
|
||||
pub fn should_commit(&self) -> bool { matches!(self.phase, TradePhase::Complete) }
|
||||
|
||||
pub fn which_party(&self, party: Uid) -> Option<usize> {
|
||||
self.parties
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, x)| **x == party)
|
||||
.map(|(i, _)| i)
|
||||
}
|
||||
|
||||
/// Invariants:
|
||||
/// - A party is never shown as offering more of an item than they own
|
||||
/// - Offers with a quantity of zero get removed from the trade
|
||||
/// - Modifications can only happen in phase 1
|
||||
/// - Whenever a trade is modified, both accept flags get reset
|
||||
/// - Accept flags only get set for the current phase
|
||||
pub fn process_trade_action(&mut self, who: usize, action: TradeAction, inventory: &Inventory) {
|
||||
use TradeAction::*;
|
||||
match action {
|
||||
AddItem {
|
||||
item,
|
||||
quantity: delta,
|
||||
} => {
|
||||
if self.phase() == TradePhase::Mutate && delta > 0 {
|
||||
let total = self.offers[who].entry(item).or_insert(0);
|
||||
let owned_quantity = inventory.get(item).map(|i| i.amount()).unwrap_or(0);
|
||||
*total = total.saturating_add(delta).min(owned_quantity);
|
||||
self.accept_flags = [false, false];
|
||||
}
|
||||
},
|
||||
RemoveItem {
|
||||
item,
|
||||
quantity: delta,
|
||||
} => {
|
||||
if self.phase() == TradePhase::Mutate {
|
||||
self.offers[who]
|
||||
.entry(item)
|
||||
.and_replace_entry_with(|_, mut total| {
|
||||
total = total.saturating_sub(delta);
|
||||
if total > 0 { Some(total) } else { None }
|
||||
});
|
||||
self.accept_flags = [false, false];
|
||||
}
|
||||
},
|
||||
Accept(phase) => {
|
||||
if self.phase == phase {
|
||||
self.accept_flags[who] = true;
|
||||
}
|
||||
if self.accept_flags[0] && self.accept_flags[1] {
|
||||
self.phase = self.phase.next();
|
||||
self.accept_flags = [false, false];
|
||||
}
|
||||
},
|
||||
Decline => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||
pub struct TradeId(usize);
|
||||
|
||||
pub struct Trades {
|
||||
pub next_id: TradeId,
|
||||
pub trades: HashMap<TradeId, PendingTrade>,
|
||||
pub entity_trades: HashMap<Uid, TradeId>,
|
||||
}
|
||||
|
||||
impl Trades {
|
||||
pub fn begin_trade(&mut self, party: Uid, counterparty: Uid) -> TradeId {
|
||||
let id = self.next_id;
|
||||
self.next_id = TradeId(id.0.wrapping_add(1));
|
||||
self.trades
|
||||
.insert(id, PendingTrade::new(party, counterparty));
|
||||
self.entity_trades.insert(party, id);
|
||||
self.entity_trades.insert(counterparty, id);
|
||||
id
|
||||
}
|
||||
|
||||
pub fn process_trade_action(
|
||||
&mut self,
|
||||
id: TradeId,
|
||||
who: Uid,
|
||||
action: TradeAction,
|
||||
inventory: &Inventory,
|
||||
) {
|
||||
trace!("for trade id {:?}, message {:?}", id, action);
|
||||
if let Some(trade) = self.trades.get_mut(&id) {
|
||||
if let Some(party) = trade.which_party(who) {
|
||||
trade.process_trade_action(party, action, inventory);
|
||||
} else {
|
||||
warn!(
|
||||
"An entity who is not a party to trade {:?} tried to modify it",
|
||||
id
|
||||
);
|
||||
}
|
||||
} else {
|
||||
warn!("Attempt to modify nonexistent trade id {:?}", id);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decline_trade(&mut self, id: TradeId, who: Uid) -> Option<Uid> {
|
||||
let mut to_notify = None;
|
||||
if let Some(trade) = self.trades.remove(&id) {
|
||||
match trade.which_party(who) {
|
||||
Some(i) => {
|
||||
self.entity_trades.remove(&trade.parties[0]);
|
||||
self.entity_trades.remove(&trade.parties[1]);
|
||||
// let the other person know the trade was declined
|
||||
to_notify = Some(trade.parties[1 - i])
|
||||
},
|
||||
None => {
|
||||
warn!(
|
||||
"An entity who is not a party to trade {:?} tried to decline it",
|
||||
id
|
||||
);
|
||||
// put it back
|
||||
self.trades.insert(id, trade);
|
||||
},
|
||||
}
|
||||
} else {
|
||||
warn!("Attempt to decline nonexistent trade id {:?}", id);
|
||||
}
|
||||
to_notify
|
||||
}
|
||||
|
||||
/// See the doc comment on `common::trade::PendingTrade` for the
|
||||
/// significance of these checks
|
||||
pub fn in_trade_with_property<F: FnOnce(&PendingTrade) -> bool>(
|
||||
&self,
|
||||
uid: &Uid,
|
||||
f: F,
|
||||
) -> bool {
|
||||
self.entity_trades
|
||||
.get(uid)
|
||||
.and_then(|trade_id| self.trades.get(trade_id))
|
||||
.map(f)
|
||||
// if any of the option lookups failed, we're not in any trade
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn in_immutable_trade(&self, uid: &Uid) -> bool {
|
||||
self.in_trade_with_property(uid, |trade| trade.phase() != TradePhase::Mutate)
|
||||
}
|
||||
|
||||
pub fn in_mutable_trade(&self, uid: &Uid) -> bool {
|
||||
self.in_trade_with_property(uid, |trade| trade.phase() == TradePhase::Mutate)
|
||||
}
|
||||
|
||||
pub fn implicit_mutation_occurred(&mut self, uid: &Uid) {
|
||||
if let Some(trade_id) = self.entity_trades.get(uid) {
|
||||
self.trades
|
||||
.get_mut(trade_id)
|
||||
.map(|trade| trade.accept_flags = [false, false]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Trades {
|
||||
fn default() -> Trades {
|
||||
Trades {
|
||||
next_id: TradeId(0),
|
||||
trades: HashMap::new(),
|
||||
entity_trades: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
@ -98,6 +98,16 @@ impl<'a> System<'a> for Sys {
|
||||
server_emitter.emit(ServerEvent::NpcInteract(entity, npc_entity));
|
||||
}
|
||||
},
|
||||
ControlEvent::InitiateInvite(inviter_uid, kind) => {
|
||||
server_emitter.emit(ServerEvent::InitiateInvite(entity, inviter_uid, kind));
|
||||
},
|
||||
ControlEvent::InviteResponse(response) => {
|
||||
server_emitter.emit(ServerEvent::InviteResponse(entity, response));
|
||||
},
|
||||
ControlEvent::PerformTradeAction(trade_id, action) => {
|
||||
server_emitter
|
||||
.emit(ServerEvent::ProcessTradeAction(entity, trade_id, action));
|
||||
},
|
||||
ControlEvent::InventoryManip(manip) => {
|
||||
server_emitter.emit(ServerEvent::InventoryManip(entity, manip.into()));
|
||||
},
|
||||
|
@ -10,6 +10,7 @@ use common::{
|
||||
span,
|
||||
terrain::{Block, TerrainChunk, TerrainGrid},
|
||||
time::DayPeriod,
|
||||
trade::Trades,
|
||||
vol::{ReadVol, WriteVol},
|
||||
};
|
||||
use common_net::sync::WorldSyncExt;
|
||||
@ -167,8 +168,8 @@ impl State {
|
||||
ecs.register::<comp::ItemDrop>();
|
||||
ecs.register::<comp::ChatMode>();
|
||||
ecs.register::<comp::Faction>();
|
||||
ecs.register::<comp::group::Invite>();
|
||||
ecs.register::<comp::group::PendingInvites>();
|
||||
ecs.register::<comp::invite::Invite>();
|
||||
ecs.register::<comp::invite::PendingInvites>();
|
||||
ecs.register::<comp::Beam>();
|
||||
ecs.register::<comp::PreviousVelDtCache>();
|
||||
|
||||
@ -190,6 +191,7 @@ impl State {
|
||||
ecs.insert(RegionMap::new());
|
||||
ecs.insert(SysMetrics::default());
|
||||
ecs.insert(PhysicsMetrics::default());
|
||||
ecs.insert(Trades::default());
|
||||
|
||||
// Load plugins from asset directory
|
||||
#[cfg(feature = "plugins")]
|
||||
|
@ -79,7 +79,7 @@ impl Client {
|
||||
},
|
||||
//Ingame related
|
||||
ServerGeneral::GroupUpdate(_)
|
||||
| ServerGeneral::GroupInvite { .. }
|
||||
| ServerGeneral::Invite { .. }
|
||||
| ServerGeneral::InvitePending(_)
|
||||
| ServerGeneral::InviteComplete { .. }
|
||||
| ServerGeneral::ExitInGameSuccess
|
||||
@ -88,7 +88,9 @@ impl Client {
|
||||
| ServerGeneral::TerrainBlockUpdates(_)
|
||||
| ServerGeneral::SetViewDistance(_)
|
||||
| ServerGeneral::Outcomes(_)
|
||||
| ServerGeneral::Knockback(_) => {
|
||||
| ServerGeneral::Knockback(_)
|
||||
| ServerGeneral::UpdatePendingTrade(_, _)
|
||||
| ServerGeneral::FinishedTrade(_) => {
|
||||
self.in_game_stream.try_lock().unwrap().send(g)
|
||||
},
|
||||
// Always possible
|
||||
@ -157,7 +159,7 @@ impl Client {
|
||||
},
|
||||
//Ingame related
|
||||
ServerGeneral::GroupUpdate(_)
|
||||
| ServerGeneral::GroupInvite { .. }
|
||||
| ServerGeneral::Invite { .. }
|
||||
| ServerGeneral::InvitePending(_)
|
||||
| ServerGeneral::InviteComplete { .. }
|
||||
| ServerGeneral::ExitInGameSuccess
|
||||
@ -166,7 +168,11 @@ impl Client {
|
||||
| ServerGeneral::TerrainBlockUpdates(_)
|
||||
| ServerGeneral::SetViewDistance(_)
|
||||
| ServerGeneral::Outcomes(_)
|
||||
| ServerGeneral::Knockback(_) => PreparedMsg::new(2, &g, &self.in_game_stream),
|
||||
| ServerGeneral::Knockback(_)
|
||||
| ServerGeneral::UpdatePendingTrade(_, _)
|
||||
| ServerGeneral::FinishedTrade(_) => {
|
||||
PreparedMsg::new(2, &g, &self.in_game_stream)
|
||||
},
|
||||
// Always possible
|
||||
ServerGeneral::PlayerListUpdate(_)
|
||||
| ServerGeneral::ChatMsg(_)
|
||||
|
@ -13,6 +13,7 @@ use common::{
|
||||
self,
|
||||
aura::{Aura, AuraKind, AuraTarget},
|
||||
buff::{BuffCategory, BuffData, BuffKind, BuffSource},
|
||||
invite::InviteKind,
|
||||
ChatType, Inventory, Item, LightEmitter, WaypointArea,
|
||||
},
|
||||
effect::Effect,
|
||||
@ -1618,10 +1619,7 @@ fn handle_group_invite(
|
||||
.expect("Failed to get uid for player");
|
||||
|
||||
ecs.read_resource::<EventBus<ServerEvent>>()
|
||||
.emit_now(ServerEvent::GroupManip(
|
||||
client,
|
||||
comp::GroupManip::Invite(uid),
|
||||
));
|
||||
.emit_now(ServerEvent::InitiateInvite(client, uid, InviteKind::Group));
|
||||
|
||||
server.notify_client(
|
||||
client,
|
||||
|
@ -1,259 +1,90 @@
|
||||
use crate::{client::Client, Server};
|
||||
use crate::{client::Client, Server, State};
|
||||
use common::{
|
||||
comp::{
|
||||
self,
|
||||
group::{self, Group, GroupManager, Invite, PendingInvites},
|
||||
group::{self, Group, GroupManager},
|
||||
invite::{InviteKind, PendingInvites},
|
||||
ChatType, GroupManip,
|
||||
},
|
||||
uid::Uid,
|
||||
};
|
||||
use common_net::{
|
||||
msg::{InviteAnswer, ServerGeneral},
|
||||
sync::WorldSyncExt,
|
||||
use common_net::{msg::ServerGeneral, sync::WorldSyncExt};
|
||||
use specs::{
|
||||
world::{Entity, WorldExt},
|
||||
ReadStorage, WriteStorage,
|
||||
};
|
||||
use specs::world::WorldExt;
|
||||
use std::time::{Duration, Instant};
|
||||
use tracing::{error, warn};
|
||||
|
||||
/// Time before invite times out
|
||||
const INVITE_TIMEOUT_DUR: Duration = Duration::from_secs(31);
|
||||
/// Reduced duration shown to the client to help alleviate latency issues
|
||||
const PRESENTED_INVITE_TIMEOUT_DUR: Duration = Duration::from_secs(30);
|
||||
pub fn can_invite(
|
||||
state: &State,
|
||||
clients: &ReadStorage<'_, Client>,
|
||||
pending_invites: &mut WriteStorage<'_, PendingInvites>,
|
||||
max_group_size: u32,
|
||||
inviter: Entity,
|
||||
invitee: Entity,
|
||||
) -> bool {
|
||||
// Disallow inviting entity that is already in your group
|
||||
let groups = state.ecs().read_storage::<Group>();
|
||||
let group_manager = state.ecs().read_resource::<GroupManager>();
|
||||
let already_in_same_group = groups.get(inviter).map_or(false, |group| {
|
||||
group_manager
|
||||
.group_info(*group)
|
||||
.map_or(false, |g| g.leader == inviter)
|
||||
&& groups.get(invitee) == Some(group)
|
||||
});
|
||||
if already_in_same_group {
|
||||
// Inform of failure
|
||||
if let Some(client) = clients.get(inviter) {
|
||||
client.send_fallible(ServerGeneral::server_msg(
|
||||
ChatType::Meta,
|
||||
"Invite failed, can't invite someone already in your group",
|
||||
));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if group max size is already reached
|
||||
// Adding the current number of pending invites
|
||||
let group_size_limit_reached = state
|
||||
.ecs()
|
||||
.read_storage()
|
||||
.get(inviter)
|
||||
.copied()
|
||||
.and_then(|group| {
|
||||
// If entity is currently the leader of a full group then they can't invite
|
||||
// anyone else
|
||||
group_manager
|
||||
.group_info(group)
|
||||
.filter(|i| i.leader == inviter)
|
||||
.map(|i| i.num_members)
|
||||
})
|
||||
.unwrap_or(1) as usize
|
||||
+ pending_invites.get(inviter).map_or(0, |p| {
|
||||
p.0.iter()
|
||||
.filter(|(_, k, _)| *k == InviteKind::Group)
|
||||
.count()
|
||||
})
|
||||
>= max_group_size as usize;
|
||||
if group_size_limit_reached {
|
||||
// Inform inviter that they have reached the group size limit
|
||||
if let Some(client) = clients.get(inviter) {
|
||||
client.send_fallible(ServerGeneral::server_msg(
|
||||
ChatType::Meta,
|
||||
"Invite failed, pending invites plus current group size have reached the group \
|
||||
size limit"
|
||||
.to_owned(),
|
||||
));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
// TODO: turn chat messages into enums
|
||||
pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupManip) {
|
||||
let max_group_size = server.settings().max_player_group_size;
|
||||
let state = server.state_mut();
|
||||
|
||||
match manip {
|
||||
GroupManip::Invite(uid) => {
|
||||
let clients = state.ecs().read_storage::<Client>();
|
||||
let invitee = match state.ecs().entity_from_uid(uid.into()) {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
// Inform of failure
|
||||
if let Some(client) = clients.get(entity) {
|
||||
client.send_fallible(ServerGeneral::server_msg(
|
||||
ChatType::Meta,
|
||||
"Invite failed, target does not exist.",
|
||||
));
|
||||
}
|
||||
return;
|
||||
},
|
||||
};
|
||||
|
||||
let uids = state.ecs().read_storage::<Uid>();
|
||||
|
||||
// Check if entity is trying to invite themselves to a group
|
||||
if uids
|
||||
.get(entity)
|
||||
.map_or(false, |inviter_uid| *inviter_uid == uid)
|
||||
{
|
||||
warn!("Entity tried to invite themselves into a group");
|
||||
return;
|
||||
}
|
||||
|
||||
// Disallow inviting entity that is already in your group
|
||||
let groups = state.ecs().read_storage::<Group>();
|
||||
let group_manager = state.ecs().read_resource::<GroupManager>();
|
||||
let already_in_same_group = groups.get(entity).map_or(false, |group| {
|
||||
group_manager
|
||||
.group_info(*group)
|
||||
.map_or(false, |g| g.leader == entity)
|
||||
&& groups.get(invitee) == Some(group)
|
||||
});
|
||||
if already_in_same_group {
|
||||
// Inform of failure
|
||||
if let Some(client) = clients.get(entity) {
|
||||
client.send_fallible(ServerGeneral::server_msg(
|
||||
ChatType::Meta,
|
||||
"Invite failed, can't invite someone already in your group",
|
||||
));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let mut pending_invites = state.ecs().write_storage::<PendingInvites>();
|
||||
|
||||
// Check if group max size is already reached
|
||||
// Adding the current number of pending invites
|
||||
let group_size_limit_reached = state
|
||||
.ecs()
|
||||
.read_storage()
|
||||
.get(entity)
|
||||
.copied()
|
||||
.and_then(|group| {
|
||||
// If entity is currently the leader of a full group then they can't invite
|
||||
// anyone else
|
||||
group_manager
|
||||
.group_info(group)
|
||||
.filter(|i| i.leader == entity)
|
||||
.map(|i| i.num_members)
|
||||
})
|
||||
.unwrap_or(1) as usize
|
||||
+ pending_invites.get(entity).map_or(0, |p| p.0.len())
|
||||
>= max_group_size as usize;
|
||||
if group_size_limit_reached {
|
||||
// Inform inviter that they have reached the group size limit
|
||||
if let Some(client) = clients.get(entity) {
|
||||
client.send_fallible(ServerGeneral::server_msg(
|
||||
ChatType::Meta,
|
||||
"Invite failed, pending invites plus current group size have reached the \
|
||||
group size limit"
|
||||
.to_owned(),
|
||||
));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let agents = state.ecs().read_storage::<comp::Agent>();
|
||||
let mut invites = state.ecs().write_storage::<Invite>();
|
||||
|
||||
if invites.contains(invitee) {
|
||||
// Inform inviter that there is already an invite
|
||||
if let Some(client) = clients.get(entity) {
|
||||
client.send_fallible(ServerGeneral::server_msg(
|
||||
ChatType::Meta,
|
||||
"This player already has a pending invite.",
|
||||
));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let mut invite_sent = false;
|
||||
// Returns true if insertion was succesful
|
||||
let mut send_invite = || {
|
||||
match invites.insert(invitee, group::Invite(entity)) {
|
||||
Err(err) => {
|
||||
error!("Failed to insert Invite component: {:?}", err);
|
||||
false
|
||||
},
|
||||
Ok(_) => {
|
||||
match pending_invites.entry(entity) {
|
||||
Ok(entry) => {
|
||||
entry
|
||||
.or_insert_with(|| PendingInvites(Vec::new()))
|
||||
.0
|
||||
.push((invitee, Instant::now() + INVITE_TIMEOUT_DUR));
|
||||
invite_sent = true;
|
||||
true
|
||||
},
|
||||
Err(err) => {
|
||||
error!(
|
||||
"Failed to get entry for pending invites component: {:?}",
|
||||
err
|
||||
);
|
||||
// Cleanup
|
||||
invites.remove(invitee);
|
||||
false
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
// If client comp
|
||||
if let (Some(client), Some(inviter)) = (clients.get(invitee), uids.get(entity).copied())
|
||||
{
|
||||
if send_invite() {
|
||||
client.send_fallible(ServerGeneral::GroupInvite {
|
||||
inviter,
|
||||
timeout: PRESENTED_INVITE_TIMEOUT_DUR,
|
||||
});
|
||||
}
|
||||
} else if agents.contains(invitee) {
|
||||
send_invite();
|
||||
} else if let Some(client) = clients.get(entity) {
|
||||
client.send_fallible(ServerGeneral::server_msg(
|
||||
ChatType::Meta,
|
||||
"Can't invite, not a player or npc",
|
||||
));
|
||||
}
|
||||
|
||||
// Notify inviter that the invite is pending
|
||||
if invite_sent {
|
||||
if let Some(client) = clients.get(entity) {
|
||||
client.send_fallible(ServerGeneral::InvitePending(uid));
|
||||
}
|
||||
}
|
||||
},
|
||||
GroupManip::Accept => {
|
||||
let clients = state.ecs().read_storage::<Client>();
|
||||
let uids = state.ecs().read_storage::<Uid>();
|
||||
let mut invites = state.ecs().write_storage::<Invite>();
|
||||
if let Some(inviter) = invites.remove(entity).and_then(|invite| {
|
||||
let inviter = invite.0;
|
||||
let mut pending_invites = state.ecs().write_storage::<PendingInvites>();
|
||||
let pending = &mut pending_invites.get_mut(inviter)?.0;
|
||||
// Check that inviter has a pending invite and remove it from the list
|
||||
let invite_index = pending.iter().position(|p| p.0 == entity)?;
|
||||
pending.swap_remove(invite_index);
|
||||
// If no pending invites remain remove the component
|
||||
if pending.is_empty() {
|
||||
pending_invites.remove(inviter);
|
||||
}
|
||||
|
||||
Some(inviter)
|
||||
}) {
|
||||
if let (Some(client), Some(target)) =
|
||||
(clients.get(inviter), uids.get(entity).copied())
|
||||
{
|
||||
client.send_fallible(ServerGeneral::InviteComplete {
|
||||
target,
|
||||
answer: InviteAnswer::Accepted,
|
||||
});
|
||||
}
|
||||
let mut group_manager = state.ecs().write_resource::<GroupManager>();
|
||||
group_manager.add_group_member(
|
||||
inviter,
|
||||
entity,
|
||||
&state.ecs().entities(),
|
||||
&mut state.ecs().write_storage(),
|
||||
&state.ecs().read_storage(),
|
||||
&uids,
|
||||
|entity, group_change| {
|
||||
clients
|
||||
.get(entity)
|
||||
.and_then(|c| {
|
||||
group_change
|
||||
.try_map(|e| uids.get(e).copied())
|
||||
.map(|g| (g, c))
|
||||
})
|
||||
.map(|(g, c)| c.send(ServerGeneral::GroupUpdate(g)));
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
GroupManip::Decline => {
|
||||
let clients = state.ecs().read_storage::<Client>();
|
||||
let uids = state.ecs().read_storage::<Uid>();
|
||||
let mut invites = state.ecs().write_storage::<Invite>();
|
||||
if let Some(inviter) = invites.remove(entity).and_then(|invite| {
|
||||
let inviter = invite.0;
|
||||
let mut pending_invites = state.ecs().write_storage::<PendingInvites>();
|
||||
let pending = &mut pending_invites.get_mut(inviter)?.0;
|
||||
// Check that inviter has a pending invite and remove it from the list
|
||||
let invite_index = pending.iter().position(|p| p.0 == entity)?;
|
||||
pending.swap_remove(invite_index);
|
||||
// If no pending invites remain remove the component
|
||||
if pending.is_empty() {
|
||||
pending_invites.remove(inviter);
|
||||
}
|
||||
|
||||
Some(inviter)
|
||||
}) {
|
||||
// Inform inviter of rejection
|
||||
if let (Some(client), Some(target)) =
|
||||
(clients.get(inviter), uids.get(entity).copied())
|
||||
{
|
||||
client.send_fallible(ServerGeneral::InviteComplete {
|
||||
target,
|
||||
answer: InviteAnswer::Declined,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
GroupManip::Leave => {
|
||||
let state = server.state_mut();
|
||||
let clients = state.ecs().read_storage::<Client>();
|
||||
let uids = state.ecs().read_storage::<Uid>();
|
||||
let mut group_manager = state.ecs().write_resource::<GroupManager>();
|
||||
@ -276,6 +107,7 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani
|
||||
);
|
||||
},
|
||||
GroupManip::Kick(uid) => {
|
||||
let state = server.state_mut();
|
||||
let clients = state.ecs().read_storage::<Client>();
|
||||
let uids = state.ecs().read_storage::<Uid>();
|
||||
let alignments = state.ecs().read_storage::<comp::Alignment>();
|
||||
@ -379,6 +211,7 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani
|
||||
}
|
||||
},
|
||||
GroupManip::AssignLeader(uid) => {
|
||||
let state = server.state_mut();
|
||||
let clients = state.ecs().read_storage::<Client>();
|
||||
let uids = state.ecs().read_storage::<Uid>();
|
||||
let target = match state.ecs().entity_from_uid(uid.into()) {
|
||||
|
@ -10,6 +10,7 @@ use common::{
|
||||
},
|
||||
consts::MAX_PICKUP_RANGE,
|
||||
recipe::default_recipe_book,
|
||||
trade::Trades,
|
||||
uid::Uid,
|
||||
util::find_dist::{self, FindDist},
|
||||
vol::ReadVol,
|
||||
@ -39,6 +40,20 @@ pub fn snuff_lantern(storage: &mut WriteStorage<comp::LightEmitter>, entity: Ecs
|
||||
#[allow(clippy::same_item_push)] // TODO: Pending review in #587
|
||||
pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::SlotManip) {
|
||||
let state = server.state_mut();
|
||||
|
||||
let uid = state
|
||||
.ecs()
|
||||
.uid_from_entity(entity)
|
||||
.expect("Couldn't get uid for entity");
|
||||
|
||||
{
|
||||
let trades = state.ecs().read_resource::<Trades>();
|
||||
if trades.in_immutable_trade(&uid) {
|
||||
// manipulating the inventory can mutate the trade
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let mut dropped_items = Vec::new();
|
||||
let mut thrown_items = Vec::new();
|
||||
|
||||
@ -568,6 +583,12 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Slo
|
||||
|
||||
new_entity.build();
|
||||
}
|
||||
|
||||
let mut trades = state.ecs().write_resource::<Trades>();
|
||||
if trades.in_mutable_trade(&uid) {
|
||||
// manipulating the inventory mutated the trade, so reset the accept flags
|
||||
trades.implicit_mutation_occurred(&uid);
|
||||
}
|
||||
}
|
||||
|
||||
fn within_pickup_range<S: FindDist<find_dist::Cylinder>>(
|
||||
|
253
server/src/events/invite.rs
Normal file
253
server/src/events/invite.rs
Normal file
@ -0,0 +1,253 @@
|
||||
use super::group_manip;
|
||||
use crate::{client::Client, Server};
|
||||
use common::{
|
||||
comp::{
|
||||
self,
|
||||
group::GroupManager,
|
||||
invite::{Invite, InviteKind, InviteResponse, PendingInvites},
|
||||
ChatType,
|
||||
},
|
||||
trade::Trades,
|
||||
uid::Uid,
|
||||
};
|
||||
use common_net::{
|
||||
msg::{InviteAnswer, ServerGeneral},
|
||||
sync::WorldSyncExt,
|
||||
};
|
||||
use specs::world::WorldExt;
|
||||
use std::time::{Duration, Instant};
|
||||
use tracing::{error, warn};
|
||||
|
||||
/// Time before invite times out
|
||||
const INVITE_TIMEOUT_DUR: Duration = Duration::from_secs(31);
|
||||
/// Reduced duration shown to the client to help alleviate latency issues
|
||||
const PRESENTED_INVITE_TIMEOUT_DUR: Duration = Duration::from_secs(30);
|
||||
|
||||
pub fn handle_invite(
|
||||
server: &mut Server,
|
||||
inviter: specs::Entity,
|
||||
invitee_uid: Uid,
|
||||
kind: InviteKind,
|
||||
) {
|
||||
let max_group_size = server.settings().max_player_group_size;
|
||||
let state = server.state_mut();
|
||||
let clients = state.ecs().read_storage::<Client>();
|
||||
let invitee = match state.ecs().entity_from_uid(invitee_uid.into()) {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
// Inform of failure
|
||||
if let Some(client) = clients.get(inviter) {
|
||||
client.send_fallible(ServerGeneral::server_msg(
|
||||
ChatType::Meta,
|
||||
"Invite failed, target does not exist.",
|
||||
));
|
||||
}
|
||||
return;
|
||||
},
|
||||
};
|
||||
|
||||
let uids = state.ecs().read_storage::<Uid>();
|
||||
|
||||
// Check if entity is trying to invite themselves
|
||||
if uids
|
||||
.get(inviter)
|
||||
.map_or(false, |inviter_uid| *inviter_uid == invitee_uid)
|
||||
{
|
||||
warn!("Entity tried to invite themselves into a group/trade");
|
||||
return;
|
||||
}
|
||||
|
||||
let mut pending_invites = state.ecs().write_storage::<PendingInvites>();
|
||||
|
||||
if let InviteKind::Group = kind {
|
||||
if !group_manip::can_invite(
|
||||
state,
|
||||
&clients,
|
||||
&mut pending_invites,
|
||||
max_group_size,
|
||||
inviter,
|
||||
invitee,
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let agents = state.ecs().read_storage::<comp::Agent>();
|
||||
let mut invites = state.ecs().write_storage::<Invite>();
|
||||
|
||||
if invites.contains(invitee) {
|
||||
// Inform inviter that there is already an invite
|
||||
if let Some(client) = clients.get(inviter) {
|
||||
client.send_fallible(ServerGeneral::server_msg(
|
||||
ChatType::Meta,
|
||||
"This player already has a pending invite.",
|
||||
));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let mut invite_sent = false;
|
||||
// Returns true if insertion was succesful
|
||||
let mut send_invite = || {
|
||||
match invites.insert(invitee, Invite { inviter, kind }) {
|
||||
Err(err) => {
|
||||
error!("Failed to insert Invite component: {:?}", err);
|
||||
false
|
||||
},
|
||||
Ok(_) => {
|
||||
match pending_invites.entry(inviter) {
|
||||
Ok(entry) => {
|
||||
entry.or_insert_with(|| PendingInvites(Vec::new())).0.push((
|
||||
invitee,
|
||||
kind,
|
||||
Instant::now() + INVITE_TIMEOUT_DUR,
|
||||
));
|
||||
invite_sent = true;
|
||||
true
|
||||
},
|
||||
Err(err) => {
|
||||
error!(
|
||||
"Failed to get entry for pending invites component: {:?}",
|
||||
err
|
||||
);
|
||||
// Cleanup
|
||||
invites.remove(invitee);
|
||||
false
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
// If client comp
|
||||
if let (Some(client), Some(inviter)) = (clients.get(invitee), uids.get(inviter).copied()) {
|
||||
if send_invite() {
|
||||
client.send_fallible(ServerGeneral::Invite {
|
||||
inviter,
|
||||
timeout: PRESENTED_INVITE_TIMEOUT_DUR,
|
||||
kind,
|
||||
});
|
||||
}
|
||||
} else if agents.contains(invitee) {
|
||||
send_invite();
|
||||
} else if let Some(client) = clients.get(inviter) {
|
||||
client.send_fallible(ServerGeneral::server_msg(
|
||||
ChatType::Meta,
|
||||
"Can't invite, not a player or npc",
|
||||
));
|
||||
}
|
||||
|
||||
// Notify inviter that the invite is pending
|
||||
if invite_sent {
|
||||
if let Some(client) = clients.get(inviter) {
|
||||
client.send_fallible(ServerGeneral::InvitePending(invitee_uid));
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn handle_invite_response(
|
||||
server: &mut Server,
|
||||
entity: specs::Entity,
|
||||
response: InviteResponse,
|
||||
) {
|
||||
match response {
|
||||
InviteResponse::Accept => handle_invite_accept(server, entity),
|
||||
InviteResponse::Decline => handle_invite_decline(server, entity),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_invite_accept(server: &mut Server, entity: specs::Entity) {
|
||||
let state = server.state_mut();
|
||||
let clients = state.ecs().read_storage::<Client>();
|
||||
let uids = state.ecs().read_storage::<Uid>();
|
||||
let mut invites = state.ecs().write_storage::<Invite>();
|
||||
if let Some((inviter, kind)) = invites.remove(entity).and_then(|invite| {
|
||||
let Invite { inviter, kind } = invite;
|
||||
let mut pending_invites = state.ecs().write_storage::<PendingInvites>();
|
||||
let pending = &mut pending_invites.get_mut(inviter)?.0;
|
||||
// Check that inviter has a pending invite and remove it from the list
|
||||
let invite_index = pending.iter().position(|p| p.0 == entity)?;
|
||||
pending.swap_remove(invite_index);
|
||||
// If no pending invites remain remove the component
|
||||
if pending.is_empty() {
|
||||
pending_invites.remove(inviter);
|
||||
}
|
||||
|
||||
Some((inviter, kind))
|
||||
}) {
|
||||
if let (Some(client), Some(target)) = (clients.get(inviter), uids.get(entity).copied()) {
|
||||
client.send_fallible(ServerGeneral::InviteComplete {
|
||||
target,
|
||||
answer: InviteAnswer::Accepted,
|
||||
kind,
|
||||
});
|
||||
}
|
||||
match kind {
|
||||
InviteKind::Group => {
|
||||
let mut group_manager = state.ecs().write_resource::<GroupManager>();
|
||||
group_manager.add_group_member(
|
||||
inviter,
|
||||
entity,
|
||||
&state.ecs().entities(),
|
||||
&mut state.ecs().write_storage(),
|
||||
&state.ecs().read_storage(),
|
||||
&uids,
|
||||
|entity, group_change| {
|
||||
clients
|
||||
.get(entity)
|
||||
.and_then(|c| {
|
||||
group_change
|
||||
.try_map(|e| uids.get(e).copied())
|
||||
.map(|g| (g, c))
|
||||
})
|
||||
.map(|(g, c)| c.send(ServerGeneral::GroupUpdate(g)));
|
||||
},
|
||||
);
|
||||
},
|
||||
InviteKind::Trade => {
|
||||
if let (Some(inviter_uid), Some(invitee_uid)) =
|
||||
(uids.get(inviter).copied(), uids.get(entity).copied())
|
||||
{
|
||||
let mut trades = state.ecs().write_resource::<Trades>();
|
||||
let id = trades.begin_trade(inviter_uid, invitee_uid);
|
||||
let trade = trades.trades[&id].clone();
|
||||
clients
|
||||
.get(inviter)
|
||||
.map(|c| c.send(ServerGeneral::UpdatePendingTrade(id, trade.clone())));
|
||||
clients
|
||||
.get(entity)
|
||||
.map(|c| c.send(ServerGeneral::UpdatePendingTrade(id, trade)));
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_invite_decline(server: &mut Server, entity: specs::Entity) {
|
||||
let state = server.state_mut();
|
||||
let clients = state.ecs().read_storage::<Client>();
|
||||
let uids = state.ecs().read_storage::<Uid>();
|
||||
let mut invites = state.ecs().write_storage::<Invite>();
|
||||
if let Some((inviter, kind)) = invites.remove(entity).and_then(|invite| {
|
||||
let Invite { inviter, kind } = invite;
|
||||
let mut pending_invites = state.ecs().write_storage::<PendingInvites>();
|
||||
let pending = &mut pending_invites.get_mut(inviter)?.0;
|
||||
// Check that inviter has a pending invite and remove it from the list
|
||||
let invite_index = pending.iter().position(|p| p.0 == entity)?;
|
||||
pending.swap_remove(invite_index);
|
||||
// If no pending invites remain remove the component
|
||||
if pending.is_empty() {
|
||||
pending_invites.remove(inviter);
|
||||
}
|
||||
|
||||
Some((inviter, kind))
|
||||
}) {
|
||||
// Inform inviter of rejection
|
||||
if let (Some(client), Some(target)) = (clients.get(inviter), uids.get(entity).copied()) {
|
||||
client.send_fallible(ServerGeneral::InviteComplete {
|
||||
target,
|
||||
answer: InviteAnswer::Declined,
|
||||
kind,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -16,15 +16,19 @@ use interaction::{
|
||||
handle_lantern, handle_mount, handle_npc_interaction, handle_possess, handle_unmount,
|
||||
};
|
||||
use inventory_manip::handle_inventory;
|
||||
use invite::{handle_invite, handle_invite_response};
|
||||
use player::{handle_client_disconnect, handle_exit_ingame};
|
||||
use specs::{Entity as EcsEntity, WorldExt};
|
||||
use trade::handle_process_trade_action;
|
||||
|
||||
mod entity_creation;
|
||||
mod entity_manipulation;
|
||||
mod group_manip;
|
||||
mod interaction;
|
||||
mod inventory_manip;
|
||||
mod invite;
|
||||
mod player;
|
||||
mod trade;
|
||||
|
||||
pub enum Event {
|
||||
ClientConnected {
|
||||
@ -103,6 +107,16 @@ impl Server {
|
||||
ServerEvent::NpcInteract(interactor, target) => {
|
||||
handle_npc_interaction(self, interactor, target)
|
||||
},
|
||||
ServerEvent::InitiateInvite(interactor, target, kind) => {
|
||||
handle_invite(self, interactor, target, kind)
|
||||
//handle_initiate_trade(self, interactor, target)
|
||||
},
|
||||
ServerEvent::InviteResponse(entity, response) => {
|
||||
handle_invite_response(self, entity, response)
|
||||
},
|
||||
ServerEvent::ProcessTradeAction(entity, trade_id, action) => {
|
||||
handle_process_trade_action(self, entity, trade_id, action);
|
||||
},
|
||||
ServerEvent::Mount(mounter, mountee) => handle_mount(self, mounter, mountee),
|
||||
ServerEvent::Unmount(mounter) => handle_unmount(self, mounter),
|
||||
ServerEvent::Possess(possessor_uid, possesse_uid) => {
|
||||
|
146
server/src/events/trade.rs
Normal file
146
server/src/events/trade.rs
Normal file
@ -0,0 +1,146 @@
|
||||
use crate::Server;
|
||||
use common::{
|
||||
comp::inventory::Inventory,
|
||||
trade::{PendingTrade, TradeAction, TradeId, TradeResult, Trades},
|
||||
};
|
||||
use common_net::{msg::ServerGeneral, sync::WorldSyncExt};
|
||||
use hashbrown::hash_map::Entry;
|
||||
use specs::{world::WorldExt, Entity as EcsEntity};
|
||||
use std::cmp::Ordering;
|
||||
use tracing::{error, trace};
|
||||
|
||||
/// Invoked when the trade UI is up, handling item changes, accepts, etc
|
||||
pub fn handle_process_trade_action(
|
||||
server: &mut Server,
|
||||
entity: EcsEntity,
|
||||
trade_id: TradeId,
|
||||
action: TradeAction,
|
||||
) {
|
||||
if let Some(uid) = server.state.ecs().uid_from_entity(entity) {
|
||||
let mut trades = server.state.ecs().write_resource::<Trades>();
|
||||
if let TradeAction::Decline = action {
|
||||
let to_notify = trades.decline_trade(trade_id, uid);
|
||||
to_notify
|
||||
.and_then(|u| server.state.ecs().entity_from_uid(u.0))
|
||||
.map(|e| {
|
||||
server.notify_client(e, ServerGeneral::FinishedTrade(TradeResult::Declined))
|
||||
});
|
||||
} else {
|
||||
if let Some(inv) = server.state.ecs().read_component::<Inventory>().get(entity) {
|
||||
trades.process_trade_action(trade_id, uid, action, inv);
|
||||
}
|
||||
if let Entry::Occupied(entry) = trades.trades.entry(trade_id) {
|
||||
let parties = entry.get().parties;
|
||||
let msg = if entry.get().should_commit() {
|
||||
let result = commit_trade(server.state.ecs(), entry.get());
|
||||
entry.remove();
|
||||
ServerGeneral::FinishedTrade(result)
|
||||
} else {
|
||||
ServerGeneral::UpdatePendingTrade(trade_id, entry.get().clone())
|
||||
};
|
||||
// send the updated state to both parties
|
||||
for party in parties.iter() {
|
||||
server
|
||||
.state
|
||||
.ecs()
|
||||
.entity_from_uid(party.0)
|
||||
.map(|e| server.notify_client(e, msg.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Commit a trade that both parties have agreed to, modifying their respective
|
||||
/// inventories
|
||||
fn commit_trade(ecs: &specs::World, trade: &PendingTrade) -> TradeResult {
|
||||
let mut entities = Vec::new();
|
||||
for party in trade.parties.iter() {
|
||||
match ecs.entity_from_uid(party.0) {
|
||||
Some(entity) => entities.push(entity),
|
||||
None => return TradeResult::Declined,
|
||||
}
|
||||
}
|
||||
let mut inventories = ecs.write_component::<Inventory>();
|
||||
for entity in entities.iter() {
|
||||
if inventories.get_mut(*entity).is_none() {
|
||||
return TradeResult::Declined;
|
||||
}
|
||||
}
|
||||
let invmsg = "inventories.get_mut(entities[who]).is_none() should have returned already";
|
||||
trace!("committing trade: {:?}", trade);
|
||||
// Compute the net change in slots of each player during the trade, to detect
|
||||
// out-of-space-ness before transferring any items
|
||||
let mut delta_slots: [isize; 2] = [0, 0];
|
||||
for who in [0, 1].iter().cloned() {
|
||||
for (slot, quantity) in trade.offers[who].iter() {
|
||||
let inventory = inventories.get_mut(entities[who]).expect(invmsg);
|
||||
let item = match inventory.get(*slot) {
|
||||
Some(item) => item,
|
||||
None => {
|
||||
error!(
|
||||
"PendingTrade invariant violation in trade {:?}: slots offered in a trade \
|
||||
should be non-empty",
|
||||
trade
|
||||
);
|
||||
return TradeResult::Declined;
|
||||
},
|
||||
};
|
||||
match item.amount().cmp(&quantity) {
|
||||
Ordering::Less => {
|
||||
error!(
|
||||
"PendingTrade invariant violation in trade {:?}: party {} offered more of \
|
||||
an item than they have",
|
||||
trade, who
|
||||
);
|
||||
return TradeResult::Declined;
|
||||
},
|
||||
Ordering::Equal => {
|
||||
delta_slots[who] -= 1; // exact, removes the whole stack
|
||||
delta_slots[1 - who] += 1; // overapproximation, assumes the stack won't merge
|
||||
},
|
||||
Ordering::Greater => {
|
||||
// No change to delta_slots[who], since they have leftovers
|
||||
delta_slots[1 - who] += 1; // overapproximation, assumes the stack won't merge
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
trace!("delta_slots: {:?}", delta_slots);
|
||||
for who in [0, 1].iter().cloned() {
|
||||
// Inventories should never exceed 2^{63} slots, so the usize -> isize
|
||||
// conversions here should be safe
|
||||
let inv = inventories.get_mut(entities[who]).expect(invmsg);
|
||||
if inv.populated_slots() as isize + delta_slots[who] > inv.capacity() as isize {
|
||||
return TradeResult::NotEnoughSpace;
|
||||
}
|
||||
}
|
||||
let mut items = [Vec::new(), Vec::new()];
|
||||
for who in [0, 1].iter().cloned() {
|
||||
for (slot, quantity) in trade.offers[who].iter() {
|
||||
// Take the items one by one, to benefit from Inventory's stack handling
|
||||
for _ in 0..*quantity {
|
||||
inventories
|
||||
.get_mut(entities[who])
|
||||
.expect(invmsg)
|
||||
.take(*slot)
|
||||
.map(|item| items[who].push(item));
|
||||
}
|
||||
}
|
||||
}
|
||||
for who in [0, 1].iter().cloned() {
|
||||
if let Err(leftovers) = inventories
|
||||
.get_mut(entities[1 - who])
|
||||
.expect(invmsg)
|
||||
.push_all(items[who].drain(..))
|
||||
{
|
||||
// This should only happen if the arithmetic above for delta_slots says there's
|
||||
// enough space and there isn't (i.e. underapproximates)
|
||||
panic!(
|
||||
"Not enough space for all the items, leftovers are {:?}",
|
||||
leftovers
|
||||
);
|
||||
}
|
||||
}
|
||||
TradeResult::Completed
|
||||
}
|
@ -4,16 +4,16 @@ use common::{
|
||||
self,
|
||||
agent::{Activity, AgentEvent, Tactic, DEFAULT_INTERACTION_TIME},
|
||||
group,
|
||||
group::Invite,
|
||||
inventory::slot::EquipSlot,
|
||||
invite::{Invite, InviteResponse},
|
||||
item::{
|
||||
tool::{ToolKind, UniqueKind},
|
||||
ItemKind,
|
||||
},
|
||||
skills::{AxeSkill, BowSkill, HammerSkill, Skill, StaffSkill, SwordSkill},
|
||||
Agent, Alignment, Body, CharacterState, ControlAction, ControlEvent, Controller, Energy,
|
||||
GroupManip, Health, Inventory, LightEmitter, MountState, Ori, PhysicsState, Pos, Scale,
|
||||
Stats, UnresolvedChatMsg, Vel,
|
||||
Health, Inventory, LightEmitter, MountState, Ori, PhysicsState, Pos, Scale, Stats,
|
||||
UnresolvedChatMsg, Vel,
|
||||
},
|
||||
event::{EventBus, ServerEvent},
|
||||
metrics::SysMetrics,
|
||||
@ -1577,7 +1577,7 @@ impl<'a> System<'a> for Sys {
|
||||
debug_assert!(inputs.look_dir.map(|e| !e.is_nan()).reduce_and());
|
||||
});
|
||||
|
||||
// Process group invites
|
||||
// Process invites
|
||||
for (_invite, /*alignment,*/ agent, controller) in
|
||||
(&invites, /*&alignments,*/ &mut agents, &mut controllers).join()
|
||||
{
|
||||
@ -1587,11 +1587,11 @@ impl<'a> System<'a> for Sys {
|
||||
*agent = Agent::default();
|
||||
controller
|
||||
.events
|
||||
.push(ControlEvent::GroupManip(GroupManip::Accept));
|
||||
.push(ControlEvent::InviteResponse(InviteResponse::Accept));
|
||||
} else {
|
||||
controller
|
||||
.events
|
||||
.push(ControlEvent::GroupManip(GroupManip::Decline));
|
||||
.push(ControlEvent::InviteResponse(InviteResponse::Decline));
|
||||
}
|
||||
}
|
||||
sys_metrics.agent_ns.store(
|
||||
|
@ -1,14 +1,14 @@
|
||||
use super::SysTimer;
|
||||
use crate::client::Client;
|
||||
use common::{
|
||||
comp::group::{Invite, PendingInvites},
|
||||
comp::invite::{Invite, PendingInvites},
|
||||
span,
|
||||
uid::Uid,
|
||||
};
|
||||
use common_net::msg::{InviteAnswer, ServerGeneral};
|
||||
use specs::{Entities, Join, ReadStorage, System, Write, WriteStorage};
|
||||
|
||||
/// This system removes timed out group invites
|
||||
/// This system removes timed out invites
|
||||
pub struct Sys;
|
||||
impl<'a> System<'a> for Sys {
|
||||
#[allow(clippy::type_complexity)] // TODO: Pending review in #587
|
||||
@ -32,13 +32,13 @@ impl<'a> System<'a> for Sys {
|
||||
|
||||
let timed_out_invites = (&entities, &invites)
|
||||
.join()
|
||||
.filter_map(|(invitee, Invite(inviter))| {
|
||||
.filter_map(|(invitee, Invite { inviter, kind })| {
|
||||
// Retrieve timeout invite from pending invites
|
||||
let pending = &mut pending_invites.get_mut(*inviter)?.0;
|
||||
let index = pending.iter().position(|p| p.0 == invitee)?;
|
||||
|
||||
// Stop if not timed out
|
||||
if pending[index].1 > now {
|
||||
if pending[index].2 > now {
|
||||
return None;
|
||||
}
|
||||
|
||||
@ -57,6 +57,7 @@ impl<'a> System<'a> for Sys {
|
||||
client.send_fallible(ServerGeneral::InviteComplete {
|
||||
target,
|
||||
answer: InviteAnswer::TimedOut,
|
||||
kind: *kind,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,7 @@ use crate::{
|
||||
use client::{self, Client};
|
||||
use common::{
|
||||
combat,
|
||||
comp::{group::Role, BuffKind, Stats},
|
||||
comp::{group::Role, invite::InviteKind, BuffKind, Stats},
|
||||
uid::{Uid, UidAllocator},
|
||||
};
|
||||
use common_net::sync::WorldSyncExt;
|
||||
@ -205,7 +205,7 @@ impl<'a> Widget for Group<'a> {
|
||||
.unwrap_or_else(|| format!("Npc<{}>", uid)),
|
||||
};
|
||||
|
||||
let open_invite = self.client.group_invite();
|
||||
let open_invite = self.client.invite();
|
||||
|
||||
let my_uid = self.client.uid();
|
||||
|
||||
@ -219,7 +219,7 @@ impl<'a> Widget for Group<'a> {
|
||||
.crop_kids()
|
||||
.set(state.ids.bg, ui);
|
||||
}
|
||||
if let Some((_, timeout_start, timeout_dur)) = open_invite {
|
||||
if let Some((_, timeout_start, timeout_dur, _)) = open_invite {
|
||||
// Group Menu button
|
||||
Button::image(self.imgs.group_icon)
|
||||
.w_h(49.0, 26.0)
|
||||
@ -792,16 +792,22 @@ impl<'a> Widget for Group<'a> {
|
||||
// into the maximum group size.
|
||||
}
|
||||
}
|
||||
if let Some((invite_uid, _, _)) = open_invite {
|
||||
if let Some((invite_uid, _, _, kind)) = open_invite {
|
||||
self.show.group = true; // Auto open group menu
|
||||
// TODO: add group name here too
|
||||
// Invite text
|
||||
|
||||
let name = uid_to_name_text(invite_uid, &self.client);
|
||||
let invite_text = self
|
||||
.localized_strings
|
||||
.get("hud.group.invite_to_join")
|
||||
.replace("{name}", &name);
|
||||
let invite_text = match kind {
|
||||
InviteKind::Group => self
|
||||
.localized_strings
|
||||
.get("hud.group.invite_to_join")
|
||||
.replace("{name}", &name),
|
||||
InviteKind::Trade => self
|
||||
.localized_strings
|
||||
.get("hud.group.invite_to_trade")
|
||||
.replace("{name}", &name),
|
||||
};
|
||||
Text::new(&invite_text)
|
||||
.mid_top_with_margin_on(state.ids.bg, 5.0)
|
||||
.font_size(12)
|
||||
|
@ -19,6 +19,7 @@ mod settings_window;
|
||||
mod skillbar;
|
||||
mod slots;
|
||||
mod social;
|
||||
mod trade;
|
||||
mod util;
|
||||
|
||||
pub use hotbar::{SlotContents as HotbarSlotContents, State as HotbarState};
|
||||
@ -44,6 +45,7 @@ use serde::{Deserialize, Serialize};
|
||||
use settings_window::{SettingsTab, SettingsWindow};
|
||||
use skillbar::Skillbar;
|
||||
use social::{Social, SocialTab};
|
||||
use trade::Trade;
|
||||
|
||||
use crate::{
|
||||
ecs::{comp as vcomp, comp::HpFloaterList},
|
||||
@ -52,7 +54,9 @@ use crate::{
|
||||
render::{Consts, Globals, RenderMode, Renderer},
|
||||
scene::camera::{self, Camera},
|
||||
settings::Fps,
|
||||
ui::{fonts::Fonts, img_ids::Rotations, slot, Graphic, Ingameable, ScaleMode, Ui},
|
||||
ui::{
|
||||
fonts::Fonts, img_ids::Rotations, slot, slot::SlotKey, Graphic, Ingameable, ScaleMode, Ui,
|
||||
},
|
||||
window::{Event as WinEvent, FullScreenSettings, GameInput},
|
||||
GlobalState,
|
||||
};
|
||||
@ -69,6 +73,7 @@ use common::{
|
||||
outcome::Outcome,
|
||||
span,
|
||||
terrain::TerrainChunk,
|
||||
trade::TradeAction,
|
||||
uid::Uid,
|
||||
util::srgba_to_linear,
|
||||
vol::RectRasterableVol,
|
||||
@ -257,6 +262,7 @@ widget_ids! {
|
||||
minimap,
|
||||
prompt_dialog,
|
||||
bag,
|
||||
trade,
|
||||
social,
|
||||
quest,
|
||||
diary,
|
||||
@ -393,6 +399,7 @@ pub enum Event {
|
||||
},
|
||||
DropSlot(comp::slot::Slot),
|
||||
ChangeHotbarState(Box<HotbarState>),
|
||||
TradeAction(TradeAction),
|
||||
Ability3(bool),
|
||||
Logout,
|
||||
Quit,
|
||||
@ -480,6 +487,7 @@ pub struct Show {
|
||||
debug: bool,
|
||||
bag: bool,
|
||||
bag_inv: bool,
|
||||
trade: bool,
|
||||
social: bool,
|
||||
diary: bool,
|
||||
group: bool,
|
||||
@ -509,6 +517,17 @@ impl Show {
|
||||
|
||||
fn toggle_bag(&mut self) { self.bag(!self.bag); }
|
||||
|
||||
fn trade(&mut self, open: bool) {
|
||||
if !self.esc_menu {
|
||||
self.bag = open;
|
||||
self.trade = open;
|
||||
self.map = false;
|
||||
self.want_grab = !open;
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_trade(&mut self) { self.trade(!self.trade); }
|
||||
|
||||
fn map(&mut self, open: bool) {
|
||||
if !self.esc_menu {
|
||||
self.map = open;
|
||||
@ -590,6 +609,7 @@ impl Show {
|
||||
|
||||
fn toggle_windows(&mut self, global_state: &mut GlobalState) {
|
||||
if self.bag
|
||||
|| self.trade
|
||||
|| self.esc_menu
|
||||
|| self.map
|
||||
|| self.social
|
||||
@ -600,6 +620,7 @@ impl Show {
|
||||
|| !matches!(self.open_windows, Windows::None)
|
||||
{
|
||||
self.bag = false;
|
||||
self.trade = false;
|
||||
self.esc_menu = false;
|
||||
self.help = false;
|
||||
self.intro = false;
|
||||
@ -774,6 +795,7 @@ impl Hud {
|
||||
debug: false,
|
||||
bag: false,
|
||||
bag_inv: false,
|
||||
trade: false,
|
||||
esc_menu: false,
|
||||
open_windows: Windows::None,
|
||||
map: false,
|
||||
@ -856,6 +878,13 @@ impl Hud {
|
||||
let inventories = ecs.read_storage::<comp::Inventory>();
|
||||
let entities = ecs.entities();
|
||||
let me = client.entity();
|
||||
|
||||
if (client.pending_trade().is_some() && !self.show.trade)
|
||||
|| (client.pending_trade().is_none() && self.show.trade)
|
||||
{
|
||||
self.show.toggle_trade();
|
||||
}
|
||||
|
||||
//self.input = client.read_storage::<comp::ControllerInputs>();
|
||||
if let Some(health) = healths.get(me) {
|
||||
// Hurt Frame
|
||||
@ -2089,6 +2118,36 @@ impl Hud {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Trade window
|
||||
if self.show.trade {
|
||||
match Trade::new(
|
||||
client,
|
||||
&self.imgs,
|
||||
&self.item_imgs,
|
||||
&self.fonts,
|
||||
tooltip_manager,
|
||||
&mut self.slot_manager,
|
||||
i18n,
|
||||
)
|
||||
.set(self.ids.trade, ui_widgets)
|
||||
{
|
||||
Some(action) => {
|
||||
if let TradeAction::Decline = action {
|
||||
self.show.stats = false;
|
||||
self.show.trade(false);
|
||||
if !self.show.social {
|
||||
self.show.want_grab = true;
|
||||
self.force_ungrab = false;
|
||||
} else {
|
||||
self.force_ungrab = true
|
||||
};
|
||||
}
|
||||
events.push(Event::TradeAction(action));
|
||||
},
|
||||
None => {},
|
||||
}
|
||||
}
|
||||
|
||||
// Buffs
|
||||
let ecs = client.state().ecs();
|
||||
let entity = client.entity();
|
||||
@ -2556,6 +2615,7 @@ impl Hud {
|
||||
Inventory(i) => Some(Slot::Inventory(i.0)),
|
||||
Equip(e) => Some(Slot::Equip(e)),
|
||||
Hotbar(_) => None,
|
||||
Trade(_) => None,
|
||||
};
|
||||
match event {
|
||||
slot::Event::Dragged(a, b) => {
|
||||
@ -2572,6 +2632,22 @@ impl Hud {
|
||||
} else if let (Hotbar(a), Hotbar(b)) = (a, b) {
|
||||
self.hotbar.swap(a, b);
|
||||
events.push(Event::ChangeHotbarState(Box::new(self.hotbar.to_owned())));
|
||||
} else if let (Inventory(i), Trade(_)) = (a, b) {
|
||||
if let Some(inventory) = inventories.get(entity) {
|
||||
events.push(Event::TradeAction(TradeAction::AddItem {
|
||||
item: i.0,
|
||||
quantity: i.amount(inventory).unwrap_or(1),
|
||||
}));
|
||||
}
|
||||
} else if let (Trade(t), Inventory(_)) = (a, b) {
|
||||
if let Some(inventory) = inventories.get(entity) {
|
||||
if let Some(invslot) = t.invslot {
|
||||
events.push(Event::TradeAction(TradeAction::RemoveItem {
|
||||
item: invslot,
|
||||
quantity: t.amount(inventory).unwrap_or(1),
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
slot::Event::Dropped(from) => {
|
||||
|
@ -21,6 +21,7 @@ pub enum SlotKind {
|
||||
Inventory(InventorySlot),
|
||||
Equip(EquipSlot),
|
||||
Hotbar(HotbarSlot),
|
||||
Trade(TradeSlot),
|
||||
/* Spellbook(SpellbookSlot), TODO */
|
||||
}
|
||||
|
||||
@ -63,6 +64,32 @@ impl SlotKey<Inventory, ItemImgs> for EquipSlot {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct TradeSlot {
|
||||
pub index: usize,
|
||||
pub quantity: u32,
|
||||
pub invslot: Option<InvSlotId>,
|
||||
}
|
||||
|
||||
impl SlotKey<Inventory, ItemImgs> for TradeSlot {
|
||||
type ImageKey = ItemKey;
|
||||
|
||||
fn image_key(&self, source: &Inventory) -> Option<(Self::ImageKey, Option<Color>)> {
|
||||
self.invslot
|
||||
.and_then(|inv_id| InventorySlot(inv_id).image_key(source))
|
||||
}
|
||||
|
||||
fn amount(&self, source: &Inventory) -> Option<u32> {
|
||||
self.invslot
|
||||
.and_then(|inv_id| InventorySlot(inv_id).amount(source))
|
||||
.map(|x| x.min(self.quantity))
|
||||
}
|
||||
|
||||
fn image_id(key: &Self::ImageKey, source: &ItemImgs) -> image::Id {
|
||||
source.img_id_or_not_found_img(key.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub enum HotbarImage {
|
||||
Item(ItemKey),
|
||||
@ -159,5 +186,8 @@ impl From<EquipSlot> for SlotKind {
|
||||
impl From<HotbarSlot> for SlotKind {
|
||||
fn from(hotbar: HotbarSlot) -> Self { Self::Hotbar(hotbar) }
|
||||
}
|
||||
impl From<TradeSlot> for SlotKind {
|
||||
fn from(trade: TradeSlot) -> Self { Self::Trade(trade) }
|
||||
}
|
||||
|
||||
impl SumSlot for SlotKind {}
|
||||
|
447
voxygen/src/hud/trade.rs
Normal file
447
voxygen/src/hud/trade.rs
Normal file
@ -0,0 +1,447 @@
|
||||
use super::{
|
||||
img_ids::Imgs,
|
||||
item_imgs::ItemImgs,
|
||||
slots::{SlotManager, TradeSlot},
|
||||
TEXT_COLOR, UI_HIGHLIGHT_0, UI_MAIN,
|
||||
};
|
||||
use crate::{
|
||||
i18n::Localization,
|
||||
ui::{
|
||||
fonts::Fonts,
|
||||
slot::{ContentSize, SlotMaker},
|
||||
TooltipManager,
|
||||
},
|
||||
};
|
||||
use client::Client;
|
||||
use common::{
|
||||
comp::Inventory,
|
||||
trade::{PendingTrade, TradeAction, TradePhase},
|
||||
};
|
||||
use common_net::sync::WorldSyncExt;
|
||||
use conrod_core::{
|
||||
color,
|
||||
position::Relative,
|
||||
widget::{self, Button, Image, Rectangle, State as ConrodState, Text},
|
||||
widget_ids, Color, Colorable, Labelable, Positionable, Sizeable, UiCell, Widget, WidgetCommon,
|
||||
};
|
||||
use vek::*;
|
||||
|
||||
pub struct State {
|
||||
ids: Ids,
|
||||
}
|
||||
|
||||
widget_ids! {
|
||||
pub struct Ids {
|
||||
trade_close,
|
||||
bg,
|
||||
bg_frame,
|
||||
trade_title_bg,
|
||||
trade_title,
|
||||
inv_alignment[],
|
||||
inv_slots[],
|
||||
inv_textslots[],
|
||||
offer_headers[],
|
||||
accept_indicators[],
|
||||
phase_indicator,
|
||||
accept_button,
|
||||
decline_button,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(WidgetCommon)]
|
||||
pub struct Trade<'a> {
|
||||
client: &'a Client,
|
||||
imgs: &'a Imgs,
|
||||
item_imgs: &'a ItemImgs,
|
||||
fonts: &'a Fonts,
|
||||
#[conrod(common_builder)]
|
||||
common: widget::CommonBuilder,
|
||||
//tooltip_manager: &'a mut TooltipManager,
|
||||
slot_manager: &'a mut SlotManager,
|
||||
localized_strings: &'a Localization,
|
||||
}
|
||||
|
||||
impl<'a> Trade<'a> {
|
||||
pub fn new(
|
||||
client: &'a Client,
|
||||
imgs: &'a Imgs,
|
||||
item_imgs: &'a ItemImgs,
|
||||
fonts: &'a Fonts,
|
||||
_tooltip_manager: &'a mut TooltipManager,
|
||||
slot_manager: &'a mut SlotManager,
|
||||
localized_strings: &'a Localization,
|
||||
) -> Self {
|
||||
Self {
|
||||
client,
|
||||
imgs,
|
||||
item_imgs,
|
||||
fonts,
|
||||
common: widget::CommonBuilder::default(),
|
||||
//tooltip_manager,
|
||||
slot_manager,
|
||||
localized_strings,
|
||||
}
|
||||
}
|
||||
}
|
||||
const MAX_TRADE_SLOTS: usize = 16;
|
||||
|
||||
impl<'a> Trade<'a> {
|
||||
fn background(&mut self, state: &mut ConrodState<'_, State>, ui: &mut UiCell<'_>) {
|
||||
Image::new(self.imgs.inv_bg_bag)
|
||||
.w_h(424.0, 708.0)
|
||||
.middle()
|
||||
.color(Some(UI_MAIN))
|
||||
.set(state.ids.bg, ui);
|
||||
Image::new(self.imgs.inv_frame_bag)
|
||||
.w_h(424.0, 708.0)
|
||||
.middle_of(state.ids.bg)
|
||||
.color(Some(UI_HIGHLIGHT_0))
|
||||
.set(state.ids.bg_frame, ui);
|
||||
}
|
||||
|
||||
fn title(&mut self, state: &mut ConrodState<'_, State>, ui: &mut UiCell<'_>) {
|
||||
Text::new(&self.localized_strings.get("hud.trade.trade_window"))
|
||||
.mid_top_with_margin_on(state.ids.bg_frame, 9.0)
|
||||
.font_id(self.fonts.cyri.conrod_id)
|
||||
.font_size(self.fonts.cyri.scale(20))
|
||||
.color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
|
||||
.set(state.ids.trade_title_bg, ui);
|
||||
Text::new(&self.localized_strings.get("hud.trade.trade_window"))
|
||||
.top_left_with_margins_on(state.ids.trade_title_bg, 2.0, 2.0)
|
||||
.font_id(self.fonts.cyri.conrod_id)
|
||||
.font_size(self.fonts.cyri.scale(20))
|
||||
.color(TEXT_COLOR)
|
||||
.set(state.ids.trade_title, ui);
|
||||
}
|
||||
|
||||
fn phase_indicator(
|
||||
&mut self,
|
||||
state: &mut ConrodState<'_, State>,
|
||||
ui: &mut UiCell<'_>,
|
||||
trade: &'a PendingTrade,
|
||||
) {
|
||||
let phase_text = match trade.phase() {
|
||||
TradePhase::Mutate => self.localized_strings.get("hud.trade.phase1_description"),
|
||||
TradePhase::Review => self.localized_strings.get("hud.trade.phase2_description"),
|
||||
TradePhase::Complete => self.localized_strings.get("hud.trade.phase3_description"),
|
||||
};
|
||||
|
||||
Text::new(&phase_text)
|
||||
.mid_top_with_margin_on(state.ids.bg, 70.0)
|
||||
.font_id(self.fonts.cyri.conrod_id)
|
||||
.font_size(self.fonts.cyri.scale(20))
|
||||
.color(Color::Rgba(1.0, 1.0, 1.0, 1.0))
|
||||
.set(state.ids.phase_indicator, ui);
|
||||
}
|
||||
|
||||
fn item_pane(
|
||||
&mut self,
|
||||
state: &mut ConrodState<'_, State>,
|
||||
ui: &mut UiCell<'_>,
|
||||
trade: &'a PendingTrade,
|
||||
who: usize,
|
||||
) -> <Self as Widget>::Event {
|
||||
let inventories = self.client.inventories();
|
||||
let uid = trade.parties[who];
|
||||
let entity = self.client.state().ecs().entity_from_uid(uid.0)?;
|
||||
// TODO: update in accordence with https://gitlab.com/veloren/veloren/-/issues/960
|
||||
let inventory = inventories.get(entity)?;
|
||||
|
||||
// Alignment for Grid
|
||||
let mut alignment = Rectangle::fill_with([200.0, 340.0], color::TRANSPARENT);
|
||||
if who % 2 == 0 {
|
||||
alignment = alignment.top_left_with_margins_on(state.ids.bg, 180.0, 46.5);
|
||||
} else {
|
||||
alignment = alignment.right_from(state.ids.inv_alignment[0], 0.0);
|
||||
}
|
||||
alignment
|
||||
.scroll_kids_vertically()
|
||||
.set(state.ids.inv_alignment[who], ui);
|
||||
|
||||
let name = self
|
||||
.client
|
||||
.player_list()
|
||||
.get(&uid)
|
||||
.map(|info| info.player_alias.clone())
|
||||
.unwrap_or_else(|| format!("Player {}", who));
|
||||
|
||||
let offer_header = self
|
||||
.localized_strings
|
||||
.get("hud.trade.persons_offer")
|
||||
.replace("{playername}", &name);
|
||||
Text::new(&offer_header)
|
||||
.up_from(state.ids.inv_alignment[who], 20.0)
|
||||
.font_id(self.fonts.cyri.conrod_id)
|
||||
.font_size(self.fonts.cyri.scale(20))
|
||||
.color(Color::Rgba(1.0, 1.0, 1.0, 1.0))
|
||||
.set(state.ids.offer_headers[who], ui);
|
||||
|
||||
let has_accepted = trade.accept_flags[who];
|
||||
let accept_indicator = self
|
||||
.localized_strings
|
||||
.get("hud.trade.has_accepted")
|
||||
.replace("{playername}", &name);
|
||||
Text::new(&accept_indicator)
|
||||
.down_from(state.ids.inv_alignment[who], 20.0)
|
||||
.font_id(self.fonts.cyri.conrod_id)
|
||||
.font_size(self.fonts.cyri.scale(20))
|
||||
.color(Color::Rgba(
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
if has_accepted { 1.0 } else { 0.0 },
|
||||
))
|
||||
.set(state.ids.accept_indicators[who], ui);
|
||||
|
||||
let mut invslots: Vec<_> = trade.offers[who].iter().map(|(k, v)| (*k, *v)).collect();
|
||||
invslots.sort();
|
||||
let tradeslots: Vec<_> = invslots
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, (k, quantity))| TradeSlot {
|
||||
index,
|
||||
quantity,
|
||||
invslot: Some(k),
|
||||
})
|
||||
.collect();
|
||||
|
||||
if matches!(trade.phase(), TradePhase::Mutate) {
|
||||
self.phase1_itemwidget(state, ui, inventory, who, &tradeslots);
|
||||
} else {
|
||||
self.phase2_itemwidget(state, ui, inventory, who, &tradeslots);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn phase1_itemwidget(
|
||||
&mut self,
|
||||
state: &mut ConrodState<'_, State>,
|
||||
ui: &mut UiCell<'_>,
|
||||
inventory: &Inventory,
|
||||
who: usize,
|
||||
tradeslots: &[TradeSlot],
|
||||
) {
|
||||
let mut slot_maker = SlotMaker {
|
||||
empty_slot: self.imgs.inv_slot,
|
||||
filled_slot: self.imgs.inv_slot,
|
||||
selected_slot: self.imgs.inv_slot_sel,
|
||||
background_color: Some(UI_MAIN),
|
||||
content_size: ContentSize {
|
||||
width_height_ratio: 1.0,
|
||||
max_fraction: 0.75,
|
||||
},
|
||||
selected_content_scale: 1.067,
|
||||
amount_font: self.fonts.cyri.conrod_id,
|
||||
amount_margins: Vec2::new(-4.0, 0.0),
|
||||
amount_font_size: self.fonts.cyri.scale(12),
|
||||
amount_text_color: TEXT_COLOR,
|
||||
content_source: inventory,
|
||||
image_source: self.item_imgs,
|
||||
slot_manager: Some(self.slot_manager),
|
||||
};
|
||||
|
||||
if state.ids.inv_slots.len() < 2 * MAX_TRADE_SLOTS {
|
||||
state.update(|s| {
|
||||
s.ids
|
||||
.inv_slots
|
||||
.resize(2 * MAX_TRADE_SLOTS, &mut ui.widget_id_generator());
|
||||
});
|
||||
}
|
||||
|
||||
for i in 0..MAX_TRADE_SLOTS {
|
||||
let x = i % 4;
|
||||
let y = i / 4;
|
||||
|
||||
let slot = tradeslots.get(i).cloned().unwrap_or(TradeSlot {
|
||||
index: i,
|
||||
quantity: 0,
|
||||
invslot: None,
|
||||
});
|
||||
// Slot
|
||||
let slot_widget = slot_maker
|
||||
.fabricate(slot, [40.0; 2])
|
||||
.top_left_with_margins_on(
|
||||
state.ids.inv_alignment[who],
|
||||
0.0 + y as f64 * (40.0),
|
||||
0.0 + x as f64 * (40.0),
|
||||
);
|
||||
slot_widget.set(state.ids.inv_slots[i + who * MAX_TRADE_SLOTS], ui);
|
||||
}
|
||||
}
|
||||
|
||||
fn phase2_itemwidget(
|
||||
&mut self,
|
||||
state: &mut ConrodState<'_, State>,
|
||||
ui: &mut UiCell<'_>,
|
||||
inventory: &Inventory,
|
||||
who: usize,
|
||||
tradeslots: &[TradeSlot],
|
||||
) {
|
||||
if state.ids.inv_textslots.len() < 2 * MAX_TRADE_SLOTS {
|
||||
state.update(|s| {
|
||||
s.ids
|
||||
.inv_textslots
|
||||
.resize(2 * MAX_TRADE_SLOTS, &mut ui.widget_id_generator());
|
||||
});
|
||||
}
|
||||
for i in 0..MAX_TRADE_SLOTS {
|
||||
let slot = tradeslots.get(i).cloned().unwrap_or(TradeSlot {
|
||||
index: i,
|
||||
quantity: 0,
|
||||
invslot: None,
|
||||
});
|
||||
let itemname = slot
|
||||
.invslot
|
||||
.and_then(|i| inventory.get(i))
|
||||
.map(|i| i.name())
|
||||
.unwrap_or("");
|
||||
let is_present = slot.quantity > 0 && slot.invslot.is_some();
|
||||
Text::new(&format!("{} x {}", slot.quantity, itemname))
|
||||
.top_left_with_margins_on(state.ids.inv_alignment[who], 10.0 + i as f64 * 30.0, 0.0)
|
||||
.font_id(self.fonts.cyri.conrod_id)
|
||||
.font_size(self.fonts.cyri.scale(20))
|
||||
.color(Color::Rgba(
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
if is_present { 1.0 } else { 0.0 },
|
||||
))
|
||||
.set(state.ids.inv_textslots[i + who * MAX_TRADE_SLOTS], ui);
|
||||
}
|
||||
}
|
||||
|
||||
fn accept_decline_buttons(
|
||||
&mut self,
|
||||
state: &mut ConrodState<'_, State>,
|
||||
ui: &mut UiCell<'_>,
|
||||
trade: &'a PendingTrade,
|
||||
) -> <Self as Widget>::Event {
|
||||
let mut event = None;
|
||||
if Button::image(self.imgs.button)
|
||||
.w_h(31.0 * 5.0, 12.0 * 2.0)
|
||||
.hover_image(self.imgs.button_hover)
|
||||
.press_image(self.imgs.button_press)
|
||||
.bottom_left_with_margins_on(state.ids.bg, 80.0, 60.0)
|
||||
.label(&self.localized_strings.get("hud.trade.accept"))
|
||||
.label_font_size(self.fonts.cyri.scale(14))
|
||||
.label_color(TEXT_COLOR)
|
||||
.label_font_id(self.fonts.cyri.conrod_id)
|
||||
.label_y(Relative::Scalar(2.0))
|
||||
.set(state.ids.accept_button, ui)
|
||||
.was_clicked()
|
||||
{
|
||||
event = Some(TradeAction::Accept(trade.phase()));
|
||||
}
|
||||
|
||||
if Button::image(self.imgs.button)
|
||||
.w_h(31.0 * 5.0, 12.0 * 2.0)
|
||||
.hover_image(self.imgs.button_hover)
|
||||
.press_image(self.imgs.button_press)
|
||||
.right_from(state.ids.accept_button, 20.0)
|
||||
.label(&self.localized_strings.get("hud.trade.decline"))
|
||||
.label_font_size(self.fonts.cyri.scale(14))
|
||||
.label_color(TEXT_COLOR)
|
||||
.label_font_id(self.fonts.cyri.conrod_id)
|
||||
.label_y(Relative::Scalar(2.0))
|
||||
.set(state.ids.decline_button, ui)
|
||||
.was_clicked()
|
||||
{
|
||||
event = Some(TradeAction::Decline);
|
||||
}
|
||||
event
|
||||
}
|
||||
|
||||
fn close_button(
|
||||
&mut self,
|
||||
state: &mut ConrodState<'_, State>,
|
||||
ui: &mut UiCell<'_>,
|
||||
) -> <Self as Widget>::Event {
|
||||
if Button::image(self.imgs.close_btn)
|
||||
.w_h(24.0, 25.0)
|
||||
.hover_image(self.imgs.close_btn_hover)
|
||||
.press_image(self.imgs.close_btn_press)
|
||||
.top_right_with_margins_on(state.ids.bg, 0.0, 0.0)
|
||||
.set(state.ids.trade_close, ui)
|
||||
.was_clicked()
|
||||
{
|
||||
Some(TradeAction::Decline)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for Trade<'a> {
|
||||
type Event = Option<TradeAction>;
|
||||
type State = State;
|
||||
type Style = ();
|
||||
|
||||
fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
|
||||
State {
|
||||
ids: Ids::new(id_gen),
|
||||
}
|
||||
}
|
||||
|
||||
fn style(&self) -> Self::Style {}
|
||||
|
||||
fn update(mut self, args: widget::UpdateArgs<Self>) -> Self::Event {
|
||||
let widget::UpdateArgs { mut state, ui, .. } = args;
|
||||
|
||||
let mut event = None;
|
||||
let trade = match self.client.pending_trade() {
|
||||
Some((_, trade)) => trade,
|
||||
None => return Some(TradeAction::Decline),
|
||||
};
|
||||
|
||||
if state.ids.inv_alignment.len() < 2 {
|
||||
state.update(|s| {
|
||||
s.ids.inv_alignment.resize(2, &mut ui.widget_id_generator());
|
||||
});
|
||||
}
|
||||
if state.ids.offer_headers.len() < 2 {
|
||||
state.update(|s| {
|
||||
s.ids.offer_headers.resize(2, &mut ui.widget_id_generator());
|
||||
});
|
||||
}
|
||||
if state.ids.accept_indicators.len() < 2 {
|
||||
state.update(|s| {
|
||||
s.ids
|
||||
.accept_indicators
|
||||
.resize(2, &mut ui.widget_id_generator());
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: item tooltips in trade preview
|
||||
/*let trade_tooltip = Tooltip::new({
|
||||
// Edge images [t, b, r, l]
|
||||
// Corner images [tr, tl, br, bl]
|
||||
let edge = &self.rot_imgs.tt_side;
|
||||
let corner = &self.rot_imgs.tt_corner;
|
||||
ImageFrame::new(
|
||||
[edge.cw180, edge.none, edge.cw270, edge.cw90],
|
||||
[corner.none, corner.cw270, corner.cw90, corner.cw180],
|
||||
Color::Rgba(0.08, 0.07, 0.04, 1.0),
|
||||
5.0,
|
||||
)
|
||||
})
|
||||
.title_font_size(self.fonts.cyri.scale(15))
|
||||
.parent(ui.window)
|
||||
.desc_font_size(self.fonts.cyri.scale(12))
|
||||
.font_id(self.fonts.cyri.conrod_id)
|
||||
.desc_text_color(TEXT_COLOR);*/
|
||||
|
||||
self.background(&mut state, ui);
|
||||
self.title(&mut state, ui);
|
||||
self.phase_indicator(&mut state, ui, &trade);
|
||||
|
||||
event = self.item_pane(&mut state, ui, &trade, 0).or(event);
|
||||
event = self.item_pane(&mut state, ui, &trade, 1).or(event);
|
||||
event = self
|
||||
.accept_decline_buttons(&mut state, ui, &trade)
|
||||
.or(event);
|
||||
event = self.close_button(&mut state, ui).or(event);
|
||||
|
||||
event
|
||||
}
|
||||
}
|
@ -19,7 +19,8 @@ pub struct KeyState {
|
||||
pub auto_walk: bool,
|
||||
pub swap_loadout: bool,
|
||||
pub respawn: bool,
|
||||
pub collect: bool,
|
||||
pub interact: bool,
|
||||
pub trade: bool,
|
||||
pub analog_matrix: Vec2<f32>,
|
||||
}
|
||||
|
||||
@ -44,7 +45,8 @@ impl Default for KeyState {
|
||||
auto_walk: false,
|
||||
swap_loadout: false,
|
||||
respawn: false,
|
||||
collect: false,
|
||||
interact: false,
|
||||
trade: false,
|
||||
analog_matrix: Vec2::zero(),
|
||||
}
|
||||
}
|
||||
|
@ -9,18 +9,25 @@ use client::{self, Client};
|
||||
use common::{
|
||||
assets::AssetExt,
|
||||
comp,
|
||||
comp::{inventory::slot::Slot, ChatMsg, ChatType, InventoryUpdateEvent, Pos, Vel},
|
||||
comp::{
|
||||
inventory::slot::Slot, invite::InviteKind, ChatMsg, ChatType, InventoryUpdateEvent, Pos,
|
||||
Vel,
|
||||
},
|
||||
consts::{MAX_MOUNT_RANGE, MAX_PICKUP_RANGE},
|
||||
outcome::Outcome,
|
||||
span,
|
||||
terrain::{Block, BlockKind},
|
||||
trade::TradeResult,
|
||||
util::{
|
||||
find_dist::{Cube, Cylinder, FindDist},
|
||||
Dir,
|
||||
},
|
||||
vol::ReadVol,
|
||||
};
|
||||
use common_net::msg::PresenceKind;
|
||||
use common_net::{
|
||||
msg::{server::InviteAnswer, PresenceKind},
|
||||
sync::WorldSyncExt,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
audio::sfx::SfxEvent,
|
||||
@ -121,6 +128,37 @@ impl SessionState {
|
||||
client::Event::Chat(m) => {
|
||||
self.hud.new_message(m);
|
||||
},
|
||||
client::Event::InviteComplete {
|
||||
target,
|
||||
answer,
|
||||
kind,
|
||||
} => {
|
||||
// TODO: i18n
|
||||
let kind_str = match kind {
|
||||
InviteKind::Group => "Group",
|
||||
InviteKind::Trade => "Trade",
|
||||
};
|
||||
let target_name = match client.player_list().get(&target) {
|
||||
Some(info) => info.player_alias.clone(),
|
||||
None => "<unknown>".to_string(),
|
||||
};
|
||||
let answer_str = match answer {
|
||||
InviteAnswer::Accepted => "accepted",
|
||||
InviteAnswer::Declined => "declined",
|
||||
InviteAnswer::TimedOut => "timed out",
|
||||
};
|
||||
let msg = format!("{} invite to {} {}", kind_str, target_name, answer_str);
|
||||
self.hud.new_message(ChatType::Meta.chat_msg(msg));
|
||||
},
|
||||
client::Event::TradeComplete { result, trade: _ } => {
|
||||
// TODO: i18n, entity names
|
||||
let msg = match result {
|
||||
TradeResult::Completed => "Trade completed successfully.",
|
||||
TradeResult::Declined => "Trade declined.",
|
||||
TradeResult::NotEnoughSpace => "Not enough space to complete the trade.",
|
||||
};
|
||||
self.hud.new_message(ChatType::Meta.chat_msg(msg));
|
||||
},
|
||||
client::Event::InventoryUpdated(inv_event) => {
|
||||
let sfx_triggers = self.scene.sfx_mgr.triggers.read();
|
||||
|
||||
@ -494,9 +532,9 @@ impl PlayState for SessionState {
|
||||
}
|
||||
},
|
||||
Event::InputUpdate(GameInput::Interact, state)
|
||||
if state != self.key_state.collect =>
|
||||
if state != self.key_state.interact =>
|
||||
{
|
||||
self.key_state.collect = state;
|
||||
self.key_state.interact = state;
|
||||
|
||||
if state {
|
||||
if let Some(interactable) = self.interactable {
|
||||
@ -524,6 +562,27 @@ impl PlayState for SessionState {
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::InputUpdate(GameInput::Trade, state)
|
||||
if state != self.key_state.trade =>
|
||||
{
|
||||
self.key_state.trade = state;
|
||||
|
||||
if state {
|
||||
if let Some(interactable) = self.interactable {
|
||||
let mut client = self.client.borrow_mut();
|
||||
match interactable {
|
||||
Interactable::Block(_, _) => {},
|
||||
Interactable::Entity(entity) => {
|
||||
client
|
||||
.state()
|
||||
.ecs()
|
||||
.uid_from_entity(entity)
|
||||
.map(|uid| client.send_invite(uid, InviteKind::Trade));
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/*Event::InputUpdate(GameInput::Charge, state) => {
|
||||
self.inputs.charge.set_state(state);
|
||||
},*/
|
||||
@ -571,14 +630,14 @@ impl PlayState for SessionState {
|
||||
},
|
||||
Event::InputUpdate(GameInput::AcceptGroupInvite, true) => {
|
||||
let mut client = self.client.borrow_mut();
|
||||
if client.group_invite().is_some() {
|
||||
client.accept_group_invite();
|
||||
if client.invite().is_some() {
|
||||
client.accept_invite();
|
||||
}
|
||||
},
|
||||
Event::InputUpdate(GameInput::DeclineGroupInvite, true) => {
|
||||
let mut client = self.client.borrow_mut();
|
||||
if client.group_invite().is_some() {
|
||||
client.decline_group_invite();
|
||||
if client.invite().is_some() {
|
||||
client.decline_invite();
|
||||
}
|
||||
},
|
||||
Event::AnalogGameInput(input) => match input {
|
||||
@ -1089,6 +1148,10 @@ impl PlayState for SessionState {
|
||||
|
||||
info!("Event! -> ChangedHotbarState")
|
||||
},
|
||||
HudEvent::TradeAction(action) => {
|
||||
let mut client = self.client.borrow_mut();
|
||||
client.perform_trade_action(action);
|
||||
},
|
||||
HudEvent::Ability3(state) => self.inputs.ability3.set_state(state),
|
||||
HudEvent::ChangeFOV(new_fov) => {
|
||||
global_state.settings.graphics.fov = new_fov;
|
||||
@ -1193,13 +1256,13 @@ impl PlayState for SessionState {
|
||||
self.client.borrow_mut().craft_recipe(&r);
|
||||
},
|
||||
HudEvent::InviteMember(uid) => {
|
||||
self.client.borrow_mut().send_group_invite(uid);
|
||||
self.client.borrow_mut().send_invite(uid, InviteKind::Group);
|
||||
},
|
||||
HudEvent::AcceptInvite => {
|
||||
self.client.borrow_mut().accept_group_invite();
|
||||
self.client.borrow_mut().accept_invite();
|
||||
},
|
||||
HudEvent::DeclineInvite => {
|
||||
self.client.borrow_mut().decline_group_invite();
|
||||
self.client.borrow_mut().decline_invite();
|
||||
},
|
||||
HudEvent::KickMember(uid) => {
|
||||
self.client.borrow_mut().kick_from_group(uid);
|
||||
|
@ -142,7 +142,8 @@ impl ControlSettings {
|
||||
GameInput::ToggleLantern => KeyMouse::Key(VirtualKeyCode::G),
|
||||
GameInput::Mount => KeyMouse::Key(VirtualKeyCode::F),
|
||||
GameInput::Map => KeyMouse::Key(VirtualKeyCode::M),
|
||||
GameInput::Bag => KeyMouse::Key(VirtualKeyCode::B),
|
||||
GameInput::Bag => KeyMouse::Key(VirtualKeyCode::R),
|
||||
GameInput::Trade => KeyMouse::Key(VirtualKeyCode::B),
|
||||
GameInput::Social => KeyMouse::Key(VirtualKeyCode::O),
|
||||
GameInput::Crafting => KeyMouse::Key(VirtualKeyCode::C),
|
||||
GameInput::Spellbook => KeyMouse::Key(VirtualKeyCode::P),
|
||||
|
@ -54,6 +54,7 @@ pub enum GameInput {
|
||||
Escape,
|
||||
Map,
|
||||
Bag,
|
||||
Trade,
|
||||
Social,
|
||||
Crafting,
|
||||
Spellbook,
|
||||
@ -107,6 +108,7 @@ impl GameInput {
|
||||
GameInput::Escape => "gameinput.escape",
|
||||
GameInput::Map => "gameinput.map",
|
||||
GameInput::Bag => "gameinput.bag",
|
||||
GameInput::Trade => "gameinput.trade",
|
||||
GameInput::Social => "gameinput.social",
|
||||
GameInput::Crafting => "gameinput.crafting",
|
||||
GameInput::Spellbook => "gameinput.spellbook",
|
||||
@ -167,6 +169,7 @@ impl GameInput {
|
||||
GameInput::Escape,
|
||||
GameInput::Map,
|
||||
GameInput::Bag,
|
||||
GameInput::Trade,
|
||||
GameInput::Social,
|
||||
GameInput::Crafting,
|
||||
GameInput::Spellbook,
|
||||
|
Loading…
Reference in New Issue
Block a user