SUBSYSTEM_DEF(supply)
name = "Supply"
wait = 300
// Supply Points
var/points = 50
var/points_per_second = 1.5 / 30
var/points_per_slip = 2
var/points_per_money = 0.06 // 1 point for $50
var/points_per_trash = 0.1 // Weighted value, tentative.
// Control
var/ordernum
var/list/shoppinglist = list() // Approved orders
var/list/legacy_supply_packs = list() // All supply packs
var/static/list/legacy_supply_categories = list()
var/list/exported_crates = list() // Crates sent from the station
var/list/order_history = list() // History of orders, showing edits made by users
var/list/adm_order_history = list() // Complete history of all orders, for admin use
var/list/adm_export_history = list() // Complete history of all crates sent back on the shuttle, for admin use
// Shuttle Movement
var/movetime = 1200
var/datum/shuttle/autodock/ferry/supply/shuttle
// TODO - Refactor to use the Supply Subsystem (SSsupply)
// Supply packs are in /code/datums/supplypacks
// Computers are in /code/game/machinery/computer/supply.dm
/datum/supply_order
var/ordernum // Unfabricatable index
var/index // Fabricatable index
var/datum/supply_pack/object = null
var/cost // Cost of the supply pack (Fabricatable) (Changes not reflected when purchasing supply packs, this is cosmetic only)
var/name // Name of the supply pack datum (Fabricatable)
var/ordered_by = null // Who requested the order
var/comment = null // What reason was given for the order
var/approved_by = null // Who approved the order
var/ordered_at // Date and time the order was requested at
var/approved_at // Date and time the order was approved at
var/status // [Requested, Accepted, Denied, Shipped]
/datum/exported_crate
var/name
var/value
var/list/contents
/datum/controller/subsystem/supply/Initialize()
ordernum = rand(1,9000)
for(var/typepath in subtypesof(/datum/supply_pack))
var/datum/supply_pack/P = new typepath()
if(!P.legacy)
continue
legacy_supply_packs[P.name] = P
legacy_supply_categories[P.category] = TRUE
P.initialize()
//Alphabetize legacy_supply_packs
tim_sort(legacy_supply_packs, /proc/cmp_auto_compare)
var/list/flattened = list()
for(var/key in legacy_supply_categories)
flattened += key
//Alphabetize flattened
tim_sort(flattened, /proc/cmp_auto_compare)
legacy_supply_categories = flattened
return SS_INIT_SUCCESS
// Supply shuttle SSticker - handles supply point regeneration
// This is called by the process scheduler every thirty seconds
/datum/controller/subsystem/supply/fire(resumed)
points += max(0, ((world.time - last_fire) / 10) * points_per_second)
// To stop things being sent to CentCom which should not be sent to centcom. Recursively checks for these types.
/datum/controller/subsystem/supply/proc/forbidden_atoms_check(atom/A)
if(isliving(A))
return 1
if(istype(A,/obj/item/disk/nuclear))
return 1
if(istype(A,/obj/machinery/nuclearbomb))
return 1
if(istype(A,/obj/item/radio/beacon))
return 1
if(istype(A,/obj/item/perfect_tele_beacon))
return 1
for(var/atom/B in A.contents)
if(.(B))
return 1
// Selling
/datum/controller/subsystem/supply/proc/sell()
// Loop over each area in the supply shuttle
for(var/area/subarea in shuttle.shuttle_area)
callHook("sell_shuttle", list(subarea));
for(var/atom/movable/MA in subarea)
if(MA.anchored)
continue
var/datum/exported_crate/EC = new /datum/exported_crate()
EC.name = "\proper[MA.name]"
EC.value = 0
EC.contents = list()
var/base_value = 0
// Must be in a crate!
if(istype(MA,/obj/structure/closet/crate))
var/obj/structure/closet/crate/CR = MA
callHook("sell_crate", list(CR, subarea))
points += points_per_money * CR.get_worth(GET_WORTH_FLAGS_SUPPLY_DETECTION)
var/find_slip = 1
for(var/atom/A in CR)
EC.contents[++EC.contents.len] = list(
"object" = "\proper[A.name]",
"value" = 0,
"quantity" = 1
)
// Sell manifests
if(find_slip && istype(A,/obj/item/paper/manifest))
var/obj/item/paper/manifest/slip = A
if(!slip.is_copy && slip.stamped && slip.stamped.len) // Yes, the clown stamp will work. clown is the highest authority on the station, it makes sense, trust me guys
points += points_per_slip
EC.contents[EC.contents.len]["value"] = points_per_slip
find_slip = 0
continue
// Sell phoron and platinum
if(istype(A, /obj/item/stack/material))
var/obj/item/stack/material/P = A
if(istype(P.material))
var/thaler_per_sheet = P.material.worth
var/total_thaler = thaler_per_sheet * P.amount
var/total_points = total_thaler * points_per_money
EC.contents[EC.contents.len]["value"] = total_points
EC.contents[EC.contents.len]["quantity"] = P.amount
EC.value += EC.contents[EC.contents.len]["value"]
// Sell spacebucks
if(istype(A, /obj/item/spacecash))
var/obj/item/spacecash/cashmoney = A
EC.contents[EC.contents.len]["value"] = cashmoney.worth * points_per_money
EC.contents[EC.contents.len]["quantity"] = cashmoney.worth
EC.value += EC.contents[EC.contents.len]["value"]
// Sell trash
if(istype(A, /obj/item/trash))
EC.contents[EC.contents.len]["value"] = points_per_trash
// Make a log of it, but it wasn't shipped properly, and so isn't worth anything
else
EC.contents = list(
"error" = "Error: Product was improperly packaged. Payment rendered null under terms of agreement."
)
exported_crates += EC
points += EC.value
EC.value += base_value
// Duplicate the receipt for the admin-side log
var/datum/exported_crate/adm = new()
adm.name = EC.name
adm.value = EC.value
adm.contents = deep_copy_list(EC.contents)
adm_export_history += adm
qdel(MA)
/datum/controller/subsystem/supply/proc/get_clear_turfs()
var/list/clear_turfs = list()
for(var/area/subarea in shuttle.shuttle_area)
for(var/turf/T in subarea)
if(T.density)
continue
var/occupied = 0
for(var/atom/A in T.contents)
if((A.atom_flags & ATOM_ABSTRACT))
continue
occupied = 1
break
if(!occupied)
clear_turfs += T
return clear_turfs
// Buying
/datum/controller/subsystem/supply/proc/buy()
var/list/shoppinglist = list()
for(var/datum/supply_order/SO in order_history)
if(SO.status == SUP_ORDER_APPROVED)
shoppinglist += SO
if(!shoppinglist.len)
return
var/orderedamount = shoppinglist.len
var/list/clear_turfs = get_clear_turfs()
for(var/datum/supply_order/SO in shoppinglist)
// if there's no space left don't cram in the rest
var/turf/T = pick_n_take(clear_turfs)
if(!T)
break
SO.status = SUP_ORDER_SHIPPED
var/datum/supply_pack/SP = SO.object
var/atom/movable/container = SP.instantiate_pack_at(T)
// todo: containerless support
if(!container)
continue
if(SO.comment)
container.name += " ([SO.comment])"
// Supply manifest generation begin
var/obj/item/paper/manifest/slip
if(!SP.legacy_contraband)
slip = new /obj/item/paper/manifest(container)
slip.is_copy = 0
// save the trip to the string tree
var/list/info = list()
info += "
[command_name()] Shipping Manifest
"
info +="Order #[SO.ordernum]
"
info +="Destination: [station_name()]
"
info +="[orderedamount] PACKAGES IN THIS SHIPMENT
"
info += "
"
info += SP.get_html_manifest(container)
info += "
"
info += "CHECK CONTENTS AND STAMP BELOW THE LINE TO CONFIRM RECEIPT OF GOODS"
info += "
"
slip.info += info.Join("")
// Will attempt to purchase the specified order, returning TRUE on success, FALSE on failure
/datum/controller/subsystem/supply/proc/approve_order(var/datum/supply_order/O, var/mob/user)
// do not double purchase!!
if(O.status != SUP_ORDER_REQUESTED)
return FALSE
// Not enough points to purchase the crate
if(SSsupply.points <= O.object.legacy_cost)
return FALSE
// Based on the current model, there shouldn't be any entries in order_history, requestlist, or shoppinglist, that aren't matched in adm_order_history
var/datum/supply_order/adm_order
for(var/datum/supply_order/temp in adm_order_history)
if(temp.ordernum == O.ordernum)
adm_order = temp
break
var/idname = "*None Provided*"
if(ishuman(user))
var/mob/living/carbon/human/H = user
idname = H.get_authentification_name()
else if(issilicon(user))
idname = user.real_name
// Update order status
O.status = SUP_ORDER_APPROVED
O.approved_by = idname
O.approved_at = stationdate2text() + " - " + stationtime2text()
// Update admin-side mirror
adm_order.status = SUP_ORDER_APPROVED
adm_order.approved_by = idname
adm_order.approved_at = stationdate2text() + " - " + stationtime2text()
// Deduct cost
SSsupply.points -= O.object.legacy_cost
return TRUE
// Will deny the specified order. Only useful if the order is currently requested, but available at any status
/datum/controller/subsystem/supply/proc/deny_order(var/datum/supply_order/O, var/mob/user)
// Based on the current model, there shouldn't be any entries in order_history, requestlist, or shoppinglist, that aren't matched in adm_order_history
var/datum/supply_order/adm_order
for(var/datum/supply_order/temp in adm_order_history)
if(temp.ordernum == O.ordernum)
adm_order = temp
break
var/idname = "*None Provided*"
if(ishuman(user))
var/mob/living/carbon/human/H = user
idname = H.get_authentification_name()
else if(issilicon(user))
idname = user.real_name
// Update order status
O.status = SUP_ORDER_DENIED
O.approved_by = idname
O.approved_at = stationdate2text() + " - " + stationtime2text()
// Update admin-side mirror
adm_order.status = SUP_ORDER_DENIED
adm_order.approved_by = idname
adm_order.approved_at = stationdate2text() + " - " + stationtime2text()
return
// Will deny all requested orders
/datum/controller/subsystem/supply/proc/deny_all_pending(var/mob/user)
for(var/datum/supply_order/O in order_history)
if(O.status == SUP_ORDER_REQUESTED)
deny_order(O, user)
// Will delete the specified order from the user-side list
/datum/controller/subsystem/supply/proc/delete_order(var/datum/supply_order/O, var/mob/user)
// Making sure they know what they're doing
if(alert(user, "Are you sure you want to delete this record? If it has been approved, cargo points will NOT be refunded!", "Delete Record","No","Yes") == "Yes")
if(alert(user, "Are you really sure? There is no way to recover the order once deleted.", "Delete Record", "No", "Yes") == "Yes")
log_admin("[key_name(user)] has deleted supply order \ref[O] [O] from the user-side order history.")
SSsupply.order_history -= O
return
// Will generate a new, requested order, for the given supply pack type
/datum/controller/subsystem/supply/proc/create_order(var/datum/supply_pack/S, var/mob/user, var/reason)
var/datum/supply_order/new_order = new()
var/datum/supply_order/adm_order = new() // Admin-recorded order must be a separate copy in memory, or user-made edits will corrupt it
var/idname = "*None Provided*"
if(ishuman(user))
var/mob/living/carbon/human/H = user
idname = H.get_authentification_name()
else if(issilicon(user))
idname = user.real_name
new_order.ordernum = ++ordernum // Ordernum is used to track the order between the playerside list of orders and the adminside list
new_order.index = new_order.ordernum // Index can be fabricated, or falsified. Ordernum is a permanent marker used to track the order
new_order.object = S
new_order.name = S.name
new_order.cost = S.legacy_cost
new_order.ordered_by = idname
new_order.comment = reason
new_order.ordered_at = stationdate2text() + " - " + stationtime2text()
new_order.status = SUP_ORDER_REQUESTED
adm_order.ordernum = new_order.ordernum
adm_order.index = new_order.index
adm_order.object = new_order.object
adm_order.name = new_order.name
adm_order.cost = new_order.cost
adm_order.ordered_by = new_order.ordered_by
adm_order.comment = new_order.comment
adm_order.ordered_at = new_order.ordered_at
adm_order.status = new_order.status
order_history += new_order
adm_order_history += adm_order
// Will delete the specified export receipt from the user-side list
/datum/controller/subsystem/supply/proc/delete_export(var/datum/exported_crate/E, var/mob/user)
// Making sure they know what they're doing
if(alert(user, "Are you sure you want to delete this record?", "Delete Record","No","Yes") == "Yes")
if(alert(user, "Are you really sure? There is no way to recover the receipt once deleted.", "Delete Record", "No", "Yes") == "Yes")
log_admin("[key_name(user)] has deleted export receipt \ref[E] [E] from the user-side export history.")
SSsupply.exported_crates -= E
return
// Will add an item entry to the specified export receipt on the user-side list
/datum/controller/subsystem/supply/proc/add_export_item(var/datum/exported_crate/E, var/mob/user)
var/new_name = input(user, "Name", "Please enter the name of the item.") as null|text
if(!new_name)
return
var/new_quantity = input(user, "Name", "Please enter the quantity of the item.") as null|num
if(!new_quantity)
return
var/new_value = input(user, "Name", "Please enter the value of the item.") as null|num
if(!new_value)
return
E.contents[++E.contents.len] = list(
"object" = new_name,
"quantity" = new_quantity,
"value" = new_value
)
//* Estimation *//
//* Entity Descriptors *//
/**
* Resolves an entity descriptor, and instantiates it
*
* directly instantiated:
* * typepath
* * ~~anonymous typepath~~ Waiting on BYOND fix.
*
* clone()'d
* * an /atom/movable
*
* instantiated with special handling
* * /datum/prototype/material typepath or instance
* * /obj/item/stack typepath or instance
* * /datum/gas typepath or instance - container_hint can be:
* ** /obj/machinery/portable_atmospherics/canister
* ** /obj/item/tank
*
* translated, when `descriptor_hint` is specified.
* * material id
* * gas id - container_hint can be:
* ** /obj/machinery/portable_atmospherics/canister
* ** /obj/item/tank
* * entity id as string (SSpersistence entity IDs)
*
* @params
* * location - where to spawn it. null is valid!
* * descriptor - the descriptor
* * amount - amount to spawn
* * descriptor_hint - SUPPLY_DESCRIPTOR_HINT_* to tell us what to translate the descriptor as; this forces the descriptor to be processed as text id lookup!
* * container_hint - container hint, if allowed; using an invalid one will runtime.
*/
/datum/controller/subsystem/supply/proc/instantiate_entity_via_descriptor(descriptor, amount = 1, descriptor_hint, container_hint, atom/location)
RETURN_TYPE(/atom/movable)
if(!location || isarea(location))
CRASH("invalid location")
// todo: byond cannot introspect anonymous typepaths so just ignore it
if(IS_ANONYMOUS_TYPEPATH(descriptor))
return
// handle instance
if(istype(descriptor, /atom/movable))
var/atom/movable/cloning_instance = descriptor
if(istype(cloning_instance, /obj/item/stack))
// lol no
descriptor = cloning_instance.type
else
// actually clone
for(var/i in 1 to min(amount, 50))
cloning_instance.clone(location, TRUE)
return
// handle material stack
if(ispath(descriptor, /obj/item/stack/material))
var/obj/item/stack/material/casted_material_stack = descriptor
descriptor = initial(casted_material_stack.material)
// handle material
if(descriptor_hint == SUPPLY_DESCRIPTOR_HINT_MATERIAL || ispath(descriptor, /datum/prototype/material))
var/datum/prototype/material/resolved_material = RSmaterials.fetch(descriptor)
resolved_material.place_sheet(location, amount)
return
// handle gas
if(descriptor_hint == SUPPLY_DESCRIPTOR_HINT_GAS || ispath(descriptor, /datum/gas))
var/datum/gas/resolved_gas
if(ispath(descriptor))
var/datum/gas/casted_gas = descriptor
resolved_gas = global.gas_data.gases[initial(casted_gas.id)]
else
resolved_gas = global.gas_data.gases[resolved_gas]
// todo: temperature support, for now everything ships at 273.15K
switch(container_hint)
if(/obj/item/tank)
// tank, if possible
var/obj/item/tank/tank_type = /obj/item/tank/shipment
var/tank_pressure = initial(tank_type.volume)
var/estimated_pressure = (R_IDEAL_GAS_EQUATION * 273.15 * amount) / tank_pressure
if(estimated_pressure > TANK_LEAK_PRESSURE)
stack_trace("tried to shove [amount] mols of [resolved_gas] into a shipment tank, which would result in a detonation")
else
var/obj/item/tank/created_tank = new /obj/item/tank/shipment(location)
created_tank.air_contents.adjust_gas_temp(resolved_gas.id, amount, 273.15)
return
var/obj/machinery/portable_atmospherics/canister/created_canister = new(location)
created_canister.air_contents.adjust_gas_temp(resolved_gas.id, amount, 273.15)
return
// translate to path
if(descriptor_hint == SUPPLY_DESCRIPTOR_HINT_PROTOTYPE)
var/entity_type = SSpersistence.prototype_id_to_path[descriptor]
descriptor = entity_type
// this point onwards: handle path
if(ispath(descriptor, /obj/item/stack))
var/obj/item/stack/casted_stack_path = descriptor
var/stack_safety = 50
var/amount_per_stack = initial(casted_stack_path.max_amount)
while(amount > 0)
var/amount_to_make = min(amount_per_stack, amount)
new casted_stack_path(location, amount_to_make)
amount -= amount_to_make
if(--stack_safety < 0)
break
return
for(var/i in 1 to min(amount, 50))
new descriptor(location)
/**
* Resolves an entity descriptor, and describes it with a string
*
* @return string
*/
/datum/controller/subsystem/supply/proc/describe_entity_via_descriptor(descriptor, amount = 1, descriptor_hint, container_hint)
RETURN_TYPE(/atom/movable)
// todo: byond cannot introspect anonymous typepaths so just ignore it
if(IS_ANONYMOUS_TYPEPATH(descriptor))
return
// handle instance
if(istype(descriptor, /atom/movable))
var/atom/movable/cloning_instance = descriptor
if(istype(cloning_instance, /obj/item/stack))
// lol no
descriptor = cloning_instance.type
else
return "[amount] [cloning_instance](s)"
// handle material stack
if(ispath(descriptor, /obj/item/stack/material))
var/obj/item/stack/material/casted_material_stack = descriptor
descriptor = initial(casted_material_stack.material)
// handle material
if(descriptor_hint == SUPPLY_DESCRIPTOR_HINT_MATERIAL || ispath(descriptor, /datum/prototype/material))
var/datum/prototype/material/resolved_material = RSmaterials.fetch(descriptor)
return "[amount] [resolved_material.sheet_plural_name] of [resolved_material.display_name]"
// handle gas
if(descriptor_hint == SUPPLY_DESCRIPTOR_HINT_GAS || ispath(descriptor, /datum/gas))
var/datum/gas/resolved_gas
if(ispath(descriptor))
var/datum/gas/casted_gas = descriptor
resolved_gas = global.gas_data.gases[initial(casted_gas.id)]
else
resolved_gas = global.gas_data.gases[resolved_gas]
// todo: display_name for gas
return "[amount] mol(s) of [resolved_gas]"
// translate to path
if(descriptor_hint == SUPPLY_DESCRIPTOR_HINT_PROTOTYPE)
var/entity_type = SSpersistence.prototype_id_to_path[descriptor]
descriptor = entity_type
// this point onwards: handle path
if(ispath(descriptor, /obj/item/stack))
var/obj/item/stack/casted_stack_path = descriptor
return "[amount] [initial(casted_stack_path.name)]"
var/atom/movable/casted_movable_path = descriptor
return "[amount] [initial(casted_movable_path.name)](s)"
/**
* Resolves an entity descriptor, and estimates its worth
*
* @return number (thalers)
*/
/datum/controller/subsystem/supply/proc/value_entity_via_descriptor(descriptor, amount = 1, descriptor_hint, container_hint)
RETURN_TYPE(/atom/movable)
// todo: byond cannot introspect anonymous typepaths so just ignore it
if(IS_ANONYMOUS_TYPEPATH(descriptor))
return
// handle instance
if(istype(descriptor, /atom/movable))
var/atom/movable/cloning_instance = descriptor
if(istype(cloning_instance, /obj/item/stack))
// lol no
descriptor = cloning_instance.type
else
return amount * cloning_instance.worth(GET_WORTH_FLAGS_SUPPLY_DETECTION)
// handle material stack
if(ispath(descriptor, /obj/item/stack/material))
var/obj/item/stack/material/casted_material_stack = descriptor
descriptor = initial(casted_material_stack.material)
// handle material
if(descriptor_hint == SUPPLY_DESCRIPTOR_HINT_MATERIAL || ispath(descriptor, /datum/prototype/material))
var/datum/prototype/material/resolved_material = RSmaterials.fetch(descriptor)
return amount * resolved_material.worth
// handle gas
if(descriptor_hint == SUPPLY_DESCRIPTOR_HINT_GAS || ispath(descriptor, /datum/gas))
var/datum/gas/resolved_gas
if(ispath(descriptor))
var/datum/gas/casted_gas = descriptor
resolved_gas = global.gas_data.gases[initial(casted_gas.id)]
else
resolved_gas = global.gas_data.gases[resolved_gas]
// todo: temperature support, for now everything ships at 273.15K
switch(container_hint)
if(/obj/item/tank)
// tank, if possible
var/obj/item/tank/tank_type = /obj/item/tank/shipment
var/tank_pressure = initial(tank_type.volume)
var/estimated_pressure = (R_IDEAL_GAS_EQUATION * 273.15 * amount) / tank_pressure
if(estimated_pressure > TANK_LEAK_PRESSURE)
stack_trace("tried to shove [amount] mols of [resolved_gas] into a shipment tank, which would result in a detonation")
return amount * resolved_gas.worth
// translate to path
if(descriptor_hint == SUPPLY_DESCRIPTOR_HINT_PROTOTYPE)
var/entity_type = SSpersistence.prototype_id_to_path[descriptor]
descriptor = entity_type
// this point onwards: handle path
if(ispath(descriptor, /obj/item/stack))
var/obj/item/stack/casted_stack_path = descriptor
return amount * initial(casted_stack_path.worth_intrinsic)
var/atom/movable/creating = new descriptor(null)
. = creating.worth(GET_WORTH_FLAGS_SUPPLY_DETECTION) * amount
qdel(creating)