Skip to content

feat(settings): add versioned player settings api with adventure keys - #181

Merged
TheMeinerLP merged 10 commits into
mainfrom
feat/player-settings
Oct 7, 2026
Merged

TheMeinerLP merged 10 commits into
mainfrom
feat/player-settings

Conversation

@TheMeinerLP

Copy link
Copy Markdown
Contributor

Summary

Per-player settings in Otis, keyed by Adventure Key. Stacked on #180 (Problem Details). Merge #180 first; this PR then retargets to main.

  • Model: a setting is identified by Mojang playerUuid plus an Adventure key namespace:value. The value is arbitrary JSON (max 64 KiB) that Otis stores without interpreting it. olf: is the conventional namespace for generic settings, and games use their own (lobby:, ...). Keys without a namespace, in minecraft: or with an empty value are rejected.
  • API (/v1, new, versioned by URI): GET /v1/players/{playerUuid}/settings[?namespace=...], and GET/PUT/DELETE /v1/players/{playerUuid}/settings/{key}.
  • Semantics: last-write-wins, with version and updatedAt in every response.
    • An idempotent PUT (semantically equal JSON) is a no-op: status 200, no version bump.
    • PUT answers 201 when it creates the setting.
    • DELETE always answers 204.
    • A concurrent first write ends up as one row: a unique violation is retried once.
  • Errors: Problem Details. Settings-specific types are missing-namespace, reserved-namespace, invalid-setting-key, invalid-setting-value, setting-value-too-large (413), player-not-found and setting-not-found.
  • Layers: controller, a new service layer, DTOs, repository and entity.
  • Traces: one INTERNAL span per use case (settings.get|list|put|delete) with player uuid, namespace, key and outcome. The value never appears in a span or log.
  • Client: settings API with net.kyori.adventure.key.Key in its public API. adventure-key 5.2.0 (aligned with velocity-api 4.2.0) is compileOnly. OtisClients.newApiClient() registers the Jackson AdventureKeyModule.

Migration (no data loss)

Flyway (micronaut-flyway) is introduced with baseline-on-migrate, baseline version 1. There are vendor-specific scripts for PostgreSQL, MariaDB and H2.

  • Existing DB (prod): Flyway finds no history, baselines at V1, and applies only V2 = CREATE TABLE player_setting (FK to otis_player(uuid) ON DELETE CASCADE). No existing table is altered or written.
  • Fresh DB: V1 creates otis_player exactly as Hibernate generates it (verified per dialect), then V2 runs.
  • Evidence:
    • ExistingDatabaseMigrationTest: 3 pre-existing rows are byte-identical after the migration, and V1 is BASELINE.
    • FreshDatabaseMigrationTest: Hibernate validate passes on the migrated schema.
    • Manual runs against throwaway MariaDB 11.8 and PostgreSQL 17 containers, the latter with hbm2ddl=none as in prod: migration plus a settings smoke test, including cascade delete.
  • Deploy prerequisite: the prod DB user needs CREATE privilege (for flyway_schema_history and player_setting). FLYWAY_ENABLED=false disables Flyway as an escape hatch.
  • Rollback: deploy the previous image, which ignores the new tables. Optionally DROP TABLE player_setting. Player data is untouched in every step.
  • Prod table name check (not verified against prod): V2 assumes the prod table is otis_player with a uuid PK column, which is what Micronaut's naming strategy produces. Please check this before deploying.
  • Local MariaDB older than 10.7: a database with binary(16) UUID columns would not match the V2 FK. Use FLYWAY_ENABLED=false or recreate the local DB.

Compatibility (old plugins)

  • /otis/** and /search/** are unchanged. ExistingEndpointsCompatibilityTest stays green. The only new effect: deleting a player also deletes their settings (cascade), with the same response as before.
  • javap -public of the generated client: 0 removed or changed lines, 91 lines added.
  • velocity-plugin builds without source changes, and its shadow JAR contains no net/kyori classes.

Deviations

Recorded in openspec/changes/add-player-settings/design.md under "Implementation Notes":

  • Flyway has no {vendor} placeholder, so a small locations customizer replaces it.
  • There is no TypeConverter for keys in the path, because it would lose the specific problem types.
  • There is no declarative transaction on the upsert (two @Primary transaction managers). Concurrent updates are last-write-wins and may skip a version increment.
  • The DTO value is a plain Java tree, because Serde cannot serialize JsonNode.

Test plan

  • ./gradlew clean build (stacked branch): 173 tests, 0 failures, 1 skipped (pre-existing disabled OtisTest)
  • Migration tests on fresh and existing DB; manual MariaDB 11.8 and PostgreSQL 17 runs
  • Concurrency test for the first write (virtual threads, no sleeps)
  • Span tests with OpenTelemetryExtension (name, kind, parent, attributes, value never present)
  • Client javap diff contains additions only; shadow JAR free of Adventure classes

https://claude.ai/code/session_012BmpdMdQ5wwagagQvfcq8S

Baseline V1 reproduces the Hibernate schema of otis_player, V2 adds
player_setting additively. Existing databases are baselined at version 1.

Claude-Session: https://claude.ai/code/session_012BmpdMdQ5wwagagQvfcq8S
@TheMeinerLP
TheMeinerLP force-pushed the feat/player-settings branch from 23ad6df to 4cfe5e2 Compare October 7, 2026 10:55
@TheMeinerLP
TheMeinerLP changed the base branch from feat/problem-details to main October 7, 2026 10:55
@TheMeinerLP
TheMeinerLP merged commit 4008e38 into main Oct 7, 2026
7 checks passed
@TheMeinerLP
TheMeinerLP deleted the feat/player-settings branch October 7, 2026 10:59
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