Fix butlerd RPC -32700: normalize integral JSON floats to ints

Godot JSON.parse decodes every JSON number as a float, so integer ids
(7670) round-trip through JSON.stringify as 7670.0, which Go's int64
fields reject with RPC error -32700. Normalize at the parse boundary so
profile/game/upload ids serialize correctly.

Also drop the nonexistent 'fresh' parameter from Fetch.ProfileOwnedKeys.
This commit is contained in:
Jose Falanga 2026-08-05 22:08:24 -03:00
parent 5d686a5b02
commit 408991a693
2 changed files with 33 additions and 7 deletions

View file

@ -184,7 +184,7 @@ func _thread_process(_delta: float) -> void:
var line: String = lines[i].strip_edges()
if line == "":
continue
var parsed: Variant = JSON.parse_string(line)
var parsed: Variant = _normalize_json(JSON.parse_string(line))
if typeof(parsed) != TYPE_DICTIONARY:
continue
var msg: Dictionary = parsed
@ -210,7 +210,7 @@ func _thread_process(_delta: float) -> void:
_recv_buffer = _recv_buffer.substr(idx + 1)
if line.strip_edges() == "":
continue
var parsed: Variant = JSON.parse_string(line)
var parsed: Variant = _normalize_json(JSON.parse_string(line))
if typeof(parsed) != TYPE_DICTIONARY:
continue
var msg: Dictionary = parsed
@ -261,6 +261,33 @@ func _send_line(text: String) -> void:
socket.put_data((text + "\n").to_utf8_buffer())
## Godot's JSON.parse decodes every JSON number as a float (so 7670 becomes
## 7670.0), and JSON.stringify then re-serializes it as "7670.0". butlerd's Go
## structs use int64 for ids, and Go's encoding/json rejects "7670.0" for an
## int64 field (RPC error -32700). Normalizing integral floats back to ints at
## the parse boundary keeps every id (profileId, game.id, upload.id, ...)
## typed correctly so downstream RPC params serialize without the trailing
## ".0".
func _normalize_json(value: Variant) -> Variant:
match typeof(value):
TYPE_DICTIONARY:
var out := {}
for key in value:
out[key] = _normalize_json(value[key])
return out
TYPE_ARRAY:
var out := []
for item in value:
out.append(_normalize_json(item))
return out
TYPE_FLOAT:
if is_finite(value) and value == floor(value):
return int(value)
return value
_:
return value
## Sends a JSON-RPC request and waits (via signal await, not a busy loop, so
## [method _thread_process] keeps ticking and can deliver the response) for
## the matching response. Mirrors the `_wait_for_command` pattern used by the
@ -306,16 +333,15 @@ func _login_with_api_key(api_key: String) -> void:
## Returns every game the logged-in profile owns a download key for.
## Each item looks like: {"downloadKey": {...}, "game": {...}}
func get_owned_games(fresh: bool = false) -> Array:
return await thread_group.exec(_get_owned_games.bind(fresh))
func get_owned_games() -> Array:
return await thread_group.exec(_get_owned_games)
func _get_owned_games(fresh: bool) -> Array:
func _get_owned_games() -> Array:
if not is_logged_in:
return []
var res := await _rpc_call("Fetch.ProfileOwnedKeys", {
"profileId": profile.get("user", {}).get("id", profile.get("id", 0)),
"fresh": fresh,
})
if "error" in res:
logger.warn("Fetch.ProfileOwnedKeys failed: " + str(res["error"]))

View file

@ -84,7 +84,7 @@ func _load_library(caching_flags: int = Cache.FLAGS.LOAD | Cache.FLAGS.SAVE) ->
return []
logger.info("Fetching itch.io library...")
var owned: Array = await itch.get_owned_games(false)
var owned: Array = await itch.get_owned_games()
var caves: Array = await itch.get_caves()
var caves_by_game_id := {}