Allow doors to open.

This commit is contained in:
Condorra 2023-04-16 01:54:03 +10:00
parent 284c49b4a1
commit 131512fbf6
15 changed files with 378 additions and 19 deletions

View File

@ -739,11 +739,10 @@ impl DBTrans {
self.pg_trans()?.execute("UPDATE items SET details=\
JSONB_SET(details, '{action_type}', $1) \
WHERE details->>'location' = $2 AND \
details->>'action_type' = $3",
details->>'action_type' = $3::JSONB::TEXT",
&[&serde_json::to_value(other_item_action_type)?,
&item.location,
&serde_json::to_value(new_action_type)?
.as_str().unwrap()
]).await?;
self.pg_trans()?.execute("UPDATE items SET details=\
JSONB_SET(details, '{action_type}', $1) \
@ -760,9 +759,9 @@ impl DBTrans {
if let Some(item) = self.pg_trans()?.query_opt(
"SELECT details FROM items WHERE \
details->>'location' = $1 AND \
details->>'action_type' = $2",
details->>'action_type' = $2::JSONB::TEXT",
&[&location,
&serde_json::to_value(action_type)?.as_str().unwrap()]).await? {
&serde_json::to_value(action_type)?]).await? {
return Ok(Some(Arc::new(serde_json::from_value::<Item>(item.get("details"))?)));
}
Ok(None)

View File

@ -29,6 +29,7 @@ mod login;
mod look;
mod map;
pub mod movement;
pub mod open;
mod page;
pub mod parsing;
mod quit;
@ -145,6 +146,8 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
"gm" => map::VERB,
"gmap" => map::VERB,
"open" => open::VERB,
"p" => page::VERB,
"page" => page::VERB,
"pg" => page::VERB,

View File

@ -45,8 +45,8 @@ static REGISTERED_HELP_PAGES: phf::Map<&'static str, &'static str> = phf_map! {
ansi!("So you've just landed in BlastMud, and want to know how to get started?\n\
You control your character, and can tell your character to move around the\n\
world, and see things through their eyes.\n\
The world (yes, even outside!) is divided up into rooms, and each room has\n
exits that you are allowed to take, normally called north, south, east, west,\n
The world (yes, even outside!) is divided up into rooms, and each room has\n\
exits that you are allowed to take, normally called north, south, east, west,\n\
northeast, northwest, southeast, southwest, up and down (sometimes you can also go in).\n\
\n\
Try <bold>look<reset> (or <bold>l<reset>) to look at the current room. It will\n\
@ -56,7 +56,9 @@ static REGISTERED_HELP_PAGES: phf::Map<&'static str, &'static str> = phf_map! {
<bold>l<reset> followed by the name of the object (shortening is okay).\n\
Once you know what direction to move, you can type the direction, using either\n\
the full name of the direction (e.g. <bold>northeast<reset> or <bold>south<reset>,\n\
or a short form (<bold>n<reset>, <bold>e<reset>, <bold>s<reset>, <bold>w<reset>, <bold>ne<reset>, <bold>nw<reset>, <bold>se<reset>, <bold>sw<reset>, <bold>up<reset>, <bold>down<reset>)."),
or a short form (<bold>n<reset>, <bold>e<reset>, <bold>s<reset>, <bold>w<reset>, <bold>ne<reset>, <bold>nw<reset>, <bold>se<reset>, <bold>sw<reset>, <bold>up<reset>, <bold>down<reset>).\n\n\
Also try the map commands - <bold>lmap<reset> for a local map including exits, and <bold>gmap<reset> for a giant \
map to let you see the broader context."),
"movement" =>
ansi!("Once you know what direction to move, you can type the direction, using either\n\
the full name of the direction (e.g. <bold>northeast<reset> or <bold>south<reset>,\n\

View File

@ -241,7 +241,7 @@ async fn list_room_contents<'l>(ctx: &'l VerbContext<'_>, item: &'l Item) -> URe
Ok(buf)
}
async fn direction_to_item(
pub async fn direction_to_item(
trans: &DBTrans,
use_location: &str,
direction: &Direction,

View File

@ -0,0 +1,255 @@
use super::{
VerbContext, UserVerb, UserVerbRef, UResult, UserError, user_error,
get_player_item_or_fail,
look::direction_to_item,
};
use async_trait::async_trait;
use crate::{
DResult,
regular_tasks::{
TaskRunContext,
TaskHandler,
queued_command::{
QueueCommandHandler,
QueueCommand,
queue_command
},
},
static_content::{
room::Direction,
possession_type::possession_data,
},
models::{
item::{Item, LocationActionType},
task::{Task, TaskMeta, TaskDetails}
},
services::comms::broadcast_to_room,
};
use std::sync::Arc;
use std::time;
use chrono::{self, Utc};
use log::info;
#[derive(Clone)]
pub struct SwingShutHandler;
pub static SWING_SHUT_HANDLER: &'static (dyn TaskHandler + Sync + Send) = &SwingShutHandler;
#[async_trait]
impl TaskHandler for SwingShutHandler {
async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult<Option<time::Duration>> {
info!("Starting swing shut");
let (room_str, direction) = match &ctx.task.details {
TaskDetails::SwingShut { room_item, direction } => (room_item, direction),
_ => { return Ok(None); }
};
let (room_item_type, room_item_code) = match room_str.split_once("/") {
None => { return Ok(None); },
Some(v) => v
};
let room_item = match ctx.trans.find_item_by_type_code(room_item_type, room_item_code).await? {
None => { return Ok(None); },
Some(v) => v
};
let mut room_item_mut = (*room_item).clone();
let mut door_state = match room_item_mut.door_states.as_mut().and_then(|ds| ds.get_mut(&direction)) {
None => { return Ok(None); },
Some(v) => v
};
(*door_state).open = false;
ctx.trans.save_item_model(&room_item_mut).await?;
let msg = format!("The door to the {} swings shut with a click.\n",
&direction.describe());
broadcast_to_room(&ctx.trans, &room_str, None, &msg, Some(&msg)).await?;
if let Ok(Some(other_room)) = direction_to_item(&ctx.trans, &room_str, &direction).await {
let msg = format!("The door to the {} swings shut with a click.\n",
&direction.reverse().map(|d| d.describe()).unwrap_or_else(|| "outside".to_owned()));
broadcast_to_room(&ctx.trans, &other_room.refstr(), None, &msg, Some(&msg)).await?;
}
info!("Finishing swing shut");
Ok(None)
}
}
pub struct QueueHandler;
#[async_trait]
impl QueueCommandHandler for QueueHandler {
async fn start_command(&self, ctx: &mut VerbContext<'_>, command: &QueueCommand)
-> UResult<time::Duration> {
info!("Starting open start_command");
let direction = match command {
QueueCommand::OpenDoor { direction } => direction,
_ => user_error("Unexpected command".to_owned())?
};
info!("Direction good");
let player_item = get_player_item_or_fail(ctx).await?;
info!("Got player");
match is_door_in_direction(ctx, &direction, &player_item).await? {
DoorSituation::NoDoor => user_error("There is no door to open.".to_owned())?,
DoorSituation::DoorIntoRoom { is_open: true, .. } | DoorSituation::DoorOutOfRoom { is_open: true, .. } =>
user_error("The door is already open.".to_owned())?,
DoorSituation::DoorIntoRoom { room_with_door: entering_room, .. } => {
let entering_room_loc = entering_room.refstr();
if let Some(revdir) = direction.reverse() {
if let Some(lock) = ctx.trans.find_by_action_and_location(
&entering_room_loc,
&LocationActionType::InstalledOnDoorAsLock(revdir)).await?
{
if let Some(lockcheck) = lock.possession_type.as_ref()
.and_then(|pt| possession_data().get(pt))
.and_then(|pd| pd.lockcheck_handler) {
lockcheck.cmd(ctx, &player_item, &lock).await?
}
}
}
}
_ => {}
}
info!("Clean exit open start_command");
Ok(time::Duration::from_secs(1))
}
#[allow(unreachable_patterns)]
async fn finish_command(&self, ctx: &mut VerbContext<'_>, command: &QueueCommand)
-> UResult<()> {
info!("Starting open finish_command");
let direction = match command {
QueueCommand::OpenDoor { direction } => direction,
_ => user_error("Unexpected command".to_owned())?
};
let player_item = get_player_item_or_fail(ctx).await?;
let (room_1, dir_in_room, room_2) = match is_door_in_direction(ctx, &direction, &player_item).await? {
DoorSituation::NoDoor => user_error("There is no door to open.".to_owned())?,
DoorSituation::DoorIntoRoom { is_open: true, .. } |
DoorSituation::DoorOutOfRoom { is_open: true, .. } =>
user_error("The door is already open.".to_owned())?,
DoorSituation::DoorIntoRoom { room_with_door, current_room, .. } => {
let entering_room_loc = room_with_door.refstr();
if let Some(revdir) = direction.reverse() {
if let Some(lock) = ctx.trans.find_by_action_and_location(
&entering_room_loc,
&LocationActionType::InstalledOnDoorAsLock(revdir.clone())).await?
{
if let Some(lockcheck) = lock.possession_type.as_ref()
.and_then(|pt| possession_data().get(pt))
.and_then(|pd| pd.lockcheck_handler) {
lockcheck.cmd(ctx, &player_item, &lock).await?
}
}
let mut entering_room_mut = (*room_with_door).clone();
if let Some(door_map) = entering_room_mut.door_states.as_mut() {
if let Some(door) = door_map.get_mut(&direction) {
(*door).open = true;
}
}
ctx.trans.save_item_model(&entering_room_mut).await?;
(room_with_door, revdir, current_room)
} else {
user_error("There's no door possible there.".to_owned())?
}
},
DoorSituation::DoorOutOfRoom { room_with_door, new_room, .. } => {
let mut entering_room_mut = (*room_with_door).clone();
if let Some(door_map) = entering_room_mut.door_states.as_mut() {
if let Some(door) = door_map.get_mut(&direction) {
(*door).open = true;
}
}
ctx.trans.save_item_model(&entering_room_mut).await?;
(room_with_door, direction.clone(), new_room)
}
};
for (loc, dir) in [(&room_1.refstr(), &dir_in_room.describe()),
(&room_2.refstr(), &dir_in_room.reverse().map(|d| d.describe())
.unwrap_or_else(|| "outside".to_owned()))] {
broadcast_to_room(
&ctx.trans,
loc,
None,
&format!("{} opens the door to the {}.\n",
&player_item.display_for_sentence(true, 1, true),
dir
),
Some(
&format!("{} opens the door to the {}.\n",
&player_item.display_for_sentence(false, 1, true),
dir
)
)
).await?;
}
ctx.trans.upsert_task(&Task {
meta: TaskMeta {
task_code: format!("{}/{}", &room_1.refstr(), &direction.describe()),
next_scheduled: Utc::now() + chrono::Duration::seconds(120),
..Default::default()
},
details: TaskDetails::SwingShut {
room_item: room_1.refstr(),
direction: dir_in_room.clone()
}
}).await?;
info!("Clean exit open finish_command");
Ok(())
}
}
pub enum DoorSituation {
NoDoor,
DoorIntoRoom { is_open: bool, room_with_door: Arc<Item>, current_room: Arc<Item> }, // Can be locked etc...
DoorOutOfRoom { is_open: bool, room_with_door: Arc<Item>, new_room: Arc<Item> } // No lockable.
}
pub async fn is_door_in_direction(ctx: &mut VerbContext<'_>, direction: &Direction, player_item: &Item) ->
UResult<DoorSituation> {
let (loc_type_t, loc_type_c) = player_item.location.split_once("/")
.ok_or_else(|| UserError("Invalid location".to_owned()))?;
let cur_loc_item = ctx.trans.find_item_by_type_code(loc_type_t, loc_type_c).await?
.ok_or_else(|| UserError("Can't find your current location anymore.".to_owned()))?;
let new_loc_item = direction_to_item(ctx.trans, &player_item.location, direction).await?
.ok_or_else(|| UserError("That exit doesn't really seem to go anywhere!".to_owned()))?;
if let Some(door_state) =
cur_loc_item.door_states.as_ref()
.and_then(|v| v.get(direction)) {
return Ok(DoorSituation::DoorOutOfRoom {
is_open: door_state.open,
room_with_door: cur_loc_item,
new_room: new_loc_item
});
}
if let Some(door_state) =
new_loc_item.door_states.as_ref()
.and_then(|v| direction.reverse().as_ref()
.and_then(|rev| v.get(rev).map(|door| door.clone()))) {
return Ok(DoorSituation::DoorIntoRoom {
is_open: door_state.open,
room_with_door: new_loc_item,
current_room: cur_loc_item
});
}
Ok(DoorSituation::NoDoor)
}
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> {
let dir = Direction::parse(remaining)
.ok_or_else(|| UserError("Unknown direction".to_owned()))?;
info!("Queueing open");
queue_command(ctx, &QueueCommand::OpenDoor { direction: dir.clone() }).await?;
info!("Returning from open handler");
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -256,7 +256,8 @@ pub enum LocationActionType {
Reclining,
Worn, // Clothing etc...
Wielded,
Attacking(Subattack)
Attacking(Subattack),
InstalledOnDoorAsLock(Direction),
}
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
@ -269,7 +270,8 @@ pub enum Sex {
pub enum ItemFlag {
NoSay,
NoSeeContents,
DroppedItemsDontExpire
DroppedItemsDontExpire,
PrivatePlace,
}
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
@ -307,6 +309,11 @@ pub struct DynamicEntrance {
pub source_item: String,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, PartialOrd)]
pub struct DoorState {
pub open: bool,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, PartialOrd)]
#[serde(default)]
pub struct Item {
@ -338,6 +345,7 @@ pub struct Item {
pub special_data: Option<ItemSpecialData>,
pub dynamic_entrance: Option<DynamicEntrance>,
pub owner: Option<String>,
pub door_states: Option<BTreeMap<Direction, DoorState>>
}
impl Item {
@ -384,6 +392,10 @@ impl Item {
)
)
}
pub fn refstr(&self) -> String {
format!("{}/{}", &self.item_type, &self.item_code)
}
}
impl Default for Item {
@ -417,6 +429,7 @@ impl Default for Item {
special_data: None,
dynamic_entrance: None,
owner: None,
door_states: None,
}
}
}

View File

@ -3,6 +3,7 @@ use serde_json::Value;
use chrono::{DateTime, Utc};
use crate::services::effect::DelayedHealthEffect;
use std::collections::VecDeque;
use crate::static_content::room::Direction;
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum TaskRecurrence {
@ -40,6 +41,10 @@ pub enum TaskDetails {
ChargeRoom {
zone_item: String,
daily_price: u64
},
SwingShut {
room_item: String,
direction: Direction
}
}
impl TaskDetails {
@ -56,6 +61,7 @@ impl TaskDetails {
DelayedHealth { .. } => "DelayedHealth",
ExpireItem { .. } => "ExpireItem",
ChargeRoom { .. } => "ChargeRoom",
SwingShut { .. } => "SwingShut",
// Don't forget to add to TASK_HANDLER_REGISTRY in regular_tasks.rs too.
}
}

View File

@ -7,7 +7,7 @@ use crate::{
listener::{ListenerMap, ListenerSend},
static_content::npc,
services::{combat, effect},
message_handler::user_commands::{drop, rent},
message_handler::user_commands::{drop, rent, open},
};
#[cfg(not(test))] use crate::models::task::{TaskParse, TaskRecurrence};
use mockall_double::double;
@ -45,7 +45,8 @@ fn task_handler_registry() -> &'static BTreeMap<&'static str, &'static (dyn Task
("RotCorpse", combat::ROT_CORPSE_HANDLER.clone()),
("DelayedHealth", effect::DELAYED_HEALTH_HANDLER.clone()),
("ExpireItem", drop::EXPIRE_ITEM_HANDLER.clone()),
("ChargeRoom", rent::CHARGE_ROOM_HANDLER.clone())
("ChargeRoom", rent::CHARGE_ROOM_HANDLER.clone()),
("SwingShut", open::SWING_SHUT_HANDLER.clone()),
).into_iter().collect()
)
}

View File

@ -20,7 +20,8 @@ use crate::message_handler::user_commands::{
use_cmd,
wield,
user_error,
get_user_or_fail
get_user_or_fail,
open
};
use crate::static_content::room::Direction;
use once_cell::sync::OnceCell;
@ -32,6 +33,7 @@ pub enum QueueCommand {
Use { possession_id: String, target_id: String },
Get { possession_id: String },
Drop { possession_id: String },
OpenDoor { direction: Direction },
}
impl QueueCommand {
pub fn name(&self) -> &'static str {
@ -42,6 +44,7 @@ impl QueueCommand {
Use {..} => "Use",
Get {..} => "Get",
Drop {..} => "Drop",
OpenDoor {..} => "OpenDoor",
}
}
}
@ -61,6 +64,7 @@ fn queue_command_registry() -> &'static BTreeMap<&'static str, &'static (dyn Que
("Movement", &movement::QueueHandler as &(dyn QueueCommandHandler + Sync + Send)),
("Use", &use_cmd::QueueHandler as &(dyn QueueCommandHandler + Sync + Send)),
("Wield", &wield::QueueHandler as &(dyn QueueCommandHandler + Sync + Send)),
("OpenDoor", &open::QueueHandler as &(dyn QueueCommandHandler + Sync + Send)),
).into_iter().collect())
}
@ -79,7 +83,7 @@ pub async fn queue_command(ctx: &mut VerbContext<'_>, command: &QueueCommand) ->
Err(CommandHandlingError::UserError(err_msg)) => {
ctx.session_dat.queue.truncate(0);
ctx.trans.save_session_model(ctx.session, ctx.session_dat).await?;
user_error(err_msg)?;
ctx.trans.queue_for_session(&ctx.session, Some(&(err_msg + "\r\n"))).await?;
}
Err(CommandHandlingError::SystemError(e)) => Err(e)?,
Ok(dur) => {

View File

@ -5,7 +5,7 @@
use super::room::{Direction, GridCoords};
use crate::{
message_handler::user_commands::{user_error, UResult},
models::item::{Item, ItemFlag, ItemSpecialData, DynamicEntrance}
models::item::{Item, ItemFlag, ItemSpecialData, DynamicEntrance, DoorState}
};
use once_cell::sync::OnceCell;
use std::collections::BTreeMap;
@ -98,6 +98,15 @@ impl Dynzone {
} else { None },
flags: room.item_flags.clone(),
owner: Some(owner.clone()),
door_states: Some(room.exits.iter()
.filter_map(|ex|
if ex.exit_type == ExitType::Doored {
Some((ex.direction.clone(), DoorState {
open: false
}))
} else {
None
}).collect()),
..Default::default()
}
).await?;
@ -118,6 +127,9 @@ impl Default for Dynzone {
}
}
// Note that either the room being entered or the room being left should be
// doored, not both. And doors should be on the inner-most room (furthest from
// public) - locks only protect entry into the room, not exit from it.
#[derive(Eq, Clone, PartialEq, Ord, PartialOrd)]
pub enum ExitType {
Doorless,

View File

@ -8,6 +8,7 @@ use super::{
super::room::GridCoords
};
use crate::static_content::room::Direction;
use crate::models::item::ItemFlag;
pub fn zone() -> Dynzone {
Dynzone {
@ -30,7 +31,7 @@ pub fn zone() -> Dynzone {
Exit {
direction: Direction::EAST,
target: ExitTarget::Intrazone { subcode: "studio" },
exit_type: ExitType::Doored
exit_type: ExitType::Doorless
}
),
grid_coords: GridCoords { x: 0, y: 0, z: 0 },
@ -53,7 +54,8 @@ pub fn zone() -> Dynzone {
),
grid_coords: GridCoords { x: 1, y: 0, z: 0 },
should_caption: true,
item_flags: vec!(),
item_flags: vec!(ItemFlag::DroppedItemsDontExpire,
ItemFlag::PrivatePlace),
..Default::default()
})
).into_iter().collect(),

View File

@ -14,6 +14,7 @@ mod fangs;
mod antenna_whip;
mod trauma_kit;
mod corp_licence;
mod lock;
pub type AttackMessageChoice = Vec<Box<dyn Fn(&Item, &Item, bool) -> String + 'static + Sync + Send>>;
pub type AttackMessageChoicePart = Vec<Box<dyn Fn(&Item, &Item, &BodyPart, bool) -> String + 'static + Sync + Send>>;
@ -135,6 +136,7 @@ pub struct PossessionData {
pub weight: u64,
pub write_handler: Option<&'static (dyn WriteHandler + Sync + Send)>,
pub sign_handler: Option<&'static (dyn ArglessHandler + Sync + Send)>,
pub lockcheck_handler: Option<&'static (dyn ArglessHandler + Sync + Send)>,
}
impl Default for PossessionData {
@ -153,6 +155,7 @@ impl Default for PossessionData {
use_data: None,
write_handler: None,
sign_handler: None,
lockcheck_handler: None,
}
}
}
@ -190,6 +193,7 @@ pub enum PossessionType {
EmptyMedicalBox,
NewCorpLicence,
CertificateOfIncorporation,
Scanlock,
}
impl Into<Item> for PossessionType {
@ -257,6 +261,7 @@ pub fn possession_data() -> &'static BTreeMap<PossessionType, PossessionData> {
(EmptyMedicalBox, trauma_kit::empty_data()),
(NewCorpLicence, corp_licence::data()),
(CertificateOfIncorporation, corp_licence::cert_data()),
(Scanlock, lock::scan()),
).into_iter().collect()
})
}

View File

@ -0,0 +1,11 @@
use super::PossessionData;
pub fn scan() -> PossessionData {
PossessionData {
display: "scanlock",
details: "A relatively basic lock with a fingerprint scanner built into it, made to ensure only the owner of something can enter or use it.",
aliases: vec!("lock"),
weight: 500,
..Default::default()
}
}

View File

@ -76,9 +76,8 @@ pub trait ExitBlocker {
}
pub enum ExitType {
Free, // Anyone can just walk it.
Free, // Anyone can just walk it (subject to any door logic).
Blocked(&'static (dyn ExitBlocker + Sync + Send)), // Custom code about who can pass.
// Future ideas: Doors with locks, etc...
}
#[allow(dead_code)]
@ -147,6 +146,22 @@ impl Direction {
_ => None
}
}
pub fn reverse(&self) -> Option<Direction> {
match self {
Direction::NORTH => Some(Direction::SOUTH),
Direction::SOUTH => Some(Direction::NORTH),
Direction::EAST => Some(Direction::WEST),
Direction::WEST => Some(Direction::EAST),
Direction::NORTHEAST => Some(Direction::SOUTHWEST),
Direction::SOUTHEAST => Some(Direction::NORTHWEST),
Direction::NORTHWEST => Some(Direction::SOUTHEAST),
Direction::SOUTHWEST => Some(Direction::NORTHEAST),
Direction::UP => Some(Direction::DOWN),
Direction::DOWN => Some(Direction::UP),
Direction::IN { .. } => None
}
}
}
#[derive(Eq,Ord,Debug,PartialEq,PartialOrd,Clone)]

View File

@ -3283,10 +3283,41 @@ pub fn room_list() -> Vec<Room> {
target: ExitTarget::UseGPS,
exit_type: ExitType::Free
},
Exit {
direction: Direction::SOUTHEAST,
target: ExitTarget::UseGPS,
exit_type: ExitType::Free
}
),
should_caption: false,
..Default::default()
},
Room {
zone: "melbs",
secondary_zones: vec!(),
code: "melbs_lockedandloaded",
name: "Locked & Loaded",
short: ansi!("<yellow>LL<reset>"),
description: "This seems to be some kind of security shop, selling locks from super high-tech to primitive. Behind a counter sits a grizzled old man, who appears eager to sell you something",
description_less_explicit: None,
grid_coords: GridCoords { x: 7, y: 8, z: 0 },
exits: vec!(
Exit {
direction: Direction::NORTHWEST,
target: ExitTarget::UseGPS,
exit_type: ExitType::Free
},
),
stock_list: vec!(
RoomStock {
possession_type: PossessionType::Scanlock,
list_price: 200,
..Default::default()
}
),
should_caption: true,
..Default::default()
},
Room {
zone: "melbs",
secondary_zones: vec!(),