1use std::cmp::Ordering;
2
3use crate::{
4 comp::inventory::{
5 Inventory, item::ItemDefinitionIdOwned, slot::InvSlotId, trade_pricing::TradePricing,
6 },
7 terrain::BiomeKind,
8 uid::Uid,
9};
10use hashbrown::HashMap;
11use serde::{Deserialize, Serialize};
12use strum::EnumIter;
13use tracing::{trace, warn};
14
15#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
16pub enum TradePhase {
17 Mutate,
18 Review,
19 Complete,
20}
21
22#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
26pub enum TradeAction {
27 AddItem {
28 item: InvSlotId,
29 quantity: u32,
30 ours: bool,
31 },
32 RemoveItem {
33 item: InvSlotId,
34 quantity: u32,
35 ours: bool,
36 },
37 Accept(TradePhase),
41 Decline,
42}
43
44#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
45pub enum TradeResult {
46 Completed,
47 Declined,
48 NotEnoughSpace,
49}
50
51#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
77pub struct PendingTrade {
78 pub parties: [Uid; 2],
81 pub offers: [HashMap<InvSlotId, u32>; 2],
84 pub phase: TradePhase,
86 pub accept_flags: [bool; 2],
89}
90
91impl TradePhase {
92 fn next(self) -> TradePhase {
93 match self {
94 TradePhase::Mutate => TradePhase::Review,
95 TradePhase::Review => TradePhase::Complete,
96 TradePhase::Complete => TradePhase::Complete,
97 }
98 }
99}
100
101impl TradeAction {
102 pub fn item(item: InvSlotId, delta: i32, ours: bool) -> Option<Self> {
103 match delta.cmp(&0) {
104 Ordering::Equal => None,
105 Ordering::Less => Some(TradeAction::RemoveItem {
106 item,
107 ours,
108 quantity: -delta as u32,
109 }),
110 Ordering::Greater => Some(TradeAction::AddItem {
111 item,
112 ours,
113 quantity: delta as u32,
114 }),
115 }
116 }
117}
118
119impl PendingTrade {
120 pub fn new(party: Uid, counterparty: Uid) -> PendingTrade {
121 PendingTrade {
122 parties: [party, counterparty],
123 offers: [HashMap::new(), HashMap::new()],
124 phase: TradePhase::Mutate,
125 accept_flags: [false, false],
126 }
127 }
128
129 pub fn phase(&self) -> TradePhase { self.phase }
130
131 pub fn should_commit(&self) -> bool { matches!(self.phase, TradePhase::Complete) }
132
133 pub fn which_party(&self, party: Uid) -> Option<usize> {
134 self.parties
135 .iter()
136 .enumerate()
137 .find(|(_, x)| **x == party)
138 .map(|(i, _)| i)
139 }
140
141 pub fn is_empty_trade(&self) -> bool { self.offers[0].is_empty() && self.offers[1].is_empty() }
142
143 pub fn process_trade_action(
150 &mut self,
151 mut who: usize,
152 action: TradeAction,
153 inventories: &[&Inventory],
154 ) {
155 use TradeAction::*;
156 match action {
157 AddItem {
158 item,
159 quantity: delta,
160 ours,
161 } => {
162 if self.phase() == TradePhase::Mutate && delta > 0 {
163 if !ours {
164 who = 1 - who;
165 }
166 let total = self.offers[who].entry(item).or_insert(0);
167 let owned_quantity =
168 inventories[who].get(item).map(|i| i.amount()).unwrap_or(0);
169 *total = total.saturating_add(delta).min(owned_quantity);
170 self.accept_flags = [false, false];
171 }
172 },
173 RemoveItem {
174 item,
175 quantity: delta,
176 ours,
177 } => {
178 if self.phase() == TradePhase::Mutate {
179 if !ours {
180 who = 1 - who;
181 }
182 self.offers[who]
183 .entry(item)
184 .and_replace_entry_with(|_, mut total| {
185 total = total.saturating_sub(delta);
186 if total > 0 { Some(total) } else { None }
187 });
188 self.accept_flags = [false, false];
189 }
190 },
191 Accept(phase) => {
192 if self.phase == phase && !self.is_empty_trade() {
193 self.accept_flags[who] = true;
194 }
195 if self.accept_flags[0] && self.accept_flags[1] {
196 self.phase = self.phase.next();
197 self.accept_flags = [false, false];
198 }
199 },
200 Decline => {},
201 }
202 }
203}
204
205#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
206pub struct TradeId(usize);
207
208pub struct Trades {
209 pub next_id: TradeId,
210 pub trades: HashMap<TradeId, PendingTrade>,
211 pub entity_trades: HashMap<Uid, TradeId>,
212}
213
214impl Trades {
215 pub fn begin_trade(&mut self, party: Uid, counterparty: Uid) -> TradeId {
216 let id = self.next_id;
217 self.next_id = TradeId(id.0.wrapping_add(1));
218 self.trades
219 .insert(id, PendingTrade::new(party, counterparty));
220 self.entity_trades.insert(party, id);
221 self.entity_trades.insert(counterparty, id);
222 id
223 }
224
225 pub fn process_trade_action<'a, F: Fn(Uid) -> Option<&'a Inventory>>(
226 &mut self,
227 id: TradeId,
228 who: Uid,
229 action: TradeAction,
230 get_inventory: F,
231 ) {
232 trace!("for trade id {:?}, message {:?}", id, action);
233 if let Some(trade) = self.trades.get_mut(&id) {
234 if let Some(party) = trade.which_party(who) {
235 let mut inventories = Vec::new();
236 for party in trade.parties.iter() {
237 match get_inventory(*party) {
238 Some(inventory) => inventories.push(inventory),
239 None => return,
240 }
241 }
242 trade.process_trade_action(party, action, &inventories);
243 } else {
244 warn!(
245 "An entity who is not a party to trade {:?} tried to modify it",
246 id
247 );
248 }
249 } else {
250 warn!("Attempt to modify nonexistent trade id {:?}", id);
251 }
252 }
253
254 pub fn decline_trade(&mut self, id: TradeId, who: Uid) -> Option<Uid> {
255 let mut to_notify = None;
256 if let Some(trade) = self.trades.remove(&id) {
257 match trade.which_party(who) {
258 Some(i) => {
259 self.entity_trades.remove(&trade.parties[0]);
260 self.entity_trades.remove(&trade.parties[1]);
261 to_notify = Some(trade.parties[1 - i])
263 },
264 None => {
265 warn!(
266 "An entity who is not a party to trade {:?} tried to decline it",
267 id
268 );
269 self.trades.insert(id, trade);
271 },
272 }
273 } else {
274 warn!("Attempt to decline nonexistent trade id {:?}", id);
275 }
276 to_notify
277 }
278
279 pub fn in_trade_with_property<F: FnOnce(&PendingTrade) -> bool>(
282 &self,
283 uid: &Uid,
284 f: F,
285 ) -> bool {
286 self.entity_trades
287 .get(uid)
288 .and_then(|trade_id| self.trades.get(trade_id))
289 .map(f)
290 .unwrap_or(false)
292 }
293
294 pub fn in_immutable_trade(&self, uid: &Uid) -> bool {
295 self.in_trade_with_property(uid, |trade| trade.phase() != TradePhase::Mutate)
296 }
297
298 pub fn in_mutable_trade(&self, uid: &Uid) -> bool {
299 self.in_trade_with_property(uid, |trade| trade.phase() == TradePhase::Mutate)
300 }
301
302 pub fn implicit_mutation_occurred(&mut self, uid: &Uid) {
303 if let Some(trade_id) = self.entity_trades.get(uid) {
304 self.trades
305 .get_mut(trade_id)
306 .map(|trade| trade.accept_flags = [false, false]);
307 }
308 }
309}
310
311impl Default for Trades {
312 fn default() -> Trades {
313 Trades {
314 next_id: TradeId(0),
315 trades: HashMap::new(),
316 entity_trades: HashMap::new(),
317 }
318 }
319}
320
321#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, Deserialize, EnumIter)]
325pub enum Good {
326 Territory(BiomeKind),
327 Flour,
328 Meat,
329 Terrain(BiomeKind),
330 Transportation,
331 Food,
332 Wood,
333 Stone,
334 Tools, Armor,
336 Ingredients, Potions,
338 Coin, RoadSecurity,
340 Recipe,
341}
342
343impl Default for Good {
344 fn default() -> Self {
345 Good::Terrain(BiomeKind::Void) }
347}
348
349impl Good {
350 pub fn trade_margin(&self) -> f32 {
352 match self {
353 Good::Tools | Good::Armor => 0.5,
354 Good::Food | Good::Potions | Good::Ingredients | Good::Wood => 0.75,
355 Good::Coin | Good::Recipe => 1.0,
356 _ => 0.0,
360 }
361 }
362}
363
364pub type SiteId = u64;
366
367#[derive(Clone, Debug)]
368pub struct SiteInformation {
369 pub id: SiteId,
370 pub unconsumed_stock: HashMap<Good, f32>,
371}
372
373#[derive(Clone, Debug, Default, Serialize, Deserialize)]
374pub struct SitePrices {
375 pub values: HashMap<Good, f32>,
376}
377
378impl SitePrices {
379 pub fn balance(
380 &self,
381 offers: &[HashMap<InvSlotId, u32>; 2],
382 inventories: &[Option<ReducedInventory>; 2],
383 who: usize,
384 reduce: bool,
385 ) -> Option<f32> {
386 offers[who]
387 .iter()
388 .map(|(slot, amount)| {
389 inventories[who]
390 .as_ref()
391 .map(|ri| {
392 let item = ri.inventory.get(slot)?;
393 let vec = TradePricing::get_materials(&item.name.as_ref())?;
394 Some(
395 vec.iter()
396 .map(|(amount2, material)| {
397 self.values.get(material).copied().unwrap_or_default()
398 * *amount2
399 * (if reduce { material.trade_margin() } else { 1.0 })
400 })
401 .sum::<f32>()
402 * (*amount as f32),
403 )
404 })
405 .unwrap_or(Some(0.0))
406 })
407 .try_fold(0.0, |a, p| Some(a + p?))
408 }
409}
410
411#[derive(Clone, Debug)]
412pub struct ReducedInventoryItem {
413 pub name: ItemDefinitionIdOwned,
414 pub amount: u32,
415}
416
417#[derive(Clone, Debug, Default)]
418pub struct ReducedInventory {
419 pub inventory: HashMap<InvSlotId, ReducedInventoryItem>,
420}
421
422impl ReducedInventory {
423 pub fn from(inventory: &Inventory) -> Self {
424 let items = inventory
425 .slots_with_id()
426 .filter(|(_, it)| it.is_some())
427 .map(|(sl, it)| {
428 (sl, ReducedInventoryItem {
429 name: it.as_ref().unwrap().item_definition_id().to_owned(),
430 amount: it.as_ref().unwrap().amount(),
431 })
432 })
433 .collect();
434 Self { inventory: items }
435 }
436}