diff --git a/code/__DEFINES/cargo.dm b/code/__DEFINES/cargo.dm index e9fdda29368..f17c56482ec 100644 --- a/code/__DEFINES/cargo.dm +++ b/code/__DEFINES/cargo.dm @@ -13,6 +13,9 @@ #define SUPPLYPOD_X_OFFSET -16 +///DO NOT GO ANY LOWER THAN X1.4 the "CARGO_CRATE_VALUE" value if using regular crates, or infinite profit will be possible! This is also unit tested against. +#define CARGO_MINIMUM_COST CARGO_CRATE_VALUE * 1.4 + /// The baseline unit for cargo crates. Adjusting this will change the cost of all in-game shuttles, crate export values, bounty rewards, and all supply pack import values, as they use this as their unit of measurement. #define CARGO_CRATE_VALUE 200 @@ -29,6 +32,12 @@ /// Universal Scanner mode for using the price tagger. #define SCAN_PRICE_TAG 3 +// Defines for use with `export_item_and_contents()`, aka the export code that sells the items. +/// Default export define, these are things that are sold to centcom. +#define EXPORT_MARKET_STATION "supply" +/// Export market for pirates. +#define EXPORT_MARKET_PIRACY "piracy" + ///Used by coupons to define that they're cursed #define COUPON_OMEN "omen" diff --git a/code/game/machinery/spaceheater.dm b/code/game/machinery/spaceheater.dm index cbd5dbb84e3..1bbe40fd795 100644 --- a/code/game/machinery/spaceheater.dm +++ b/code/game/machinery/spaceheater.dm @@ -89,6 +89,11 @@ cell = null return ..() +/obj/machinery/space_heater/Exited(atom/movable/gone, direction) + . = ..() + if(gone == cell) + cell = null + /obj/machinery/space_heater/examine(mob/user) . = ..() . += "\The [src] is [on ? "on" : "off"], and the hatch is [panel_open ? "open" : "closed"]." @@ -288,7 +293,6 @@ if("eject") if(panel_open && cell) usr.put_in_hands(cell) - cell = null . = TRUE /obj/machinery/space_heater/proc/toggle_power(user) diff --git a/code/game/objects/structures/crates_lockers/crates/secure.dm b/code/game/objects/structures/crates_lockers/crates/secure.dm index e80542c32b1..40527d95bdd 100644 --- a/code/game/objects/structures/crates_lockers/crates/secure.dm +++ b/code/game/objects/structures/crates_lockers/crates/secure.dm @@ -111,6 +111,7 @@ icon_state = "atmos_secure" base_icon_state = "atmos_secure" + /obj/structure/closet/crate/secure/science name = "secure science crate" desc = "A crate with a lock on it, painted in the scheme of the station's scientists." diff --git a/code/modules/antagonists/pirate/pirate_shuttle_equipment.dm b/code/modules/antagonists/pirate/pirate_shuttle_equipment.dm index da9df063c90..10544818967 100644 --- a/code/modules/antagonists/pirate/pirate_shuttle_equipment.dm +++ b/code/modules/antagonists/pirate/pirate_shuttle_equipment.dm @@ -367,7 +367,7 @@ ///there are still licing mobs inside that item. Stop, don't sell it ffs. if(locate(/mob/living) in item_on_pad.get_all_contents()) continue - export_item_and_contents(item_on_pad, apply_elastic = FALSE, dry_run = dry_run, delete_unsold = FALSE, external_report = report, ignore_typecache = nosell_typecache) + export_item_and_contents(item_on_pad, apply_elastic = FALSE, dry_run = dry_run, delete_unsold = FALSE, external_report = report, ignore_typecache = nosell_typecache, export_market = EXPORT_MARKET_PIRACY) return report /// Prepares to sell the items on the pad @@ -401,6 +401,9 @@ pad.icon_state = pad.idle_state deltimer(sending_timer) +/datum/export/pirate + sales_market = EXPORT_MARKET_PIRACY + /// Attempts to find the thing on station /datum/export/pirate/proc/find_loot() return diff --git a/code/modules/bitrunning/objects/vendor.dm b/code/modules/bitrunning/objects/vendor.dm index 96c415b9bfe..119c7609fd8 100644 --- a/code/modules/bitrunning/objects/vendor.dm +++ b/code/modules/bitrunning/objects/vendor.dm @@ -77,6 +77,7 @@ hidden = TRUE crate_name = "bitrunning delivery crate" access = list(ACCESS_BIT_DEN) + abstract = TRUE /datum/supply_pack/bitrunning/New(purchaser, cost, list/contains) . = ..() diff --git a/code/modules/cargo/exports.dm b/code/modules/cargo/exports.dm index 5239ebceae2..099801290b4 100644 --- a/code/modules/cargo/exports.dm +++ b/code/modules/cargo/exports.dm @@ -46,8 +46,9 @@ Then the player gets the profit from selling his own wasted time. ** dry_run: if the item should be actually sold, or if it's just a pirce test ** external_report: works as "transaction" object, pass same one in if you're doing more than one export in single go ** ignore_typecache: typecache containing types that should be completely ignored + ** export_market: Defines the market that the items are being sold to. */ -/proc/export_item_and_contents(atom/movable/exported_atom, apply_elastic = TRUE, delete_unsold = TRUE, dry_run = FALSE, datum/export_report/external_report, list/ignore_typecache) +/proc/export_item_and_contents(atom/movable/exported_atom, apply_elastic = TRUE, delete_unsold = TRUE, dry_run = FALSE, datum/export_report/external_report, list/ignore_typecache, export_market = EXPORT_MARKET_STATION) external_report = init_export(external_report) var/list/contents = exported_atom.get_all_contents_ignoring(ignore_typecache) @@ -55,7 +56,7 @@ Then the player gets the profit from selling his own wasted time. // We go backwards, so it'll be innermost objects sold first. We also make sure nothing is accidentally delete before everything is sold. var/list/to_delete = list() for(var/atom/movable/thing as anything in reverse_range(contents)) - var/sold = _export_loop(thing, apply_elastic, dry_run, external_report) + var/sold = _export_loop(thing, apply_elastic, dry_run, external_report, export_market) if(!dry_run && (sold || delete_unsold) && sold != EXPORT_SOLD_DONT_DELETE) if(ismob(thing)) thing.investigate_log("deleted through cargo export", INVESTIGATE_CARGO) @@ -68,10 +69,10 @@ Then the player gets the profit from selling his own wasted time. return external_report /// It works like export_item_and_contents(), however it ignores the contents. Meaning only `exported_atom` will be valued. -/proc/export_single_item(atom/movable/exported_atom, apply_elastic = TRUE, delete_unsold = TRUE, dry_run = FALSE, datum/export_report/external_report) +/proc/export_single_item(atom/movable/exported_atom, apply_elastic = TRUE, delete_unsold = TRUE, dry_run = FALSE, datum/export_report/external_report, export_market = EXPORT_MARKET_STATION) external_report = init_export(external_report) - var/sold = _export_loop(exported_atom, apply_elastic, dry_run, external_report) + var/sold = _export_loop(exported_atom, apply_elastic, dry_run, external_report, export_market) if(!dry_run && (sold || delete_unsold) && sold != EXPORT_SOLD_DONT_DELETE) if(ismob(exported_atom)) exported_atom.investigate_log("deleted through cargo export", INVESTIGATE_CARGO) @@ -80,10 +81,10 @@ Then the player gets the profit from selling his own wasted time. return external_report /// The main bit responsible for selling the item. Shared by export_single_item() and export_item_and_contents() -/proc/_export_loop(atom/movable/exported_atom, apply_elastic = TRUE, dry_run = FALSE, datum/export_report/external_report) +/proc/_export_loop(atom/movable/exported_atom, apply_elastic = TRUE, dry_run = FALSE, datum/export_report/external_report, export_market) var/sold = EXPORT_NOT_SOLD for(var/datum/export/export as anything in GLOB.exports_list) - if(export.applies_to(exported_atom, apply_elastic)) + if(export.applies_to(exported_atom, apply_elastic, export_market)) if(!dry_run && (SEND_SIGNAL(exported_atom, COMSIG_ITEM_PRE_EXPORT) & COMPONENT_STOP_EXPORT)) break //Don't add value of unscannable items for a dry run report @@ -116,6 +117,8 @@ Then the player gets the profit from selling his own wasted time. var/list/exclude_types = list() /// Set to false if the cost shouldn't be determinable by an export scanner var/scannable = TRUE + /// Export market that this export applies to. Defaults to EXPORT_MARKET_STATION for items sold to the standard supply shuttle, replacements exist for pirates, etc. + var/sales_market = EXPORT_MARKET_STATION /// cost includes elasticity, this does not. var/init_cost @@ -157,13 +160,15 @@ Then the player gets the profit from selling his own wasted time. return 1 /// Checks if the item is fit for export datum. -/datum/export/proc/applies_to(obj/exported_item, apply_elastic = TRUE) +/datum/export/proc/applies_to(obj/exported_item, apply_elastic = TRUE, export_market) if(!is_type_in_typecache(exported_item, export_types)) return FALSE if(include_subtypes && is_type_in_typecache(exported_item, exclude_types)) return FALSE if(!get_cost(exported_item, apply_elastic)) return FALSE + if(export_market != sales_market) + return FALSE if(exported_item.flags_1 & HOLOGRAM_1) return FALSE return TRUE diff --git a/code/modules/cargo/exports/tools.dm b/code/modules/cargo/exports/tools.dm index 94e9c264365..eea9afe94a0 100644 --- a/code/modules/cargo/exports/tools.dm +++ b/code/modules/cargo/exports/tools.dm @@ -153,6 +153,6 @@ export_types = list(/obj/item/soap/omega) /datum/export/candle - cost = CARGO_CRATE_VALUE * 0.125 + cost = CARGO_CRATE_VALUE * 0.06125 unit_name = "candle" export_types = list(/obj/item/flashlight/flare/candle) diff --git a/code/modules/cargo/goodies.dm b/code/modules/cargo/goodies.dm index d2ea575618a..ba867de6031 100644 --- a/code/modules/cargo/goodies.dm +++ b/code/modules/cargo/goodies.dm @@ -3,6 +3,7 @@ access = NONE group = "Goodies" goody = TRUE + crate_type = null discountable = SUPPLY_PACK_STD_DISCOUNTABLE /datum/supply_pack/goody/clear_pda diff --git a/code/modules/cargo/packs/_packs.dm b/code/modules/cargo/packs/_packs.dm index 94f71245882..2119025b357 100644 --- a/code/modules/cargo/packs/_packs.dm +++ b/code/modules/cargo/packs/_packs.dm @@ -37,10 +37,12 @@ var/special_pod /// Was this spawned through an admin proc? var/admin_spawned = FALSE - /// Goodies can only be purchased by private accounts and can have coupons apply to them. They also come in a lockbox instead of a full crate, so the 700 min doesn't apply + /// Goodies can only be purchased by private accounts and can have coupons apply to them. They also come in a lockbox instead of a full crate, so the crate price min doesn't apply var/goody = FALSE /// Can coupons target this pack? If so, how rarely? var/discountable = SUPPLY_PACK_NOT_DISCOUNTABLE + /// Is this supply pack considered unpredictable for the purposes of testing unit testing? Examples include the stock market, or miner supply crates. If true, exempts from unit testing + var/abstract = FALSE /datum/supply_pack/New() id = type @@ -59,6 +61,12 @@ return data +/** + * Proc that takes a given supply_pack, and attempts to create a crate containing the pack's contents as determined by fill() + * + * @ atom/A: The location or turf that the pack is being generated onto. Cargo shuttle provides an empty turf, other generate()s call this either null or otherwise. + * @ datum/bank_account/paying_account: The account to associate the supply pack with when going and generating the crate. Only the paying account can open said secure crate/case. + */ /datum/supply_pack/proc/generate(atom/A, datum/bank_account/paying_account) var/obj/structure/closet/crate/C if(paying_account) @@ -121,6 +129,7 @@ hidden = TRUE crate_name = "shaft mining delivery crate" access = ACCESS_MINING + abstract = TRUE /datum/supply_pack/custom/New(purchaser, cost, list/contains) . = ..() diff --git a/code/modules/cargo/packs/general.dm b/code/modules/cargo/packs/general.dm index 5f3a4645933..03ec5be7738 100644 --- a/code/modules/cargo/packs/general.dm +++ b/code/modules/cargo/packs/general.dm @@ -178,7 +178,7 @@ /datum/supply_pack/misc/candles_bulk name = "Candle Box Crate" desc = "Keep your local chapel lit with three candle boxes!" - cost = CARGO_CRATE_VALUE * 1.5 + cost = CARGO_CRATE_VALUE * 2 contains = list(/obj/item/storage/fancy/candle_box = 3) crate_name = "candle box crate" @@ -214,6 +214,7 @@ contains = list() crate_name = "syndicate gear crate" crate_type = /obj/structure/closet/crate + abstract = TRUE // Not ///Total TC worth of contained uplink items var/crate_value = 30 ///What uplink the contents are pulled from diff --git a/code/modules/cargo/packs/livestock.dm b/code/modules/cargo/packs/livestock.dm index da51ee497f6..0ca10a57b82 100644 --- a/code/modules/cargo/packs/livestock.dm +++ b/code/modules/cargo/packs/livestock.dm @@ -231,6 +231,7 @@ /datum/supply_pack/critter/fish crate_type = /obj/structure/closet/crate + abstract = ABSTRACT // However, we should be wary of how possible it is to get more valuable fish out of this on average, depending on sample size. /datum/supply_pack/critter/fish/aquarium_fish name = "Aquarium Fish Case" diff --git a/code/modules/cargo/packs/materials.dm b/code/modules/cargo/packs/materials.dm index bfda2f7f9b1..a92d5542d47 100644 --- a/code/modules/cargo/packs/materials.dm +++ b/code/modules/cargo/packs/materials.dm @@ -75,6 +75,7 @@ cost = CARGO_CRATE_VALUE * 0.05 contains = list(/obj/machinery/portable_atmospherics/canister) crate_type = /obj/structure/closet/crate/large + abstract = TRUE /datum/supply_pack/materials/gas_canisters/generate_supply_packs() var/list/canister_packs = list() @@ -100,6 +101,8 @@ pack.cost = cost + moleCount * initial(gas.base_value) * 1.6 pack.cost = CEILING(pack.cost, 10) + pack.abstract = FALSE + pack.contains = list(GLOB.gas_id_to_canister[initial(gas.id)]) pack.crate_type = crate_type diff --git a/code/modules/cargo/packs/security.dm b/code/modules/cargo/packs/security.dm index 8227305f743..a6f762b568f 100644 --- a/code/modules/cargo/packs/security.dm +++ b/code/modules/cargo/packs/security.dm @@ -20,7 +20,7 @@ /datum/supply_pack/security/armor name = "Armor Crate" desc = "Three vests of well-rounded, decently-protective armor." - cost = CARGO_CRATE_VALUE * 2 + cost = CARGO_CRATE_VALUE * 3 access_view = ACCESS_SECURITY contains = list(/obj/item/clothing/suit/armor/vest = 3) crate_name = "armor crate" @@ -53,7 +53,7 @@ /datum/supply_pack/security/helmets name = "Helmets Crate" desc = "Contains three standard-issue brain buckets." - cost = CARGO_CRATE_VALUE * 2 + cost = CARGO_CRATE_VALUE * 3 contains = list(/obj/item/clothing/head/helmet/sec = 3) crate_name = "helmet crate" @@ -154,7 +154,7 @@ /datum/supply_pack/security/baton name = "Stun Batons Crate" desc = "Arm the Civil Protection Forces with three stun batons. Batteries included." - cost = CARGO_CRATE_VALUE * 2 + cost = CARGO_CRATE_VALUE * 3 access_view = ACCESS_SECURITY contains = list(/obj/item/melee/baton/security/loaded = 3) crate_name = "stun baton crate" diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index 5a16c56eb71..51bc02eb2e4 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -2983,7 +2983,7 @@ GLOBAL_LIST_EMPTY(fire_appearances) /** * Totals the physical cash on the mob and returns the total. */ -/mob/living/verb/tally_physical_credits() +/mob/living/proc/tally_physical_credits() //Here is all the possible non-ID payment methods. var/list/counted_money = list() var/physical_cash_total = 0 diff --git a/code/modules/mob/living/simple_animal/bot/mulebot.dm b/code/modules/mob/living/simple_animal/bot/mulebot.dm index e1117e3200f..7f6a2972807 100644 --- a/code/modules/mob/living/simple_animal/bot/mulebot.dm +++ b/code/modules/mob/living/simple_animal/bot/mulebot.dm @@ -86,7 +86,7 @@ suffix = null if(name == "\improper MULEbot") name = "\improper MULEbot [id]" - set_home(loc) + set_home(get_turf(src)) /mob/living/simple_animal/bot/mulebot/Exited(atom/movable/gone, direction) . = ..() diff --git a/code/modules/power/cell.dm b/code/modules/power/cell.dm index addb8cdb6b8..2b0072c8641 100644 --- a/code/modules/power/cell.dm +++ b/code/modules/power/cell.dm @@ -102,6 +102,7 @@ maxcharge = STANDARD_CELL_CHARGE * 10 custom_materials = list(/datum/material/glass=SMALL_MATERIAL_AMOUNT*0.6) chargerate = STANDARD_CELL_RATE * 0.75 + /obj/item/stock_parts/power_store/cell/high/empty empty = TRUE diff --git a/code/modules/unit_tests/_unit_tests.dm b/code/modules/unit_tests/_unit_tests.dm index 12200a7cce5..94bc3b250b1 100644 --- a/code/modules/unit_tests/_unit_tests.dm +++ b/code/modules/unit_tests/_unit_tests.dm @@ -114,6 +114,7 @@ #include "can_see.dm" #include "card_mismatch.dm" #include "cardboard_cutouts.dm" +#include "cargo_crate_sanity.dm" #include "cargo_dep_order_locations.dm" #include "cargo_selling.dm" #include "chain_pull_through_space.dm" diff --git a/code/modules/unit_tests/cargo_crate_sanity.dm b/code/modules/unit_tests/cargo_crate_sanity.dm new file mode 100644 index 00000000000..0e7e952db56 --- /dev/null +++ b/code/modules/unit_tests/cargo_crate_sanity.dm @@ -0,0 +1,42 @@ +/** + * This unit test loops through all cargo crates that are available to purchase, and confirms that they're below the expected sanity minimum when sold. + * This prevents us from merging a crate that sells for more that it costs to buy. + */ + +/datum/unit_test/cargo_crate_sanity + +/datum/unit_test/cargo_crate_sanity/Run() + + for(var/crate in subtypesof(/datum/supply_pack)) + var/datum/supply_pack/new_crate = allocate(crate) + if(new_crate.abstract) + continue // We can safely ignore custom supply packs like the stock market or mining supply crates. + if(!new_crate?.crate_type) + continue + var/obj/crate_type = allocate(new_crate.crate_type) + var/turf/open/floor/testing_floor = get_turf(crate_type) + var/datum/export_report/minimum_cost = export_item_and_contents(crate_type, delete_unsold = TRUE, dry_run = TRUE) + var/crate_value = counterlist_sum(minimum_cost.total_value) + + var/obj/results = new_crate.generate(testing_floor) + var/datum/export_report/export_log = export_item_and_contents(results, apply_elastic = TRUE, delete_unsold = TRUE, export_market = EXPORT_MARKET_STATION) + + // The value of the crate and all of it's contents. + var/value = counterlist_sum(export_log.total_value) + + // We're selling the crate and it's contents for more value than it's supply_pack costs. + if(value > new_crate.get_cost()) + TEST_FAIL("Cargo crate [new_crate.type] had a sale value of [value], Selling for more than [new_crate.get_cost()], the cost to buy") + + // We're selling the crate & it's contents for less than the value of it's own crate, meaning you can buy and infinite number + if(crate_value > new_crate.get_cost()) + TEST_FAIL("Cargo crate [new_crate.type] container sells for [crate_value], Selling for more than [new_crate.get_cost()], the cost to buy") + for(var/atom/stuff as anything in results.contents) + qdel(stuff) + stuff = null + + qdel(results) + results = null + new_crate = null + minimum_cost = null + export_log = null