Files
SorrowfulwindsandGitHub 9c50a3a137 Alphabetize the cargo order terminal (#7372)
<!-- Write **BELOW** The Headers and **ABOVE** The comments else it may
not be viewable. -->
<!-- You can view Contributing.MD for a detailed description of the pull
request process. -->

## About The Pull Request
This PR will make the cargo ordering terminal alphabetize the categories
and the items in those categories. Since its really awkward to look for
items in a randomly sorted list.
<!-- Describe The Pull Request. Please be sure every change is
documented or this can delay review and even discourage maintainers from
merging your PR! -->

## Why It's Good For The Game
Cargo ordering speeds up 400%
<!-- Argue for the merits of your changes and how they benefit the game,
especially if they are controversial and/or far reaching. If you can't
actually explain WHY what you are doing will improve the game, then it
probably isn't good for the game in the first place. -->

## Changelog
-Alphabetizes cargo order categories and items
<!-- If your PR modifies aspects of the game that can be concretely
observed by players or admins you should add a changelog. If your change
does NOT meet this description, remove this section. Please note that
maintainers freely reserve the right to remove and add tags should they
deem it appropriate. You can attempt to finagle the system all you want,
but it's best to shoot for clear communication right off the bat. -->

🆑
code: adds a string sort to the cargo order subsystem to sort the
categories and items.
/🆑

<!-- Both 🆑's are required for the changelog to work! You can put
your name to the right of the first 🆑 if you want to overwrite your
GitHub username as author ingame. -->
<!-- You can use multiple of the same prefix (they're only used for the
icon ingame) and delete the unneeded ones. Despite some of the tags,
changelogs should generally represent how a player might be affected by
the changes rather than a summary of the PR's contents. -->
2025-10-31 11:19:12 -04:00

583 lines
22 KiB
Plaintext

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 += "<h3>[command_name()] Shipping Manifest</h3><hr><br>"
info +="Order #[SO.ordernum]<br>"
info +="Destination: [station_name()]<br>"
info +="[orderedamount] PACKAGES IN THIS SHIPMENT<br>"
info += "<hr>"
info += SP.get_html_manifest(container)
info += "<hr>"
info += "CHECK CONTENTS AND STAMP BELOW THE LINE TO CONFIRM RECEIPT OF GOODS"
info += "<hr>"
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)