diff --git a/.github/workflows/run_extra_map_tests.yml b/.github/workflows/run_extra_map_tests.yml index 11c376b366..38680d2fe3 100644 --- a/.github/workflows/run_extra_map_tests.yml +++ b/.github/workflows/run_extra_map_tests.yml @@ -36,7 +36,6 @@ jobs: sudo dpkg --add-architecture i386 sudo apt update || true sudo apt install zlib1g-dev:i386 - ldd libbapi_dmm_reader.so ldd libverdigris.so - name: Install rust-g run: | diff --git a/.github/workflows/run_integration_tests.yml b/.github/workflows/run_integration_tests.yml index a6d2f9cfa1..bb664823d7 100644 --- a/.github/workflows/run_integration_tests.yml +++ b/.github/workflows/run_integration_tests.yml @@ -42,7 +42,6 @@ jobs: sudo dpkg --add-architecture i386 sudo apt update || true sudo apt install zlib1g-dev:i386 - ldd libbapi_dmm_reader.so ldd libverdigris.so - name: Install rust-g run: | diff --git a/bapi_dmm_reader.dll b/bapi_dmm_reader.dll deleted file mode 100644 index 0262c5a680..0000000000 Binary files a/bapi_dmm_reader.dll and /dev/null differ diff --git a/code/__defines/map.dm b/code/__defines/map.dm index 045119d1c6..0b05207b16 100644 --- a/code/__defines/map.dm +++ b/code/__defines/map.dm @@ -16,3 +16,6 @@ #define SUBMAP_MAP_EDGE_PAD 8 // Automatically created submaps are forbidden from being this close to the main map's edge. #define CELL_ALIVE(VAL) (VAL == cell_live_value) + +/// A map key that corresponds to being one exclusively for Space. +#define SPACE_KEY "space" diff --git a/code/controllers/subsystems/mapping.dm b/code/controllers/subsystems/mapping.dm index 50fb5f2bc9..b9964c92d8 100644 --- a/code/controllers/subsystems/mapping.dm +++ b/code/controllers/subsystems/mapping.dm @@ -230,10 +230,3 @@ SUBSYSTEM_DEF(mapping) if (!GLOB.Debug2) return // Only show up in stat panel if debugging is enabled. . = ..() - -// VOREStation Edit: BAPI-dmm -/datum/controller/subsystem/mapping/Shutdown() - // Force bapi to drop it's cached maps on server shutdown. - _bapidmm_clear_map_data() - fdel("data/baked_dmm_files/") -// VOREStation Edit End diff --git a/code/game/world.dm b/code/game/world.dm index 0edb019fe5..362d5b1d88 100644 --- a/code/game/world.dm +++ b/code/game/world.dm @@ -710,6 +710,44 @@ GLOBAL_VAR_INIT(failed_db_connections, 0) GLOB.living_players_by_zlevel.len++ GLOB.living_players_by_zlevel[GLOB.living_players_by_zlevel.len] = list() +/** + * Handles increasing the world's maxx var and initializing the new turfs and assigning them to the global area. + * If map_load_z_cutoff is passed in, it will only load turfs up to that z level, inclusive. + * This is because maploading will handle the turfs it loads itself. + */ +/world/proc/increase_max_x(new_maxx, map_load_z_cutoff = maxz) + if(new_maxx <= maxx) + return + // var/old_max = world.maxx + maxx = new_maxx + // if(!map_load_z_cutoff) + // return + // var/area/global_area = GLOB.areas_by_type[world.area] // We're guaranteed to be touching the global area, so we'll just do this + // LISTASSERTLEN(global_area.turfs_by_zlevel, map_load_z_cutoff, list()) + // for (var/zlevel in 1 to map_load_z_cutoff) + // var/list/to_add = block( + // old_max + 1, 1, zlevel, + // maxx, maxy, zlevel + // ) + + // global_area.turfs_by_zlevel[zlevel] += to_add + +/world/proc/increase_max_y(new_maxy, map_load_z_cutoff = maxz) + if(new_maxy <= maxy) + return + // var/old_maxy = maxy + maxy = new_maxy + // if(!map_load_z_cutoff) + // return + // var/area/global_area = GLOB.areas_by_type[world.area] // We're guaranteed to be touching the global area, so we'll just do this + // LISTASSERTLEN(global_area.turfs_by_zlevel, map_load_z_cutoff, list()) + // for (var/zlevel in 1 to map_load_z_cutoff) + // var/list/to_add = block( + // 1, old_maxy + 1, 1, + // maxx, maxy, map_load_z_cutoff + // ) + // global_area.turfs_by_zlevel[zlevel] += to_add + // Call this to make a new blank z-level, don't modify maxz directly. /world/proc/increment_max_z() maxz++ diff --git a/code/modules/maps/bapi-dmm/bapi_bindings.dm b/code/modules/maps/bapi-dmm/bapi_bindings.dm deleted file mode 100644 index b88871414b..0000000000 --- a/code/modules/maps/bapi-dmm/bapi_bindings.dm +++ /dev/null @@ -1,38 +0,0 @@ -//THIS FILE IS AUTOMATICALLY GENERATED BY BAPI_DMM_READER, PLEASE DO NOT TOUCH IT -//PROC DEFINITIONS MAY MOVE AROUND, THIS IS NORMAL - -/* This comment bypasses grep checks */ /var/__bapi_dmm_reader - -/proc/__detect_bapi_dmm_reader() - if (world.system_type == UNIX) - if(fexists("./libbapi_dmm_reader.so")) - // No need for LD_LIBRARY_PATH badness. - return __bapi_dmm_reader = "./libbapi_dmm_reader.so" - // It's not in the current directory, so try others - return __bapi_dmm_reader = "libbapi_dmm_reader" - else - return __bapi_dmm_reader = "bapi_dmm_reader" - -#define BAPI_DMM_READER (__bapi_dmm_reader || __detect_bapi_dmm_reader()) - -/proc/_bapidmm_test_connection() - return call_ext(BAPI_DMM_READER, "byond:_bapidmm_test_connection_ffi")() - -/proc/_bapidmm_parse_map_blocking(dmm_file, map_datum) - return call_ext(BAPI_DMM_READER, "byond:_bapidmm_parse_map_blocking_ffi")(dmm_file, map_datum) - -/proc/_bapidmm_load_map_buffered(parsed_map, x_offset, y_offset, z_offset, crop_map, no_changeturf, x_lower, -x_upper, y_lower, y_upper, z_lower, z_upper, place_on_top, new_z) - return call_ext(BAPI_DMM_READER, "byond:_bapidmm_load_map_buffered_ffi")(parsed_map, x_offset, y_offset, z_offset, crop_map, no_changeturf, x_lower, -x_upper, y_lower, y_upper, z_lower, z_upper, place_on_top, new_z) - -/proc/_bapidmm_work_commandbuffer(parsed_map, resume_key) - return call_ext(BAPI_DMM_READER, "byond:_bapidmm_work_commandbuffer_ffi")(parsed_map, resume_key) - -/proc/_bapidmm_clear_map_data() - return call_ext(BAPI_DMM_READER, "byond:_bapidmm_clear_map_data_ffi")() - -/proc/bapidmm_generate_automata(limit_x, limit_y, iterations, initial_wall_cell) - return call_ext(BAPI_DMM_READER, "byond:bapidmm_generate_automata_ffi")(limit_x, limit_y, iterations, initial_wall_cell) - -#undef BAPI_DMM_READER diff --git a/code/modules/maps/bapi-dmm/bapi_dmm_reader.dm b/code/modules/maps/bapi-dmm/bapi_dmm_reader.dm deleted file mode 100644 index 20c5beedb9..0000000000 --- a/code/modules/maps/bapi-dmm/bapi_dmm_reader.dm +++ /dev/null @@ -1,274 +0,0 @@ -// This file provides manually written utility types and such for the BAPI DMM Reader - -GLOBAL_LIST_EMPTY(cached_maps) - -GLOBAL_VAR_INIT(use_preloader, FALSE) -GLOBAL_VAR_INIT(_preloader_path, null) -GLOBAL_LIST_EMPTY(_preloader_attributes) - -/world/proc/preloader_setup(list/the_attributes, path) - if(LAZYLEN(the_attributes)) - GLOB.use_preloader = TRUE - GLOB._preloader_attributes = the_attributes - GLOB._preloader_path = path - -/world/proc/preloader_load(atom/what) - GLOB.use_preloader = FALSE - var/list/attributes = GLOB._preloader_attributes - for(var/attribute in attributes) - var/value = attributes[attribute] - if(islist(value)) - value = deepCopyList(value) - #ifdef TESTING - if(what.vars[attribute] == value) - var/message = span_green("[what.type]") + " at [AREACOORD(what)] - " + span_bold("VAR:") + " " + span_red("[attribute] = [isnull(value) ? "null" : (isnum(value) ? value : "\"[value]\"")]") - world.log << "DIRTY VAR: [message]" - dirty_vars += message - #endif - what.vars[attribute] = value - -#define MAP_DMM "dmm" -/** - * TGM SPEC: - * TGM is a derevation of DMM, with restrictions placed on it - * to make it easier to parse and to reduce merge conflicts/ease their resolution - * - * Requirements: - * Each "statement" in a key's details ends with a new line, and wrapped in (...) - * All paths end with either a comma or occasionally a {, then a new line - * Excepting the area, who is listed last and ends with a ) to mark the end of the key - * - * {} denotes a list of variable edits applied to the path that came before the first { - * the final } is followed by a comma, and then a new line - * Variable edits have the form \tname = value;\n - * Except the last edit, which has no final ;, and just ends in a newline - * No extra padding is permitted - * Many values are supported. See parse_constant() - * Strings must be wrapped in "...", files in '...', and lists in list(...) - * Files are kinda susy, and may not actually work. buyer beware - * Lists support assoc values as expected - * These constants can be further embedded into lists - * - * There can be no padding in front of, or behind a path - * - * Therefore: - * "key" = ( - * /path, - * /other/path{ - * var = list("name" = 'filepath'); - * other_var = /path - * }, - * /turf, - * /area) - * - */ -#define MAP_TGM "tgm" -#define MAP_UNKNOWN "unknown" - -/// Returned from parse_map to give some metadata about the map -/datum/bapi_parsed_map - var/_internal_index = -1 - - var/original_path = "" - var/map_format = MAP_UNKNOWN - var/key_len = 0 - var/line_len = 0 - var/expanded_y = FALSE - var/expanded_x = FALSE - - /// Unoffset bounds. Null on parse failure. - var/list/bounds = list() - /// Offset bounds. Same as parsed_bounds until load(). - var/list/parsed_bounds = list() - - ///any turf in this list is skipped inside of build_coordinate. Lazy assoc list - var/list/turf_blacklist - - var/loading = FALSE - var/loaded_warnings = list() - -/** - * Helper and recommened way to load a map file - * - dmm_file: The path to the map file - * - x_offset: The x offset to load the map at - * - y_offset: The y offset to load the map at - * - z_offset: The z offset to load the map at - * - crop_map: If true, the map will be cropped to the world bounds - * - measure_only: If true, the map will not be loaded, but the bounds will be calculated - * - no_changeturf: If true, the map will not call /turf/AfterChange - * - x_lower: The minimum x coordinate to load - * - x_upper: The maximum x coordinate to load - * - y_lower: The minimum y coordinate to load - * - y_upper: The maximum y coordinate to load - * - z_lower: The minimum z coordinate to load - * - z_upper: The maximum z coordinate to load - * - place_on_top: Whether to use /turf/proc/PlaceOnTop rather than /turf/proc/ChangeTurf - * - new_z: If true, a new z level will be created for the map - */ -/proc/load_map_bapi( - dmm_file, - x_offset = 1, - y_offset = 1, - z_offset, - crop_map = FALSE, - measure_only = FALSE, - no_changeturf = FALSE, - x_lower = -INFINITY, - x_upper = INFINITY, - y_lower = -INFINITY, - y_upper = INFINITY, - z_lower = -INFINITY, - z_upper = INFINITY, - place_on_top = FALSE, - new_z = FALSE, -) - if(isfile(dmm_file)) - log_mapping("bapi-dmm was passed a file instead of a path string: [dmm_file]") - var/name = sanitize_filename("[dmm_file]") - var/path = "data/baked_dmm_files/[name]" - if(!fexists(path)) - var/text = file2text(dmm_file) - rustg_file_write(text, path) - dmm_file = path - - if(!fexists(dmm_file)) - stack_trace("Invalid map path: [dmm_file]") - return - - if(!(dmm_file in GLOB.cached_maps)) - GLOB.cached_maps[dmm_file] = new /datum/bapi_parsed_map(dmm_file) - - // Bay assumption - if(!z_offset) - z_offset = world.maxz + 1 - - var/datum/bapi_parsed_map/parsed_map = GLOB.cached_maps[dmm_file] - parsed_map = parsed_map.copy() - if(!measure_only && !isnull(parsed_map.bounds)) - parsed_map.load(x_offset, y_offset, z_offset, crop_map, no_changeturf, x_lower, x_upper, y_lower, y_upper, z_lower, z_upper, place_on_top, new_z) - return parsed_map - -/datum/bapi_parsed_map/New(tfile) - if(isnull(tfile)) - return // create a new datum without loading a map - var/ret = _bapidmm_parse_map_blocking(tfile, src) - if(!ret) - CRASH("Failed to load map [tfile], check rust_log.txt") - -/datum/bapi_parsed_map/Destroy() - ..() - SSatoms.map_loader_stop(REF(src)) // Just in case, I don't want to double up here - if(turf_blacklist) - turf_blacklist.Cut() - parsed_bounds.Cut() - bounds.Cut() - return QDEL_HINT_HARDDEL_NOW - -/datum/bapi_parsed_map/proc/copy() - // Avoids duped work just in case - var/datum/bapi_parsed_map/newfriend = new() - // use the same under-the-hood data - newfriend._internal_index = _internal_index - newfriend.original_path = original_path - newfriend.map_format = map_format - newfriend.key_len = key_len - newfriend.line_len = line_len - newfriend.parsed_bounds = parsed_bounds.Copy() - // Copy parsed bounds to reset to initial values - newfriend.bounds = parsed_bounds.Copy() - newfriend.turf_blacklist = turf_blacklist?.Copy() - // Explicitly do NOT copy `loaded` and `loaded_warnings` - return newfriend - -/datum/bapi_parsed_map/proc/load( - x_offset = 1, - y_offset = 1, - z_offset = 1, - crop_map = FALSE, - no_changeturf = FALSE, - x_lower = -INFINITY, - x_upper = INFINITY, - y_lower = -INFINITY, - y_upper = INFINITY, - z_lower = -INFINITY, - z_upper = INFINITY, - place_on_top = FALSE, - new_z = FALSE, -) - Master.StartLoadingMap() - . = _load_impl(x_offset, y_offset, z_offset, crop_map, no_changeturf, x_lower, x_upper, y_lower, y_upper, z_lower, z_upper, place_on_top, new_z) - Master.StopLoadingMap() - -/datum/bapi_parsed_map/proc/_load_impl( - x_offset = 1, - y_offset = 1, - z_offset = 1, - crop_map = FALSE, - no_changeturf = FALSE, - x_lower = -INFINITY, - x_upper = INFINITY, - y_lower = -INFINITY, - y_upper = INFINITY, - z_lower = -INFINITY, - z_upper = INFINITY, - place_on_top = FALSE, - new_z = FALSE, -) - PRIVATE_PROC(TRUE) - SSatoms.map_loader_begin(REF(src)) - - // `loading` var handled by bapidmm - var/resume_key = _bapidmm_load_map_buffered( - src, - x_offset, - y_offset, - z_offset, - crop_map, - no_changeturf, - x_lower, - x_upper, - y_lower, - y_upper, - z_lower, - z_upper, - place_on_top, - new_z - ) - - if(!resume_key) - SSatoms.map_loader_stop(REF(src)) - CRASH("Failed to generate command buffer, check rust_log.txt and other runtimes") - - var/work_remaining = FALSE - do - work_remaining = _bapidmm_work_commandbuffer(src, resume_key) - stoplag() - while(work_remaining) - - SSatoms.map_loader_stop(REF(src)) - - // if(new_z) - // for(var/z_index in bounds[MAP_MINZ] to bounds[MAP_MAXZ]) - // SSmapping.build_area_turfs(z_index) - - // if(!no_changeturf) - // var/list/turfs = block( - // locate(bounds[MAP_MINX], bounds[MAP_MINY], bounds[MAP_MINZ]), - // locate(bounds[MAP_MAXX], bounds[MAP_MAXY], bounds[MAP_MAXZ])) - // for(var/turf/T as anything in turfs) - // //we do this after we load everything in. if we don't, we'll have weird atmos bugs regarding atmos adjacent turfs - // T.AfterChange(CHANGETURF_IGNORE_AIR) - - // if(expanded_x || expanded_y) - // SEND_GLOBAL_SIGNAL(COMSIG_GLOB_EXPANDED_WORLD_BOUNDS, expanded_x, expanded_y) - - return TRUE - -/datum/bapi_parsed_map/proc/has_warnings() - if(length(loaded_warnings)) - return TRUE - return FALSE - -// #undef MAP_DMM -// #undef MAP_TGM -// #undef MAP_UNKNOWN diff --git a/code/modules/maps/bapi-dmm/bapi_helpers.dm b/code/modules/maps/bapi-dmm/bapi_helpers.dm deleted file mode 100644 index 815e4764fd..0000000000 --- a/code/modules/maps/bapi-dmm/bapi_helpers.dm +++ /dev/null @@ -1,110 +0,0 @@ -// Internal bapi-dmm helpers -/datum/bapi_parsed_map/proc/_bapi_add_warning(warning) - loaded_warnings += list(warning) - -/datum/bapi_parsed_map/proc/_bapi_expand_map(x, y, z, new_z, z_offset) - if(x > world.maxx) - expanded_x = TRUE - world.maxx = x - // if(new_z) - // world.increase_max_x(x, map_load_z_cutoff = z_offset - 1) - // else - // world.increase_max_x(x) - if(y > world.maxy) - expanded_y = TRUE - world.maxy = y - // if(new_z) - // world.increase_max_y(y, map_load_z_cutoff = z_offset - 1) - // else - // world.increase_max_y(y) - if(z > world.maxz) - while(world.maxz < z) - world.increment_max_z() - -/proc/_bapi_helper_get_world_bounds() - . = list(world.maxx, world.maxy, world.maxz) - -/proc/_bapi_helper_text2path(text) - . = text2path(text) - -/proc/_bapi_helper_text2file(text) - . = file(text) - -/proc/_bapi_create_atom(path, crds) - set waitfor = FALSE - . = new path (crds) - -/proc/_bapi_setup_preloader(list/attributes, path) - world.preloader_setup(attributes, path) - -/proc/_bapi_apply_preloader(atom/A) - if(GLOB.use_preloader) - world.preloader_load(A) - -/proc/_bapi_new_atom(text_path, turf/crds, list/attributes) - var/path = text2path(text_path) - if(!path) - CRASH("Bad atom path [text_path]") - - if(attributes != null) - world.preloader_setup(attributes, path) - - var/atom/instance = _bapi_create_atom(path, crds) // first preloader pass - - if(GLOB.use_preloader && instance) // second preloader pass for atoms that don't ..() in New() - world.preloader_load(instance) - -/proc/_bapi_create_or_get_area(text_path) - var/path = text2path(text_path) - if(!path) - CRASH("Bad area path [text_path]") - - var/area/area_instance = GLOB.areas_by_type[path] - if(!area_instance) - area_instance = new path(null) - if(!area_instance) - CRASH("[path] failed to be new'd, what'd you do?") - - return area_instance - -/proc/_bapi_handle_area_contain(turf/T, area/A) - // var/area/old_area = T.loc - // if(old_area == A) - // return // no changing areas that already contain this turf, it'll confuse them - // LISTASSERTLEN(old_area.turfs_to_uncontain_by_zlevel, T.z, list()) - // LISTASSERTLEN(A.turfs_by_zlevel, T.z, list()) - // old_area.turfs_to_uncontain_by_zlevel[T.z] += T - // A.turfs_by_zlevel[T.z] += T - -/proc/_bapi_create_turf(turf/crds, text_path, list/attributes, place_on_top, no_changeturf) - var/path = text2path(text_path) - if(!path) - CRASH("Bad turf path [text_path]") - - if(attributes != null) - world.preloader_setup(attributes, path) - - var/atom/instance - if(no_changeturf) - instance = _bapi_create_atom(path, crds) - else - instance = crds.ChangeTurf(path, FALSE, TRUE) - - if(GLOB.use_preloader && instance) // second preloader pass for atoms that don't ..() in New() - world.preloader_load(instance) - -/proc/_bapi_add_turf_to_area(area/A, turf/T) - if(!A || !T) - return - A.contents.Add(T) - -/proc/_bapi_helper_get_world_type_turf() - return "[world.turf]" - -/proc/_bapi_helper_get_world_type_area() - return "[world.area]" - -/// Implement this to have bapi-dmm sleep occasionally -/// by returning true -/proc/_bapi_helper_tick_check() - return TICK_CHECK diff --git a/code/modules/maps/tg/map_template.dm b/code/modules/maps/map_template.dm similarity index 81% rename from code/modules/maps/tg/map_template.dm rename to code/modules/maps/map_template.dm index 1f524e503d..e9bc6abc61 100644 --- a/code/modules/maps/tg/map_template.dm +++ b/code/modules/maps/map_template.dm @@ -1,12 +1,16 @@ /datum/map_template var/name = "Default Template Name" - var/desc = "Some text should go here. Maybe." - var/name_alias // Override to what this map gets registered as in map_templates_loaded - var/template_group = null // If this is set, no more than one template in the same group will be spawned, per submap seeding. var/width = 0 var/height = 0 var/mappath = null var/loaded = 0 // Times loaded this round + var/datum/parsed_map/cached_map + var/keep_cached_map = FALSE + + // Virgo stuff + var/desc = "Some text should go here. Maybe." + var/name_alias // Override to what this map gets registered as in map_templates_loaded + var/template_group = null // If this is set, no more than one template in the same group will be spawned, per submap seeding. var/annihilate = FALSE // If true, all (movable) atoms at the location where the map is loaded will be deleted before the map is loaded in. var/cost = null /* The map generator has a set 'budget' it spends to place down different submaps. It will pick available submaps randomly until @@ -15,26 +19,28 @@ var/allow_duplicates = FALSE // If false, only one map template will be spawned by the game. Doesn't affect admins spawning then manually. var/discard_prob = 0 // If non-zero, there is a chance that the map seeding algorithm will skip this template when selecting potential templates to use. -/datum/map_template/New(path = null, rename = null) +/datum/map_template/New(path = null, rename = null, cache = FALSE) SHOULD_CALL_PARENT(TRUE) . = ..() if(path) mappath = path if(mappath) - preload_size(mappath) + preload_size(mappath, cache) if(rename) name = rename -/datum/map_template/proc/preload_size(path) - var/datum/bapi_parsed_map/map = load_map_bapi(path, 1, 1, 1, crop_map=FALSE, measure_only=TRUE) - var/list/bounds = map.parsed_bounds +/datum/map_template/proc/preload_size(path, cache = FALSE) + var/datum/parsed_map/parsed = new(file(path)) + var/bounds = parsed?.bounds if(bounds) width = bounds[MAP_MAXX] // Assumes all templates are rectangular, have a single Z level, and begin at 1,1,1 height = bounds[MAP_MAXY] + if(cache) + cached_map = parsed return bounds -/datum/map_template/proc/initTemplateBounds(var/list/bounds) - if (SSatoms.initialized == INITIALIZATION_INSSATOMS) +/datum/map_template/proc/initTemplateBounds(list/bounds) + if(SSatoms.initialized == INITIALIZATION_INSSATOMS) return // let proper initialisation handle it later var/prev_shuttle_queue_state = SSshuttles.block_init_queue @@ -92,9 +98,20 @@ x = round((world.maxx - width)/2) y = round((world.maxy - height)/2) - on_map_preload(world.maxz + 1) //VOREStation Edit - var/datum/bapi_parsed_map/map = load_map_bapi(mappath, x, y, no_changeturf = TRUE) - var/list/bounds = map.bounds + // This would normally be handled by SSmapping + world.increment_max_z() + + on_map_preload(world.maxz) //VOREStation Edit + var/datum/parsed_map/parsed = load_map( + file(mappath), + x, + y, + world.maxz, + no_changeturf = TRUE, // (SSatoms.initialized == INITIALIZATION_INSSATOMS), + place_on_top = FALSE, // should_place_on_top, + new_z = TRUE, + ) + var/list/bounds = parsed.bounds if(!bounds) return FALSE @@ -120,11 +137,24 @@ if(annihilate) annihilate_bounds(old_T, centered) - var/datum/bapi_parsed_map/map = load_map_bapi(mappath, T.x, T.y, T.z, crop_map = TRUE) - var/list/bounds = map.bounds - if(!bounds) + // Accept cached maps, but don't save them automatically - we don't want + // ruins clogging up memory for the whole round. + var/datum/parsed_map/parsed = cached_map || new(file(mappath)) + cached_map = keep_cached_map ? parsed : null + + if(!parsed.load( + T.x, + T.y, + T.z, + crop_map = TRUE, + no_changeturf = FALSE, // (SSatoms.initialized == INITIALIZATION_INSSATOMS), // otherwise space turfs break + place_on_top = FALSE, // should_place_on_top, + )) return + var/list/bounds = parsed.bounds + if(!bounds) + return // if(!SSmapping.loading_ruins) //Will be done manually during mapping ss init // repopulate_sorted_areas() @@ -138,10 +168,10 @@ /datum/map_template/proc/get_affected_turfs(turf/T, centered = FALSE) var/turf/placement = T if(centered) - var/turf/corner = locate(placement.x - round((width)/2), placement.y - round((height)/2), placement.z) // %180 catches East/West (90,270) rotations on true, North/South (0,180) rotations on false + var/turf/corner = locate(placement.x - round(width/2), placement.y - round(height/2), placement.z) if(corner) placement = corner - return block(placement, locate(placement.x+(width)-1, placement.y+(height)-1, placement.z)) + return block(placement.x, placement.y, placement.z, placement.x+width-1, placement.y+height-1, placement.z) /datum/map_template/proc/annihilate_bounds(turf/origin, centered = FALSE) var/deleted_atoms = 0 @@ -154,12 +184,32 @@ qdel(AM) admin_notice(span_danger("Annihilated [deleted_atoms] objects."), R_DEBUG) +/// Takes in a type path, locates an instance of that type in the cached map, and calculates its offset from the origin of the map, returns this offset in the form list(x, y). +/datum/map_template/proc/discover_offset(obj/marker) + var/key + var/list/models = cached_map.grid_models + for(key in models) + if(findtext(models[key], "[marker]")) // Yay compile time checks + break // This works by assuming there will ever only be one mobile dock in a template at most + + for(var/datum/grid_set/gset as anything in cached_map.gridSets) + var/ycrd = gset.ycrd + for(var/line in gset.gridLines) + var/xcrd = gset.xcrd + for(var/j in 1 to length(line) step cached_map.key_len) + if(key == copytext(line, j, j + cached_map.key_len)) + return list(xcrd, ycrd) + ++xcrd + --ycrd //for your ever biggening badminnery kevinz000 //❤ - Cyberboss /proc/load_new_z_level(var/file, var/name) - var/datum/map_template/template = new(file, name) + var/datum/map_template/template = new(file, name, TRUE) + if(!template.cached_map || template.cached_map.check_for_errors()) + return FALSE template.load_new_z() + return TRUE // Very similar to the /tg/ version. /proc/seed_submaps(var/list/z_levels, var/budget = 0, var/whitelist = /area/space, var/desired_map_template_type = null) @@ -307,10 +357,3 @@ else admin_notice("Submaps loaded.", R_DEBUG) admin_notice("Loaded: [english_list(pretty_submap_list)]", R_DEBUG) - -/area/template_noop - name = "Area Passthrough" - -/turf/template_noop - name = "Turf Passthrough" - icon_state = "template_void" diff --git a/code/modules/maps/tg/map_template_vr.dm b/code/modules/maps/map_template_vr.dm similarity index 100% rename from code/modules/maps/tg/map_template_vr.dm rename to code/modules/maps/map_template_vr.dm diff --git a/code/modules/maps/preloader.dm b/code/modules/maps/preloader.dm new file mode 100644 index 0000000000..706a6f1572 --- /dev/null +++ b/code/modules/maps/preloader.dm @@ -0,0 +1,31 @@ +GLOBAL_VAR_INIT(use_preloader, FALSE) +GLOBAL_VAR_INIT(_preloader_path, null) +GLOBAL_LIST_EMPTY(_preloader_attributes) + +/world/proc/preloader_setup(list/the_attributes, path) + if(LAZYLEN(the_attributes)) + GLOB.use_preloader = TRUE + GLOB._preloader_attributes = the_attributes + GLOB._preloader_path = path + +/world/proc/preloader_load(atom/what) + GLOB.use_preloader = FALSE + var/list/attributes = GLOB._preloader_attributes + for(var/attribute in attributes) + var/value = attributes[attribute] + if(islist(value)) + value = deepCopyList(value) + #ifdef TESTING + if(what.vars[attribute] == value) + var/message = span_green("[what.type]") + " at [AREACOORD(what)] - " + span_bold("VAR:") + " " + span_red("[attribute] = [isnull(value) ? "null" : (isnum(value) ? value : "\"[value]\"")]") + world.log << "DIRTY VAR: [message]" + // dirty_vars += message + #endif + what.vars[attribute] = value + +/area/template_noop + name = "Area Passthrough" + +/turf/template_noop + name = "Turf Passthrough" + icon_state = "template_void" diff --git a/code/modules/maps/reader.dm b/code/modules/maps/reader.dm new file mode 100644 index 0000000000..d6c8bb3e03 --- /dev/null +++ b/code/modules/maps/reader.dm @@ -0,0 +1,1118 @@ +/////////////////////////////////////////////////////////////// +//SS13 Optimized Map loader +////////////////////////////////////////////////////////////// +// We support two different map formats +// It is kinda possible to process them together, but if we split them up +// I can make optimization decisions more easily +/** + * DMM SPEC: + * DMM is split into two parts. First we have strings of text linked to lists of paths and their modifications (I will call this the cache) + * We call these strings "keys" and the things they point to members. Keys have a static length + * + * The second part is a list of locations matched to a string of keys. (I'll be calling this the grid) + * These are used to lookup the cache we built earlier. + * We store location lists as grid_sets. the lines represent different things depending on the spec + * + * In standard DMM (which you can treat as the base case, since it also covers weird modifications) each line + * represents an x file, and there's typically only one grid set per z level. + * The meme is you can look at a DMM formatted map and literally see what it should roughly look like + * This differs in TGM, and we can pull some performance from this + * + * Any restrictions here also apply to TGM + * + * /tg/ Restrictions: + * Paths have a specified order. First atoms in the order in which they should be loaded, then a single turf, then the area of the cell + * DMM technically supports turf stacking, but this is deprecated for all formats + + */ +#define MAP_DMM "dmm" +/** + * TGM SPEC: + * TGM is a derevation of DMM, with restrictions placed on it + * to make it easier to parse and to reduce merge conflicts/ease their resolution + * + * Requirements: + * Each "statement" in a key's details ends with a new line, and wrapped in (...) + * All paths end with either a comma or occasionally a {, then a new line + * Excepting the area, who is listed last and ends with a ) to mark the end of the key + * + * {} denotes a list of variable edits applied to the path that came before the first { + * the final } is followed by a comma, and then a new line + * Variable edits have the form \tname = value;\n + * Except the last edit, which has no final ;, and just ends in a newline + * No extra padding is permitted + * Many values are supported. See parse_constant() + * Strings must be wrapped in "...", files in '...', and lists in list(...) + * Files are kinda susy, and may not actually work. buyer beware + * Lists support assoc values as expected + * These constants can be further embedded into lists + * + * There can be no padding in front of, or behind a path + * + * Therefore: + * "key" = ( + * /path, + * /other/path{ + * var = list("name" = 'filepath'); + * other_var = /path + * }, + * /turf, + * /area) + * + */ +#define MAP_TGM "tgm" +#define MAP_UNKNOWN "unknown" + +GLOBAL_LIST_EMPTY(cached_maps) + +/datum/grid_set + var/xcrd + var/ycrd + var/zcrd + var/gridLines + +/datum/parsed_map + var/original_path + var/map_format + /// The length of a key in this file. This is promised by the standard to be static + var/key_len = 0 + /// The length of a line in this file. Not promised by dmm but standard dmm uses it, so we can trust it + var/line_len = 0 + /// If we've expanded world.maxx + var/expanded_y = FALSE + /// If we've expanded world.maxy + var/expanded_x = FALSE + var/list/grid_models = list() + var/list/gridSets = list() + /// List of area types we've loaded AS A PART OF THIS MAP + /// We do this to allow non unique areas, so we'll only load one per map + var/list/area/loaded_areas = list() + + var/list/modelCache + + /// Unoffset bounds. Null on parse failure. + var/list/parsed_bounds + /// Offset bounds. Same as parsed_bounds until load(). + var/list/bounds + + ///any turf in this list is skipped inside of build_coordinate. Lazy assoc list + var/list/turf_blacklist + + // raw strings used to represent regexes more accurately + // '' used to avoid confusing syntax highlighting + var/static/regex/dmm_regex = new(@'"([a-zA-Z]+)" = (?:\(\n|\()((?:.|\n)*?)\)\n(?!\t)|\((\d+),(\d+),(\d+)\) = \{"([a-zA-Z\n]*)"\}', "g") + /// Matches key formats in TMG (IE: newline after the \() + var/static/regex/matches_tgm = new(@'^"[A-z]*"[\s]*=[\s]*\([\s]*\n', "m") + /// Pulls out key value pairs for TGM + var/static/regex/var_edits_tgm = new(@'^\t([A-z0-9]*) = (.*?);?$') + /// Pulls out model paths for DMM + var/static/regex/model_path = new(@'(\/[^\{]*?(?:\{.*?\})?)(?:,|$)', "g") + + /// If we are currently loading this map + var/loading = FALSE + + #ifdef TESTING + var/turfsSkipped = 0 + #endif + +/datum/parsed_map/proc/copy() + // Avoids duped work just in case + build_cache() + var/datum/parsed_map/newfriend = new() + newfriend.original_path = original_path + newfriend.map_format = map_format + newfriend.key_len = key_len + newfriend.line_len = line_len + newfriend.grid_models = grid_models.Copy() + newfriend.gridSets = gridSets.Copy() + newfriend.modelCache = modelCache.Copy() + newfriend.parsed_bounds = parsed_bounds.Copy() + // Copy parsed bounds to reset to initial values + newfriend.bounds = parsed_bounds.Copy() + newfriend.turf_blacklist = turf_blacklist?.Copy() + return newfriend + +/** + * Helper and recommened way to load a map file + * - dmm_file: The path to the map file + * - x_offset: The x offset to load the map at + * - y_offset: The y offset to load the map at + * - z_offset: The z offset to load the map at + * - crop_map: If true, the map will be cropped to the world bounds + * - measure_only: If true, the map will not be loaded, but the bounds will be calculated + * - no_changeturf: If true, the map will not call /turf/AfterChange + * - x_lower: The minimum x coordinate to load + * - x_upper: The maximum x coordinate to load + * - y_lower: The minimum y coordinate to load + * - y_upper: The maximum y coordinate to load + * - z_lower: The minimum z coordinate to load + * - z_upper: The maximum z coordinate to load + * - place_on_top: Whether to use /turf/proc/PlaceOnTop rather than /turf/proc/ChangeTurf + * - new_z: If true, a new z level will be created for the map + */ +/proc/load_map( + dmm_file, + x_offset = 0, + y_offset = 0, + z_offset = 0, + crop_map = FALSE, + measure_only = FALSE, + no_changeturf = FALSE, + x_lower = -INFINITY, + x_upper = INFINITY, + y_lower = -INFINITY, + y_upper = INFINITY, + z_lower = -INFINITY, + z_upper = INFINITY, + place_on_top = FALSE, + new_z = FALSE, +) + if(!(dmm_file in GLOB.cached_maps)) + GLOB.cached_maps[dmm_file] = new /datum/parsed_map(dmm_file) + + var/datum/parsed_map/parsed_map = GLOB.cached_maps[dmm_file] + parsed_map = parsed_map.copy() + if(!measure_only && !isnull(parsed_map.bounds)) + parsed_map.load(x_offset, y_offset, z_offset, crop_map, no_changeturf, x_lower, x_upper, y_lower, y_upper, z_lower, z_upper, place_on_top, new_z) + return parsed_map + +/// Parse a map, possibly cropping it. +/datum/parsed_map/New(tfile, x_lower = -INFINITY, x_upper = INFINITY, y_lower = -INFINITY, y_upper=INFINITY, z_lower = -INFINITY, z_upper=INFINITY, measureOnly=FALSE) + // This proc sleeps for like 6 seconds. why? + // Is it file accesses? if so, can those be done ahead of time, async to save on time here? I wonder. + // Love ya :) + if(isfile(tfile)) + original_path = "[tfile]" + tfile = file2text(tfile) + else if(isnull(tfile)) + // create a new datum without loading a map + return + + src.bounds = parsed_bounds = list(1.#INF, 1.#INF, 1.#INF, -1.#INF, -1.#INF, -1.#INF) + + if(findtext(tfile, matches_tgm)) + map_format = MAP_TGM + else + map_format = MAP_DMM // Fallback + + // lists are structs don't you know :) + var/list/bounds = src.bounds + var/list/grid_models = src.grid_models + var/key_len = src.key_len + var/line_len = src.line_len + + var/stored_index = 1 + var/list/regexOutput + //multiz lool + while(dmm_regex.Find(tfile, stored_index)) + stored_index = dmm_regex.next + // Datum var lookup is expensive, this isn't + regexOutput = dmm_regex.group + + // "aa" = (/type{vars=blah}) + if(regexOutput[1]) // Model + var/key = regexOutput[1] + if(grid_models[key]) // Duplicate model keys are ignored in DMMs + continue + if(key_len != length(key)) + if(!key_len) + key_len = length(key) + else + CRASH("Inconsistent key length in DMM") + if(!measureOnly) + grid_models[key] = regexOutput[2] + + // (1,1,1) = {"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"} + else if(regexOutput[3]) // Coords + if(!key_len) + CRASH("Coords before model definition in DMM") + + var/curr_x = text2num(regexOutput[3]) + if(curr_x < x_lower || curr_x > x_upper) + continue + + var/curr_y = text2num(regexOutput[4]) + if(curr_y < y_lower || curr_y > y_upper) + continue + + var/curr_z = text2num(regexOutput[5]) + if(curr_z < z_lower || curr_z > z_upper) + continue + + var/datum/grid_set/gridSet = new + + gridSet.xcrd = curr_x + gridSet.ycrd = curr_y + gridSet.zcrd = curr_z + + bounds[MAP_MINX] = min(bounds[MAP_MINX], curr_x) + bounds[MAP_MINZ] = min(bounds[MAP_MINZ], curr_y) + bounds[MAP_MAXZ] = max(bounds[MAP_MAXZ], curr_z) + + var/list/gridLines = splittext(regexOutput[6], "\n") + gridSet.gridLines = gridLines + + var/leadingBlanks = 0 + while(leadingBlanks < length(gridLines) && gridLines[++leadingBlanks] == "") + if(leadingBlanks > 1) + gridLines.Cut(1, leadingBlanks) // Remove all leading blank lines. + + if(!length(gridLines)) // Skip it if only blank lines exist. + continue + + gridSets += gridSet + + if(gridLines[length(gridLines)] == "") + gridLines.Cut(length(gridLines)) // Remove only one blank line at the end. + + bounds[MAP_MINY] = min(bounds[MAP_MINY], gridSet.ycrd) + gridSet.ycrd += length(gridLines) - 1 // Start at the top and work down + bounds[MAP_MAXY] = max(bounds[MAP_MAXY], gridSet.ycrd) + + if(!line_len) + line_len = length(gridLines[1]) + + var/maxx = curr_x + if(length(gridLines)) //Not an empty map + maxx = max(maxx, curr_x + line_len / key_len - 1) + + bounds[MAP_MAXX] = max(bounds[MAP_MAXX], maxx) + CHECK_TICK + + // Indicate failure to parse any coordinates by nulling bounds + if(bounds[1] == 1.#INF) + src.bounds = null + else + // Clamp all our mins and maxes down to the proscribed limits + bounds[MAP_MINX] = clamp(bounds[MAP_MINX], x_lower, x_upper) + bounds[MAP_MAXX] = clamp(bounds[MAP_MAXX], x_lower, x_upper) + bounds[MAP_MINY] = clamp(bounds[MAP_MINY], y_lower, y_upper) + bounds[MAP_MAXY] = clamp(bounds[MAP_MAXY], y_lower, y_upper) + bounds[MAP_MINZ] = clamp(bounds[MAP_MINZ], z_lower, z_upper) + bounds[MAP_MAXZ] = clamp(bounds[MAP_MAXZ], z_lower, z_upper) + + parsed_bounds = src.bounds + src.key_len = key_len + src.line_len = line_len + +/// Iterates over all grid sets and returns ones with z values within the given bounds. Inclusive +/datum/parsed_map/proc/filter_grid_sets_based_on_z_bounds(lower_z, upper_z) + var/list/filtered_sets = list() + for(var/datum/grid_set/grid_set as anything in gridSets) + if(grid_set.zcrd < lower_z) + continue + if(grid_set.zcrd > upper_z) + continue + filtered_sets += grid_set + return filtered_sets + +/// Load the parsed map into the world. You probably want [/proc/load_map]. Keep the signature the same. +/datum/parsed_map/proc/load(x_offset = 0, y_offset = 0, z_offset = 0, crop_map = FALSE, no_changeturf = FALSE, x_lower = -INFINITY, x_upper = INFINITY, y_lower = -INFINITY, y_upper = INFINITY, z_lower = -INFINITY, z_upper = INFINITY, place_on_top = FALSE, new_z = FALSE) + //How I wish for RAII + Master.StartLoadingMap() + . = _load_impl(x_offset, y_offset, z_offset, crop_map, no_changeturf, x_lower, x_upper, y_lower, y_upper, z_lower, z_upper, place_on_top, new_z) + Master.StopLoadingMap() + +#define MAPLOADING_CHECK_TICK \ + if(TICK_CHECK) { \ + if(loading) { \ + SSatoms.map_loader_stop(REF(src)); \ + stoplag(); \ + SSatoms.map_loader_begin(REF(src)); \ + } else { \ + stoplag(); \ + } \ + } + +// Do not call except via load() above. +/datum/parsed_map/proc/_load_impl(x_offset, y_offset, z_offset, crop_map, no_changeturf, x_lower, x_upper, y_lower, y_upper, z_lower, z_upper, place_on_top, new_z) + PRIVATE_PROC(TRUE) + // Tell ss atoms that we're doing maploading + // We'll have to account for this in the following tick_checks so it doesn't overflow + loading = TRUE + SSatoms.map_loader_begin(REF(src)) + + // Loading used to be done in this proc + // We make the assumption that if the inner procs runtime, we WANT to do cleanup on them, but we should stil tell our parents we failed + // Since well, we did + var/sucessful = FALSE + switch(map_format) + if(MAP_TGM) + sucessful = _tgm_load(x_offset, y_offset, z_offset, crop_map, no_changeturf, x_lower, x_upper, y_lower, y_upper, z_lower, z_upper, place_on_top, new_z) + else + sucessful = _dmm_load(x_offset, y_offset, z_offset, crop_map, no_changeturf, x_lower, x_upper, y_lower, y_upper, z_lower, z_upper, place_on_top, new_z) + + // And we are done lads, call it off + SSatoms.map_loader_stop(REF(src)) + loading = FALSE + + // if(new_z) + // for(var/z_index in bounds[MAP_MINZ] to bounds[MAP_MAXZ]) + // SSmapping.build_area_turfs(z_index) + + // if(!no_changeturf) + // var/list/turfs = block( + // bounds[MAP_MINX], bounds[MAP_MINY], bounds[MAP_MINZ], + // bounds[MAP_MAXX], bounds[MAP_MAXY], bounds[MAP_MAXZ] + // ) + // for(var/turf/T as anything in turfs) + // //we do this after we load everything in. if we don't, we'll have weird atmos bugs regarding atmos adjacent turfs + // T.AfterChange(CHANGETURF_IGNORE_AIR) + + if(expanded_x || expanded_y) + SEND_GLOBAL_SIGNAL(COMSIG_GLOB_EXPANDED_WORLD_BOUNDS, expanded_x, expanded_y) + + #ifdef TESTING + if(turfsSkipped) + testing("Skipped loading [turfsSkipped] default turfs") + #endif + + return sucessful + +// Wanna clear something up about maps, talking in 255x255 here +// In the tgm format, each gridset contains 255 lines, each line representing one tile, with 255 total gridsets +// In the dmm format, each gridset contains 255 lines, each line representing one row of tiles, containing 255 * line length characters, with one gridset per z +// You can think of dmm as storing maps in rows, whereas tgm stores them in columns +/datum/parsed_map/proc/_tgm_load(x_offset, y_offset, z_offset, crop_map, no_changeturf, x_lower, x_upper, y_lower, y_upper, z_lower, z_upper, place_on_top, new_z) + // setup + var/list/modelCache = build_cache(no_changeturf) + var/space_key = modelCache[SPACE_KEY] + var/list/bounds + src.bounds = bounds = list(1.#INF, 1.#INF, 1.#INF, -1.#INF, -1.#INF, -1.#INF) + + // Building y coordinate ranges + var/y_relative_to_absolute = y_offset - 1 + var/x_relative_to_absolute = x_offset - 1 + + // Ok so like. something important + // We talk in "relative" coords here, so the coordinate system of the map datum + // This is so we can do offsets, but it is NOT the same as positions in game + // That's why there's some uses of - y_relative_to_absolute here, to turn absolute positions into relative ones + // TGM maps process in columns, so the starting y will always be the max size + // We know y starts at 1 + var/datum/grid_set/first_column = gridSets[1] + var/relative_y = first_column.ycrd + var/highest_y = relative_y + y_relative_to_absolute + + if(!crop_map && highest_y > world.maxy) + if(new_z) + // Need to avoid improperly loaded area/turf_contents + world.increase_max_y(highest_y, map_load_z_cutoff = z_offset - 1) + else + world.increase_max_y(highest_y) + expanded_y = TRUE + + // Skip Y coords that are above the smallest of the three params + // So maxy and y_upper get to act as thresholds, and relative_y can play + var/y_skip_above = min(world.maxy - y_relative_to_absolute, y_upper, relative_y) + // How many lines to skip because they'd be above the y cuttoff line + var/y_starting_skip = relative_y - y_skip_above + highest_y -= y_starting_skip + + // Y is the LOWEST it will ever be here, so we can easily set a threshold for how low to go + var/line_count = length(first_column.gridLines) + var/lowest_y = relative_y - (line_count - 1) // -1 because we decrement at the end of the loop, not the start + var/y_ending_skip = max(max(y_lower, 1 - y_relative_to_absolute) - lowest_y, 0) + + // X setup + var/x_delta_with = x_upper + if(crop_map) + // Take our smaller crop threshold yes? + x_delta_with = min(x_delta_with, world.maxx) + + // We're gonna skip all the entries above the upper x, or maxx if cropMap is set + // The last column is guarenteed to have the highest x value we;ll encounter + // Even if z scales, this still works + var/datum/grid_set/last_column = gridSets[length(gridSets)] + var/final_x = last_column.xcrd + x_relative_to_absolute + + if(final_x > x_delta_with) + // If our relative x is greater then X upper, well then we've gotta limit our expansion + var/delta = max(final_x - x_delta_with, 0) + final_x -= delta + if(final_x > world.maxx && !crop_map) + if(new_z) + // Need to avoid improperly loaded area/turf_contents + world.increase_max_x(final_x, map_load_z_cutoff = z_offset - 1) + else + world.increase_max_x(final_x) + expanded_x = TRUE + + var/lowest_x = max(x_lower, 1 - x_relative_to_absolute) + + // Amount we offset the grid zcrd to get the true zcrd + var/grid_z_offset = z_offset - 1 + var/z_upper_set = z_upper < INFINITY + var/z_lower_set = z_lower > -INFINITY + + // We make the assumption that the last block of turfs will have the highest embedded z in it + // true max zcrd + var/map_bounds_z_max = last_column.zcrd + var/z_upper_parsed = map_bounds_z_max + z_offset - 1 + if(z_upper_set) + z_upper_parsed -= map_bounds_z_max - z_upper + if(z_lower_set) + var/offset_amount = z_lower - 1 + z_upper_parsed -= offset_amount + grid_z_offset -= offset_amount + + var/list/target_grid_sets = gridSets + if(z_lower_set || z_upper_set) // bounds are set, filter out gridsets for z levels we don't want + target_grid_sets = filter_grid_sets_based_on_z_bounds(z_lower, z_upper) + + var/z_threshold = world.maxz + if(z_upper_parsed > z_threshold && crop_map) + for(var/i in z_threshold + 1 to z_upper_parsed) //create a new z_level if needed + world.increment_max_z() + if(!no_changeturf) + WARNING("Z-level expansion occurred without no_changeturf set, this may cause problems when /turf/AfterChange is called") + + for(var/datum/grid_set/gset as anything in target_grid_sets) + var/true_xcrd = gset.xcrd + x_relative_to_absolute + + // any cutoff of x means we just shouldn't iterate this gridset + if(final_x < true_xcrd || lowest_x > gset.xcrd) + continue + + var/zcrd = gset.zcrd + grid_z_offset + // If we're using changeturf, we disable it if we load into a z level we JUST created + var/no_afterchange = no_changeturf || zcrd > z_threshold + + // We're gonna track the first and last pairs of coords we find + // Since x is always incremented in steps of 1, we only need to deal in y + // The first x is guarenteed to be the lowest, the first y the highest, and vis versa + // This is faster then doing mins and maxes inside the hot loop below + var/first_found = FALSE + var/first_y = 0 + var/last_y = 0 + + var/ycrd = highest_y + // Everything following this line is VERY hot. + for(var/i in 1 + y_starting_skip to line_count - y_ending_skip) + if(gset.gridLines[i] == space_key && no_afterchange) + #ifdef TESTING + ++turfsSkipped + #endif + ycrd-- + MAPLOADING_CHECK_TICK + continue + + var/list/cache = modelCache[gset.gridLines[i]] + if(!cache) + SSatoms.map_loader_stop(REF(src)) + CRASH("Undefined model key in DMM: [gset.gridLines[i]]") + build_coordinate(cache, locate(true_xcrd, ycrd, zcrd), no_afterchange, place_on_top, new_z) + + // only bother with bounds that actually exist + if(!first_found) + first_found = TRUE + first_y = ycrd + last_y = ycrd + ycrd-- + MAPLOADING_CHECK_TICK + + // The x coord never changes, so not tracking first x is safe + // If no ycrd is found, we assume this row is totally empty and just continue on + if(first_found) + bounds[MAP_MINX] = min(bounds[MAP_MINX], true_xcrd) + bounds[MAP_MINY] = min(bounds[MAP_MINY], last_y) + bounds[MAP_MINZ] = min(bounds[MAP_MINZ], zcrd) + bounds[MAP_MAXX] = max(bounds[MAP_MAXX], true_xcrd) + bounds[MAP_MAXY] = max(bounds[MAP_MAXY], first_y) + bounds[MAP_MAXZ] = max(bounds[MAP_MAXZ], zcrd) + return TRUE + +/// Stanrdard loading, not used in production +/// Doesn't take advantage of any tgm optimizations, which makes it slower but also more general +/// Use this if for some reason your map format is messy +/datum/parsed_map/proc/_dmm_load(x_offset, y_offset, z_offset, crop_map, no_changeturf, x_lower, x_upper, y_lower, y_upper, z_lower, z_upper, place_on_top, new_z) + // setup + var/list/modelCache = build_cache(no_changeturf) + var/space_key = modelCache[SPACE_KEY] + var/list/bounds + var/key_len = src.key_len + src.bounds = bounds = list(1.#INF, 1.#INF, 1.#INF, -1.#INF, -1.#INF, -1.#INF) + + var/y_relative_to_absolute = y_offset - 1 + var/x_relative_to_absolute = x_offset - 1 + var/line_len = src.line_len + + // Amount we offset the grid zcrd to get the true zcrd + var/grid_z_offset = z_offset - 1 + var/z_upper_set = z_upper < INFINITY + var/z_lower_set = z_lower > -INFINITY + + // we now need to find the maximum z, fun! + var/map_bounds_z_max = 1 + for(var/datum/grid_set/grid_set as anything in gridSets) + map_bounds_z_max = max(map_bounds_z_max, grid_set.zcrd) + + var/z_upper_parsed = map_bounds_z_max + z_offset - 1 + if(z_upper_set) + z_upper_parsed -= map_bounds_z_max - z_upper + if(z_lower_set) + var/offset_amount = z_lower - 1 + z_upper_parsed -= offset_amount + grid_z_offset -= offset_amount + + var/list/target_grid_sets = gridSets + if(z_lower_set || z_upper_set) // bounds are set, filter out gridsets for z levels we don't want + target_grid_sets = filter_grid_sets_based_on_z_bounds(z_lower, z_upper) + + for(var/datum/grid_set/gset as anything in target_grid_sets) + var/relative_x = gset.xcrd + var/relative_y = gset.ycrd + var/true_xcrd = relative_x + x_relative_to_absolute + var/ycrd = relative_y + y_relative_to_absolute + var/zcrd = gset.zcrd + grid_z_offset + if(!crop_map && ycrd > world.maxy) + if(new_z) + // Need to avoid improperly loaded area/turf_contents + world.increase_max_y(ycrd, map_load_z_cutoff = z_offset - 1) + else + world.increase_max_y(ycrd) + expanded_y = TRUE + var/zexpansion = zcrd > world.maxz + var/no_afterchange = no_changeturf + if(zexpansion) + if(crop_map) + continue + else + while (zcrd > world.maxz) //create a new z_level if needed + world.increment_max_z() + if(!no_changeturf) + WARNING("Z-level expansion occurred without no_changeturf set, this may cause problems when /turf/AfterChange is called") + no_afterchange = TRUE + // Ok so like. something important + // We talk in "relative" coords here, so the coordinate system of the map datum + // This is so we can do offsets, but it is NOT the same as positions in game + // That's why there's some uses of - y_relative_to_absolute here, to turn absolute positions into relative ones + + // Skip Y coords that are above the smallest of the three params + // So maxy and y_upper get to act as thresholds, and relative_y can play + var/y_skip_above = min(world.maxy - y_relative_to_absolute, y_upper, relative_y) + // How many lines to skip because they'd be above the y cuttoff line + var/y_starting_skip = relative_y - y_skip_above + ycrd += y_starting_skip + + // Y is the LOWEST it will ever be here, so we can easily set a threshold for how low to go + var/line_count = length(gset.gridLines) + var/lowest_y = relative_y - (line_count - 1) // -1 because we decrement at the end of the loop, not the start + var/y_ending_skip = max(max(y_lower, 1 - y_relative_to_absolute) - lowest_y, 0) + + // Now we're gonna precompute the x thresholds + // We skip all the entries below the lower x, or 1 + var/starting_x_delta = max(max(x_lower, 1 - x_relative_to_absolute) - relative_x, 0) + // The x loop counts by key length, so we gotta multiply here + var/x_starting_skip = starting_x_delta * key_len + true_xcrd += starting_x_delta + + // We're gonna skip all the entries above the upper x, or maxx if cropMap is set + var/x_target = line_len - key_len + 1 + var/x_step_count = ROUND_UP(x_target / key_len) + var/final_x = relative_x + (x_step_count - 1) + var/x_delta_with = x_upper + if(crop_map) + // Take our smaller crop threshold yes? + x_delta_with = min(x_delta_with, world.maxx) + if(final_x > x_delta_with) + // If our relative x is greater then X upper, well then we've gotta limit our expansion + var/delta = max(final_x - x_delta_with, 0) + x_step_count -= delta + final_x -= delta + x_target = x_step_count * key_len + if(final_x > world.maxx && !crop_map) + if(new_z) + // Need to avoid improperly loaded area/turf_contents + world.increase_max_x(final_x, map_load_z_cutoff = z_offset - 1) + else + world.increase_max_x(final_x) + expanded_x = TRUE + + // We're gonna track the first and last pairs of coords we find + // The first x is guarenteed to be the lowest, the first y the highest, and vis versa + // This is faster then doing mins and maxes inside the hot loop below + var/first_found = FALSE + var/first_x = 0 + var/first_y = 0 + var/last_x = 0 + var/last_y = 0 + + // Everything following this line is VERY hot. How hot depends on the map format + // (Yes this does mean dmm is technically faster to parse. shut up) + for(var/i in 1 + y_starting_skip to line_count - y_ending_skip) + var/line = gset.gridLines[i] + + var/xcrd = true_xcrd + for(var/tpos in 1 + x_starting_skip to x_target step key_len) + var/model_key = copytext(line, tpos, tpos + key_len) + if(model_key == space_key && no_afterchange) + #ifdef TESTING + ++turfsSkipped + #endif + MAPLOADING_CHECK_TICK + ++xcrd + continue + var/list/cache = modelCache[model_key] + if(!cache) + SSatoms.map_loader_stop(REF(src)) + CRASH("Undefined model key in DMM: [model_key]") + build_coordinate(cache, locate(xcrd, ycrd, zcrd), no_afterchange, place_on_top, new_z) + + // only bother with bounds that actually exist + if(!first_found) + first_found = TRUE + first_x = xcrd + first_y = ycrd + last_x = xcrd + last_y = ycrd + MAPLOADING_CHECK_TICK + ++xcrd + ycrd-- + MAPLOADING_CHECK_TICK + bounds[MAP_MINX] = min(bounds[MAP_MINX], first_x) + bounds[MAP_MINY] = min(bounds[MAP_MINY], last_y) + bounds[MAP_MINZ] = min(bounds[MAP_MINZ], zcrd) + bounds[MAP_MAXX] = max(bounds[MAP_MAXX], last_x) + bounds[MAP_MAXY] = max(bounds[MAP_MAXY], first_y) + bounds[MAP_MAXZ] = max(bounds[MAP_MAXZ], zcrd) + + return TRUE + +GLOBAL_LIST_EMPTY(map_model_default) + +/datum/parsed_map/proc/build_cache(no_changeturf, bad_paths) + if(map_format == MAP_TGM) + return tgm_build_cache(no_changeturf, bad_paths) + return dmm_build_cache(no_changeturf, bad_paths) + +/datum/parsed_map/proc/tgm_build_cache(no_changeturf, bad_paths=null) + if(modelCache && !bad_paths) + return modelCache + . = modelCache = list() + var/list/grid_models = src.grid_models + var/set_space = FALSE + // Use where a list is needed, but where it will not be modified + // Used here to remove the cost of needing to make a new list for each fields entry when it's set manually later + var/static/list/default_list = GLOB.map_model_default // It's stupid, but it saves += list(list) + var/static/list/wrapped_default_list = list(default_list) // It's stupid, but it saves += list(list) + var/static/regex/var_edits = var_edits_tgm + + var/path_to_init = "" + // Reference to the attributes list we're currently filling, if any + var/list/current_attributes + // If we are currently editing a path or not + var/editing = FALSE + for(var/model_key in grid_models) + // We're going to split models by newline + // This guarentees that each entry will be of interest to us + // Then we'll process them step by step + // Hopefully this reduces the cost from read_list that we'd otherwise have + var/list/lines = splittext(grid_models[model_key], "\n") + // Builds list of path/edits for later + // Of note: we cannot preallocate them to save time in list expansion later + // But fortunately lists allocate at least 8 entries normally anyway, and + // We are unlikely to have more then that many members + //will contain all members (paths) in model (in our example : /turf/unsimulated/wall) + var/list/members = list() + //will contain lists filled with corresponding variables, if any (in our example : list(icon_state = "rock") and list()) + var/list/members_attributes = list() + + ///////////////////////////////////////////////////////// + //Constructing members and corresponding variables lists + //////////////////////////////////////////////////////// + // string representation of the path to init + for(var/line in lines) + // We do this here to avoid needing to check at each return statement + // No harm in it anyway + MAPLOADING_CHECK_TICK + + switch(line[length(line)]) + if(";") // Var edit, we'll apply it + // Var edits look like \tname = value; + // I'm gonna try capturing them with regex, since it ought to be the fastest here + // Should hand back key = value + var_edits.Find(line) + var/value = parse_constant(var_edits.group[2]) + if(istext(value)) + value = apply_text_macros(value) + current_attributes[var_edits.group[1]] = value + continue // Keep on keeping on brother + if("{") // Start of an edit, and so also the start of a path + editing = TRUE + current_attributes = list() // Init the list we'll be filling + members_attributes += list(current_attributes) + path_to_init = copytext(line, 1, -1) + if(",") // Either the end of a path, or the end of an edit + if(editing) // it was the end of a path + editing = FALSE + continue + members_attributes += wrapped_default_list // We know this is a path, and we also know it has no vv's. so we'll just set this to the default list + // Drop the last char mind + path_to_init = copytext(line, 1, -1) + if("}") // Gotta be the end of an area edit, let's check to be sure + if(editing) // it was the end of an area edit (shouldn't do those anyhow) + editing = FALSE + continue + stack_trace("ended a line on JUST a }, with no ongoing edit. What? Area shit?") + else // If we're editing, this is a var edit entry. the last one in a stack, cause god hates me. Otherwise, it's an area + if(editing) // I want inline I want inline I want inline + // Var edits look like \tname = value; + // I'm gonna try capturing them with regex, since it ought to be the fastest here + // Should hand back key = value + var_edits.Find(line) + var/value = parse_constant(var_edits.group[2]) + if(istext(value)) + value = apply_text_macros(value) + current_attributes[var_edits.group[1]] = value + continue // Keep on keeping on brother + + members_attributes += wrapped_default_list // We know this is a path, and we also know it has no vv's. so we'll just set this to the default list + path_to_init = line + + + // Alright, if we've gotten to this point, our string is a path + // Oh and we don't trim it, because we require no padding for these + // Saves like 1.5 deciseconds + var/atom_def = text2path(path_to_init) //path definition, e.g /obj/foo/bar + + if(!ispath(atom_def, /atom)) // Skip the item if the path does not exist. Fix your crap, mappers! + if(bad_paths) + // Rare case, avoid the var to save time most of the time + LAZYOR(bad_paths[copytext(line, 1, -1)], model_key) + continue + // Index is already incremented either way, just gotta set the path and all + members += atom_def + + //check and see if we can just skip this turf + //So you don't have to understand this horrid statement, we can do this if + // 1. the space_key isn't set yet + // 2. no_changeturf is set + // 3. there are exactly 2 members + // 4. with no attributes + // 5. and the members are world.turf and world.area + // Basically, if we find an entry like this: "XXX" = (/turf/default, /area/default) + // We can skip calling this proc every time we see XXX + if(!set_space \ + && no_changeturf \ + && members_attributes.len == 2 \ + && members.len == 2 \ + && members_attributes[1] == default_list \ + && members_attributes[2] == default_list \ + && members[2] == world.area \ + && members[1] == world.turf + ) + set_space = TRUE + .[SPACE_KEY] = model_key + continue + + .[model_key] = list(members, members_attributes) + return . + +/// Builds key caches for general formats +/// Slower then the proc above, tho it could still be optimized slightly. it's just not a priority +/// Since we don't run DMM maps, ever. +/datum/parsed_map/proc/dmm_build_cache(no_changeturf, bad_paths=null) + if(modelCache && !bad_paths) + return modelCache + . = modelCache = list() + var/list/grid_models = src.grid_models + var/set_space = FALSE + // Use where a list is needed, but where it will not be modified + // Used here to remove the cost of needing to make a new list for each fields entry when it's set manually later + var/static/list/default_list = list(GLOB.map_model_default) + for(var/model_key in grid_models) + //will contain all members (paths) in model (in our example : /turf/unsimulated/wall) + var/list/members = list() + //will contain lists filled with corresponding variables, if any (in our example : list(icon_state = "rock") and list()) + var/list/members_attributes = list() + + var/model = grid_models[model_key] + ///////////////////////////////////////////////////////// + //Constructing members and corresponding variables lists + //////////////////////////////////////////////////////// + + var/model_index = 1 + while(model_path.Find(model, model_index)) + var/variables_start = 0 + var/member_string = model_path.group[1] + model_index = model_path.next + //findtext is a bit expensive, lets only do this if the last char of our string is a } (IE: we know we have vars) + //this saves about 25 miliseconds on my machine. Not a major optimization + if(member_string[length(member_string)] == "}") + variables_start = findtext(member_string, "{") + + var/path_text = trim(copytext(member_string, 1, variables_start)) + var/atom_def = text2path(path_text) //path definition, e.g /obj/foo/bar + + if(!ispath(atom_def, /atom)) // Skip the item if the path does not exist. Fix your crap, mappers! + if(bad_paths) + LAZYOR(bad_paths[path_text], model_key) + continue + members += atom_def + + //transform the variables in text format into a list (e.g {var1="derp"; var2; var3=7} => list(var1="derp", var2, var3=7)) + // OF NOTE: this could be made faster by replacing readlist with a progressive regex + // I'm just too much of a bum to do it rn, especially since we mandate tgm format for any maps in repo + var/list/fields = default_list + if(variables_start)//if there's any variable + member_string = copytext(member_string, variables_start + length(member_string[variables_start]), -length(copytext_char(member_string, -1))) //removing the last '}' + fields = list(readlist(member_string, ";")) + for(var/I in fields) + var/value = fields[I] + if(istext(value)) + fields[I] = apply_text_macros(value) + + //then fill the members_attributes list with the corresponding variables + members_attributes += fields + MAPLOADING_CHECK_TICK + + //check and see if we can just skip this turf + //So you don't have to understand this horrid statement, we can do this if + // 1. the space_key isn't set yet + // 2. no_changeturf is set + // 3. there are exactly 2 members + // 4. with no attributes + // 5. and the members are world.turf and world.area + // Basically, if we find an entry like this: "XXX" = (/turf/default, /area/default) + // We can skip calling this proc every time we see XXX + if(!set_space \ + && no_changeturf \ + && members.len == 2 \ + && members_attributes.len == 2 \ + && length(members_attributes[1]) == 0 \ + && length(members_attributes[2]) == 0 \ + && (world.area in members) \ + && (world.turf in members)) + set_space = TRUE + .[SPACE_KEY] = model_key + continue + + .[model_key] = list(members, members_attributes) + return . + +/datum/parsed_map/proc/build_coordinate(list/model, turf/crds, no_changeturf as num, placeOnTop as num, new_z) + // If we don't have a turf, nothing we will do next will actually acomplish anything, so just go back + // Note, this would actually drop area vvs in the tile, but like, why tho + if(!crds) + return + var/index + var/list/members = model[1] + var/list/members_attributes = model[2] + + // We use static lists here because it's cheaper then passing them around + var/static/list/default_list = GLOB.map_model_default + //////////////// + //Instanciation + //////////////// + + if(turf_blacklist?[crds]) + return + + //The next part of the code assumes there's ALWAYS an /area AND a /turf on a given tile + //first instance the /area and remove it from the members list + index = members.len + // var/area/old_area + if(members[index] != /area/template_noop) + if(members_attributes[index] != default_list) + world.preloader_setup(members_attributes[index], members[index])//preloader for assigning set variables on atom creation + var/area/area_instance = loaded_areas[members[index]] + if(!area_instance) + var/area_type = members[index] + // If this parsed map doesn't have that area already, we check the global cache + area_instance = GLOB.areas_by_type[area_type] + // If the global list DOESN'T have this area it's either not a unique area, or it just hasn't been created yet + if (!area_instance) + area_instance = new area_type(null) + if(!area_instance) + CRASH("[area_type] failed to be new'd, what'd you do?") + loaded_areas[area_type] = area_instance + + // if(!new_z) + // old_area = crds.loc + // LISTASSERTLEN(old_area.turfs_to_uncontain_by_zlevel, crds.z, list()) + // LISTASSERTLEN(area_instance.turfs_by_zlevel, crds.z, list()) + // old_area.turfs_to_uncontain_by_zlevel[crds.z] += crds + // area_instance.turfs_by_zlevel[crds.z] += crds + area_instance.contents.Add(crds) + + if(GLOB.use_preloader) + world.preloader_load(area_instance) + + // Index right before /area is /turf + index-- + var/atom/instance + //then instance the /turf + //NOTE: this used to place any turfs before the last "underneath" it using .appearance and underlays + //We don't actually use this, and all it did was cost cpu, so we don't do this anymore + if(members[index] != /turf/template_noop) + if(members_attributes[index] != default_list) + world.preloader_setup(members_attributes[index], members[index]) + + // Note: we make the assertion that the last path WILL be a turf. if it isn't, this will fail. + // if(placeOnTop) + // instance = crds.load_on_top(members[index], CHANGETURF_DEFER_CHANGE | (no_changeturf ? CHANGETURF_SKIP : NONE)) + if(no_changeturf) + instance = create_atom(members[index], crds)//first preloader pass + else + instance = crds.ChangeTurf(members[index], FALSE, TRUE) //null, CHANGETURF_DEFER_CHANGE) + + if(GLOB.use_preloader && instance)//second preloader pass, for those atoms that don't ..() in New() + world.preloader_load(instance) + // If this isn't template work, we didn't change our turf and we changed area, then we've gotta handle area lighting transfer + // else if(!no_changeturf && old_area) + // // Don't do contain/uncontain stuff, this happens a few lines up when the area actally changes + // crds.on_change_area(old_area, crds.loc) + MAPLOADING_CHECK_TICK + + //finally instance all remainings objects/mobs + for(var/atom_index in 1 to index-1) + if(members_attributes[atom_index] != default_list) + world.preloader_setup(members_attributes[atom_index], members[atom_index]) + + // We make the assertion that only /atom s will be in this portion of the code. if that isn't true, this will fail + instance = create_atom(members[atom_index], crds)//first preloader pass + + if(GLOB.use_preloader && instance)//second preloader pass, for those atoms that don't ..() in New() + world.preloader_load(instance) + MAPLOADING_CHECK_TICK + +//////////////// +//Helpers procs +//////////////// + +/datum/parsed_map/proc/create_atom(path, crds) + set waitfor = FALSE + . = new path (crds) + +//find the position of the next delimiter,skipping whatever is comprised between opening_escape and closing_escape +//returns 0 if reached the last delimiter +/datum/parsed_map/proc/find_next_delimiter_position(text as text,initial_position as num, delimiter=",",opening_escape="\"",closing_escape="\"") + var/position = initial_position + var/next_delimiter = findtext(text,delimiter,position,0) + var/next_opening = findtext(text,opening_escape,position,0) + + while((next_opening != 0) && (next_opening < next_delimiter)) + position = findtext(text,closing_escape,next_opening + 1,0)+1 + next_delimiter = findtext(text,delimiter,position,0) + next_opening = findtext(text,opening_escape,position,0) + + return next_delimiter + +//build a list from variables in text form (e.g {var1="derp"; var2; var3=7} => list(var1="derp", var2, var3=7)) +//return the filled list +/datum/parsed_map/proc/readlist(text as text, delimiter=",") + . = list() + if (!text) + return + + var/position + var/old_position = 1 + while(position != 0) + // find next delimiter that is not within "..." + position = find_next_delimiter_position(text, old_position, delimiter) + + // check if this is a simple variable (as in list(var1, var2)) or an associative one (as in list(var1="foo",var2=7)) + var/equal_position = find_next_delimiter_position(text, old_position, "=") + var/trim_left = trim(copytext(text, old_position, (equal_position ? equal_position : position))) + if(!trim_left) // damn newlines man. Exists to provide behavior consistency with the above loop. not a major cost becuase this path is cold + if(position) + old_position = position + length(text[position]) + continue + + var/is_simple = TRUE //linear list + var/trim_right = trim_left //simple var + if(equal_position) + // Associative var, so do the association. + // Note that numbers cannot be keys - the RHS is dropped if so. + trim_right = trim(copytext(text, equal_position + length(text[equal_position]), position)) + is_simple = FALSE + + //right value is a list and since we used the delimiter , this text would be incomplete so we need to parse the full string + if(copytext(trim_right, 1, 6) == "list(") + var/start_index = is_simple ? old_position : equal_position + length(text[equal_position]) + var/opening_count = 0 + var/closing_count = 0 + var/index = start_index + var/begin = FALSE + while(!begin || (opening_count != closing_count)) + var/char = text[index] + if(char == "(") + opening_count += 1 + begin = TRUE + else if(char == ")") + closing_count += 1 + index += length(char) + trim_right = trim(copytext(text, start_index, index)) + if(is_simple) + trim_left = trim_right + if(index >= length(text)) //stops a wasteful iteration when we reach the end + position = 0 + else + old_position = index + length(text[index]) //this moves our pointer past , to the next element + else if(position) + old_position = position + length(text[position]) + + //assign value + var/left_constant = parse_constant(trim_left) + if(is_simple) + . += list(left_constant) + else + .[left_constant] = parse_constant(trim_right) + +/datum/parsed_map/proc/parse_constant(text) + // empty text + if(!text) + return "" + + // number + var/num = text2num(text) + if(isnum(num)) + return num + + // string + if(text[1] == "\"") + // insert implied locate \" and length("\"") here + // It's a minimal timesave but it is a timesave + // Safe becuase we're guarenteed trimmed constants + return copytext(text, 2, -1) + + // list + if(copytext(text, 1, 6) == "list(")//6 == length("list(") + 1 + return readlist(copytext(text, 6, -1)) + + // typepath + var/path = text2path(text) + if(ispath(path)) + return path + + // file + if(text[1] == "'") + return file(copytext_char(text, 2, -1)) + + // null + if(text == "null") + return null + + // not parsed: + // - pops: /obj{name="foo"} + // - new(), newlist(), icon(), matrix(), sound() + + // fallback: string + return text + +/datum/parsed_map/Destroy() + ..() + SSatoms.map_loader_stop(REF(src)) // Just in case, I don't want to double up here + if(turf_blacklist) + turf_blacklist.Cut() + parsed_bounds.Cut() + bounds.Cut() + grid_models.Cut() + gridSets.Cut() + return QDEL_HINT_HARDDEL_NOW + +#undef MAP_DMM +#undef MAP_TGM +#undef MAP_UNKNOWN +#undef MAPLOADING_CHECK_TICK diff --git a/code/modules/maps/verify.dm b/code/modules/maps/verify.dm new file mode 100644 index 0000000000..b7211bac6a --- /dev/null +++ b/code/modules/maps/verify.dm @@ -0,0 +1,109 @@ +/// Global list of map report datums +GLOBAL_LIST_EMPTY(map_reports) + +/// An error report generated by [/datum/parsed_map/proc/check_for_errors]. +/datum/map_report + var/original_path + var/list/bad_paths = list() + var/list/bad_keys = list() + /// Whether this map can be loaded safely despite the errors. + var/loadable = TRUE + var/crashed = TRUE + + var/static/tag_number = 0 + +/datum/map_report/New(datum/parsed_map/map) + original_path = map.original_path || "Untitled" + GLOB.map_reports += src + +/datum/map_report/Destroy(force) + GLOB.map_reports -= src + return ..() + + +/// Show a rendered version of this report to a client. +/datum/map_report/proc/show_to(client/C) + var/list/html = list() + if(crashed) + html += "

Validation crashed: check the runtime logs.

" + if(!loadable) + html += "

Not loadable: some tiles are missing their turfs or areas.

" + + if(bad_paths.len) + html += "

Bad paths:

    " + for(var/path in bad_paths) + var/list/keys = bad_paths[path] + html += "
  1. [path]: used in ([keys.len]): [keys.Join(", ")]" + html += "

" + + if(bad_keys.len) + html += "

Bad keys:

" + var/datum/browser/browser = new(C.mob, "[tag]", "Report for map file [original_path]", 600, 400) + browser.set_content(html.Join()) + browser.open() + +/datum/map_report/Topic(href, href_list) + . = ..() + if(. || !check_rights(R_ADMIN, FALSE) || !usr.client.holder.CheckAdminHref(href, href_list)) + return + + if (href_list["show"]) + show_to(usr) + + +/// Check a parsed but not yet loaded map for errors. +/// +/// Returns a [/datum/map_report] if there are errors or `FALSE` otherwise. +/datum/parsed_map/proc/check_for_errors() + var/datum/map_report/report = new(src) + . = report + + // build_cache will check bad paths for us + var/list/modelCache = build_cache(TRUE, report.bad_paths) + + var/static/regex/area_or_turf = regex(@"/(turf|area)/") + for(var/path in report.bad_paths) + if(area_or_turf.Find("[path]", 1, 1)) + report.loadable = FALSE + + // check for tiles with the wrong number of turfs or areas + for(var/key in modelCache) + if(key == SPACE_KEY) + continue + var/model = modelCache[key] + var/list/members = model[1] + + var/turfs = 0 + var/areas = 0 + for(var/i in 1 to members.len) + var/atom/path = members[i] + + turfs += ispath(path, /turf) + areas += ispath(path, /area) + + if(turfs == 0) + report.loadable = FALSE + LAZYADD(report.bad_keys[key], "no turf") + else if(turfs > 1) + LAZYADD(report.bad_keys[key], "[turfs] stacked turfs") + + if(areas != 1) + report.loadable = FALSE + LAZYADD(report.bad_keys[key], "[areas] areas instead of 1") + + // return the report + if(report.bad_paths.len || report.bad_keys.len || !report.loadable) + // keep the report around so it can be referenced later + report.tag = "mapreport_[++report.tag_number]" + report.crashed = FALSE + else + return FALSE diff --git a/libbapi_dmm_reader.so b/libbapi_dmm_reader.so deleted file mode 100644 index 601f5bf919..0000000000 Binary files a/libbapi_dmm_reader.so and /dev/null differ diff --git a/vorestation.dme b/vorestation.dme index 621cfe809d..e9c3715314 100644 --- a/vorestation.dme +++ b/vorestation.dme @@ -3185,12 +3185,12 @@ #include "code\modules\maintenance_panels\maintenance_panel.dm" #include "code\modules\maintenance_panels\maintpanel_stack.dm" #include "code\modules\makeup\nailpolish.dm" +#include "code\modules\maps\map_template.dm" +#include "code\modules\maps\map_template_vr.dm" #include "code\modules\maps\merge_conflicts.dm" -#include "code\modules\maps\bapi-dmm\bapi_bindings.dm" -#include "code\modules\maps\bapi-dmm\bapi_dmm_reader.dm" -#include "code\modules\maps\bapi-dmm\bapi_helpers.dm" -#include "code\modules\maps\tg\map_template.dm" -#include "code\modules\maps\tg\map_template_vr.dm" +#include "code\modules\maps\preloader.dm" +#include "code\modules\maps\reader.dm" +#include "code\modules\maps\verify.dm" #include "code\modules\materials\fifty_spawner.dm" #include "code\modules\materials\fifty_spawner_mats.dm" #include "code\modules\materials\material_synth.dm"