Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 71 additions & 12 deletions voidcrew/edits/ship_access.dm
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@
* than on the /obj/machinery/door override above is deliberate: it runs ahead of
* `emergency` and `unres_sides`, neither of which should punch a hole in a lock the
* captain deliberately set.
*
* The lock refuses NPCs too. Most ship doors carry no mapped access, so the NPC rule
* above never touches them, and before this a pirate boarding party walked through a
* crew-locked hull's airlocks while its players were turned away. A clientless mob gets
* through only if it is that ship's crew by is_npc_ship_crew(). Bots and silicons stay on
* the upstream path, as with the NPC rule above.
*/
GLOBAL_LIST_EMPTY(crew_locked_ships)

Expand All @@ -108,25 +114,81 @@ GLOBAL_LIST_EMPTY(crew_locked_ships)
/obj/machinery/door/proc/refused_by_ship_crew_lock(mob/user)
if(!length(GLOB.crew_locked_ships))
return FALSE
// Players only. Clientless mobs are covered by the NPC rule above, and bots and
// silicons stay on the upstream path so their own credentials are still read.
if(!user || !GET_CLIENT(user))
if(!user)
return FALSE
// Bots and silicons stay on the upstream path so their own credentials are still read.
if(isbot(user) || HAS_SILICON_ACCESS(user))
return FALSE
var/area/shuttle/voidcrew/ship_area = get_area(src)
if(!istype(ship_area))
var/has_client = !!GET_CLIENT(user)
if(!has_client && !isliving(user))
return FALSE
var/obj/structure/overmap/ship/ship = ship_area.shuttle_port?.current_ship
if(isnull(ship) || !ship.crew_only_airlocks)
var/obj/structure/overmap/ship/ship = crew_locked_ship()
if(isnull(ship))
return FALSE
if(!has_client)
return !ship.is_npc_ship_crew(user)
if(ship.is_ship_crew(user))
return FALSE
if(TIMER_COOLDOWN_FINISHED(user, "ship_crew_lock_refusal"))
TIMER_COOLDOWN_START(user, "ship_crew_lock_refusal", 5 SECONDS)
to_chat(user, span_warning("This airlock is keyed to the [ship.name]'s crew."))
return TRUE

/**
* The crew-locked ship this door belongs to, or null. Only airlocks and windoors carry the
* lock. Costs one list read while no ship has the lock on.
*/
/obj/machinery/door/proc/crew_locked_ship()
if(!length(GLOB.crew_locked_ships))
return null
if(!istype(src, /obj/machinery/door/airlock) && !istype(src, /obj/machinery/door/window))
return null
var/area/shuttle/voidcrew/ship_area = get_area(src)
if(!istype(ship_area))
return null
var/obj/structure/overmap/ship/ship = ship_area.shuttle_port?.current_ship
if(isnull(ship) || !ship.crew_only_airlocks)
return null
return ship

/**
* Whether a mob with no player in it counts as this ship's crew for the crew lock:
* - its mind is on the roster (a crewmember's body with nobody in it right now),
* - it is a stray who joined this ship, aboard or following the crew there,
* - or it is an animal one of the ship's crew has befriended, like a tamed pet.
* Everything else, pirate boarders included, is not crew.
*/
/obj/structure/overmap/ship/proc/is_npc_ship_crew(mob/living/npc)
if(is_ship_crew(npc))
return TRUE
if(istype(npc, /mob/living/basic/ambient_npc/stray))
var/mob/living/basic/ambient_npc/stray/stray = npc
return !stray.is_wild() && stray.get_ship() == src
var/list/friends = npc.ai_controller?.blackboard[BB_FRIENDS_LIST]
for(var/mob/friend as anything in friends)
if(!QDELETED(friend) && is_ship_crew(friend))
return TRUE
return FALSE

/**
* Whether `door` stays shut for an NPC that carries no access: it has mapped access, or its
* ship's crew lock is on and `npc` is not that ship's crew. Without `npc`, any crew lock
* counts, which is right for planning a boarding party's route.
*
* Every NPC door planner asks this to decide which doors to break instead of open, so it
* has to agree with what allowed() does for an NPC at that door.
*/
/proc/door_refuses_npc(obj/machinery/door/door, mob/living/npc)
if(QDELETED(door))
return FALSE
if((istype(door, /obj/machinery/door/airlock) || istype(door, /obj/machinery/door/window)) \
&& (LAZYLEN(door.req_access) || LAZYLEN(door.req_one_access)))
return TRUE
var/obj/structure/overmap/ship/ship = door.crew_locked_ship()
if(isnull(ship))
return FALSE
return isnull(npc) || !ship.is_npc_ship_crew(npc)

/obj/machinery/door/airlock/allowed(mob/user)
if(refused_by_ship_crew_lock(user))
return FALSE
Expand Down Expand Up @@ -154,11 +216,8 @@ GLOBAL_LIST_EMPTY(crew_locked_ships)
/// Whether a crew-locked ship's door must stay shut for this item, which was thrown by
/// someone who is not crew, or by nobody at all.
/obj/machinery/door/proc/refuses_thrown_item(obj/item/thrown)
var/area/shuttle/voidcrew/ship_area = get_area(src)
if(!istype(ship_area))
return FALSE
var/obj/structure/overmap/ship/ship = ship_area.shuttle_port?.current_ship
if(isnull(ship) || !ship.crew_only_airlocks)
var/obj/structure/overmap/ship/ship = crew_locked_ship()
if(isnull(ship))
return FALSE
var/mob/thrower = thrown.throwing?.get_thrower()
return isnull(thrower) || !ship.is_ship_crew(thrower)
Expand Down
34 changes: 9 additions & 25 deletions voidcrew/modules/npc_ships/code/mob_patrol.dm
Original file line number Diff line number Diff line change
Expand Up @@ -246,10 +246,10 @@
PATROL_LOG("FAILED: Path too short ([length(path)] doors)")
return null

// Also build a list of doors that require access (pirates have none, so these need attacking)
// Also build a list of doors that refuse NPCs (pirates have no access and are never crew, so these need attacking)
var/list/locked_doors = list()
for(var/obj/machinery/door/D in path)
if(door_requires_access(D))
if(door_refuses_npc(D))
locked_doors[REF(D)] = TRUE

// Cache the locked doors list
Expand Down Expand Up @@ -605,28 +605,6 @@
controller.clear_blackboard_key(BB_EXPLORATION_INDEX)
controller.clear_blackboard_key(BB_EXPLORATION_TARGET)

/**
* Check if a door requires access to open.
* Pirates have no access, so any door with req_access or req_one_access will need to be attacked.
*/
/proc/door_requires_access(obj/machinery/door/D)
if(!D)
return FALSE

// Check for airlock-specific access requirements
if(istype(D, /obj/machinery/door/airlock))
var/obj/machinery/door/airlock/A = D
if(LAZYLEN(A.req_access) || LAZYLEN(A.req_one_access))
return TRUE

// Check for windoor access requirements
if(istype(D, /obj/machinery/door/window))
var/obj/machinery/door/window/W = D
if(LAZYLEN(W.req_access) || LAZYLEN(W.req_one_access))
return TRUE

return FALSE

/**
* Get or generate the cached patrol path for a ship.
* Paths are cached in GLOB.boarding_patrol_paths keyed by ship ref.
Expand Down Expand Up @@ -882,8 +860,14 @@

// Pre-populate the failed doors list with doors that require access
var/list/locked_doors = GLOB.boarding_locked_doors[ship_ref]
locked_doors = locked_doors ? locked_doors.Copy() : list()
// The captain can turn the crew lock on after the path was cached, so judge it again now
if(target_ship.crew_only_airlocks)
for(var/obj/machinery/door/door in path)
if(door_refuses_npc(door))
locked_doors[REF(door)] = TRUE
if(length(locked_doors))
controller.blackboard["_failed_blocking_doors"] = locked_doors.Copy()
controller.blackboard["_failed_blocking_doors"] = locked_doors
PATROL_LOG("Pre-marked [length(locked_doors)] doors as needing attack (no access)")

PATROL_LOG("SUCCESS: Assigned [mob_to_assign] to patrol, start_index=[start_index], path_length=[length(path)]")
Expand Down
2 changes: 1 addition & 1 deletion voidcrew/modules/npc_ships/code/outpost_patrol.dm
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,7 @@ GLOBAL_LIST_EMPTY(outpost_patrol_caches)

var/list/locked_doors = list()
for(var/obj/machinery/door/door as anything in path)
if(door_requires_access(door))
if(door_refuses_npc(door))
locked_doors[REF(door)] = TRUE
GLOB.boarding_locked_doors[key] = locked_doors

Expand Down
5 changes: 3 additions & 2 deletions voidcrew/modules/npc_ships/code/pirate_crew_ai.dm
Original file line number Diff line number Diff line change
Expand Up @@ -401,8 +401,9 @@
var/obj/machinery/door/airlock/airlock = door
if(airlock.seal)
return FALSE
// A docked or air-safe external airlock opens for anyone from inside (try_safety_unlock())
if(istype(airlock, /obj/machinery/door/airlock/external))
// A docked or air-safe external airlock opens for anyone from inside (try_safety_unlock()),
// unless its ship's crew lock refuses us (try_to_activate_door() in ship_access.dm)
if(istype(airlock, /obj/machinery/door/airlock/external) && !(airlock.requiresID() && airlock.refused_by_ship_crew_lock(pawn)))
var/obj/machinery/door/airlock/external/hatch = airlock
if(hatch.space_dir && (hatch.shuttledocked || hatch.cyclelinkedairlock?.shuttledocked || is_safe_turf(get_step(hatch, hatch.space_dir), TRUE, FALSE)))
return TRUE
Expand Down
62 changes: 62 additions & 0 deletions voidcrew/modules/unit_tests/voidcrew_ship_access.dm
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,68 @@
QDEL_NULL(toolbox.throwing)
ship.ship_team.remove_member(crewmember.mind)

/// A crew lock keeps out pirate NPCs, not just players, and the boarders' planners break those doors.
/datum/unit_test/voidcrew_ship_crew_airlock_npcs
parent_type = /datum/unit_test/voidcrew_ship_crew_airlocks

/datum/unit_test/voidcrew_ship_crew_airlock_npcs/Destroy()
if(ship)
clear_ship_patrol_path(ship)
return ..()

/datum/unit_test/voidcrew_ship_crew_airlock_npcs/Run()
var/obj/machinery/door/airlock/instant/door = allocate(/obj/machinery/door/airlock/instant)
door.autoclose = FALSE
door_turf = get_turf(door)
original_area = door_turf.loc
ship_area = new
door_turf.change_area(original_area, ship_area)
var/obj/docking_port/mobile/voidcrew/port = allocate(/obj/docking_port/mobile/voidcrew)
ship = allocate(/obj/structure/overmap/ship)
ship_area.shuttle_port = port
port.current_ship = ship
ship.ship_team = new /datum/team/voidcrew()
ship.ship_team.ship = ship

var/mob/living/basic/trooper/pirate/melee/pirate = allocate(/mob/living/basic/trooper/pirate/melee, get_step(door, EAST))
var/mob/living/carbon/human/consistent/crewmember = allocate(/mob/living/carbon/human/consistent, get_step(door, EAST))
crewmember.mock_client = allocate(/datum/client_interface)
crewmember.mind_initialize()
ship.ship_team.add_member(crewmember.mind)
// A crewmember's body with nobody in it, e.g. a disconnected player
var/mob/living/carbon/human/consistent/vacant_crew = allocate(/mob/living/carbon/human/consistent, get_step(door, EAST))
vacant_crew.mind_initialize()
ship.ship_team.add_member(vacant_crew.mind)

// Lock off: a door with no access opens for an NPC, as upstream
TEST_ASSERT(door.allowed(pirate), "an unlocked ship's unaccessed airlock refused a pirate")
TEST_ASSERT(!door_refuses_npc(door), "the door planner marked an unlocked, unaccessed airlock for attack")
TEST_ASSERT(pirate_can_open_door(pirate, door), "the pirate AI thinks an unlocked ship's airlock is shut to it")

TEST_ASSERT(ship.set_crew_only_airlocks(TRUE), "could not enable crew-only airlocks")
TEST_ASSERT(!door.allowed(pirate), "a crew-locked ship's airlock let a pirate NPC through")
pirate.last_bumped = 0
door.Bumped(pirate)
TEST_ASSERT(door.density && !door.operating, "a pirate bumped open a crew-locked airlock")
TEST_ASSERT(door.allowed(crewmember), "the crew lock refused a rostered player")
TEST_ASSERT(door.allowed(vacant_crew), "the crew lock refused a rostered body with no player in it")
TEST_ASSERT(door_refuses_npc(door), "the door planner did not mark a crew-locked airlock for attack")
TEST_ASSERT(door_refuses_npc(door, pirate), "the door planner thinks a crew-locked airlock opens for a pirate")
TEST_ASSERT(!door_refuses_npc(door, vacant_crew), "the door planner thinks a crew-locked airlock refuses its own crew")
TEST_ASSERT(!pirate_can_open_door(pirate, door), "the pirate AI thinks a crew-locked airlock opens for it")

// The boarding patrol pre-marks crew-locked doors for attack, even on a path cached before the lock
GLOB.boarding_patrol_paths[REF(ship)] = list(door)
GLOB.boarding_locked_doors[REF(ship)] = list()
TEST_ASSERT(assign_mob_to_patrol(pirate, ship), "could not put the pirate on the boarding patrol")
var/list/attack_doors = pirate.ai_controller.blackboard["_failed_blocking_doors"]
TEST_ASSERT(LAZYACCESS(attack_doors, REF(door)), "the boarding patrol did not mark the crew-locked airlock for attack")

TEST_ASSERT(ship.set_crew_only_airlocks(FALSE), "could not disable crew-only airlocks")
TEST_ASSERT(door.allowed(pirate), "turning the crew lock off did not let the pirate through again")
ship.ship_team.remove_member(crewmember.mind)
ship.ship_team.remove_member(vacant_crew.mind)

/// Same-type ship jobs must not confer command, prevent acting command, or survive demotion.
/datum/unit_test/voidcrew_pill_captain_commands/Run()
var/datum/map_template/shuttle/voidcrew/pill/template = allocate(/datum/map_template/shuttle/voidcrew/pill)
Expand Down
Loading