Files
modbison/scripts/mp_game_flow_client.gd
T
daddy 7bd8124199 PC3 Phase 3.2: real age verification (date of birth, not a checkbox)
POST /api/auth/register now requires a real date_of_birth (ISO YYYY-MM-DD).
The server computes age (auth.calculate_age) and rejects under-18s with 403;
a future/nonsensical date is 400. The DOB is stored and surfaced only to
that user (/api/auth/me).

Still self-attested (a user can enter a false DOB) -- but a concrete,
stored, legally-meaningful data point and the seam for a future ID-check
pass. Encryption-at-rest for the column and the Tier-2 hook are not done;
the limitation is stated plainly in docs/architecture/content_moderation.md 5.2.

- auth.py: RegisterRequest.date_of_birth (date); MIN_AGE=18; register()
  computes + enforces; UserResponse.date_of_birth typed `date | None`
  (the old `str | None` would have 500'd /me once a DOB was actually stored).
- api_client.gd / main_menu.gd: the Over18 checkbox is now a
  "Date of birth (YYYY-MM-DD)" field with a client-side sanity check.
- tests: test_auth_age_gate.py rewritten around the endpoint + age math
  (adult/minor/exactly-18/future-dob/stored); ~25 over_18 references across
  6 backend test files + 6 game smoke tests updated. Backend 74 passed;
  game smokes green (auth 6/6, menu_full_flow 12/12, e2e 4/4, etc.);
  fresh-clone boot PASS.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015jtxAiu5dAW4WxCNdGkrkJ
2026-09-05 22:18:38 +00:00

78 lines
2.6 KiB
GDScript

extends SceneTree
## One client process for the full networked game-flow test
## (scripts/run_game_multiplayer_test.sh): register -> POST /api/worlds/{id}/join
## -> Session.begin -> load scenes/game.tscn -> run networked.
##
## godot --headless --path . --script res://scripts/mp_game_flow_client.gd -- \
## --role a|b --world 1
##
## role b: joins, holds still, prints RESULT b in_world and stays ~40 s.
## role a: joins, moves forward, prints RESULT a remotes=<n> moved=<bool>;
## exits 0 iff it saw a remote player node AND moved.
var role := "b"
var world_id := 1
func _initialize() -> void:
var args := OS.get_cmdline_user_args()
for i in args.size():
if args[i] == "--role" and i + 1 < args.size():
role = args[i + 1]
elif args[i] == "--world" and i + 1 < args.size():
world_id = int(args[i + 1])
_run()
func _run() -> void:
await process_frame
var api := root.get_node("ApiClient")
var session := root.get_node("Session")
var uname := "mpflow_%s_%d" % [role, int(Time.get_unix_time_from_system() * 1000) % 1000000]
var reg: Dictionary = await api.register(uname, uname + "@e.com", "secret123", "1990-01-01")
if not reg.ok:
print("RESULT %s register_failed %s" % [role, reg.detail])
quit(1); return
var join: Dictionary = await api.join_world(world_id)
if not (join.ok and join.data is Dictionary):
print("RESULT %s join_failed status=%d %s" % [role, join.status, join.detail])
quit(1); return
var d: Dictionary = join.data
session.begin(world_id, str(d["host"]), int(d["port"]))
change_scene_to_file("res://scenes/game.tscn")
# Wait for the scene + NetworkClient connect (game.gd awaits internally).
for i in 180:
await process_frame
var game := current_scene
var netc = game.get_node_or_null("NetworkClient") if game else null
if netc == null or not netc.is_connected_to_server():
print("RESULT %s not_connected" % role)
quit(1); return
if role == "b":
print("RESULT b in_world")
for i in 1200:
await process_frame
quit(0); return
# role a: move and look for a remote node.
var player = game.get_node("Player")
var start: Vector3 = player.global_position
var max_remotes := 0
for i in 120:
# Drive the player forward by faking held input.
player._input_direction = Vector2(0, 1)
await process_frame
var n := 0
for c in game.get_children():
if c.name.begins_with("Remote_"):
n += 1
max_remotes = maxi(max_remotes, n)
var moved: bool = player.global_position.distance_to(start) > 1.0
print("RESULT a remotes=%d moved=%s" % [max_remotes, str(moved)])
quit(0 if (moved and max_remotes >= 1) else 1)