Skip to content

Commit ccf29dd

Browse files
committed
Deal with nulls and 0s in 'store' fields
This properly filters out 0s and nulls for our longterm data, Store, while allowing them in updates to represent removing or emptying that particular resource.
1 parent 13c59fc commit ccf29dd

File tree

4 files changed

+123
-7
lines changed

4 files changed

+123
-7
lines changed

src/websocket/types/room/objects/spawn.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ mod test {
9191
"y": 28
9292
});
9393

94-
let mut obj = StructureSpawn::deserialize(json).unwrap();
94+
let obj = StructureSpawn::deserialize(json).unwrap();
9595

9696
assert_eq!(
9797
obj,

src/websocket/types/room/objects/terminal.rs

+26-1
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ mod test {
104104
..
105105
} if user == "5788389e3fd9069e6b546e2d"
106106
&& id == "59a5cc4f4733bb4c785ec4e7"
107-
&& *store == store! {Energy: 25000, Catalyst: 50189, CatalyzedGhodiumAcid: 0} =>
107+
&& *store == store! {Energy: 25000, Catalyst: 50189} =>
108108
{
109109
()
110110
}
@@ -139,4 +139,29 @@ mod test {
139139

140140
assert_eq!(obj.cooldown_time, 0);
141141
}
142+
143+
#[test]
144+
fn parse_npc_terminal_with_null_store() {
145+
let json = json!({
146+
"_id": "57cd3a30c0551957424a1f0b",
147+
"room": "E40N40",
148+
"store": {
149+
"H": null,
150+
"K": null,
151+
"L": null,
152+
"O": null,
153+
"U": null,
154+
"X": null,
155+
"Z": null,
156+
"energy": 0,
157+
"power": null
158+
},
159+
"storeCapacity": 0,
160+
"type": "terminal",
161+
"x": 35,
162+
"y": 45
163+
});
164+
let obj = StructureTerminal::deserialize(json).unwrap();
165+
assert_eq!(obj.store, store!());
166+
}
142167
}

src/websocket/types/room/objects/tombstone.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ impl Tombstone {
4848
mod test {
4949
use serde::Deserialize;
5050

51-
use super::{ResourceType, Tombstone};
51+
use super::{Tombstone};
5252

5353
#[test]
5454
fn parse_simple_tombstone() {

src/websocket/types/room/resources.rs

+95-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
//! Managing and parsing resource
2-
use std::collections::HashMap;
2+
use std::{cmp, collections::HashMap, fmt};
3+
4+
use serde::de::{Deserialize, Deserializer, MapAccess, Visitor};
35

46
use crate::websocket::room_object_macros::Updatable;
57

@@ -202,7 +204,7 @@ impl ResourceType {
202204
}
203205

204206
/// The resources and amounts of each resource some game object holds.
205-
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq)]
207+
#[derive(Serialize, Clone, Debug, Default, PartialEq, Eq)]
206208
#[serde(transparent)]
207209
pub struct Store(pub HashMap<ResourceType, i32>);
208210

@@ -218,21 +220,110 @@ impl Store {
218220
}
219221
}
220222

223+
struct StoreVisitor;
224+
225+
impl<'de> Visitor<'de> for StoreVisitor {
226+
type Value = Store;
227+
228+
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
229+
f.write_str("a map")
230+
}
231+
232+
#[inline]
233+
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
234+
where
235+
A: MapAccess<'de>,
236+
{
237+
// Adopted from `HashMap` `Deserialize` impl from
238+
// https://github.com/serde-rs/serde/blob/master/serde/src/de/impls.rs
239+
let mut values = HashMap::with_capacity(cmp::min(map.size_hint().unwrap_or(0), 4096));
240+
241+
while let Some((key, value)) = map.next_entry::<_, Option<i32>>()? {
242+
let value = value.unwrap_or(0);
243+
if value != 0 {
244+
values.insert(key, value);
245+
}
246+
}
247+
248+
Ok(Store(values))
249+
}
250+
}
251+
252+
impl<'de> Deserialize<'de> for Store {
253+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
254+
where
255+
D: Deserializer<'de>,
256+
{
257+
deserializer.deserialize_map(StoreVisitor)
258+
}
259+
}
260+
261+
/// Update structure for Store. The difference is that StoreUpdate allows 0 values.
262+
#[derive(Serialize, Clone, Debug, Default, PartialEq, Eq)]
263+
#[serde(transparent)]
264+
pub struct StoreUpdate(pub HashMap<ResourceType, i32>);
265+
266+
/// Like `StoreVisitor`, but keeps 0s and nulls.
267+
struct StoreUpdateVisitor;
268+
269+
impl<'de> Visitor<'de> for StoreUpdateVisitor {
270+
type Value = StoreUpdate;
271+
272+
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
273+
f.write_str("a map")
274+
}
275+
276+
#[inline]
277+
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
278+
where
279+
A: MapAccess<'de>,
280+
{
281+
// Adopted from `HashMap` `Deserialize` impl from
282+
// https://github.com/serde-rs/serde/blob/master/serde/src/de/impls.rs
283+
let mut values = HashMap::with_capacity(cmp::min(map.size_hint().unwrap_or(0), 4096));
284+
285+
while let Some((key, value)) = map.next_entry::<_, Option<i32>>()? {
286+
let value = value.unwrap_or(0);
287+
values.insert(key, value);
288+
}
289+
290+
Ok(StoreUpdate(values))
291+
}
292+
}
293+
294+
impl<'de> Deserialize<'de> for StoreUpdate {
295+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
296+
where
297+
D: Deserializer<'de>,
298+
{
299+
deserializer.deserialize_map(StoreUpdateVisitor)
300+
}
301+
}
302+
221303
impl Updatable for Store {
222304
type Update = Store;
223305
fn apply_update(&mut self, update: Self::Update) {
224-
self.0.extend(update.0);
306+
for (key, value) in update.0 {
307+
if value == 0 {
308+
self.0.remove(&key);
309+
} else {
310+
self.0.insert(key, value);
311+
}
312+
}
225313
}
226314

227315
fn create_from_update(update: Self::Update) -> Option<Self> {
228-
Some(update)
316+
let mut values = update.0;
317+
values.retain(|_k, v| *v != 0);
318+
Some(Store(values))
229319
}
230320
}
231321

232322
#[cfg(test)]
233323
macro_rules! store {
234324
($($name:ident: $val:expr),*$(,)?) => (
235325
{
326+
#[allow(unused_mut)]
236327
let mut store = crate::websocket::types::room::resources::Store::default();
237328

238329
$(

0 commit comments

Comments
 (0)