Yeah, Godot lets me step through. I still havn't tracked down when or where I get the duplicate email bug to trigger. It was formerly in the email menu itself, but im pretty sure I fixed that iteration of the bug. I certainly wouldn't say no to a save file / deets on your experience with when the bug manifested. Also, enjoy looking under the hood, but I will warn you, Hardcoded has a lot of wack code! I made over the course of like 8 years and started with no programming knowledge. I've learned a lot during development, but there's a lot of stuff that's still in there that is very poorly coded.
Sorry for the late reply; it's been...a week.
Here's one of my save files: https://drmccoy.de/zeugs/hardcoded_beryl_mail_bug-savefile0.json
Just load it, go to Beryl's apartment and the conversation that adds the mail will pop right up, even though the mail is already on the "read" list. Twice, even.
Still happens with 1.05 too, I just tried.
Okay, I snooped a bit, and I found out why the hasemail() function doesn't work (though not *why* it doesn't work)
This issue is that the contents of the emailreadlist (and emailunreadlist), i.e. the email ids, are floats. type_string(typeof(globals.save_data['emailreadlist'][0])) is "float", while the type of the parameter id in hasemail is an int. So the Array.has() method won't find it.
I don't know why that is, I don't know how the Godot type system works. I don't see you adding, say, 33.0 or something to the list, so I would have assume it's always an int. But I don't know. Maybe it comes from loading a save?
Going to the computer in your apartment and reading the newly received email makes it int, though.
If you change the if expression in hasemail() to
if save_data['emailreadlist'].has(id) or save_data['emailunreadlist'].has(id) or save_data['emailreadlist'].has(float(id)) or save_data['emailunreadlist'].has(float(id)):
(checking for both int and float, just to be on the save side) then it works, but that's a bit ugly. It's a workaround, I guess?
Omg I think I know.... In godot 2 i could save games very directly just by writing save_data to a file. In godot 4 I have to convert to JSON to save and then convert back when loading the game. The problem is what you guessed. During the saving process, the ints actually are turned into floats. I had to do a similar fix for the apartment decoration system. The fix was bad, I just manually changed everything to an int on load iirc, but it worked and i can totally do it here, too. Thank you for figuring this out!
Ah, okay, yeah, thinking about it, that makes sense, I guess. JSON itself doesn't really make any distinction between integers and floats, it just has a generic "number" data type. (Which is technically not even a float, but a real number of arbitrary precision.)
So when Godot loads a dictionary from a JSON, it can't really say if it originally was an integer or a float, so I guess it just makes it all floats as a compromise. It could do some semi-intelligent heuristics, like if it does have something behind the decimal point, make it a float, but then the question would be, what to do with Arrays. You could maybe check all values in the Array, but that all gets ugly fast.
A different solution would be to, maybe, have an additionally type dict in there as well, which maps names (or "paths", if you will) to explicit type, and handle that automatically on serialization/deserialization. But that of course makes it more complex as well, and cuts down on thethe easy editable nature of JSON. Or have each value be a dict with explict type information, but that too sacrifices the human-readability.
So I assume that's the compromise they made: numbers in save data will fold to float on save/load.