spaceship/core/CatalogCheck.gd
2026-08-31 15:23:25 +02:00

143 lines
7.2 KiB
GDScript

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)
_check_planets(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)
if definition.visual_scene != null:
var visual: Node = definition.visual_scene.instantiate()
var provider: Node = visual.get_node_or_null("ActionProvider")
if provider != null and Array(provider.actions).is_empty():
found.append("building %s has an action provider with no actions" % id)
visual.free()
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)
static func _check_planets(found: PackedStringArray) -> void:
for definition in PlanetDefinitions.DEFINITIONS.values():
var id: String = String(definition.definition_id)
if definition.names.is_empty():
found.append("planet %s has no generated names" % id)