class_name CatalogCheck extends RefCounted # Forgejo #173: "in the web export version the fleet can not store any # resources." # # That symptom has a history in this project, and it is not a web bug. Opening # the project in the Godot editor re-saves .tres files, and the editor's # writer DROPS properties it considers default-valued. An empty-looking # PackedStringArray is the usual casualty, and it has now cost this project # three times: # # GasBurnerBuildingDefinition lost `capabilities` and `collected_outputs`, # which took the burner's whole conversion with it. # # InterceptorVesselDefinition lost `capabilities`, `hull_points`, # `build_time`, `icon` and `glow_color`. The starting vessel kept its # cargo_capacity of 20 and stopped claiming the "cargo" capability, which # is the one thing fleet_cargo_capacity() gates on - so the hold was zero # and nothing could be mined or carried. # # The second one is #173, word for word. A build exported from a tree in that # state has a fleet that cannot store any resources, and NOTHING SAYS SO: the # resource still loads, it is just quietly less than it was. # # test_definitions_intact caught it in the suite. What it could not do is # catch it in a build, and an export is taken from the working tree at # whatever moment the editor last touched it - which is exactly when the # damage is done and exactly where nobody is running tests. # # So the same check now runs at STARTUP, in every build (see GameState._ready) # and in the test, from this one function. On the web its push_error lands in # the on-page error log that web_shell.html mirrors from the console, which is # the only place a phone can read an error at all. A player still cannot mine, # but the reason is on the screen instead of being reasoned about for an # evening. # # It asserts the SHAPE of the catalog rather than any one value: every # definition must carry the things that make it what it is, and two facts that # have to agree - a hold and the capability that admits to it - must agree. # Everything wrong with the catalog, in the order found. Empty is the healthy # answer, which is what both callers check. static func faults() -> PackedStringArray: var found: PackedStringArray = PackedStringArray() _check_vessels(found) _check_buildings(found) return found # get() answers null for a property a definition's script never declared, and # null has no is_empty(). These keep a missing field from looking like an empty # one - and from taking the rest of the sweep down with it. static func _text(definition: Resource, field: String) -> String: var value: Variant = definition.get(field) return "" if value == null else str(value) static func _list(definition: Resource, field: String) -> Array: var value: Variant = definition.get(field) if value == null: return [] return Array(value.keys() if value is Dictionary else value) static func _check_vessels(found: PackedStringArray) -> void: for definition in VesselDefinitions.all(): var id: String = String(definition.definition_id) # --- whole ------------------------------------------------------ if String(definition.display_name).strip_edges() == "": found.append("vessel %s has no display name" % id) if definition.base_stats.is_empty(): found.append("vessel %s has no stats at all" % id) # Only the hulls that actually join the fleet: a repair drone is a # logistics unit driven by GameState, with nothing bolted to it. if definition.fleet_eligible and definition.component_scenes.is_empty(): found.append("vessel %s flies in the fleet with no components - an empty hull" % id) if definition.hull_points.is_empty(): found.append("vessel %s has no hull points - it would draw as the fallback triangle" % id) # --- cargo: the #173 case --------------------------------------- # The hold and the capability are two recordings of one fact, and # fleet_cargo_capacity() reads only the second. They must agree. var carries: bool = float(definition.base_stats.get("cargo_capacity", 0.0)) > 0.0 var claims: bool = "cargo" in definition.capabilities if carries != claims: found.append("vessel %s: cargo_capacity %s but capabilities %s - a hold nothing can fill, or the reverse" \ % [id, definition.base_stats.get("cargo_capacity", 0.0), definition.capabilities]) if "weapon" in definition.capabilities \ and float(definition.base_stats.get("fire_range", 0.0)) <= 0.0: found.append("vessel %s claims a weapon and has no range" % id) static func _check_buildings(found: PackedStringArray) -> void: for definition in BuildingDefinitions.all(): var id: String = String(definition.definition_id) # --- whole ------------------------------------------------------ if String(definition.display_name).strip_edges() == "": found.append("building %s has no display name" % id) if String(definition.description).strip_edges() == "": found.append("building %s has no description - its info popup would be blank" % id) if int(definition.cost) <= 0: found.append("building %s is free" % id) if int(definition.max_hp) <= 0: found.append("building %s has no hit points" % id) # Space-placed structures only: an extractor is a marker on a planet # rather than a thing standing in the world, and has no scene. if definition.placement_mode == "space" and definition.visual_scene == null: found.append("building %s stands in space with nothing to draw" % id) for field in ["menu_button_name", "menu_label_name", "menu_cost_name"]: if str(definition.get(field)).strip_edges() == "": found.append("building %s has no %s - the build menu could not place it" % [id, field]) if definition.capabilities.is_empty(): found.append("building %s declares no capabilities - it would do nothing at all" % id) # --- converters ------------------------------------------------- # These live on the LOGISTICS profile, a sub-resource of the # definition, not on the definition itself. Asking the definition for # them answers null and the whole check quietly never fires - which is # how the first draft of this passed while the gas burner sat there # with its collected_outputs missing. var logistics: Resource = definition.logistics if logistics == null or _text(logistics, "conversion_input") == "": continue if _text(logistics, "conversion_output") == "": found.append("building %s converts something into nothing" % id) if _list(logistics, "collected_outputs").is_empty(): found.append("building %s converts but lists no collected outputs - what it makes would never be fetched" % id) if _list(logistics, "input_buffers").is_empty(): found.append("building %s converts with no input buffer" % id)