Files
nevimer 6e9f2ccfc0 Merge remote-tracking branch 'tgstation/master' into upstream-12-15
# Conflicts:
#	.github/workflows/compile_all_maps.yml
#	.github/workflows/run_integration_tests.yml
#	_maps/map_files/CatwalkStation/CatwalkStation_2023.dmm
#	code/_onclick/hud/credits.dm
#	code/controllers/subsystem/networks/id_access.dm
#	code/datums/diseases/advance/advance.dm
#	code/datums/diseases/advance/symptoms/heal.dm
#	code/game/machinery/doors/door.dm
#	code/game/objects/structures/crates_lockers/closets/secure/medical.dm
#	code/game/objects/structures/crates_lockers/closets/secure/security.dm
#	code/modules/antagonists/malf_ai/malf_ai_modules.dm
#	code/modules/jobs/job_types/_job.dm
#	code/modules/loadout/categories/accessories.dm
#	code/modules/loadout/loadout_helpers.dm
#	code/modules/loadout/loadout_items.dm
#	code/modules/loadout/loadout_preference.dm
#	code/modules/mob/living/silicon/robot/robot_defense.dm
#	code/modules/mod/mod_theme.dm
#	code/modules/projectiles/ammunition/energy/laser.dm
#	code/modules/reagents/reagent_containers/cups/drinks.dm
#	code/modules/shuttle/mobile_port/variants/supply.dm
#	code/modules/surgery/organs/internal/eyes/_eyes.dm
#	code/modules/unit_tests/screenshots/screenshot_antag_icons_heretic.png
#	icons/hud/screen_full.dmi
2025-12-15 18:12:29 -05:00

1213 lines
50 KiB
Plaintext

/**
* Tram specific variant of the generic linear transport controller.
*
* Hierarchy
* The sstransport subsystem manages a list of controllers,
* A controller manages a list of transport modules (individual tiles) which together make up a transport unit (in this case a tram)
*/
/datum/transport_controller/linear/tram
///whether this controller is active (any state we don't accept new orders, not nessecarily moving)
var/controller_active = FALSE
///whether all required parts of the tram are considered operational
var/controller_operational = TRUE
///the controller cabinet located on the tram
var/obj/machinery/transport/tram_controller/paired_cabinet
///the home controller located in telecoms
var/obj/machinery/transport/tram_controller/tcomms/home_controller
///if we're travelling, what direction are we going
var/travel_direction = NONE
///if we're travelling, how far do we have to go
var/travel_remaining = 0
///how far in total we'll be travelling
var/travel_trip_length = 0
///multiplier on how much damage/force the tram imparts on things it hits
var/collision_lethality = 1
/// reference to the navigation landmark associated with this tram. since we potentially span multiple z levels we dont actually
/// know where on us this platform is. as long as we know THAT its on us we can just move the distance and direction between this
/// and the destination landmark.
var/obj/effect/landmark/transport/nav_beacon/tram/nav/nav_beacon
/// reference to the landmark we consider ourself stationary at.
var/obj/effect/landmark/transport/nav_beacon/tram/platform/idle_platform
/// reference to the destination landmark we consider ourselves travelling towards.
var/obj/effect/landmark/transport/nav_beacon/tram/platform/destination_platform
var/current_speed = 0
var/current_load = 0
///decisecond delay between horizontal movement. cannot make the tram move faster than 1 movement per world.tick_lag.
var/speed_limiter = 0.5
///version of speed_limiter that gets set in init and is considered our base speed if our lift gets slowed down
var/base_speed_limiter = 0.5
///the world.time we should next move at. in case our speed is set to less than 1 movement per tick
var/scheduled_move = INFINITY
///whether we have been slowed down automatically
var/recovery_mode = FALSE
///how many times we moved while costing more than SStransport.max_time milliseconds per movement.
///if this exceeds SStransport.max_exceeding_moves
var/recovery_activate_count = 0
///how many times we moved while costing less than 0.5 * SStransport.max_time milliseconds per movement
var/recovery_clear_count = 0
///if the tram's next stop will be the tram malfunction event sequence
var/malf_active = TRANSPORT_SYSTEM_NORMAL
///fluff information of the tram, such as ongoing kill count and age
var/datum/tram_mfg_info/tram_registration
///previous trams that have been destroyed
var/list/tram_history
/datum/tram_mfg_info
///serial number of this tram (what round ID it first appeared in)
var/serial_number
///is it the active tram for the map
var/active = TRUE
///date the tram was created
var/mfg_date
///what map the tram is used on
var/install_location
///lifetime distance the tram has travelled
var/distance_travelled = 0
///lifetime number of players hit by the tram
var/collisions = 0
/**
* Assign registration details to a new tram.
*
* When a new tram is created, we give it a builder's plate with the date it was created.
* We track a few stats about it, and keep a small historical record on the
* information plate inside the tram.
*/
/datum/tram_mfg_info/New(specific_transport_id)
if(GLOB.round_id)
serial_number = "LT306TG[add_leading(GLOB.round_id, 6, "0")]"
else
serial_number = "LT306TG[rand(000000, 999999)]"
mfg_date = "[CURRENT_STATION_YEAR]-[time2text(world.timeofday, "MM-DD", NO_TIMEZONE)]"
install_location = specific_transport_id
/datum/tram_mfg_info/proc/load_from_json(list/json_data)
serial_number = json_data["serial_number"]
active = json_data["active"]
mfg_date = json_data["mfg_date"]
install_location = json_data["install_location"]
distance_travelled = json_data["distance_travelled"]
collisions = json_data["collisions"]
/datum/tram_mfg_info/proc/export_to_json()
var/list/new_data = list()
new_data["serial_number"] = serial_number
new_data["active"] = active
new_data["mfg_date"] = mfg_date
new_data["install_location"] = install_location
new_data["distance_travelled"] = distance_travelled
new_data["collisions"] = collisions
return new_data
/**
* Make sure all modules have matching speed limiter vars, pull save data from persistence
*
* We track a few stats about it, and keep a small historical record on the
* information plate inside the tram.
*/
/datum/transport_controller/linear/tram/New(obj/structure/transport/linear/tram/transport_module)
. = ..()
speed_limiter = transport_module.speed_limiter
base_speed_limiter = transport_module.speed_limiter
tram_history = SSpersistence.load_tram_history(specific_transport_id)
var/datum/tram_mfg_info/previous_tram = peek(tram_history)
if(!isnull(previous_tram) && previous_tram.active)
tram_registration = pop(tram_history)
else
tram_registration = new /datum/tram_mfg_info(specific_transport_id)
check_starting_landmark()
/**
* If someone VVs the base speed limiter of the tram, copy it to the current active speed limiter.
*/
/datum/transport_controller/linear/tram/vv_edit_var(var_name, var_value)
. = ..()
if(var_name == "base_speed_limiter")
speed_limiter = max(speed_limiter, base_speed_limiter)
/datum/transport_controller/linear/tram/Destroy()
paired_cabinet = null
set_status_code(SYSTEM_FAULT, TRUE)
SEND_SIGNAL(SStransport, COMSIG_TRANSPORT_ACTIVE, src, FALSE, controller_status, travel_direction, destination_platform)
tram_registration.active = FALSE
SSblackbox.record_feedback("amount", "tram_destroyed", 1)
SSpersistence.save_tram_history(specific_transport_id)
return ..()
/**
* Register transport modules to the controller
*
* Spreads out searching neighbouring tiles for additional transport modules, to combine into one full tram.
* We register to every module's signal that it's collided with something, be it mob, structure, etc.
*/
/datum/transport_controller/linear/tram/add_transport_modules(obj/structure/transport/linear/new_transport_module)
. = ..()
RegisterSignal(new_transport_module, COMSIG_MOVABLE_BUMP, PROC_REF(gracefully_break))
/**
* The mapper should have placed the tram at one of the stations, the controller will search for a landmark within
* its control area and set it as its idle position.
*/
/datum/transport_controller/linear/tram/check_for_landmarks(obj/structure/transport/linear/tram/new_transport_module)
. = ..()
for(var/turf/platform_loc as anything in new_transport_module.locs)
var/obj/effect/landmark/transport/nav_beacon/tram/platform/initial_destination = locate() in platform_loc
var/obj/effect/landmark/transport/nav_beacon/tram/nav/beacon = locate() in platform_loc
if(initial_destination)
idle_platform = initial_destination
destination_platform = initial_destination
if(beacon)
nav_beacon = beacon
/**
* Verify tram is in a valid starting location, start the subsystem.
*
* Throw an error if someone mapped a tram with no landmarks available for it to register.
* The processing subsystem starts off because not all maps have elevators/transports.
* Now that the tram is aware of its surroundings, we start the subsystem.
*/
/datum/transport_controller/linear/tram/proc/check_starting_landmark()
if(!idle_platform || !nav_beacon)
CRASH("a tram lift_master was initialized without the required landmarks to give it direction!")
SStransport.can_fire = TRUE
return TRUE
/**
* The tram explodes if it hits a few types of objects.
*
* Signal for when the tram runs into a field of which it cannot go through.
* Stops the train's travel fully, sends a message, and destroys the train.
* Arguments:
* * bumped_atom - The atom this tram bumped into
*/
/datum/transport_controller/linear/tram/proc/gracefully_break(atom/bumped_atom)
SIGNAL_HANDLER
travel_remaining = 0
bumped_atom.visible_message(span_userdanger("\The [bumped_atom] crashes into the field violently!"))
for(var/obj/structure/transport/linear/tram/transport_module as anything in transport_modules)
transport_module.set_travelling(FALSE)
for(var/explosive_target in transport_module.transport_contents)
if(iseffect(explosive_target))
continue
if(isliving(explosive_target))
explosion(explosive_target, devastation_range = rand(0, 1), heavy_impact_range = 2, light_impact_range = 3) //50% chance of gib
else if(prob(9))
explosion(explosive_target, devastation_range = 1, heavy_impact_range = 2, light_impact_range = 3)
explosion(transport_module, devastation_range = 1, heavy_impact_range = 2, light_impact_range = 3)
qdel(transport_module)
send_transport_active_signal()
/**
* Calculate the journey details to the requested platform
*
* These will eventually be passed to the transport modules as args telling them where to move.
* We do some sanity checking in case of discrepencany between where the subsystem thinks the
* tram is and where the tram actually is. (For example, moving the landmarks after round start.)
*/
/datum/transport_controller/linear/tram/proc/calculate_route(obj/effect/landmark/transport/nav_beacon/tram/destination)
if(destination == idle_platform)
return FALSE
destination_platform = destination
travel_direction = get_dir(nav_beacon, destination_platform)
travel_remaining = get_dist(nav_beacon, destination_platform)
travel_trip_length = travel_remaining
log_transport("TC: [specific_transport_id] trip calculation: src: [nav_beacon.x], [nav_beacon.y], [nav_beacon.z] dst: [destination_platform] [destination_platform.x], [destination_platform.y], [destination_platform.z] = Dir [travel_direction] Dist [travel_remaining].")
return TRUE
/**
* Handles moving the tram
*
* Called by the subsystem, the controller tells the individual tram parts where to actually go and has extra safety checks
* incase multiple inputs get through, preventing conflicting directions and the tram literally ripping itself apart.
* All of the actual movement is handled by SStransport.
*
* If we're this far all the PRE_DEPARTURE checks should have passed, so we leave the PRE_DEPARTURE status and actually move.
* We send a signal to anything registered that cares about the physical movement of the tram.
*
* Arguments:
* * destination_platform - where the subsystem wants it to go
*/
/datum/transport_controller/linear/tram/proc/dispatch_transport(obj/effect/landmark/transport/nav_beacon/tram/destination_platform)
log_transport("TC: [specific_transport_id] starting departure.")
set_status_code(PRE_DEPARTURE, FALSE)
if(controller_status & EMERGENCY_STOP)
set_status_code(EMERGENCY_STOP, FALSE)
playsound(paired_cabinet, 'sound/machines/synth/synth_yes.ogg', 40, vary = FALSE, extrarange = SHORT_RANGE_SOUND_EXTRARANGE)
paired_cabinet.say("Controller reset.")
SEND_SIGNAL(src, COMSIG_TRAM_TRAVEL, idle_platform, destination_platform)
for(var/obj/structure/transport/linear/tram/transport_module as anything in transport_modules) //only thing everyone needs to know is the new location.
if(transport_module.travelling) //wee woo wee woo there was a double action queued. damn multi tile structs
return //we don't care to undo cover_locked controls, though, as that will resolve itself
if(malf_active == TRANSPORT_LOCAL_WARNING)
if(transport_module.check_for_humans())
throw_chance *= 1.75
malf_active = TRANSPORT_LOCAL_FAULT
addtimer(CALLBACK(src, PROC_REF(announce_malf_event)), 1 SECONDS)
transport_module.verify_transport_contents()
transport_module.glide_size_override = DELAY_TO_GLIDE_SIZE(speed_limiter)
transport_module.set_travelling(TRUE)
scheduled_move = world.time + speed_limiter
START_PROCESSING(SStransport, src)
/**
* Tram processing loop
*
* Moves the tram to its set destination.
* When it arrives at its destination perform callback to the post-arrival procs like controls and lights.
* We update the odometer and kill the process until we need to move again.
*
* If the status is EMERGENCY_STOP the tram should immediately come to a stop regardless of the travel_remaining.
* Some extra things happen in an emergency stop (throwing the passengers) and when reset will run a
* recovery procedure to head to the nearest platform and sync logical and physical location data
* (idle_platform and nav_beacon) once the issue is resolved.
*/
/datum/transport_controller/linear/tram/process(seconds_per_tick)
if(isnull(paired_cabinet))
set_status_code(SYSTEM_FAULT, TRUE)
if(controller_status & SYSTEM_FAULT || controller_status & EMERGENCY_STOP)
halt_and_catch_fire()
return PROCESS_KILL
if(!travel_remaining)
if(!controller_operational || malf_active == TRANSPORT_LOCAL_FAULT)
degraded_stop()
else
normal_stop()
return PROCESS_KILL
else if(world.time >= scheduled_move)
var/start_time = TICK_USAGE
travel_remaining--
move_transport_horizontally(travel_direction)
var/duration = TICK_USAGE_TO_MS(start_time)
current_load = duration
current_speed = transport_modules[1].glide_size
if(recovery_mode)
if(duration <= (SStransport.max_time / 2))
recovery_clear_count++
else
recovery_clear_count = 0
if(recovery_clear_count >= SStransport.max_cheap_moves)
speed_limiter = base_speed_limiter
recovery_mode = FALSE
recovery_clear_count = 0
log_transport("TC: [specific_transport_id] removing speed limiter, performance issue resolved. Last tick was [duration]ms.")
else if(duration > SStransport.max_time)
recovery_activate_count++
if(recovery_activate_count >= SStransport.max_exceeding_moves)
message_admins("The tram at [ADMIN_JMP(transport_modules[1])] is taking [duration] ms which is more than [SStransport.max_time] ms per movement for [recovery_activate_count] ticks. Reducing its movement speed until it recovers. If this continues to be a problem you can reset the tram contents to its original state, and clear added objects on the Debug tab.")
log_transport("TC: [specific_transport_id] activating speed limiter due to poor performance. Last tick was [duration]ms.")
speed_limiter = base_speed_limiter * 2 //halves its speed
recovery_mode = TRUE
recovery_activate_count = 0
else
recovery_activate_count = max(recovery_activate_count - 1, 0)
scheduled_move = world.time + speed_limiter
/**
* Tram stops normally, performs post-trip actions and updates the tram registration.
*/
/datum/transport_controller/linear/tram/proc/normal_stop()
cycle_doors(CYCLE_OPEN)
log_transport("TC: [specific_transport_id] trip completed. Info: nav_pos ([nav_beacon.x], [nav_beacon.y], [nav_beacon.z]) idle_pos ([destination_platform.x], [destination_platform.y], [destination_platform.z]).")
addtimer(CALLBACK(src, PROC_REF(unlock_controls)), 2 SECONDS)
if((controller_status & SYSTEM_FAULT) && (nav_beacon.loc == destination_platform.loc)) //position matches between controller and tram, we're back on track
set_status_code(SYSTEM_FAULT, FALSE)
playsound(paired_cabinet, 'sound/machines/synth/synth_yes.ogg', 40, vary = FALSE, extrarange = SHORT_RANGE_SOUND_EXTRARANGE)
paired_cabinet.say("Controller reset.")
log_transport("TC: [specific_transport_id] position data successfully reset.")
speed_limiter = initial(speed_limiter)
idle_platform = destination_platform
tram_registration.distance_travelled += (travel_trip_length - travel_remaining)
travel_trip_length = 0
current_speed = 0
current_load = 0
speed_limiter = initial(speed_limiter)
/**
* Tram comes to an in-station degraded stop, throwing the players. Caused by power loss or tram malfunction event.
*/
/datum/transport_controller/linear/tram/proc/degraded_stop()
crash_fx()
log_transport("TC: [specific_transport_id] trip completed with a degraded status. Info: [TC_TS_STATUS] nav_pos ([nav_beacon.x], [nav_beacon.y], [nav_beacon.z]) idle_pos ([destination_platform.x], [destination_platform.y], [destination_platform.z]).")
addtimer(CALLBACK(src, PROC_REF(unlock_controls)), 4 SECONDS)
if(controller_status & SYSTEM_FAULT)
set_status_code(SYSTEM_FAULT, FALSE)
playsound(paired_cabinet, 'sound/machines/synth/synth_yes.ogg', 40, vary = FALSE, extrarange = SHORT_RANGE_SOUND_EXTRARANGE)
paired_cabinet.say("Controller reset.")
log_transport("TC: [specific_transport_id] position data successfully reset. ")
speed_limiter = initial(speed_limiter)
if(malf_active == TRANSPORT_LOCAL_FAULT)
set_status_code(SYSTEM_FAULT, TRUE)
addtimer(CALLBACK(src, PROC_REF(cycle_doors), CYCLE_OPEN), 2 SECONDS)
malf_active = TRANSPORT_SYSTEM_NORMAL
throw_chance = initial(throw_chance)
playsound(paired_cabinet, 'sound/machines/buzz/buzz-sigh.ogg', 60, vary = FALSE, extrarange = SHORT_RANGE_SOUND_EXTRARANGE)
paired_cabinet.say("Controller error. Please contact your engineering department.")
idle_platform = destination_platform
tram_registration.distance_travelled += (travel_trip_length - travel_remaining)
travel_trip_length = 0
current_speed = 0
current_load = 0
speed_limiter = initial(speed_limiter)
var/throw_direction = travel_direction
for(var/obj/structure/transport/linear/tram/module in transport_modules)
module.estop_throw(throw_direction)
/**
* Tram comes to an emergency stop without completing its trip. Caused by emergency stop button or some catastrophic tram failure.
*/
/datum/transport_controller/linear/tram/proc/halt_and_catch_fire()
if(controller_status & SYSTEM_FAULT)
if(!isnull(paired_cabinet))
playsound(paired_cabinet, 'sound/machines/buzz/buzz-sigh.ogg', 60, vary = FALSE, extrarange = SHORT_RANGE_SOUND_EXTRARANGE)
paired_cabinet.say("Controller error. Please contact your engineering department.")
log_transport("TC: [specific_transport_id] Transport Controller failed!")
if(travel_remaining)
travel_remaining = 0
crash_fx()
var/throw_direction = travel_direction
for(var/obj/structure/transport/linear/tram/module in transport_modules)
module.estop_throw(throw_direction)
addtimer(CALLBACK(src, PROC_REF(unlock_controls)), 4 SECONDS)
addtimer(CALLBACK(src, PROC_REF(cycle_doors), CYCLE_OPEN), 2 SECONDS)
idle_platform = null
log_transport("TC: [specific_transport_id] Transport Controller needs new position data from the tram.")
tram_registration.distance_travelled += (travel_trip_length - travel_remaining)
travel_trip_length = 0
current_speed = 0
current_load = 0
/**
* Performs a reset of the tram's position data by finding a predetermined reference landmark, then driving to it.
*/
/datum/transport_controller/linear/tram/proc/reset_position()
malf_active = TRANSPORT_SYSTEM_NORMAL
if(idle_platform)
if(get_turf(idle_platform) == get_turf(nav_beacon))
set_status_code(SYSTEM_FAULT, FALSE)
set_status_code(EMERGENCY_STOP, FALSE)
playsound(paired_cabinet, 'sound/machines/synth/synth_yes.ogg', 40, vary = FALSE, extrarange = SHORT_RANGE_SOUND_EXTRARANGE)
paired_cabinet.say("Controller reset.")
log_transport("TC: [specific_transport_id] Transport Controller reset was requested, but the tram nav data seems correct. Info: nav_pos ([nav_beacon.x], [nav_beacon.y], [nav_beacon.z]) idle_pos ([idle_platform.x], [idle_platform.y], [idle_platform.z]).")
return
log_transport("TC: [specific_transport_id] performing Transport Controller reset. Locating closest reset beacon to ([nav_beacon.x], [nav_beacon.y], [nav_beacon.z])")
var/tram_velocity_sign
if(travel_direction & (NORTH|SOUTH))
tram_velocity_sign = travel_direction & NORTH ? OUTBOUND : INBOUND
else
tram_velocity_sign = travel_direction & EAST ? OUTBOUND : INBOUND
var/reset_beacon = closest_nav_in_travel_dir(nav_beacon, tram_velocity_sign, specific_transport_id)
if(!reset_beacon)
playsound(paired_cabinet, 'sound/machines/buzz/buzz-sigh.ogg', 60, vary = FALSE, extrarange = SHORT_RANGE_SOUND_EXTRARANGE)
paired_cabinet.say("Controller reset failed. Contact manufacturer.") // If you screwed up the tram this bad, I don't even
log_transport("TC: [specific_transport_id] non-recoverable error! Tram is at ([nav_beacon.x], [nav_beacon.y], [nav_beacon.z] [tram_velocity_sign ? "OUTBOUND" : "INBOUND"]) and can't find a reset beacon.")
message_admins("Tram ID [specific_transport_id] is in a non-recoverable error state at [ADMIN_JMP(nav_beacon)]. If it's causing problems, delete the controller datum from the 'Reset Tram' proc in the Debug tab.")
return
travel_direction = get_dir(nav_beacon, reset_beacon)
travel_remaining = get_dist(nav_beacon, reset_beacon)
travel_trip_length = travel_remaining
destination_platform = reset_beacon
speed_limiter = 1.5
playsound(paired_cabinet, 'sound/machines/ping.ogg', 40, vary = FALSE, extrarange = SHORT_RANGE_SOUND_EXTRARANGE)
paired_cabinet.say("Peforming controller reset... Navigating to reset point.")
log_transport("TC: [specific_transport_id] trip calculation: src: [nav_beacon.x], [nav_beacon.y], [nav_beacon.z] dst: [destination_platform] [destination_platform.x], [destination_platform.y], [destination_platform.z] = Dir [travel_direction] Dist [travel_remaining].")
cycle_doors(CYCLE_CLOSED)
set_active(TRUE)
set_status_code(CONTROLS_LOCKED, TRUE)
addtimer(CALLBACK(src, PROC_REF(dispatch_transport), reset_beacon), 3 SECONDS)
log_transport("TC: [specific_transport_id] trying to reset at [destination_platform].")
/datum/transport_controller/linear/tram/proc/estop()
playsound(paired_cabinet, 'sound/machines/buzz/buzz-sigh.ogg', 60, vary = FALSE, extrarange = SHORT_RANGE_SOUND_EXTRARANGE)
paired_cabinet.say("Emergency stop activated!")
set_status_code(EMERGENCY_STOP, TRUE)
log_transport("TC: [specific_transport_id] requested emergency stop.")
/**
* Tram crash sound and visuals
*/
/datum/transport_controller/linear/tram/proc/crash_fx()
playsound(source = nav_beacon, soundin = 'sound/vehicles/car_crash.ogg', vol = 100, vary = FALSE, falloff_distance = DEFAULT_TRAM_LENGTH)
nav_beacon.audible_message(span_userdanger("You hear metal grinding as the tram comes to a sudden, complete stop!"))
for(var/mob/living/tram_passenger in range(DEFAULT_TRAM_LENGTH - 2, nav_beacon))
if(tram_passenger.stat != CONSCIOUS)
continue
shake_camera(M = tram_passenger, duration = 0.2 SECONDS, strength = 3)
/**
* Handles unlocking the tram controls for use after moving
*
* More safety checks to make sure the tram has actually docked properly
* at a location before users are allowed to interact with the tram console again.
* Tram finds its location at this point before fully unlocking controls to the user.
*/
/datum/transport_controller/linear/tram/proc/unlock_controls()
controls_lock(FALSE)
for(var/obj/structure/transport/linear/tram/transport_module as anything in transport_modules) //only thing everyone needs to know is the new location.
transport_module.set_travelling(FALSE)
set_active(FALSE)
/**
* Sets the active status for the controller and sends a signal to listeners.
*
* The main signal used by most components, it has the active status, the bitfield of the controller's status, its direction, and set destination.
*
* Arguments:
* new_status - The active status of the controller (whether it's busy doing something and not taking commands right now)
*/
/datum/transport_controller/linear/tram/proc/set_active(new_status)
if(controller_active == new_status)
return
controller_active = new_status
send_transport_active_signal()
log_transport("TC: [specific_transport_id] controller state [controller_active ? "READY > PROCESSING" : "PROCESSING > READY"].")
/**
* Sets the controller status bitfield
*
* This status var is used by various components like lights, crossing signals, signs
* Sent via signal the listening components will perform required actions based on
* the status codes.
*
* Arguments:
* * code - The status bitflag we're changing
* * value - boolean TRUE/FALSE to set the code
*/
/datum/transport_controller/linear/tram/proc/set_status_code(code, value)
if(code != DOORS_READY)
log_transport("TC: [specific_transport_id] status change [value ? "+" : "-"][english_list(bitfield_to_list(code, TRANSPORT_FLAGS))].")
switch(value)
if(TRUE)
controller_status |= code
if(FALSE)
controller_status &= ~code
else
stack_trace("Transport controller received invalid status code request [code]/[value]")
return
send_transport_active_signal()
/datum/transport_controller/linear/tram/proc/send_transport_active_signal()
SEND_SIGNAL(SStransport, COMSIG_TRANSPORT_ACTIVE, src, controller_active, controller_status, travel_direction, destination_platform)
/**
* Part of the pre-departure list, checks the status of the doors on the tram
*
* Checks if all doors are closed, and updates the status code accordingly.
*
* TODO: this is probably better renamed check_door_status()
*/
/datum/transport_controller/linear/tram/proc/update_status()
for(var/obj/machinery/door/airlock/tram/door as anything in SStransport.doors)
if(door.transport_linked_id != specific_transport_id)
continue
if(door.crushing_in_progress)
log_transport("TC: [specific_transport_id] door [door.id_tag] failed crush status check.")
set_status_code(DOORS_READY, FALSE)
return
set_status_code(DOORS_READY, TRUE)
/**
* Cycle all the doors on the tram.
*/
/datum/transport_controller/linear/tram/proc/cycle_doors(door_status, rapid)
switch(door_status)
if(CYCLE_OPEN)
for(var/obj/machinery/door/airlock/tram/door as anything in SStransport.doors)
if(door.transport_linked_id == specific_transport_id)
INVOKE_ASYNC(door, TYPE_PROC_REF(/obj/machinery/door/airlock/tram, open), rapid)
if(CYCLE_CLOSED)
for(var/obj/machinery/door/airlock/tram/door as anything in SStransport.doors)
if(door.transport_linked_id == specific_transport_id)
INVOKE_ASYNC(door, TYPE_PROC_REF(/obj/machinery/door/airlock/tram, close), rapid)
/datum/transport_controller/linear/tram/proc/notify_controller(obj/machinery/transport/tram_controller/new_cabinet)
paired_cabinet = new_cabinet
RegisterSignal(new_cabinet, COMSIG_MACHINERY_POWER_LOST, PROC_REF(power_lost))
RegisterSignal(new_cabinet, COMSIG_MACHINERY_POWER_RESTORED, PROC_REF(power_restored))
RegisterSignal(new_cabinet, COMSIG_QDELETING, PROC_REF(on_cabinet_qdel))
log_transport("TC: [specific_transport_id] is now paired with [new_cabinet].")
if(controller_status & SYSTEM_FAULT)
set_status_code(SYSTEM_FAULT, FALSE)
reset_position()
/datum/transport_controller/linear/tram/proc/set_home_controller(obj/machinery/transport/tram_controller/tcomms/tcomms_unit)
home_controller = tcomms_unit
RegisterSignal(tcomms_unit, COMSIG_MACHINERY_POWER_LOST, PROC_REF(home_power_lost))
RegisterSignal(tcomms_unit, COMSIG_MACHINERY_POWER_RESTORED, PROC_REF(home_power_restored))
RegisterSignal(tcomms_unit, COMSIG_QDELETING, PROC_REF(on_home_qdel))
log_transport("TC: [specific_transport_id] is now paired with home controller [tcomms_unit].")
if(controller_status & COMM_ERROR)
set_status_code(COMM_ERROR, FALSE)
/datum/transport_controller/linear/tram/proc/on_cabinet_qdel()
paired_cabinet = null
log_transport("TC: [specific_transport_id] received QDEL from controller cabinet.")
set_status_code(SYSTEM_FAULT, TRUE)
/datum/transport_controller/linear/tram/proc/on_home_qdel()
home_controller = null
log_transport("TC: [specific_transport_id] received QDEL from controller cabinet.")
set_status_code(COMM_ERROR, TRUE)
/datum/transport_controller/linear/tram/proc/home_power_lost()
set_status_code(COMM_ERROR, TRUE)
/datum/transport_controller/linear/tram/proc/home_power_restored()
set_status_code(COMM_ERROR, FALSE)
/**
* Tram malfunction random event. Set comm error, requiring engineering or AI intervention.
*/
/datum/transport_controller/linear/tram/proc/start_malf_event()
malf_active = TRANSPORT_LOCAL_WARNING
paired_cabinet.update_appearance()
throw_chance *= 1.25
log_transport("TC: [specific_transport_id] starting Tram Malfunction event.")
/**
* Remove effects of tram malfunction event.
*
* If engineers didn't already repair the tram by the end of the event,
* automagically reset it remotely.
*/
/datum/transport_controller/linear/tram/proc/end_malf_event()
if(!(malf_active))
return
malf_active = TRANSPORT_SYSTEM_NORMAL
paired_cabinet.update_appearance()
throw_chance = initial(throw_chance)
log_transport("TC: [specific_transport_id] ending Tram Malfunction event.")
/datum/transport_controller/linear/tram/proc/announce_malf_event()
priority_announce("Our automated control system has lost contact with the tram's onboard computer. Please stand by, engineering has been dispatched to the tram to perform a reset.", "[command_name()] Engineering Division")
/datum/transport_controller/linear/tram/proc/register_collision(points = 1)
tram_registration.collisions += points
SEND_TRANSPORT_SIGNAL(COMSIG_TRAM_COLLISION, SSpersistence.tram_hits_this_round)
/datum/transport_controller/linear/tram/proc/power_lost()
set_operational(FALSE)
log_transport("TC: [specific_transport_id] power lost.")
send_transport_active_signal()
/datum/transport_controller/linear/tram/proc/power_restored()
set_operational(TRUE)
log_transport("TC: [specific_transport_id] power restored.")
cycle_doors(CYCLE_OPEN)
send_transport_active_signal()
/datum/transport_controller/linear/tram/proc/set_operational(new_value)
if(controller_operational != new_value)
controller_operational = new_value
/**
* Returns the closest tram nav beacon to an atom
*
* Creates a list of nav beacons in the requested direction
* and returns the closest to be passed to the industrial_lift
*
* Arguments: source: the starting point to find a beacon
* travel_dir: travel direction in tram form, INBOUND or OUTBOUND
* beacon_type: what list of beacons we pull from
*/
/datum/transport_controller/linear/tram/proc/closest_nav_in_travel_dir(atom/origin, travel_dir, beacon_type)
if(!istype(origin) || !origin.z)
return FALSE
var/list/obj/effect/landmark/transport/nav_beacon/tram/inbound_candidates = list()
var/list/obj/effect/landmark/transport/nav_beacon/tram/outbound_candidates = list()
for(var/obj/effect/landmark/transport/nav_beacon/tram/candidate_beacon in SStransport.nav_beacons[beacon_type])
if(candidate_beacon.z != origin.z || candidate_beacon.z != nav_beacon.z)
continue
switch(nav_beacon.dir)
if(EAST, WEST)
if(candidate_beacon.y != nav_beacon.y)
continue
else if(candidate_beacon.x < nav_beacon.x)
inbound_candidates += candidate_beacon
else
outbound_candidates += candidate_beacon
if(NORTH, SOUTH)
if(candidate_beacon.x != nav_beacon.x)
continue
else if(candidate_beacon.y < nav_beacon.y)
inbound_candidates += candidate_beacon
else
outbound_candidates += candidate_beacon
switch(travel_dir)
if(INBOUND)
var/obj/effect/landmark/transport/nav_beacon/tram/nav/selected = get_closest_atom(/obj/effect/landmark/transport/nav_beacon/tram, inbound_candidates, origin)
if(selected)
return selected
stack_trace("No inbound beacon candidate found for [origin]. Cancelling dispatch.")
return FALSE
if(OUTBOUND)
var/obj/effect/landmark/transport/nav_beacon/tram/nav/selected = get_closest_atom(/obj/effect/landmark/transport/nav_beacon/tram, outbound_candidates, origin)
if(selected)
return selected
stack_trace("No outbound beacon candidate found for [origin]. Cancelling dispatch.")
return FALSE
else
stack_trace("Tram receieved invalid travel direction [travel_dir]. Cancelling dispatch.")
return FALSE
/**
* Moves the tram when hit by an immovable rod
*
* Tells the individual tram parts where to actually go and has an extra safety checks
* incase multiple inputs get through, preventing conflicting directions and the tram
* literally ripping itself apart. all of the actual movement is handled by SStramprocess
*
* Arguments: collided_rod (the immovable rod that hit the tram)
* Return: push_destination (the landmark /obj/effect/landmark/transport/nav_beacon/tram/nav that the tram is being pushed to due to the rod's trajectory)
*/
/datum/transport_controller/linear/tram/proc/rod_collision(obj/effect/immovablerod/collided_rod)
log_transport("TC: [specific_transport_id] hit an immovable rod.")
if(!controller_operational)
return
var/rod_velocity_sign
// Determine inbound or outbound
if(collided_rod.dir & (NORTH|SOUTH))
rod_velocity_sign = collided_rod.dir & NORTH ? OUTBOUND : INBOUND
else
rod_velocity_sign = collided_rod.dir & EAST ? OUTBOUND : INBOUND
var/obj/effect/landmark/transport/nav_beacon/tram/nav/push_destination = closest_nav_in_travel_dir(origin = nav_beacon, travel_dir = rod_velocity_sign, beacon_type = IMMOVABLE_ROD_DESTINATIONS)
if(!push_destination)
return
travel_direction = get_dir(nav_beacon, push_destination)
travel_remaining = get_dist(nav_beacon, push_destination)
travel_trip_length = travel_remaining
destination_platform = push_destination
log_transport("TC: [specific_transport_id] collided at ([nav_beacon.x], [nav_beacon.y], [nav_beacon.z]) towards [push_destination] ([push_destination.x], [push_destination.y], [push_destination.z]) Dir [travel_direction] Dist [travel_remaining].")
// Don't bother processing crossing signals, where this tram's going there are no signals
//for(var/obj/machinery/transport/crossing_signal/xing as anything in SStransport.crossing_signals)
// xing.temp_malfunction()
priority_announce("In a turn of rather peculiar events, it appears that [GLOB.station_name] has struck an immovable rod. (Don't ask us where it came from.) This has led to a station brakes failure on one of the tram platforms.\n\n\
Our diligent team of engineers have been informed and they're rushing over - although not quite at the speed of our recently flying tram.\n\n\
So while we all look in awe at the universe's mysterious sense of humour, please stand clear of the tracks and remember to stand behind the yellow line.", "Braking News")
set_active(TRUE)
set_status_code(CONTROLS_LOCKED, TRUE)
dispatch_transport(destination_platform = push_destination)
return push_destination
/datum/transport_controller/linear/tram/slow //for some reason speed is set to initial() in the code but if i touched it it would probably break so
speed_limiter = 3
base_speed_limiter = 3
/**
* The physical cabinet on the tram. Acts as the interface between players and the controller datum.
*/
/obj/machinery/transport/tram_controller
name = "tram controller"
desc = "Makes the tram go, or something. Holds the tram's electronics, controls, and maintenance panel. A sticker above the card reader says 'Engineering access only.'"
icon = 'icons/obj/tram/tram_controllers.dmi'
icon_state = "tram-controller"
base_icon_state = "tram"
anchored = TRUE
density = FALSE
armor_type = /datum/armor/transport_module
resistance_flags = LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF
interaction_flags_machine = parent_type::interaction_flags_machine | INTERACT_MACHINE_OFFLINE
max_integrity = 750
integrity_failure = 0.25
layer = SIGN_LAYER
req_access = list(ACCESS_TCOMMS)
idle_power_usage = BASE_MACHINE_IDLE_CONSUMPTION * 0.25
power_channel = AREA_USAGE_ENVIRON
var/datum/transport_controller/linear/tram/controller_datum
/// If this machine has a cover installed
var/has_cover = TRUE
/// If the cover is open
var/cover_open = FALSE
/// If the cover is locked
var/cover_locked = TRUE
COOLDOWN_DECLARE(manual_command_cooldown)
/obj/machinery/transport/tram_controller/hilbert
configured_transport_id = HILBERT_LINE_1
/obj/machinery/transport/tram_controller/wrench_act_secondary(mob/living/user, obj/item/tool)
return NONE
/obj/machinery/transport/tram_controller/Initialize(mapload)
. = ..()
register_context()
if(!id_tag)
id_tag = assign_random_name()
/**
* Mapped or built tram cabinet isn't located on a transport module.
*/
/obj/machinery/transport/tram_controller/post_machine_initialize()
. = ..()
SStransport.hello(src, name, id_tag)
find_controller()
update_appearance()
/obj/machinery/transport/tram_controller/atom_break()
set_machine_stat(machine_stat | BROKEN)
..()
/obj/machinery/transport/tram_controller/add_context(atom/source, list/context, obj/item/held_item, mob/user)
if(held_item?.tool_behaviour == TOOL_SCREWDRIVER && has_cover)
context[SCREENTIP_CONTEXT_RMB] = panel_open ? "close panel" : "open panel"
if(!held_item && has_cover)
context[SCREENTIP_CONTEXT_LMB] = cover_open ? "access controls" : "open cabinet"
context[SCREENTIP_CONTEXT_RMB] = cover_open ? "close cabinet" : "toggle lock"
if(panel_open)
if(held_item?.tool_behaviour == TOOL_WRENCH)
context[SCREENTIP_CONTEXT_RMB] = "unscrew cabinet"
if(malfunctioning || methods_to_fix.len)
context[SCREENTIP_CONTEXT_LMB] = "repair electronics"
if(held_item?.tool_behaviour == TOOL_WELDER)
context[SCREENTIP_CONTEXT_LMB] = "repair frame"
if(istype(held_item, /obj/item/card/emag) && !(obj_flags & EMAGGED))
context[SCREENTIP_CONTEXT_LMB] = "emag controller"
return CONTEXTUAL_SCREENTIP_SET
/obj/machinery/transport/tram_controller/update_current_power_usage()
return // We get power from area rectifiers
/obj/machinery/transport/tram_controller/examine(mob/user)
. = ..()
if(has_cover)
. += span_notice("The door appears to be [cover_locked ? "locked. Swipe an ID card to unlock" : "unlocked. Swipe an ID card to lock"].")
if(panel_open)
. += span_notice("It is secured to the tram wall with [EXAMINE_HINT("bolts.")]")
. += span_notice("The maintenance panel can be closed with a [EXAMINE_HINT("screwdriver.")]")
else
. += span_notice("The maintenance panel can be opened with a [EXAMINE_HINT("screwdriver.")]")
if(cover_open || !has_cover)
. += span_notice("The [EXAMINE_HINT("yellow reset button")] resets the tram controller if a problem occurs or needs to be restarted.")
. += span_notice("The [EXAMINE_HINT("red stop button")] immediately stops the tram, requiring a reset afterwards.")
. += span_notice("The cabinet can be closed with a [EXAMINE_HINT("Right-click.")]")
else
. += span_notice("The cabinet can be opened with a [EXAMINE_HINT("Left-click.")]")
/obj/machinery/transport/tram_controller/attackby(obj/item/weapon, mob/living/user, list/modifiers, list/attack_modifiers)
if(user.combat_mode || cover_open)
return ..()
if(has_cover)
var/obj/item/card/id/id_card = user.get_id_in_hand()
if(!isnull(id_card))
try_toggle_lock(user, id_card)
return
return ..()
/obj/machinery/transport/tram_controller/attack_hand(mob/living/user, params)
. = ..()
if(cover_open || !has_cover)
return
if(cover_locked)
var/obj/item/card/id/id_card = user.get_idcard(TRUE)
if(isnull(id_card))
balloon_alert(user, "access denied!")
return
try_toggle_lock(user, id_card)
return
toggle_door()
/obj/machinery/transport/tram_controller/attack_hand_secondary(mob/living/user, params)
. = ..()
if(!has_cover)
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
if(!cover_open)
var/obj/item/card/id/id_card = user.get_idcard(TRUE)
if(isnull(id_card))
balloon_alert(user, "access denied!")
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
try_toggle_lock(user, id_card)
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
toggle_door()
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
/obj/machinery/transport/tram_controller/proc/toggle_door()
if(!cover_open)
playsound(loc, 'sound/machines/closet/closet_open.ogg', 35, TRUE, -3)
else
playsound(loc, 'sound/machines/closet/closet_close.ogg', 50, TRUE, -3)
cover_open = !cover_open
update_appearance()
/obj/machinery/transport/tram_controller/proc/try_toggle_lock(mob/living/user, obj/item/card/id_card, params)
if(isnull(id_card))
id_card = user.get_idcard(TRUE)
if(obj_flags & EMAGGED)
balloon_alert(user, "access controller damaged!")
return FALSE
if(check_access(id_card))
cover_locked = !cover_locked
balloon_alert(user, "controls [cover_locked ? "locked" : "unlocked"]")
update_appearance()
return TRUE
balloon_alert(user, "access denied!")
return FALSE
/obj/machinery/transport/tram_controller/wrench_act_secondary(mob/living/user, obj/item/tool)
. = ..()
if(!has_cover)
return
if(panel_open && cover_open)
balloon_alert(user, "unsecuring...")
tool.play_tool_sound(src)
if(!tool.use_tool(src, user, 6 SECONDS))
return
playsound(loc, 'sound/items/deconstruct.ogg', 50, vary = TRUE)
balloon_alert(user, "unsecured")
deconstruct(TRUE)
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
/obj/machinery/transport/tram_controller/screwdriver_act_secondary(mob/living/user, obj/item/tool)
. = ..()
if(!cover_open)
return
tool.play_tool_sound(src)
panel_open = !panel_open
balloon_alert(user, "[panel_open ? "mounting bolts exposed" : "mounting bolts hidden"]")
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
/obj/machinery/transport/tram_controller/on_deconstruction(disassembled)
var/turf/drop_location = find_obstruction_free_location(1, src)
if(disassembled)
new /obj/item/wallframe/tram(drop_location)
else
new /obj/item/stack/sheet/mineral/titanium(drop_location, 2)
new /obj/item/stack/sheet/iron(drop_location)
/**
* Update the blinky lights based on the controller status, allowing to quickly check without opening up the cabinet.
*/
/obj/machinery/transport/tram_controller/update_overlays()
. = ..()
if(has_cover)
if(!cover_open)
. += mutable_appearance(icon, "[base_icon_state]-closed")
if(cover_locked)
. += mutable_appearance(icon, "[base_icon_state]-locked")
else
var/mutable_appearance/controller_door = mutable_appearance(icon, "[base_icon_state]-open")
controller_door.pixel_w = -3
. += controller_door
if(machine_stat & NOPOWER)
. += mutable_appearance(icon, "[base_icon_state]-estop")
. += emissive_appearance(icon, "[base_icon_state]-estop", src, alpha = src.alpha)
return
. += mutable_appearance(icon, "[base_icon_state]-power")
. += emissive_appearance(icon, "[base_icon_state]-power", src, alpha = src.alpha)
if(!controller_datum)
. += mutable_appearance(icon, "[base_icon_state]-fatal")
. += emissive_appearance(icon, "[base_icon_state]-fatal", src, alpha = src.alpha)
return
if(controller_datum.controller_status & EMERGENCY_STOP)
. += mutable_appearance(icon, "[base_icon_state]-estop")
. += emissive_appearance(icon, "[base_icon_state]-estop", src, alpha = src.alpha)
return
if(controller_datum.controller_status & SYSTEM_FAULT || controller_datum.malf_active != TRANSPORT_SYSTEM_NORMAL)
. += mutable_appearance(icon, "[base_icon_state]-fault")
. += emissive_appearance(icon, "[base_icon_state]-fault", src, alpha = src.alpha)
return
if(!(controller_datum.controller_status & DOORS_READY))
. += mutable_appearance(icon, "[base_icon_state]-doors")
. += emissive_appearance(icon, "[base_icon_state]-doors", src, alpha = src.alpha)
if(controller_datum.controller_active)
. += mutable_appearance(icon, "[base_icon_state]-active")
. += emissive_appearance(icon, "[base_icon_state]-active", src, alpha = src.alpha)
if(controller_datum.controller_status & COMM_ERROR)
. += mutable_appearance(icon, "[base_icon_state]-comms")
. += emissive_appearance(icon, "[base_icon_state]-comms", src, alpha = src.alpha)
else
. += mutable_appearance(icon, "[base_icon_state]-normal")
. += emissive_appearance(icon, "[base_icon_state]-normal", src, alpha = src.alpha)
/**
* Find the controller associated with the transport module the cabinet is sitting on.
*/
/obj/machinery/transport/tram_controller/proc/find_controller()
var/obj/structure/transport/linear/tram/tram_structure = locate() in src.loc
if(!tram_structure)
return
controller_datum = tram_structure.transport_controller_datum
if(!controller_datum)
return
controller_datum.notify_controller(src)
RegisterSignal(SStransport, COMSIG_TRANSPORT_ACTIVE, PROC_REF(sync_controller))
/obj/machinery/transport/tram_controller/hilbert/find_controller()
for(var/datum/transport_controller/linear/tram/tram as anything in SStransport.transports_by_type[TRANSPORT_TYPE_TRAM])
if(tram.specific_transport_id == configured_transport_id)
controller_datum = tram
break
if(!controller_datum)
return
controller_datum.notify_controller(src)
RegisterSignal(SStransport, COMSIG_TRANSPORT_ACTIVE, PROC_REF(sync_controller))
/**
* Since the machinery obj is a dumb terminal for the controller datum, sync the display with the status bitfield of the tram
*/
/obj/machinery/transport/tram_controller/proc/sync_controller(source, controller, controller_status, travel_direction, destination_platform)
use_energy(active_power_usage)
if(controller != controller_datum)
return
update_appearance()
/obj/machinery/transport/tram_controller/emag_act(mob/user, obj/item/card/emag/emag_card)
if(obj_flags & EMAGGED)
balloon_alert(user, "already fried!")
return FALSE
obj_flags |= EMAGGED
cover_locked = FALSE
playsound(src, SFX_SPARKS, 100, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
balloon_alert(user, "access controller shorted")
return TRUE
/obj/machinery/transport/tram_controller/ui_status(mob/user, datum/ui_state/state)
if(HAS_SILICON_ACCESS(user) && (controller_datum.controller_status & SYSTEM_FAULT || controller_datum.controller_status & COMM_ERROR || !is_operational))
to_chat(user, span_warning("An error code flashes: Communications fault! The [src] is not responding to remote inputs!"))
return UI_CLOSE
return ..()
/obj/machinery/transport/tram_controller/ui_interact(mob/user, datum/tgui/ui)
. = ..()
if(!cover_open && !HAS_SILICON_ACCESS(user) && !isobserver(user))
return
if(machine_stat & BROKEN)
return
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "TramController")
ui.open()
/obj/machinery/transport/tram_controller/ui_data(mob/user)
var/list/data = list()
data = list(
"transportId" = controller_datum.specific_transport_id,
"controllerActive" = controller_datum.controller_active,
"controllerOperational" = controller_datum.controller_operational,
"travelDirection" = controller_datum.travel_direction,
"destinationPlatform" = controller_datum.destination_platform,
"idlePlatform" = controller_datum.idle_platform,
"recoveryMode" = controller_datum.recovery_mode,
"currentSpeed" = controller_datum.current_speed,
"currentLoad" = controller_datum.current_load,
"statusSF" = controller_datum.controller_status & SYSTEM_FAULT || controller_datum.malf_active != TRANSPORT_SYSTEM_NORMAL,
"statusCE" = controller_datum.controller_status & COMM_ERROR,
"statusES" = controller_datum.controller_status & EMERGENCY_STOP,
"statusPD" = controller_datum.controller_status & PRE_DEPARTURE,
"statusDR" = controller_datum.controller_status & DOORS_READY,
"statusCL" = controller_datum.controller_status & CONTROLS_LOCKED,
"statusBS" = controller_datum.controller_status & BYPASS_SENSORS,
)
return data
/obj/machinery/transport/tram_controller/ui_static_data(mob/user)
var/list/data = list()
data["destinations"] = SStransport.detailed_destination_list(controller_datum.specific_transport_id)
return data
/obj/machinery/transport/tram_controller/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if (.)
return
if(!COOLDOWN_FINISHED(src, manual_command_cooldown))
return
if(machine_stat & NOPOWER)
visible_message(span_warning("The button doesn't appear to do anything, the [src]'s power failure status is flashing!"), vision_distance = COMBAT_MESSAGE_RANGE)
return
switch(action)
if("dispatch")
var/obj/effect/landmark/transport/nav_beacon/tram/platform/destination_platform
for (var/obj/effect/landmark/transport/nav_beacon/tram/platform/destination as anything in SStransport.nav_beacons[controller_datum.specific_transport_id])
if(destination.name == params["tripDestination"])
destination_platform = destination
break
if(!destination_platform)
return FALSE
SEND_SIGNAL(src, COMSIG_TRANSPORT_REQUEST, controller_datum.specific_transport_id, destination_platform.platform_code)
update_appearance()
if("estop")
controller_datum.estop()
if("reset")
controller_datum.reset_position()
if("dclose")
controller_datum.cycle_doors(CYCLE_CLOSED)
if("dopen")
controller_datum.cycle_doors(CYCLE_OPEN)
if("togglesensors")
if(controller_datum.controller_status & BYPASS_SENSORS)
controller_datum.set_status_code(BYPASS_SENSORS, FALSE)
else
controller_datum.set_status_code(BYPASS_SENSORS, TRUE)
COOLDOWN_START(src, manual_command_cooldown, 2 SECONDS)
/// Controller that sits in the telecoms room
/obj/machinery/transport/tram_controller/tcomms
name = "tram central controller"
desc = "This semiconductor is half of the brains controlling the tram and its auxiliary equipment."
icon_state = "home-controller"
base_icon_state = "home"
density = TRUE
layer = BELOW_OBJ_LAYER
power_channel = AREA_USAGE_EQUIP
cover_open = TRUE
has_cover = FALSE
/// Handles the machine being affected by an EMP, causing signal failure.
/obj/machinery/transport/tram_controller/tcomms/emp_act(severity)
. = ..()
if(. & EMP_PROTECT_SELF)
return
if(prob(100/severity) && !(machine_stat & EMPED))
set_machine_stat(machine_stat | EMPED)
controller_datum.set_status_code(COMM_ERROR, TRUE)
var/duration = (300 SECONDS)/severity
addtimer(CALLBACK(src, PROC_REF(de_emp)), rand(duration - 2 SECONDS, duration + 2 SECONDS))
/// Handles the machine stopping being affected by an EMP.
/obj/machinery/transport/tram_controller/tcomms/proc/de_emp()
set_machine_stat(machine_stat & ~EMPED)
controller_datum.set_status_code(COMM_ERROR, FALSE)
/obj/machinery/transport/tram_controller/tcomms/find_controller()
link_tram()
return
/obj/machinery/transport/tram_controller/tcomms/link_tram()
. = ..()
var/datum/transport_controller/linear/tram/tram = transport_ref?.resolve()
controller_datum = tram
if(!controller_datum)
return
controller_datum.set_home_controller(src)
RegisterSignal(SStransport, COMSIG_TRANSPORT_ACTIVE, PROC_REF(sync_controller))
/obj/item/wallframe/tram
name = "tram controller cabinet"
desc = "A box that contains the equipment to control a tram. Just secure to the tram wall."
icon = 'icons/obj/tram/tram_controllers.dmi'
icon_state = "tram-controller"
custom_materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT * 20)
result_path = /obj/machinery/transport/tram_controller
pixel_shift = 32
/obj/item/wallframe/tram/find_support_structure(atom/structure)
return astype(structure, /obj/structure/tram)
/obj/item/wallframe/tram/try_build(obj/structure/tram/on_tram, mob/user)
var/turf/tram_turf = get_turf(user)
var/obj/structure/thermoplastic/tram_floor = locate() in tram_turf
if(!istype(tram_floor))
balloon_alert(user, "needs tram!")
return FALSE
return ..()