Files
Paradise/code/modules/space_management/space_transition.dm
AffectedArc07 2c37ae8fd0 The road to Initialize() stability - Enforcing parent call on New(), Initialize() and Destroy() (#14719)
* The road to Initialize() stability

* Fixes sanity, for now
2020-10-28 12:35:36 -04:00

430 lines
14 KiB
Plaintext

//This is realisation of the working torus-looping randomized-per-round space map, this kills the cube
/proc/get_opposite_direction(direction)
switch(direction)
if(Z_LEVEL_NORTH)
return Z_LEVEL_SOUTH
if(Z_LEVEL_SOUTH)
return Z_LEVEL_NORTH
if(Z_LEVEL_EAST)
return Z_LEVEL_WEST
if(Z_LEVEL_WEST)
return Z_LEVEL_EAST
// Do this before setting up new connections, or the old ones will haunt you
/datum/space_level/proc/reset_connections()
neighbors.Cut()
/datum/space_level/proc/link_to_self()
reset_connections()
neighbors = list()
var/list/L = list(Z_LEVEL_NORTH,Z_LEVEL_SOUTH,Z_LEVEL_EAST,Z_LEVEL_WEST)
for(var/A in L)
neighbors[A] = src
// Only call this when the `linkage_map` is already built
/datum/space_level/proc/add_to_space_network(datum/spacewalk_grid/SW)
// Make sure we don't bring any noise data into the network
reset_connections()
xi = initial(xi)
yi = initial(yi)
var/datum/point/P = SW.get_empty_node()
P.set_space_level(src)
/datum/space_level/proc/remove_from_space_network(datum/spacewalk_grid/SW)
var/datum/point/P = SW.get(xi,yi)
SW.release_node(P)
// Only do this when we're done, or we'll trample vars needed for releasing
// the level
xi = initial(xi)
yi = initial(yi)
reset_connections()
// This proc takes another space level, and establishes a connection between the
// two depending on how the `xi` and the `yi` values compare
/datum/space_level/proc/link_levels(datum/space_level/S)
if(S.xi == xi)
if(S.yi == yi+1)
add_connection(S, Z_LEVEL_NORTH)
else if(S.yi == yi-1)
add_connection(S, Z_LEVEL_SOUTH)
else if(S.yi == yi)
if(S.xi == xi+1)
add_connection(S, Z_LEVEL_EAST)
else if(S.xi == xi-1)
add_connection(S, Z_LEVEL_WEST)
else // yell about evil wizards, this shouldn't happen
log_debug("Two z levels attempted to link, but were not adjacent! Our z:([xi],[yi]). Other z:([S.xi],[S.yi])")
// `direction` here is the direction from `src` to `S`
/datum/space_level/proc/add_connection(datum/space_level/S, direction)
var/oppose = get_opposite_direction(direction)
neighbors[direction] = S
S.neighbors[oppose] = src
GLOB.space_manager.unbuilt_space_transitions |= src
GLOB.space_manager.unbuilt_space_transitions |= S
/datum/space_level/proc/get_connection(direction)
if(direction in neighbors)
return neighbors[direction]
// It's in a direction that loops - so we step as far in the opposite direction to get where to wrap to
var/datum/space_level/S = src
var/oppose = get_opposite_direction(direction)
// Loop all the way in the other direction that we can
while(S.neighbors[oppose])
if(S.neighbors[oppose] == src) // we've got a tesseract, boys
CRASH("Tesseract formed when routing connections between z levels. Culprit: z level '[S.zpos]' to '[src.zpos]', direction [oppose]")
S = S.neighbors[oppose]
return S
/datum/point //this is explicitly utilitarian datum type made specially for the space map generation and are absolutely unusable for anything else
var/list/neighbors = list()
var/x
var/y
var/datum/space_level/spl
/datum/point/New(nx, ny)
x = nx
y = ny
/datum/point/proc/hash()
return "([x],[y])"
// This is called only on `filled_nodes` - meaning we have a guarantee that
// it is already surrounded on all sides
/datum/point/proc/set_neighbors(datum/spacewalk_grid/SW)
neighbors.Cut()
neighbors |= SW.get(x+1, y, allow_empty = 1)
neighbors |= SW.get(x-1, y, allow_empty = 1)
neighbors |= SW.get(x, y+1, allow_empty = 1)
neighbors |= SW.get(x, y-1, allow_empty = 1)
// Updates variables of the space level
/datum/point/proc/set_space_level(datum/space_level/S)
spl = S
S.xi = x
S.yi = y
for(var/datum/point/P in neighbors)
if(istype(P.spl))
// Since each time this proc is called, it is the first time
// that the z level is added to the grid, we know for certain
// that no other z level has this as its neighbor
spl.link_levels(P.spl)
// Returns a list of all neighbors that don't have a space level yet
// If a node doesn't exist yet, it will create it
// This is only called for nodes within the `available_nodes` list
/datum/point/proc/get_empty_neighbors(datum/spacewalk_grid/SW)
var/list/result
var/datum/point/up = SW.get(x, y+1, allow_empty = 1)
var/datum/point/down = SW.get(x, y-1, allow_empty = 1)
var/datum/point/left = SW.get(x-1, y, allow_empty = 1)
var/datum/point/right = SW.get(x+1, y, allow_empty = 1)
if(isnull(up))
up = new(x, y+1)
SW.add_available_node(up)
if(isnull(down))
down = new(x, y-1)
SW.add_available_node(down)
if(isnull(left))
left = new(x-1, y)
SW.add_available_node(left)
if(isnull(right))
right = new(x+1, y)
SW.add_available_node(right)
// Nodes with a space datum are not empty
result = list(up, down, left, right)
for(var/datum/point/thing in result)
if(!isnull(thing.spl))
result -= thing
return result
// This looks around itself to see if it has any active nodes within the cardinal directions
/datum/point/proc/has_no_neighbors(datum/spacewalk_grid/SW)
var/result = 1
if(!isnull(SW.get(x+1,y)))
result = 0
if(!isnull(SW.get(x-1,y)))
result = 0
if(!isnull(SW.get(x,y+1)))
result = 0
if(!isnull(SW.get(x,y-1)))
result = 0
return result
/datum/point/proc/deactivate()
if(!spl)
throw EXCEPTION("Attempted to deactivate inactive point")
for(var/direction in spl.neighbors)
var/datum/space_level/S = spl.neighbors[direction]
var/oppose = get_opposite_direction(direction)
S.neighbors.Remove(oppose)
GLOB.space_manager.unbuilt_space_transitions |= S
spl.reset_connections()
spl = initial(spl)
// This is like the old algorithm, except this one
// can expand indefinitely and add new points on the fly
// The algorithm is: Start in the center, and add all adjacent points to a list of things to select
// Then repeatedly do the cycle of choosing a node next to any previously-selected node, then
// add all points next to your chosen node to the list of things to select
/datum/spacewalk_grid
// This is a list of fully initialized nodes - these have their neighbors
// assigned, and are surrounded
var/list/filled_nodes = list()
// These nodes are not fully initialized - their neighbors field is empty,
// and they do not correspond to a space level
var/list/available_nodes = list()
var/list/all_nodes = list() // filled_nodes | available_nodes
var/min_x = 0
var/min_y = 0
var/max_x = 0
var/max_y = 0
/datum/spacewalk_grid/New()
var/datum/point/P = new(0,0)
add_available_node(P)
/datum/spacewalk_grid/Destroy()
for(var/datum/point/P in filled_nodes)
release_node(P)
if(available_nodes.len > 1)
log_debug("Multiple nodes left behind after SW grid qdel: [available_nodes.len]")
for(var/datum/point/P in available_nodes)
log_debug("([P.x],[P.y])")
return ..()
/datum/spacewalk_grid/proc/add_available_node(datum/point/P)
var/hash = P.hash()
if(hash in all_nodes)
log_debug("Hash overlap! [hash]")
all_nodes[P.hash()] = P
available_nodes |= P
// We call this to flag a node as in use - all required variables will be
// ready after this is called
/datum/spacewalk_grid/proc/consume_node(datum/point/P)
filled_nodes |= P
available_nodes -= P
// This proc adds any new neighbors to the list of available nodes
P.get_empty_neighbors(src)
if(min_x > P.x)
min_x = P.x
else if(max_x < P.x)
max_x = P.x
if(min_y > P.y)
min_y = P.y
else if(max_y < P.y)
max_y = P.y
P.set_neighbors(src)
// You can call this with an active point, to remove it from the grid -
// this is important if you want to separate a z level from the network
/datum/spacewalk_grid/proc/release_node(datum/point/P)
filled_nodes -= P
available_nodes |= P
// Go through each neighbor node, and delete it if it's not
// next to any other active nodes
for(var/datum/point/P2 in P.neighbors)
var/isolated = P2.has_no_neighbors(src)
if(isolated)
if(!P2.spl)
available_nodes -= P2
all_nodes -= P2.hash()
qdel(P)
else
log_debug("Isolated z level at ([P2.x],[P2.y]): [P2.spl.zpos]")
P.deactivate()
P.neighbors.Cut()
// Now that we've cleaned out inactive nodes, we will update the bounds
var/datum/point/outer_bound
if(P.x == max_x)
for(var/i = min_y, i <= max_y, i++)
outer_bound = get(P.x, i)
if(!isnull(outer_bound)) // There is still an active node in this column
break
if(isnull(outer_bound))
max_x--
if(P.x == min_x)
for(var/i = min_y, i <= max_y, i++)
outer_bound = get(P.x, i)
if(!isnull(outer_bound)) // There is still an active node in this column
break
if(isnull(outer_bound))
min_x++
if(P.y == max_y)
for(var/i = min_x, i <= max_x, i++)
outer_bound = get(i, P.y)
if(!isnull(outer_bound)) // There is still an active node in this column
break
if(isnull(outer_bound))
max_y--
if(P.y == min_y)
for(var/i = min_x, i <= max_x, i++)
outer_bound = get(i, P.y)
if(!isnull(outer_bound)) // There is still an active node in this column
break
if(isnull(outer_bound))
min_y++
// If the node isn't in the grid, this will return null
/datum/spacewalk_grid/proc/get(x,y, allow_empty = 0)
var/datum/point/P = all_nodes["([x],[y])"]
if(!allow_empty && !(P in filled_nodes))
P = null // active nodes only
return P
/datum/spacewalk_grid/proc/get_width()
return 1 + max_x - min_x
/datum/spacewalk_grid/proc/get_height()
return 1 + max_y - min_y
// This function chooses an available point next to any node in the grid
/datum/spacewalk_grid/proc/get_empty_node()
var/datum/point/P = pick(available_nodes)
if(isnull(P))
throw EXCEPTION("The `available_nodes` list was either empty or contained a null entry")
consume_node(P)
return P
// This function is called repeatedly to build the map
/datum/spacewalk_grid/proc/add_level(datum/space_level/S)
var/datum/point/P = get_empty_node()
P.set_space_level(S)
// This proc substantiates the grid of points used to determine routes between levels
// Separating this from initialization gives us time in which we can add more crosslink z levels
// before we bake in all our connections
/datum/zlev_manager/proc/route_linkage()
var/list/crosslinks = list()
var/datum/space_level/D
for(var/A in z_list)
D = z_list[A]
if(D.linkage == CROSSLINKED)
crosslinks.Add(D)
D.reset_connections()
// We create an imaginary, square, grid, with dimensions that are
// twice the number of z levels, plus one, per side
// This is big enough to hold a straight line from the center to any side
// `point_grid` is indexed as a 2 way matrix of these points
// `grid` is a flat list of these same above points
// Each point represents a possible z level position
if(linkage_map)
qdel(linkage_map)
linkage_map = new
// Now, we pop entries in a random order from our list of space levels
// and assign its connections based on the grid
while(crosslinks.len)
D = pick(crosslinks)
crosslinks.Remove(D)
// Add it to our space grid
D.add_to_space_network(linkage_map)
// Used to loop through turfs in world, now just goes through each level's
// transit turf cache
/datum/zlev_manager/proc/setup_space_destinations(force_all_rebuilds = FALSE)
var/timer = start_watch()
log_debug("Assigning space turf destinations...")
var/datum/space_level/D
var/datum/space_level/E
var/turf/space/S
var/list/levels_to_rebuild = unbuilt_space_transitions
if(force_all_rebuilds)
// Assuming we don't have like 9000 zlevels this shouldn't hurt
levels_to_rebuild = list()
for(var/A in z_list)
levels_to_rebuild.Add(z_list[A])
for(var/foo in levels_to_rebuild) //Define the transitions of the z levels
D = foo
log_debug("Z level [D.zpos]")
switch(D.linkage)
if(UNAFFECTED)
for(var/B in D.transit_west | D.transit_east | D.transit_south | D.transit_north)
S = B
S.remove_transitions()
if(SELFLOOPING,CROSSLINKED)
// Left border
for(var/B in D.transit_west)
S = B
E = D.get_connection(Z_LEVEL_WEST)
S.set_transition_west(E.zpos)
// Right border
for(var/B in D.transit_east)
S = B
E = D.get_connection(Z_LEVEL_EAST)
S.set_transition_east(E.zpos)
// Bottom border
for(var/B in D.transit_south)
S = B
E = D.get_connection(Z_LEVEL_SOUTH)
S.set_transition_south(E.zpos)
// Top border
for(var/B in D.transit_north)
S = B
E = D.get_connection(Z_LEVEL_NORTH)
S.set_transition_north(E.zpos)
unbuilt_space_transitions -= D
log_debug("Assigning space turf destinations complete. Took [stop_watch(timer)]s.")
// Nothing fancy, just does it all at once
/datum/zlev_manager/proc/do_transition_setup()
route_linkage()
setup_space_destinations(force_all_rebuilds = TRUE)
// A debugging proc that expresses the map's shape as a bunch of turfs
/datum/zlev_manager/proc/map_as_turfs(turf/center)
// size is odd
// -1, /2 to get distance from the center
// center - radius = bottom left coordinate
var/datum/point/P
var/turf/our_spot
var/grid_desc = ""
for(var/i = linkage_map.min_x, i <= linkage_map.max_x, i++)
for(var/j = linkage_map.min_y, j <= linkage_map.max_y, j++)
P = linkage_map.get(i, j)
our_spot = locate(center.x + i, center.y + j, center.z)
grid_desc = "([i],[j])"
if(!isnull(P))
our_spot = our_spot.ChangeTurf(/turf/simulated/floor/plating/snow)
grid_desc += ": Z level [P.spl.zpos]. "
var/datum/space_level/up = P.spl.get_connection(Z_LEVEL_NORTH)
var/datum/space_level/down = P.spl.get_connection(Z_LEVEL_SOUTH)
var/datum/space_level/right = P.spl.get_connection(Z_LEVEL_EAST)
var/datum/space_level/left = P.spl.get_connection(Z_LEVEL_WEST)
grid_desc += "Up: [up.zpos], "
grid_desc += "Down: [down.zpos], "
grid_desc += "Right: [right.zpos], "
grid_desc += "Left: [left.zpos]"
else
our_spot = our_spot.ChangeTurf(/turf/simulated/floor/fakespace)
our_spot.desc = grid_desc