Skip to content

fix(api): preserve scalar values through game.storage.set/player_set#46

Merged
Taure merged 1 commit into
mainfrom
fix/storage-accept-scalar-values
May 14, 2026
Merged

fix(api): preserve scalar values through game.storage.set/player_set#46
Taure merged 1 commit into
mainfrom
fix/storage-accept-scalar-values

Conversation

@Taure
Copy link
Copy Markdown
Contributor

@Taure Taure commented May 14, 2026

Summary

`game.storage.set` and `game.storage.player_set` silently coerced any non-map, non-string-keyed-proplist value into an empty map before persisting, because their wrapper applied `to_map/1`'s catch-all clause to the `value` arg. `game.storage.player_set(pid, "c", "k", 5)` stored `{}`, not `5`. Scripts had to wrap scalars as `{ n = 5 }` to avoid silent data loss.

Adds `to_storage_value/1` (exported) and swaps it in at the two storage call sites. It preserves JSON-compatible leaf types and recurses into containers:

Input Stored
number / binary / boolean / nil as-is
map recurse over values
string-keyed proplist (Luerl map) map (recurse)
integer-keyed proplist (Luerl array) list (recurse, ordered)
plain list (post-deep_decode) list (recurse)
empty list `#{}` — preserves prior behaviour
anything else `nil` (instead of silently `#{}`)

Specs for `storage_get/3` and `storage_set/4` widened from `map()` to `term()` to admit scalar round-trips.

Test plan

  • `rebar3 compile`
  • `rebar3 fmt --check`
  • `rebar3 xref`
  • `rebar3 dialyzer`
  • `~/bin/elp eqwalize asobi_lua_api` — same baseline (4 errors, same as main)
  • `rebar3 eunit` — 216/216 pass (was 209; +7 regression tests)
  • `rebar3 ct` — 8/8 pass
  • New eunit tests cover (a) the coercion table directly for scalars/maps/arrays/nested, and (b) a Lua-side round-trip that asserts the unwrapped value reaches the kura changeset via meck history

Caller cleanup (separate PR)

Found via barrow. With this PR in, the workaround in Taure/barrow (wrapping `{ n = N }` around stored integers) can be dropped — barrow's `run_loot`/`stash` lua then just `game.storage.player_set(pid, "barrow", key, value)`.

Note: per-player rows still need the `player_id` workaround in the storage key until widgrensit/asobi#122 ships.

`to_map/1` was applied to the `value` argument of `game.storage.set` and
`game.storage.player_set` before persisting, but its catch-all clause
silently coerced anything that wasn't a map or string-keyed proplist
into an empty map. As a result, `game.storage.player_set(pid, "c",
"k", 5)` stored `{}` rather than `5` — scripts could only store
map-shaped values, and the lossy coercion was undocumented.

Add `to_storage_value/1` (exported), used in place of `to_map/1` for
the two storage call sites. It preserves the JSON-compatible leaf
types and recurses into containers:

  number, binary, boolean, nil    -> kept as-is
  map                              -> recurse over values
  string-keyed proplist            -> map (recurse)
  integer-keyed proplist           -> list (recurse, ordered)
  plain list (post-deep_decode)    -> list (recurse)
  empty list                       -> #{} (preserves prior behaviour)
  anything else                    -> nil (instead of #{})

`storage_get/3` and `storage_set/4` spec returns widen from `map()` to
`term()` to admit scalar round-trips.

Eunit covers the coercion table directly (`to_storage_value_scalars/0`
et al.) plus a Lua-side round-trip that asserts the unwrapped value
reaches the kura changeset via meck history (`storage_player_set_scalar/0`
et al.).
@Taure Taure merged commit ea73237 into main May 14, 2026
13 checks passed
@Taure Taure deleted the fix/storage-accept-scalar-values branch May 14, 2026 13:01
Taure added a commit to Taure/kura that referenced this pull request May 14, 2026
* fix(types): accept scalar values in jsonb cast/dump/load

`cast(jsonb, ...)` only matched maps, lists, and binaries — any other
JSON-compatible scalar (integer, float, boolean) was rejected as
`cannot cast to jsonb`. That was inherited from a time when jsonb was
treated as "structured", but postgres accepts any valid JSON at the
root of a jsonb column, including scalars.

Add the missing clauses to `cast/2`, `dump/2`, and `load/2` so numbers
and booleans round-trip. `null` continues to land in the generic
`cast(_, null) -> {ok, undefined}` and `load(_, null) -> {ok, undefined}`
clauses higher up, keeping the kura convention of materialising SQL
NULLs as the `undefined` atom.

Found via barrow's bank-on-extract loop: scripts wanted
`game.storage.player_set(pid, "barrow", "run_loot", 5)` but had to
wrap as `{ n = 5 }` because the changeset cast rejected the scalar.
Pairs with the asobi_lua-side coercion fix in widgrensit/asobi_lua#46.

* chore: remove stray elvis.config from previous commit

elvis.config was an untracked file in the worktree from an unrelated
experiment that got swept into the jsonb commit via `git add -A`.
It's not part of this PR's scope.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant