## WorkshopBrowser — Server-Side Map Directory Scanner ## ## Singleton (or child of ServerMain) that scans the server's maps ## directory for .pck files and .tscn references, maintaining a ## Dictionary of available maps. ## ## ## Map Directory Resolution ## ## Default: user://maps/ (resolves via ProjectSettings.globalize_path) ## Override: MAP_DIR env var (absolute path, for container/VPS deployments) ## ## The browser scans for `*.pck` files and reads their file metadata. ## For built-in (non-PCK) maps, it checks `res://scenes/map/.tscn`. ## ## ## Map Info Dictionary Shape ## ## { ## "name": String, ## "description": String, ## "file_size": int, ## "map_type": String, # "pck" | "builtin" ## "player_count": int, # inferred or 0 (reserved for future metadata) ## "path": String, # absolute path to the .pck or .tscn ## } ## ## ## Signals ## ## maps_refreshed(map_list: Array) — emitted after refresh_maps() completes ## ## ============================================================================= extends Node # --------------------------------------------------------------------------- # Signals # --------------------------------------------------------------------------- ## Emitted after refresh_maps() completes. signal maps_refreshed(map_list: Array) # --------------------------------------------------------------------------- # Constants # --------------------------------------------------------------------------- ## Default maps directory (relative to user://). const DEFAULT_MAPS_DIR: String = "user://maps/" ## Path to built-in map scenes (non-PCK). const BUILTIN_MAP_DIR: String = "res://scenes/map/" # --------------------------------------------------------------------------- # State # --------------------------------------------------------------------------- ## Available maps keyed by map_id (String → Dictionary). var _available_maps: Dictionary = {} ## Absolute path to the maps directory being scanned. var _maps_dir: String = "" # --------------------------------------------------------------------------- # Lifecycle / Initialisation # --------------------------------------------------------------------------- func _ready() -> void: _resolve_maps_dir() print("[WorkshopBrowser] Maps directory: %s" % _maps_dir) call_deferred(&"refresh_maps") ## Resolve the maps directory — env var override, then user:// default. func _resolve_maps_dir() -> void: if OS.has_environment("MAP_DIR"): _maps_dir = OS.get_environment("MAP_DIR") if not _maps_dir.ends_with("/"): _maps_dir += "/" return _maps_dir = ProjectSettings.globalize_path(DEFAULT_MAPS_DIR) if not _maps_dir.ends_with("/"): _maps_dir += "/" # --------------------------------------------------------------------------- # Public API # --------------------------------------------------------------------------- ## Rescan the maps directory and rebuild the available map list. ## Returns the Array of map info Dictionaries (same as get_map_list()). func refresh_maps() -> Array: _available_maps.clear() _scan_pck_files() _scan_builtin_maps() var list: Array = get_map_list() maps_refreshed.emit(list) print("[WorkshopBrowser] Refreshed maps: %d available" % list.size()) return list ## Return a serialized list of all available maps as Array[Dictionary]. ## Each entry matches the map info shape documented above. func get_map_list() -> Array: var result: Array = [] for map_id in _available_maps: result.append(_available_maps[map_id]) return result ## Get info for a single map by its map_id (file stem, e.g. "de_dust2"). ## Returns an empty Dictionary if the map is not found. func get_map_info(map_id: String) -> Dictionary: return _available_maps.get(map_id, {}) ## Check if a map exists in the available list. func has_map(map_id: String) -> bool: return _available_maps.has(map_id) ## Get the total number of available maps. func get_map_count() -> int: return _available_maps.size() ## Get the absolute path of the maps directory being scanned. func get_maps_directory() -> String: return _maps_dir # --------------------------------------------------------------------------- # Internal: Scanning # --------------------------------------------------------------------------- ## Scan for .pck files in the maps directory. func _scan_pck_files() -> void: var dir := DirAccess.open(_maps_dir) if dir == null: push_warning("[WorkshopBrowser] Cannot open maps directory: %s" % _maps_dir) return dir.list_dir_begin() var file_name: String = dir.get_next() while not file_name.is_empty(): if file_name.ends_with(".pck"): _register_pck_file(file_name) file_name = dir.get_next() dir.list_dir_end() ## Register a single .pck file found during scanning. func _register_pck_file(file_name: String) -> void: var map_id: String = file_name.trim_suffix(".pck") var full_path: String = _maps_dir.path_join(file_name) var file_size: int = 0 if FileAccess.file_exists(full_path): file_size = FileAccess.get_size(full_path) var map_info: Dictionary = { name = map_id, description = "", # reserved for future sidecar metadata file_size = file_size, map_type = "pck", player_count = 0, # inferred or from metadata (future) path = full_path, } _available_maps[map_id] = map_info print("[WorkshopBrowser] Registered PCK map: %s (%d bytes)" % [map_id, file_size]) ## Scan for built-in .tscn maps (non-PCK, part of the game resources). func _scan_builtin_maps() -> void: # Use DirAccess on res:// to find .tscn files in the map directory. # This only works in non-exported builds or with packed resources. var res_path: String = BUILTIN_MAP_DIR if not ResourceLoader.exists(res_path): # res://scenes/map/ might not be a directory; skip silently. return var dir := DirAccess.open(res_path) if dir == null: return dir.list_dir_begin() var file_name: String = dir.get_next() while not file_name.is_empty(): if file_name.ends_with(".tscn"): var map_id: String = file_name.trim_suffix(".tscn") # Don't override a PCK-registered map if _available_maps.has(map_id): file_name = dir.get_next() continue var full_path: String = res_path.path_join(file_name) var map_info: Dictionary = { name = map_id, description = "", file_size = 0, map_type = "builtin", player_count = 0, path = full_path, } _available_maps[map_id] = map_info print("[WorkshopBrowser] Registered builtin map: %s" % map_id) file_name = dir.get_next() dir.list_dir_end()