Weird setter function behaviour causes stack overflow?

Godot Version

Godot 4.5 Stable

Question

I’ve been getting this weird issue when using setter functions in my small minesweeper type game

This is the structure of each tile instance in the scene:

UiButton #seen as $".." in the code, reacts to inputs and changes of variables
|_StaticBody2D #seen as $"Processor" in code, stores all variable
  |_CollisionShape2D
  |_Raycast * 8

When i started making it, i put everything inside a _process() function just to see if the stuff i wrote is actually working and then optimize it later on. It was all working fine, until i went to optimize it, i wanted it so that the button only updates when the variable for it being revealed changes, like this, using a setter function:

#Variables are stored in the StaticBody2D
@export var Revealed:bool:
	set(v):
		if Revealed == true:
			print("working")
			$"..".hasrevealed()

but when i do this, i get a stack overflow error that crashes the game, something that never happened when i used _process() with errors being reported in three different location in the stack, one in the setter function on the $"..".hasrevealed() line, and another two on the snippets below:

#Snippet comes from the Raycasts
if $"..".NearbyMines == 0 and #all other conditions are met...
    print(str(name) + "quickrevealcalled")
    get_collider().Revealed = true #this is the line where the error is reported
    print("revealing "+ str(get_collider().name))
#Script comes from UiButton
if $Processor.NearbyMines == 0 and $Processor.IsMine == false:
    print(str(name)+"istryingtocallquickreveal")
    $Processor/Down.quickreveal()#error is reported on the first two
    print("calledD")
	$Processor/Up.quickreveal() #so this one is also included
    print("calledU")
	$Processor/Right.quickreveal()#but not this one or the others below
    print("calledR")
    ...

and the game crashes…

the terminal output looks something like this:

400 #number of mines present in the grid
checkbuttonclick #print to see if it actually went through
@Button@211istryingtocallquickreveal #which tile has called quickreveal
Downquickrevealcalled #raycast being called
@Button@231istryingtocallquickreveal
Downquickrevealcalled
#...a bunch of other buttons were revealed here in the same structure as above, just with different names
@Button@351istryingtocallquickreveal
Downquickrevealcalled
@Button@371istryingtocallquickreveal #from here on it starts to repeat
Downquickrevealcalled
@Button@391istryingtocallquickreveal
calledD #and only now it prints the check?????
Upquickrevealcalled
@Button@371istryingtocallquickreveal
Downquickrevealcalled
@Button@391istryingtocallquickreveal
calledD

i wanted it to work so once the UiButton detects its been left clicked, it goes to it’s child Processor, sets the Revealed variable to true, which the processor then detects and reports back to it’s parent button, to then act on if the ammount of mines near it is 0 or it is a mine. If there are no nearby mines, it calls quickreveal on every raycaster child, which then causes the processor of other tiles to report they have been revealed that then opens up a cave, like an actual game of minesweeper, but for some reason it keeps breaking and i have no clue how to fix it

tbf i already read another topic that ran into a similar issue to mine while also coincidentally also making a minesweeper clone in a somewhat similar structure as to what im doing now, but none of the fixes proposed on that forum post have helped me because the issue doesnt seem to happen in the same place, this is somewhat a last resort for me because i can’t seem to find a fix for it anywhere :confused: any help would be much appreciated! thanks for reading!

There is a lot going on here, but the very first problem worth mentioning is: your setter does not set the value of Revealed.

It really should.

Somewhere in there it should say Revealed = v.

I don’t know if you’re willing to share, but the original working _process code would be really helpful as well.

What makes people extra helpful is if you start by applying best practices from the gdscript style guide (that makes the nature of what things in your code are much more legible):

Your code also contains a lot of references to parent nodes via $".." which is a bit unsafe in the long run because it needs the nesting in your scene tree to remain the same forever.

But a refactor of that exact scene tree may be in order later as well (if you were to continue developing this anyway). I’ve never seen physics bodies as children of UI nodes (Button) used like this before.

I kind of see what you’re trying to achieve with it, I guess, but it’s not totally clear to me why this is needed. If it’s minesweeper like, is it to detect what all the neighboring squares are doing? Wouldn’t it make more sense to keep track of that in a Global (singleton) array than the built in physics engine?

thanks for replying!

I’ll provide the process code, all it does is change the way the button looks to display the correct numbers and colors, and what to do if the StaticBody2D is a mine

heres how the input is detected:

func _on_gui_input(event: InputEvent) -> void:
	if event is InputEventMouseButton and event.is_pressed():
		match event.button_index:
			MOUSE_BUTTON_LEFT:
				if $Processor.Flagged == false:
					$Processor.Revealed = true
				else:
					pass
			MOUSE_BUTTON_RIGHT:
				if $Processor.Flagged == false:
					$Processor.Flagged = true
				else:
					$Processor.Flagged = false
	await get_tree().create_timer(0.1).timeout

and here’s the process:

func _process():
	if $Processor.IsMine == true:
		add_theme_color_override("font_color",Color(0.0, 0.0, 0.0, 1.0))
		$"../../ModPlayer".playing = false
		$"../../AudioStreamPlayer".playing = true
		await get_tree().create_timer(0.1).timeout
		get_tree().reload_current_scene()
	if $Processor.IsMine == false:
		if $Processor.NearbyMines > 0:
			text = str($Processor.NearbyMines)
			match $Processor.NearbyMines:
				1:
					add_theme_color_override("font_color",Color(blue))
				2:
					add_theme_color_override("font_color",Color(green))
				3:
					add_theme_color_override("font_color",Color(red))
				4:
					add_theme_color_override("font_color",Color(darkblue))
				5:
					add_theme_color_override("font_color",Color(mustardyel))
				6:
					add_theme_color_override("font_color",Color(aqua))
				8:
					add_theme_color_override("font_color",Color(grey))
				7:
					add_theme_color_override("font_color",Color(black))
		var fontsize = size.x / 2
		add_theme_font_size_override("font_size",fontsize)
		theme = preload("res://Pc98Revealed.tres")
		if $Processor.NearbyMines == 0 and $Processor.IsMine == false:
			print(str(name)+"istryingtocallquickreveal")
			$Processor/Down.quickreveal()#crashes occur from here
			print("calledD")
			$Processor/Up.quickreveal()#to here
			print("calledU")
			$Processor/Right.quickreveal()
			print("calledR")
			$Processor/Left.quickreveal()
			print("calledL")
			$Processor/DiagRDown.quickreveal()
			print("calledRD")
			$Processor/DiagLDown.quickreveal()
			print("calledLD")
			$Processor/DiagLUp.quickreveal()
			print("calledLU")
			$Processor/DiagRUp.quickreveal()
			print("calledRU")
			$Processor.queue_free()
			return
	if $Processor.Flagged == true:
		icon = load("res://RetroFlag.png")
		add_theme_color_override("icon_normal_color",Color(chooserandomly)
	if $Processor.Flagged == false:
			icon = null

also, im kind of only using static bodies so the collision shape can work, i know its janky but i thought that’s what i had to deal with if i wanted to have customizable themes qwq

You shouldn’t be using physics bodies and raycasts in a minesweeper clone to begin with. It makes things way way more complicated (and bug prone) than they need to be. Grid based systems typically won’t need any physics collisions to determine the state. It’s a waste of resources and models the game in an inadequate way. Instead, keep the state in a 2D array or a dictionary with Vector2i keys.

This somehow feels like you jumped into Godot with at least some programming experience, but kind of without doing the basic learning materials…

Have you already done the basic 2D tutorial?

It may also be helpful to read up on some basic paradigms too, especially what a GUI is:

And what 2D … is:

In short. If I were to build minesweeper I’d just use the GUI Control nodes. When I first encountered Godot I jumped right into 2D so then I might’ve used a TileMaplayer (don’t think that’s the best choice by the way)

But not both mixed together.

Try rewriting it like @normalized suggests: use a Dictionary to keep track of the state of each square in your grid and represent that state visually with a texture on your button.

To find out what neighbors what just use your common sense about what a grid looks like, but not the physics engine.

Oh and don’t forget to apply the style guide when writing gdscript:

The hint to your problem shows in these (repeating) lines:

It shows that these events happen:

  1. @Button@371 executes your third snippet (if $Processor.NearbyMines == 0 and $Processor.IsMine == false:, …)
  2. as part of that @Button@371 executes $Processor/Down.quickreveal()
  3. this leads to executing that third snippet in @Button@391
  4. @Button@391 has no downward element so $Processor/Down.quickreveal() does nothing
  5. @Button@391 then executes $Processor/Up.quickreveal()
  6. upward from @Button@391 is @Button#371 and this leads to step 1 above in an (endless) sequence until the stack overflows.

The condition at the start of your third snippet should be

if $Processor.NearbyMines == 0 and not $Processor.IsMine and not $Processor.Revealed:
	print(str(name)+"istryingtocallquickreveal")
	$Processor/Down.quickreveal()
	print("calledD")
	$Processor/Up.quickreveal()
	print("calledU")
	$Processor/Right.quickreveal()
	print("calledR")

This will however only work if you first fix your setter:

@export var Revealed: bool:
	set(v):
		if v and not Revealed:
			Revealed = true
			print("working")
			$"..".hasrevealed()
		else: # you probably never set Revealed to false, so this might not be needed
			Revealed = false