fix: enforce storage and tooling invariants

This commit is contained in:
Rijad Zuzo
2026-07-30 13:00:59 +02:00
parent e3aa3440a2
commit e284d3774d
9 changed files with 283 additions and 30 deletions
+5 -1
View File
@@ -13,7 +13,7 @@
# Godot C# / Mono generated files
.mono/
data_*/
/data_*/
mono_crash.*
*.csproj
*.sln
@@ -57,7 +57,11 @@ crash_handler*
.DS_Store
.AppleDouble
.LSOverride
# Finder's custom-folder icon file ends with a carriage return. Keep this
# broad match, but explicitly allow real source directories named "icons".
Icon?
!**/icons/
!**/icons/**
._*
.DocumentRevisions-V100/
.Spotlight-V100/
+65
View File
@@ -0,0 +1,65 @@
# The Steward
The Steward is a Godot 4.7 simulation prototype about readable autonomous
village life in a magical Jajce-inspired valley. The project is built as a
living systems garden: simulation state is authoritative, while scenes provide
presentation, navigation, and interaction geometry.
## Quick start
Requirements:
- Godot 4.7.x;
- Python 3;
- the pinned development tools from `requirements-dev.txt`.
Set up the local tools with `uv`:
```bash
uv venv .venv
uv pip install --python .venv/bin/python -r requirements-dev.txt
```
Open `project.godot` in Godot 4.7 and run the main scene.
## Quality gate
Run the complete local gate before committing:
```bash
./tools/quality.sh
```
On Windows:
```powershell
powershell -ExecutionPolicy Bypass -File .\tools\quality.ps1
```
For a faster changed-GDScript formatting and lint pass, while still running
the Godot checks and scenarios:
```bash
./tools/quality.sh --changed
```
The gate enforces Godot 4.7, the pinned formatter and linter, headless project
scenarios, and the vendored GUT suite. See
[`docs/local_quality_gate.md`](docs/local_quality_gate.md) for setup and
troubleshooting.
## Project guide
- [`AGENTS.md`](AGENTS.md) — development loop, architecture rules, validation,
documentation, and Git expectations;
- [`docs/PROJECT_CONTEXT.md`](docs/PROJECT_CONTEXT.md) — vision, current
implementation, and constraints;
- [`docs/ARCHITECTURE_OVERVIEW.md`](docs/ARCHITECTURE_OVERVIEW.md) — runtime
ownership and dependency map;
- [`docs/LEARNING_ROADMAP.md`](docs/LEARNING_ROADMAP.md) — milestone order and
the current next slice;
- [`docs/README.md`](docs/README.md) — complete documentation map.
Start with the current code and tests when they disagree with a plan, preserve
in-progress work, and prefer one complete gameplay proof over a broad
abstraction.
+10 -1
View File
@@ -78,7 +78,16 @@ NPCStateRecord v3 adds carried inventory. SimulationStateRecord v2 adds
StorageStateRecord entries; world-schema v1 migrates legacy village food and
wood into the stable `village_pantry` and `village_woodpile` records. Parsed
storage values are canonicalized so save/restore continuation retains
byte-stable checksums.
byte-stable checksums. Current parsing rejects empty item IDs, negative or
non-finite amounts, negative or non-finite capacity, and contents above
capacity. Runtime deposit and withdrawal requests also reject empty item IDs
and non-finite amounts without mutating authoritative stock. When the authored
pantry or woodpile is present, current world parsing also requires its
authoritative food or wood amount to match the duplicated village summary;
contradictory saves are rejected instead of silently changing checksum during
restore. World-v1 migration expands a storage's inferred capacity when needed
so valid legacy stock above the current default is preserved rather than
discarded.
SimulationStateRecord v3 adds ordered `EconomicEventRecord` entries and
`next_event_id`. World schemas v1 and v2 migrate explicitly to an empty event
+13 -6
View File
@@ -14,7 +14,8 @@ parser errors, and LLM-generated garbage before committing.
```
On Windows, use `.venv/Scripts/python.exe` for the `--python` value.
- **Godot 4** binary on `PATH`, or set the `GODOT_BIN` environment variable
- **Godot 4.7.x** binary on `PATH`, or set the `GODOT_BIN` environment
variable. The gate rejects other engine series before importing or testing.
- **GUT 9.7.1** is vendored under `addons/gut` for Godot 4.7. No separate
package-manager install is needed.
@@ -24,7 +25,8 @@ The downloaded release archive SHA-256 is
`14969aa46adc84aa08cdd21b9f6d1a64addd92ae60b36f02d0521ed305aa4086`.
The quality and format scripts discover `.venv` automatically; activating it
is optional.
is optional. Missing `gdformat` or `gdlint` is a gate failure rather than a
silent skip.
## Usage
@@ -71,6 +73,7 @@ If everything passes:
```
QUALITY RESULT: PASS
Godot: 4.7.stable.official.5b4e0cb0f
godot-import PASS
gdformat PASS
@@ -87,6 +90,7 @@ On failure, only relevant errors and fix suggestions are shown:
```
QUALITY RESULT: FAIL
Godot: 4.7.stable.official.5b4e0cb0f
godot-import PASS
gdformat FAIL
@@ -113,18 +117,20 @@ to the terminal.
| Variable | Description |
|---|---|
| `GODOT_BIN` | Path to the Godot executable (auto-detected from PATH if unset) |
| `GODOT_BIN` | Path to the Godot 4.7.x executable (auto-detected if unset) |
## CI setup
To add this to CI, install the dependencies and run:
To add this to CI, provide a Godot 4.7.x executable, install the dependencies,
and run:
```bash
python -m pip install -r requirements-dev.txt
./tools/quality.sh
```
The script exits non-zero on any failure, so it will fail the CI step.
The script exits non-zero on any failure, including a wrong Godot series or
missing pinned formatter/linter, so it will fail the CI step.
The shell gate isolates Godot's cross-platform user-data paths under
`logs/quality/godot_profile`. Before running scenarios it imports project and
@@ -134,4 +140,5 @@ parse or load markers are failures because headless Godot can report those
errors while returning a zero process exit code. Every Godot subprocess has a
portable watchdog on stock macOS as well as Linux/Windows, and nonzero
`gdformat`/`gdlint` exits fail the gate even when their output is not a familiar
diagnostic string.
diagnostic string. Import receives a 60-second timeout on both shell and
PowerShell; the focused checks retain their 30-second timeout.
+24 -2
View File
@@ -288,6 +288,26 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
storage_ids[storage_id] = true
storage_records_by_id[storage_id] = storage_record
record.storages.append(storage_record)
var pantry := (
storage_records_by_id.get(SimulationIds.STORAGE_VILLAGE_PANTRY) as StorageStateRecord
)
if (
pantry != null
and not is_equal_approx(
float(record.village.data["food"]), pantry.get_amount(SimulationIds.RESOURCE_FOOD)
)
):
return null
var woodpile := (
storage_records_by_id.get(SimulationIds.STORAGE_VILLAGE_WOODPILE) as StorageStateRecord
)
if (
woodpile != null
and not is_equal_approx(
float(record.village.data["wood"]), woodpile.get_amount(SimulationIds.RESOURCE_WOOD)
)
):
return null
var event_ids := {}
var event_records_by_id := {}
@@ -689,7 +709,8 @@ static func _migrate_legacy(legacy_data: Dictionary, version: int) -> Dictionary
StorageStateRecord
. create(
SimulationIds.STORAGE_VILLAGE_PANTRY,
{String(SimulationIds.RESOURCE_FOOD): initial_food}
{String(SimulationIds.RESOURCE_FOOD): initial_food},
maxf(StorageStateRecord.DEFAULT_CAPACITY, initial_food)
)
. to_dictionary()
),
@@ -697,7 +718,8 @@ static func _migrate_legacy(legacy_data: Dictionary, version: int) -> Dictionary
StorageStateRecord
. create(
SimulationIds.STORAGE_VILLAGE_WOODPILE,
{String(SimulationIds.RESOURCE_WOOD): initial_wood}
{String(SimulationIds.RESOURCE_WOOD): initial_wood},
maxf(StorageStateRecord.DEFAULT_CAPACITY, initial_wood)
)
. to_dictionary()
)
+25 -4
View File
@@ -2,6 +2,7 @@ class_name StorageStateRecord
extends RefCounted
const SCHEMA_VERSION := 1
const DEFAULT_CAPACITY := 100.0
var data: Dictionary
@@ -11,7 +12,7 @@ func _init(record_data: Dictionary = {}) -> void:
static func create(
storage_id: StringName, initial_amounts: Dictionary, capacity: float = 100.0
storage_id: StringName, initial_amounts: Dictionary, capacity: float = DEFAULT_CAPACITY
) -> StorageStateRecord:
return StorageStateRecord.new(
{
@@ -32,12 +33,28 @@ static func from_dictionary(record_data: Dictionary) -> StorageStateRecord:
return null
if not record_data["amounts"] is Dictionary:
return null
var capacity := float(record_data["capacity"])
if not is_finite(capacity) or capacity < 0.0:
return null
var normalized := record_data.duplicate(true)
normalized["schema_version"] = SCHEMA_VERSION
normalized["capacity"] = float(record_data["capacity"])
normalized["capacity"] = capacity
var normalized_amounts := {}
for item_id in record_data["amounts"]:
normalized_amounts[String(item_id)] = float(record_data["amounts"][item_id])
var total_amount := 0.0
for raw_item_id in record_data["amounts"]:
var item_id := String(raw_item_id)
var amount := float(record_data["amounts"][raw_item_id])
if (
item_id.is_empty()
or normalized_amounts.has(item_id)
or not is_finite(amount)
or amount < 0.0
):
return null
total_amount += amount
if not is_finite(total_amount) or total_amount > capacity:
return null
normalized_amounts[item_id] = amount
normalized["amounts"] = normalized_amounts
return StorageStateRecord.new(normalized)
@@ -62,6 +79,8 @@ func get_available_capacity() -> float:
func deposit(item_id: StringName, requested_amount: float) -> float:
if item_id.is_empty() or not is_finite(requested_amount) or requested_amount <= 0.0:
return 0.0
var accepted := minf(maxf(requested_amount, 0.0), get_available_capacity())
if accepted <= 0.0:
return 0.0
@@ -70,6 +89,8 @@ func deposit(item_id: StringName, requested_amount: float) -> float:
func withdraw(item_id: StringName, requested_amount: float) -> float:
if item_id.is_empty() or not is_finite(requested_amount) or requested_amount <= 0.0:
return 0.0
var removed := minf(maxf(requested_amount, 0.0), get_amount(item_id))
if removed <= 0.0:
return 0.0
+86 -7
View File
@@ -24,6 +24,7 @@ func _run() -> void:
_test_relationship_schema_rejection()
_test_opportunity_schema_rejection()
_test_animal_schema_rejection()
_test_storage_schema_rejection()
_test_schema_rejection()
if failures.is_empty():
@@ -154,6 +155,80 @@ func _test_schema_rejection() -> void:
manager.free()
func _test_storage_schema_rejection() -> void:
var valid_storage := (
StorageStateRecord
. create(&"schema_test_storage", {String(SimulationIds.RESOURCE_FOOD): 1.0}, 2.0)
. to_dictionary()
)
var negative_amount := valid_storage.duplicate(true)
negative_amount["amounts"][String(SimulationIds.RESOURCE_FOOD)] = -1.0
_check(
StorageStateRecord.from_dictionary(negative_amount) == null,
"Storage records should reject negative item amounts"
)
var non_finite_amount := valid_storage.duplicate(true)
non_finite_amount["amounts"][String(SimulationIds.RESOURCE_FOOD)] = NAN
_check(
StorageStateRecord.from_dictionary(non_finite_amount) == null,
"Storage records should reject non-finite item amounts"
)
var invalid_capacity := valid_storage.duplicate(true)
invalid_capacity["capacity"] = -1.0
_check(
StorageStateRecord.from_dictionary(invalid_capacity) == null,
"Storage records should reject negative capacity"
)
var over_capacity := valid_storage.duplicate(true)
over_capacity["amounts"][String(SimulationIds.RESOURCE_FOOD)] = 3.0
_check(
StorageStateRecord.from_dictionary(over_capacity) == null,
"Storage records should reject contents above capacity"
)
var empty_item_id := valid_storage.duplicate(true)
empty_item_id["amounts"] = {"": 1.0}
_check(
StorageStateRecord.from_dictionary(empty_item_id) == null,
"Storage records should reject empty item IDs"
)
var storage := StorageStateRecord.from_dictionary(valid_storage)
_check(storage != null, "A valid bounded storage record should still parse")
if storage != null:
_check(
storage.deposit(SimulationIds.RESOURCE_FOOD, NAN) == 0.0,
"Storage deposits should reject non-finite requests"
)
_check(
storage.withdraw(SimulationIds.RESOURCE_FOOD, INF) == 0.0,
"Storage withdrawals should reject non-finite requests"
)
_check(
is_equal_approx(storage.get_amount(SimulationIds.RESOURCE_FOOD), 1.0),
"Rejected storage requests should not mutate authoritative stock"
)
var manager := _create_manager(42)
var mismatched_food: Dictionary = manager.create_state_record().to_dictionary()
mismatched_food["village"]["food"] = float(mismatched_food["village"]["food"]) + 1.0
_check(
SimulationStateRecord.from_dictionary(mismatched_food) == null,
"World records should reject village food that disagrees with pantry authority"
)
var mismatched_wood: Dictionary = manager.create_state_record().to_dictionary()
mismatched_wood["village"]["wood"] = float(mismatched_wood["village"]["wood"]) + 1.0
_check(
SimulationStateRecord.from_dictionary(mismatched_wood) == null,
"World records should reject village wood that disagrees with woodpile authority"
)
manager.free()
func _test_legacy_resource_migration() -> void:
var migrated := ResourceStateRecord.from_dictionary(
{
@@ -205,6 +280,10 @@ func _test_legacy_world_storage_migration() -> void:
var legacy_data: Dictionary = manager.create_state_record().to_dictionary()
legacy_data["schema_version"] = SimulationStateRecord.LEGACY_SCHEMA_VERSION
legacy_data.erase("storages")
var legacy_food := StorageStateRecord.DEFAULT_CAPACITY + 25.0
var legacy_wood := StorageStateRecord.DEFAULT_CAPACITY + 35.0
legacy_data["village"]["food"] = legacy_food
legacy_data["village"]["wood"] = legacy_wood
var migrated := SimulationStateRecord.from_dictionary(legacy_data)
_check(migrated != null, "World schema v1 should migrate authored storage")
if migrated != null:
@@ -219,14 +298,12 @@ func _test_legacy_world_storage_migration() -> void:
(
pantry != null
and woodpile != null
and is_equal_approx(
pantry.get_amount(SimulationIds.RESOURCE_FOOD), manager.village.food
)
and is_equal_approx(
woodpile.get_amount(SimulationIds.RESOURCE_WOOD), manager.village.wood
)
and is_equal_approx(pantry.get_amount(SimulationIds.RESOURCE_FOOD), legacy_food)
and is_equal_approx(woodpile.get_amount(SimulationIds.RESOURCE_WOOD), legacy_wood)
and is_equal_approx(pantry.get_available_capacity(), 0.0)
and is_equal_approx(woodpile.get_available_capacity(), 0.0)
),
"World migration should preserve legacy village resources in their storage"
"World migration should preserve legacy stock above the default storage capacity"
)
manager.free()
@@ -842,6 +919,7 @@ func _test_opportunity_schema_rejection() -> void:
for storage_data in already_supplied["storages"]:
if StringName(storage_data["storage_id"]) == SimulationIds.STORAGE_VILLAGE_PANTRY:
storage_data["amounts"][String(SimulationIds.RESOURCE_FOOD)] = 1.0
already_supplied["village"]["food"] = 1.0
_check(
SimulationStateRecord.from_dictionary(already_supplied) == null,
"An open pantry need should reject a save whose target is already supplied"
@@ -985,6 +1063,7 @@ func _test_opportunity_schema_rejection() -> void:
func _build_opportunity_world(manager: Node, resolved: bool) -> Dictionary:
var world: Dictionary = manager.create_state_record().to_dictionary()
world["simulation"]["tick_count"] = 10
world["village"]["food"] = 1.0 if resolved else 0.0
for storage_data in world["storages"]:
if StringName(storage_data["storage_id"]) == SimulationIds.STORAGE_VILLAGE_PANTRY:
storage_data["amounts"][String(SimulationIds.RESOURCE_FOOD)] = (
+30 -5
View File
@@ -16,6 +16,7 @@ $OVERALL = $false
$ERRORS = @()
$FIXES = @()
$OWNED_GDSCRIPT_ROOTS = @("player", "simulation", "tests", "tools", "world")
$REQUIRED_GODOT_SERIES = "4.7"
# -- tool detection -----------------------------------------------------------
function ConvertTo-ConsoleGodotPath($Path) {
@@ -128,7 +129,10 @@ function To-ResPath($p) {
}
function Invoke-GodotWithTimeout {
param([string]$ArgumentString)
param(
[string]$ArgumentString,
[int]$TimeoutSeconds = 30
)
$psi = New-Object Diagnostics.ProcessStartInfo
$psi.FileName = $GODOT
$psi.Arguments = $ArgumentString
@@ -148,7 +152,7 @@ function Invoke-GodotWithTimeout {
}
$outputTask = $p.StandardOutput.ReadToEndAsync()
$errorTask = $p.StandardError.ReadToEndAsync()
$exited = $p.WaitForExit(30000)
$exited = $p.WaitForExit($TimeoutSeconds * 1000)
if (-not $exited) {
$p.Kill()
$p.WaitForExit()
@@ -167,6 +171,18 @@ if (-not $GODOT) {
Write-Host "ERROR: Godot binary not found. Set GODOT_BIN or add godot/godot4 to PATH."
exit 1
}
$godotVersionOutput = @(& $GODOT --version 2>&1)
$godotVersionExit = $LASTEXITCODE
if ($godotVersionExit -ne 0 -or $godotVersionOutput.Count -eq 0) {
Write-Host "ERROR: Could not query the Godot version from '$GODOT'."
exit 1
}
$GODOT_VERSION = ([string]$godotVersionOutput[0]).Trim()
$requiredVersionPattern = '^' + [regex]::Escape($REQUIRED_GODOT_SERIES) + '([.-]|$)'
if ($GODOT_VERSION -notmatch $requiredVersionPattern) {
Write-Host "ERROR: Godot $REQUIRED_GODOT_SERIES.x is required; found '$GODOT_VERSION'."
exit 1
}
# -- 0. Godot import bootstrap ------------------------------------------------
$importResult = "PASS"
@@ -182,10 +198,13 @@ $needsImport = (-not $hasProjectClasses) -or
($changedGd.Count -gt 0)
if ($needsImport) {
$importArgs = "--headless --path `"$ROOT`" --import"
$importOutput, $importExit = Invoke-GodotWithTimeout -ArgumentString $importArgs
$importOutput, $importExit = Invoke-GodotWithTimeout -ArgumentString $importArgs -TimeoutSeconds 60
$importOutput | Add-Content -Path "$LOG/godot-import.log" -Encoding utf8
if ($importExit -eq $null -or $importExit -ne 0) {
$importResult = "FAIL"
if ($importExit -eq $null) {
Add-Content -Path "$LOG/godot-import.log" -Value "[timed out after 60s]" -Encoding utf8
}
}
}
$hasProjectClasses = (Test-Path $classCache) -and
@@ -210,8 +229,10 @@ $fmtResult = "PASS"
$fmtFailed = $false
Set-Content -Path "$LOG/gdformat.log" -Value "" -Encoding utf8
if (-not $fmtAvail) {
$fmtResult = "SKIPPED"
$fmtResult = "FAIL"
Add-Content -Path "$LOG/gdformat.log" -Value "gdformat not found -- install requirements-dev.txt (see docs/local_quality_gate.md)" -Encoding utf8
$ERRORS += "gdformat: required formatter not found"
$FIXES += "Install the pinned dev tools from requirements-dev.txt"
} else {
if ($Changed) {
$files = Get-ChangedGd
@@ -260,8 +281,10 @@ $lintResult = "PASS"
$lintFailed = $false
Set-Content -Path "$LOG/gdlint.log" -Value "" -Encoding utf8
if (-not $lintAvail) {
$lintResult = "SKIPPED"
$lintResult = "FAIL"
Add-Content -Path "$LOG/gdlint.log" -Value "gdlint not found -- install requirements-dev.txt (see docs/local_quality_gate.md)" -Encoding utf8
$ERRORS += "gdlint: required linter not found"
$FIXES += "Install the pinned dev tools from requirements-dev.txt"
} else {
if ($Changed) {
$files = Get-ChangedGd
@@ -393,6 +416,7 @@ if ($importResult -eq 'FAIL' -or $fmtResult -eq 'FAIL' -or $lintResult -eq 'FAIL
Write-Host ""
if ($OVERALL) { Write-Host "QUALITY RESULT: FAIL" } else { Write-Host "QUALITY RESULT: PASS" }
Write-Host "Godot: $GODOT_VERSION"
Write-Host ""
Write-Host " godot-import $importResult"
Write-Host " gdformat $fmtResult"
@@ -430,6 +454,7 @@ Write-Host ""
if ($OVERALL) { $resultText = "FAIL" } else { $resultText = "PASS" }
$summary = @"
QUALITY RESULT: $resultText
Godot: $GODOT_VERSION
godot-import $importResult
gdformat $fmtResult
+25 -4
View File
@@ -18,6 +18,7 @@ OVERALL=false
ERRORS=()
FIXES=()
OWNED_GDSCRIPT_ROOTS=(player simulation tests tools world)
REQUIRED_GODOT_SERIES="4.7"
# -- tool detection -----------------------------------------------------------
console_godot_path() {
@@ -50,6 +51,7 @@ find_godot() {
if [[ -n "${GODOT_BIN:-}" ]]; then console_godot_path "$GODOT_BIN" && return 0; echo "$GODOT_BIN"; return 0; fi
find_project_godot && return 0
for c in godot godot4; do command -v "$c" &>/dev/null && echo "$c" && return 0; done
console_godot_path "/Applications/Godot.app/Contents/MacOS/Godot" && return 0
return 1
}
@@ -144,6 +146,19 @@ if [[ -z "$GODOT" ]]; then
echo "ERROR: Godot binary not found. Set GODOT_BIN or add godot/godot4 to PATH."
exit 1
fi
if ! GODOT_VERSION="$("$GODOT" --version 2>&1)"; then
echo "ERROR: Could not query the Godot version from '$GODOT'."
exit 1
fi
GODOT_VERSION="${GODOT_VERSION%%$'\n'*}"
GODOT_VERSION="${GODOT_VERSION%$'\r'}"
case "$GODOT_VERSION" in
"$REQUIRED_GODOT_SERIES"|"$REQUIRED_GODOT_SERIES".*|"$REQUIRED_GODOT_SERIES"-*) ;;
*)
echo "ERROR: Godot $REQUIRED_GODOT_SERIES.x is required; found '$GODOT_VERSION'."
exit 1
;;
esac
GODOT_ENV=(env "HOME=$GODOT_PROFILE" "APPDATA=$GODOT_PROFILE" "LOCALAPPDATA=$GODOT_PROFILE")
# -- 0. Godot import bootstrap ------------------------------------------------
@@ -185,8 +200,10 @@ fmt_result="PASS"
fmt_failed=false
: > "$LOG/gdformat.log"
if ! has_gdformat; then
fmt_result="SKIPPED"
fmt_result="FAIL"
echo "gdformat not found — install requirements-dev.txt (see docs/local_quality_gate.md)" > "$LOG/gdformat.log"
ERRORS+=("gdformat: required formatter not found")
FIXES+=("Install the pinned dev tools from requirements-dev.txt")
else
if $CHANGED; then
files=$(changed_gd)
@@ -231,8 +248,10 @@ lint_result="PASS"
lint_failed=false
: > "$LOG/gdlint.log"
if ! has_gdlint; then
lint_result="SKIPPED"
lint_result="FAIL"
echo "gdlint not found — install requirements-dev.txt (see docs/local_quality_gate.md)" > "$LOG/gdlint.log"
ERRORS+=("gdlint: required linter not found")
FIXES+=("Install the pinned dev tools from requirements-dev.txt")
else
if $CHANGED; then
files=$(changed_gd)
@@ -294,9 +313,9 @@ if grep -qE "SCRIPT ERROR:|Parse Error:|Failed to load script" "$LOG/godot-check
godot_result="FAIL"
fi
if [[ "$godot_result" == "FAIL" ]]; then
grep -iE "error|warning|parse|syntax" "$LOG/godot-check.log" 2>/dev/null | head -20 | while IFS= read -r errline; do
while IFS= read -r errline; do
ERRORS+=("godot: $errline")
done
done < <(grep -iE "error|warning|parse|syntax" "$LOG/godot-check.log" 2>/dev/null | head -20)
FIXES+=("Fix Godot parser errors")
fi
@@ -356,6 +375,7 @@ if $OVERALL; then
else
echo "QUALITY RESULT: PASS"
fi
echo "Godot: $GODOT_VERSION"
echo ""
echo " godot-import $import_result"
echo " gdformat $fmt_result"
@@ -393,6 +413,7 @@ echo ""
{
echo "QUALITY RESULT: $($OVERALL && echo FAIL || echo PASS)"
echo "Godot: $GODOT_VERSION"
echo ""
echo " godot-import $import_result"
echo " gdformat $fmt_result"