Everything lived in one flat directory, so a diff gave no clue what area it touched and finding the file you wanted meant knowing its exact name. Twelve folders now, grouped by what a thing IS rather than what type it is - a vessel's definition, its component scenes and its upgrade resources sit together, because that is the set you read when you change one of them. buildings core controllers enemies logistics planets resources shaders surface ui vessels vfx world 332 files moved and every res:// reference rewritten - 306 files touched, including the autoload and main-scene paths in project.godot. Nothing renamed: this is purely where files sit, so the diff is a pile of pure moves and no behaviour is buried in it. ui_theme.tres, icon.svg and web_shell.html stay at the root, where Godot and the export preset expect them. Verified three ways, because a suite alone would not have caught all of it: the full 117 pass, the web export builds clean, and the game renders. The export is why that matters. The FIRST export after the move failed with "Attempt to open script 'res://GameState.gd'" - the old path - while project.godot on disk plainly said res://core/GameState.gd. That is this project's documented Android FUSE mtime trap: Godot served a cached copy of a file whose content had changed. A `touch` fixed it. Nothing in the test suite would have found that, because the suite runs from a copy on a native filesystem - the one place the bug cannot happen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
171 lines
6.5 KiB
GDScript
171 lines
6.5 KiB
GDScript
extends SceneTree
|
|
|
|
# Headless test: run with
|
|
# godot --headless -s res://tests/test_planet_map_assignment.gd
|
|
# Every planet should draw a map of its own surface type at random, and then
|
|
# keep it - leaving a planet and coming back must land on the same layout, or
|
|
# the place stops feeling like a place.
|
|
#
|
|
# Two things could break that, and both are silent:
|
|
# - the candidate list is built by scanning a directory, and the order a
|
|
# filesystem lists files in is not something to seed a choice with. The
|
|
# pick indexes into that list, so an unstable order re-deals every planet
|
|
# - the pool itself changes the moment the map editor saves a new setup,
|
|
# which would re-deal planets mid-session even with a perfect seed
|
|
#
|
|
# Covers:
|
|
# A. the same planet resolves to the same setup, every time
|
|
# B. the setup it draws is always authored for that planet's own profile
|
|
# C. different planets do not all collapse onto one setup
|
|
# D. an assignment survives a new setup file appearing in the pool
|
|
# E. candidates come back in a deterministic order regardless of scan order
|
|
|
|
const SCRATCH_ID: String = "test_assignment_scratch"
|
|
const SCRATCH_PATH: String = "res://PlanetSetups/%s.tres" % SCRATCH_ID
|
|
# This test needs a pool of several maps of one type to draw from. It used to
|
|
# use whatever rocky setups the project happened to ship, which made it a test
|
|
# of the content folder as much as of the code - deleting a map in the editor
|
|
# dropped the pool to one and this failed, reporting a broken draw when
|
|
# nothing was broken. It now plants its own.
|
|
const POOL_IDS: Array = [
|
|
"test_assignment_pool_a", "test_assignment_pool_b", "test_assignment_pool_c",
|
|
]
|
|
|
|
|
|
func _initialize() -> void:
|
|
_remove_scratch()
|
|
if not _plant_pool():
|
|
return
|
|
PlanetSetups.rescan()
|
|
|
|
# --- E. deterministic ordering ------------------------------------------
|
|
var profile_id: String = "rocky"
|
|
var ordered: Array = PlanetSetups.setups_for_profile(profile_id)
|
|
if ordered.size() < 2:
|
|
_remove_scratch()
|
|
_fail("only %d '%s' setups exist even after planting %d - the pool is not seeing them" \
|
|
% [ordered.size(), profile_id, POOL_IDS.size()])
|
|
return
|
|
var ids: Array = []
|
|
for setup in ordered:
|
|
ids.append(String(setup.setup_id))
|
|
var sorted_ids: Array = ids.duplicate()
|
|
sorted_ids.sort()
|
|
if ids != sorted_ids:
|
|
_fail("setups_for_profile returned %s, which is not sorted - the draw indexes into this, so the order cannot be left to the filesystem" % str(ids))
|
|
return
|
|
print("'%s' pool is deterministic: %s" % [profile_id, str(ids)])
|
|
|
|
# --- A/B/C. stable, correctly typed, and actually varied -----------------
|
|
var first_pass: Dictionary = {}
|
|
var planet_ids: Array = []
|
|
for i in range(40):
|
|
planet_ids.append("planet_%d_%d" % [i * 7, i * 13])
|
|
for planet_id in planet_ids:
|
|
var setup: Resource = PlanetSetups.setup_for_planet(planet_id, profile_id)
|
|
if setup == null:
|
|
_fail("%s drew no setup at all" % planet_id)
|
|
return
|
|
if String(setup.surface_profile_id) != profile_id:
|
|
_fail("%s is a '%s' planet but drew a '%s' map" \
|
|
% [planet_id, profile_id, setup.surface_profile_id])
|
|
return
|
|
first_pass[planet_id] = setup
|
|
|
|
for planet_id in planet_ids:
|
|
for repeat in range(3):
|
|
if PlanetSetups.setup_for_planet(planet_id, profile_id) != first_pass[planet_id]:
|
|
_fail("%s drew a different map on a later visit" % planet_id)
|
|
return
|
|
print("40 planets each re-drew the same map on every revisit")
|
|
|
|
var distinct: Dictionary = {}
|
|
for planet_id in first_pass:
|
|
distinct[first_pass[planet_id].setup_id] = true
|
|
if distinct.size() < 2:
|
|
_fail("all 40 planets drew the same map (%s), so the draw is not random at all" % str(distinct.keys()))
|
|
return
|
|
print("across 40 planets the draw used %d of the %d available maps" % [distinct.size(), ids.size()])
|
|
|
|
# --- D. a new map appearing does not re-deal the planets already visited -
|
|
var scratch: Resource = load("res://surface/PlanetSetup.gd").new()
|
|
scratch.setup_id = SCRATCH_ID
|
|
scratch.display_name = "Assignment Scratch"
|
|
scratch.surface_profile_id = profile_id
|
|
var err: int = ResourceSaver.save(scratch, SCRATCH_PATH)
|
|
if err != OK:
|
|
_fail("could not write the scratch setup (error %d)" % err)
|
|
return
|
|
# Exactly what the map editor does after a save.
|
|
PlanetSetups.rescan()
|
|
var live: Dictionary = {}
|
|
var scratch_in_pool: bool = false
|
|
for setup in PlanetSetups.setups_for_profile(profile_id):
|
|
live[setup] = true
|
|
if String(setup.setup_id) == SCRATCH_ID:
|
|
scratch_in_pool = true
|
|
if not scratch_in_pool:
|
|
_remove_scratch()
|
|
_fail("the scratch setup did not enter the pool, so this case proves nothing")
|
|
return
|
|
|
|
var moved: int = 0
|
|
var checked: int = 0
|
|
for planet_id in planet_ids:
|
|
# Skipped if the map this planet drew is no longer in the pool. The
|
|
# suite runs tests in parallel against one project directory, so
|
|
# another test's scratch setup can appear and vanish underneath this
|
|
# one - and a planet losing an assignment because its MAP was deleted
|
|
# is correct behaviour, not the re-deal this case is looking for.
|
|
if not live.has(first_pass[planet_id]):
|
|
continue
|
|
checked += 1
|
|
if PlanetSetups.setup_for_planet(planet_id, profile_id) != first_pass[planet_id]:
|
|
moved += 1
|
|
if checked == 0:
|
|
_remove_scratch()
|
|
_fail("every planet's map disappeared from the pool mid-test, so nothing could be checked")
|
|
return
|
|
_remove_scratch()
|
|
PlanetSetups.rescan()
|
|
if moved > 0:
|
|
_fail("saving one new map re-dealt %d of %d already-visited planets" % [moved, checked])
|
|
return
|
|
print("saving a new map left all %d existing assignments alone" % checked)
|
|
|
|
_remove_scratch()
|
|
PlanetSetups.rescan()
|
|
print("TEST_PLANET_MAP_ASSIGNMENT: PASS")
|
|
quit(0)
|
|
|
|
|
|
func _plant_pool() -> bool:
|
|
for pool_id in POOL_IDS:
|
|
var setup: Resource = load("res://surface/PlanetSetup.gd").new()
|
|
setup.setup_id = pool_id
|
|
setup.display_name = pool_id
|
|
setup.surface_profile_id = "rocky"
|
|
setup.capture_points = PackedVector2Array([Vector2(100.0, 100.0)])
|
|
if ResourceSaver.save(setup, "res://PlanetSetups/%s.tres" % pool_id) != OK:
|
|
_remove_scratch()
|
|
_fail("could not plant the scratch map pool")
|
|
return false
|
|
return true
|
|
|
|
|
|
func _remove_scratch() -> void:
|
|
var paths: Array = [SCRATCH_PATH]
|
|
for pool_id in POOL_IDS:
|
|
paths.append("res://PlanetSetups/%s.tres" % pool_id)
|
|
for res_path in paths:
|
|
var path: String = ProjectSettings.globalize_path(res_path)
|
|
if FileAccess.file_exists(path):
|
|
DirAccess.remove_absolute(path)
|
|
if FileAccess.file_exists(path + ".uid"):
|
|
DirAccess.remove_absolute(path + ".uid")
|
|
|
|
|
|
func _fail(message: String) -> void:
|
|
_remove_scratch()
|
|
print("TEST_PLANET_MAP_ASSIGNMENT: FAIL (%s)" % message)
|
|
quit(1)
|