From 4e09dea03aff33f6490f01a99026063860f01544 Mon Sep 17 00:00:00 2001 From: megaproxy Date: Sat, 16 May 2026 00:35:42 +0100 Subject: [PATCH] Fix bill-editor crash on mode-change and remove MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mode OptionButton and remove Button both called _populate_bills() from inside their own signal callbacks. _populate_bills() rebuilds the bill list — freeing the very widget mid-emit, which crashes Godot. Defer the rebuild via call_deferred("_populate_bills") so it runs after the signal frame completes. Reproduced by user changing a bill from FOREVER → "Do until 10". --- scenes/ui/workbench_panel.gd | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/scenes/ui/workbench_panel.gd b/scenes/ui/workbench_panel.gd index b7cf4de..0bfe0b5 100644 --- a/scenes/ui/workbench_panel.gd +++ b/scenes/ui/workbench_panel.gd @@ -292,9 +292,10 @@ func _make_bill_row(bill: Bill) -> VBoxContainer: mode_btn.item_selected.connect(func(idx: int) -> void: bill.mode = idx as Bill.Mode Audit.log("workbench_ui", "%s: bill mode → %d" % [current_workbench.label_text, idx]) - # Repopulate so conditional rows update; OptionButton state survives because - # we set mode on bill before the rebuild, and the new row reads bill.mode. - _populate_bills() + # Defer the rebuild — we must NOT free mode_btn while its item_selected + # signal is still emitting (instant crash). call_deferred runs the + # repopulate after the signal frame completes. + call_deferred("_populate_bills") ) mode_row.add_child(mode_btn) @@ -366,7 +367,8 @@ func _make_bill_row(bill: Bill) -> VBoxContainer: current_workbench.label_text, bill.recipe.id if bill.recipe != null else "null" ]) - _populate_bills() + # Defer — same reason as mode_btn: don't free this button mid-emit. + call_deferred("_populate_bills") ) remove_row.add_child(remove_btn)