ravensight-playtest 0.5.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +319 -14
  2. package/addons/ravensight_driver/driver.gd +406 -8
  3. package/addons/ravensight_simulate/simulate.gd +360 -0
  4. package/docs/godot-driver.md +70 -8
  5. package/package.json +4 -3
  6. package/src/api/README.md +4 -1
  7. package/src/api/client.js +120 -3
  8. package/src/api/errors.js +17 -1
  9. package/src/api/http.js +43 -17
  10. package/src/assets/formats.js +267 -0
  11. package/src/assets/index.js +260 -0
  12. package/src/assets/integration.js +429 -0
  13. package/src/assets/inventory.js +337 -0
  14. package/src/assets/measure.js +392 -0
  15. package/src/cli.js +233 -0
  16. package/src/commands/assets.js +62 -0
  17. package/src/commands/check.js +4 -1
  18. package/src/commands/create.js +734 -0
  19. package/src/commands/fakeCore.js +16 -1
  20. package/src/commands/feature.js +845 -0
  21. package/src/commands/fix.js +647 -70
  22. package/src/commands/init.js +19 -3
  23. package/src/commands/lists.js +206 -0
  24. package/src/commands/profile.js +25 -6
  25. package/src/commands/punch.js +116 -0
  26. package/src/commands/replay.js +204 -0
  27. package/src/commands/resume.js +7 -0
  28. package/src/commands/run.js +200 -24
  29. package/src/commands/tape.js +74 -0
  30. package/src/commands/template.js +70 -0
  31. package/src/commands/validate.js +414 -0
  32. package/src/commands/validateHosted.js +292 -0
  33. package/src/create/scaffold.js +192 -0
  34. package/src/run/acceptance.js +212 -0
  35. package/src/run/args.js +18 -1
  36. package/src/run/checks.js +705 -0
  37. package/src/run/dimension.js +95 -0
  38. package/src/run/drivers/godot-observation.js +59 -11
  39. package/src/run/drivers/godot-project.js +201 -31
  40. package/src/run/drivers/godot.js +532 -45
  41. package/src/run/drivers/web.js +18 -0
  42. package/src/run/evidence.js +170 -0
  43. package/src/run/featureLoop.js +288 -0
  44. package/src/run/fixLoop.js +82 -12
  45. package/src/run/git.js +110 -20
  46. package/src/run/json.js +65 -9
  47. package/src/run/labsOutcome.js +136 -0
  48. package/src/run/liveWork.js +103 -0
  49. package/src/run/model.js +105 -5
  50. package/src/run/openPr.js +107 -0
  51. package/src/run/paths.js +4 -1
  52. package/src/run/pendingRegister.js +81 -0
  53. package/src/run/personaLoop.js +111 -4
  54. package/src/run/profile.js +68 -7
  55. package/src/run/replay.js +236 -0
  56. package/src/run/repoTools.js +227 -27
  57. package/src/run/report.js +7 -1
  58. package/src/run/resume.js +19 -1
  59. package/src/run/secretScan.js +35 -0
  60. package/src/run/settle.js +41 -0
  61. package/src/run/tools.js +33 -4
  62. package/src/run/trace.js +124 -0
  63. package/src/simulation/godotPack.js +112 -0
  64. package/src/ui/index.js +61 -9
  65. package/src/upload/allowlist.js +6 -2
  66. package/src/upload/index.js +5 -4
  67. package/templates/godot-2d/README.md +36 -0
  68. package/templates/godot-2d/addons/ravensight/LICENSE +21 -0
  69. package/templates/godot-2d/addons/ravensight/Ravensight.gd +1823 -0
  70. package/templates/godot-2d/export_presets.cfg +27 -0
  71. package/templates/godot-2d/gitignore +10 -0
  72. package/templates/godot-2d/project.godot +58 -0
  73. package/templates/godot-2d/scenes/main.tscn +67 -0
  74. package/templates/godot-2d/scripts/autoload/game_config.gd +83 -0
  75. package/templates/godot-2d/scripts/autoload/game_state.gd +44 -0
  76. package/templates/godot-2d/scripts/autoload/telemetry.gd +37 -0
  77. package/templates/godot-2d/scripts/main.gd +32 -0
  78. package/templates/godot-2d/scripts/player.gd +10 -0
  79. package/templates/godot-2d/scripts/state_hook.gd +16 -0
  80. package/templates/godot-2d/template.json +14 -0
  81. package/templates/godot-2d/tools/inject_release_key.sh +19 -0
  82. package/templates/godot-2d/tools/selftest.gd +70 -0
  83. package/templates/godot-3d/README.md +49 -0
  84. package/templates/godot-3d/addons/ravensight/LICENSE +21 -0
  85. package/templates/godot-3d/addons/ravensight/Ravensight.gd +1823 -0
  86. package/templates/godot-3d/export_presets.cfg +27 -0
  87. package/templates/godot-3d/gitignore +10 -0
  88. package/templates/godot-3d/project.godot +56 -0
  89. package/templates/godot-3d/scenes/main.tscn +154 -0
  90. package/templates/godot-3d/scripts/autoload/game_config.gd +83 -0
  91. package/templates/godot-3d/scripts/autoload/game_state.gd +44 -0
  92. package/templates/godot-3d/scripts/autoload/telemetry.gd +37 -0
  93. package/templates/godot-3d/scripts/beacon.gd +20 -0
  94. package/templates/godot-3d/scripts/main.gd +49 -0
  95. package/templates/godot-3d/scripts/player.gd +19 -0
  96. package/templates/godot-3d/scripts/state_hook.gd +25 -0
  97. package/templates/godot-3d/template.json +16 -0
  98. package/templates/godot-3d/tools/inject_release_key.sh +19 -0
  99. package/templates/godot-3d/tools/selftest.gd +77 -0
@@ -0,0 +1,27 @@
1
+ [preset.0]
2
+
3
+ name="Web"
4
+ platform="Web"
5
+ runnable=true
6
+ export_filter="all_resources"
7
+ include_filter="ravensight_key.release.cfg"
8
+ exclude_filter="ravensight_key.cfg,tools/*,DESIGN.md"
9
+ export_path="build/web/index.html"
10
+
11
+ [preset.0.options]
12
+
13
+ html/export_icon=true
14
+
15
+ [preset.1]
16
+
17
+ name="Desktop"
18
+ platform="Linux"
19
+ runnable=false
20
+ export_filter="all_resources"
21
+ include_filter="ravensight_key.release.cfg"
22
+ exclude_filter="ravensight_key.cfg,tools/*,DESIGN.md"
23
+ export_path="build/desktop/game.x86_64"
24
+
25
+ [preset.1.options]
26
+
27
+ binary_format/embed_pck=true
@@ -0,0 +1,10 @@
1
+ .godot/
2
+ .ravensight/brief.local.json
3
+ .ravensight/jobs/
4
+ .ravensight/create.json
5
+ .DS_Store
6
+ /build/
7
+ export_credentials.cfg
8
+ # The development key stays on this machine, and the release key is written at build time.
9
+ ravensight_key.cfg
10
+ ravensight_key.release.cfg
@@ -0,0 +1,58 @@
1
+ ; Engine configuration file.
2
+ config_version=5
3
+
4
+ [application]
5
+
6
+ config/name="__GAME_NAME__"
7
+ config/description="A placeholder prototype made with ravensight-playtest create."
8
+ config/version="0.1.0"
9
+ run/main_scene="res://scenes/main.tscn"
10
+ config/features=PackedStringArray("4.3", "GL Compatibility")
11
+ boot_splash/show_image=false
12
+
13
+ [autoload]
14
+
15
+ GameConfig="*res://scripts/autoload/game_config.gd"
16
+ Ravensight="*res://addons/ravensight/Ravensight.gd"
17
+ Telemetry="*res://scripts/autoload/telemetry.gd"
18
+ GameState="*res://scripts/autoload/game_state.gd"
19
+
20
+ [display]
21
+
22
+ window/size/viewport_width=640
23
+ window/size/viewport_height=384
24
+ window/size/window_width_override=1280
25
+ window/size/window_height_override=768
26
+ window/stretch/mode="canvas_items"
27
+ window/stretch/aspect="keep"
28
+
29
+ [input]
30
+
31
+ move_left={
32
+ "deadzone": 0.25,
33
+ "events": [Object(InputEventKey,"physical_keycode":65), Object(InputEventKey,"physical_keycode":4194319), Object(InputEventJoypadMotion,"device":-1,"axis":0,"axis_value":-1.0)]
34
+ }
35
+ move_right={
36
+ "deadzone": 0.25,
37
+ "events": [Object(InputEventKey,"physical_keycode":68), Object(InputEventKey,"physical_keycode":4194321), Object(InputEventJoypadMotion,"device":-1,"axis":0,"axis_value":1.0)]
38
+ }
39
+ move_up={
40
+ "deadzone": 0.25,
41
+ "events": [Object(InputEventKey,"physical_keycode":87), Object(InputEventKey,"physical_keycode":4194320), Object(InputEventJoypadMotion,"device":-1,"axis":1,"axis_value":-1.0)]
42
+ }
43
+ move_down={
44
+ "deadzone": 0.25,
45
+ "events": [Object(InputEventKey,"physical_keycode":83), Object(InputEventKey,"physical_keycode":4194322), Object(InputEventJoypadMotion,"device":-1,"axis":1,"axis_value":1.0)]
46
+ }
47
+ action={
48
+ "deadzone": 0.5,
49
+ "events": [Object(InputEventKey,"physical_keycode":32), Object(InputEventKey,"physical_keycode":69), Object(InputEventJoypadButton,"device":-1,"button_index":0)]
50
+ }
51
+ pause={
52
+ "deadzone": 0.5,
53
+ "events": [Object(InputEventKey,"physical_keycode":4194305), Object(InputEventJoypadButton,"device":-1,"button_index":6)]
54
+ }
55
+
56
+ [rendering]
57
+
58
+ renderer/rendering_method="gl_compatibility"
@@ -0,0 +1,67 @@
1
+ [gd_scene load_steps=4 format=3]
2
+
3
+ [ext_resource type="Script" path="res://scripts/main.gd" id="1"]
4
+ [ext_resource type="Script" path="res://scripts/player.gd" id="2"]
5
+ [ext_resource type="Script" path="res://scripts/state_hook.gd" id="3"]
6
+
7
+ [node name="Main" type="Node2D"]
8
+ script = ExtResource("1")
9
+
10
+ [node name="Ground" type="ColorRect" parent="."]
11
+ offset_right = 640.0
12
+ offset_bottom = 384.0
13
+ color = Color(0.1, 0.12, 0.16, 1)
14
+
15
+ [node name="Player" type="Node2D" parent="."]
16
+ position = Vector2(320, 192)
17
+ script = ExtResource("2")
18
+
19
+ [node name="Shape" type="Polygon2D" parent="Player"]
20
+ color = Color(0.95, 0.75, 0.3, 1)
21
+ polygon = PackedVector2Array(-10, -10, 10, -10, 10, 10, -10, 10)
22
+
23
+ [node name="StateHook" type="Node" parent="."]
24
+ script = ExtResource("3")
25
+ player_path = NodePath("../Player")
26
+
27
+ [node name="Interface" type="CanvasLayer" parent="."]
28
+
29
+ [node name="Hud" type="Label" parent="Interface"]
30
+ offset_left = 8.0
31
+ offset_top = 6.0
32
+ offset_right = 632.0
33
+ offset_bottom = 30.0
34
+ text = ""
35
+
36
+ [node name="Settings" type="Control" parent="Interface"]
37
+ layout_mode = 3
38
+ anchors_preset = 15
39
+ anchor_right = 1.0
40
+ anchor_bottom = 1.0
41
+
42
+ [node name="Panel" type="PanelContainer" parent="Interface/Settings"]
43
+ layout_mode = 1
44
+ anchors_preset = 8
45
+ anchor_left = 0.5
46
+ anchor_top = 0.5
47
+ anchor_right = 0.5
48
+ anchor_bottom = 0.5
49
+ offset_left = -160.0
50
+ offset_top = -60.0
51
+ offset_right = 160.0
52
+ offset_bottom = 60.0
53
+
54
+ [node name="Rows" type="VBoxContainer" parent="Interface/Settings/Panel"]
55
+ layout_mode = 2
56
+
57
+ [node name="Title" type="Label" parent="Interface/Settings/Panel/Rows"]
58
+ layout_mode = 2
59
+ text = "Settings"
60
+
61
+ [node name="Analytics" type="CheckBox" parent="Interface/Settings/Panel/Rows"]
62
+ layout_mode = 2
63
+ text = "Share anonymous analytics"
64
+
65
+ [node name="Hint" type="Label" parent="Interface/Settings/Panel/Rows"]
66
+ layout_mode = 2
67
+ text = "Esc to go back"
@@ -0,0 +1,83 @@
1
+ extends Node
2
+ ## Runs before the Ravensight autoload and hands it this build's key, so the key is
3
+ ## never typed into a script or committed.
4
+ ##
5
+ ## Where the key comes from, first match wins:
6
+ ## 1. --ravensight-key-file=<path> after -- on the command line
7
+ ## 2. the RAVENSIGHT_INGEST_KEY environment variable
8
+ ## 3. res://ravensight_key.release.cfg, in an exported build only
9
+ ## 4. res://ravensight_key.cfg, the development key on this machine
10
+ ## --no-key skips all four. Both files are ignored by git. With no key the SDK is
11
+ ## inert and the game plays exactly the same.
12
+
13
+ const GAME_VERSION := "0.1.0"
14
+ const DEVELOPMENT_KEY := "res://ravensight_key.cfg"
15
+ const RELEASE_KEY := "res://ravensight_key.release.cfg"
16
+
17
+ ## Which of the four answered: "argument", "environment", "release", "development" or "none".
18
+ var key_source := "none"
19
+
20
+ func _ready() -> void:
21
+ var found := find_key(_user_args(), OS.has_feature("editor"))
22
+ key_source = String(found["source"])
23
+ if not String(found["key"]).is_empty():
24
+ Ravensight.ingest_key = String(found["key"])
25
+ if not String(found["api_url"]).is_empty():
26
+ Ravensight.api_url = String(found["api_url"])
27
+ Ravensight.optional = true
28
+ Ravensight.game_version = GAME_VERSION
29
+ Ravensight.distribution = _build_label()
30
+
31
+ ## The key this run should use, and what it came from. No side effects, so a test
32
+ ## can call it.
33
+ func find_key(args: Dictionary, in_editor: bool) -> Dictionary:
34
+ var none := {"key": "", "api_url": "", "source": "none"}
35
+ if args.has("no-key"):
36
+ return none
37
+ if args.has("ravensight-key-file"):
38
+ var from_file := _read_key(String(args["ravensight-key-file"]))
39
+ if not String(from_file["key"]).is_empty():
40
+ from_file["source"] = "argument"
41
+ return from_file
42
+ var from_environment := OS.get_environment("RAVENSIGHT_INGEST_KEY").strip_edges()
43
+ if not from_environment.is_empty():
44
+ return {"key": from_environment, "api_url": OS.get_environment("RAVENSIGHT_API_URL").strip_edges(), "source": "environment"}
45
+ if not in_editor:
46
+ var release := _read_key(RELEASE_KEY)
47
+ if not String(release["key"]).is_empty():
48
+ release["source"] = "release"
49
+ return release
50
+ var development := _read_key(DEVELOPMENT_KEY)
51
+ if not String(development["key"]).is_empty():
52
+ development["source"] = "development"
53
+ return development
54
+ return none
55
+
56
+ func _read_key(path: String) -> Dictionary:
57
+ var answer := {"key": "", "api_url": "", "source": "none"}
58
+ if not FileAccess.file_exists(path):
59
+ return answer
60
+ var file := ConfigFile.new()
61
+ if file.load(path) != OK:
62
+ return answer
63
+ answer["key"] = String(file.get_value("ravensight", "ingest_key", "")).strip_edges()
64
+ answer["api_url"] = String(file.get_value("ravensight", "api_url", "")).strip_edges()
65
+ return answer
66
+
67
+ func _user_args() -> Dictionary:
68
+ var out := {}
69
+ for raw in OS.get_cmdline_user_args():
70
+ var text := String(raw).trim_prefix("--")
71
+ var split := text.find("=")
72
+ if split >= 0:
73
+ out[text.substr(0, split)] = text.substr(split + 1)
74
+ else:
75
+ out[text] = "true"
76
+ return out
77
+
78
+ func _build_label() -> String:
79
+ if OS.has_feature("web"):
80
+ return "web"
81
+ if OS.has_feature("editor"):
82
+ return "editor"
83
+ return "native-" + OS.get_name().to_lower()
@@ -0,0 +1,44 @@
1
+ extends Node
2
+ ## What the game is, in numbers. Nothing here draws anything. A persona reads this
3
+ ## through scripts/state_hook.gd, and the playable check reads core_loops_completed.
4
+
5
+ ## A milestone calls complete_core_loop() once for each pass of the game's core loop.
6
+ signal core_loop_completed(total: int)
7
+
8
+ var score := 0
9
+ var level := 1
10
+ var deaths := 0
11
+ var core_loops_completed := 0
12
+ var started := false
13
+
14
+ ## The game started playing. Reported once a run.
15
+ func start_game() -> void:
16
+ if started:
17
+ return
18
+ started = true
19
+ Telemetry.track("game_play_started", {"level": "level_%d" % level}, "level_start")
20
+
21
+ ## One whole pass of the core loop, which is what "playable" means here.
22
+ func complete_core_loop() -> void:
23
+ core_loops_completed += 1
24
+ Telemetry.track("core_loop_completed", {"level": "level_%d" % level, "total": core_loops_completed}, "level_complete")
25
+ # The moment worth not losing if the player closes the game now.
26
+ Ravensight.flush()
27
+ core_loop_completed.emit(core_loops_completed)
28
+
29
+ func add_score(amount: int) -> void:
30
+ score += amount
31
+
32
+ func record_death() -> void:
33
+ deaths += 1
34
+ Telemetry.track("player_died", {"level": "level_%d" % level, "deaths": deaths}, "death")
35
+
36
+ ## Plain values only, so it can cross to a playtest as JSON.
37
+ func snapshot() -> Dictionary:
38
+ return {
39
+ "started": started,
40
+ "score": score,
41
+ "level": level,
42
+ "deaths": deaths,
43
+ "core_loops_completed": core_loops_completed,
44
+ }
@@ -0,0 +1,37 @@
1
+ extends Node
2
+ ## Every gameplay event goes through track(), which always says what the event
3
+ ## means. Ravensight reads a level start, a level completion or a death from the
4
+ ## reserved rs_kind property first, and only guesses from the event's name when it
5
+ ## is missing. A natural name such as "room_restarted" or "run_finished" would
6
+ ## otherwise be read as something it is not, so nothing here leaves that to chance.
7
+
8
+ const KINDS := ["level_start", "level_complete", "death", "none"]
9
+
10
+ ## The last event this run asked to send, name and data, for a test to read. Set
11
+ ## whether or not the SDK has a key.
12
+ var last_event := {}
13
+
14
+ ## Queue an event. `kind` is one of KINDS. Anything else is sent as "none", with a
15
+ ## warning, because a wrong kind would move the game's funnel.
16
+ func track(event_name: String, data: Dictionary = {}, kind: String = "none") -> void:
17
+ var prepared := prepare(data, kind)
18
+ last_event = {"name": event_name, "data": prepared}
19
+ Ravensight.track_event(event_name, prepared)
20
+
21
+ ## The event's data as it will be sent.
22
+ func prepare(data: Dictionary, kind: String) -> Dictionary:
23
+ var out := data.duplicate()
24
+ if KINDS.has(kind):
25
+ out["rs_kind"] = kind
26
+ else:
27
+ push_warning("Telemetry: unknown kind %s, sent as none" % kind)
28
+ out["rs_kind"] = "none"
29
+ return out
30
+
31
+ ## The player's own choice: the settings overlay calls this. The SDK saves it and
32
+ ## it holds on every later run. False stops all analytics, including what is queued.
33
+ func set_analytics_enabled(value: bool) -> void:
34
+ Ravensight.set_tracking_enabled(value)
35
+
36
+ func analytics_enabled() -> bool:
37
+ return not FileAccess.file_exists(Ravensight.OPTOUT_PATH)
@@ -0,0 +1,32 @@
1
+ extends Node2D
2
+ ## The one scene the scaffold starts on. Milestones build the game here.
3
+
4
+ @onready var hud: Label = $Interface/Hud
5
+ @onready var settings: Control = $Interface/Settings
6
+ @onready var analytics: CheckBox = $Interface/Settings/Panel/Rows/Analytics
7
+
8
+ func _ready() -> void:
9
+ process_mode = Node.PROCESS_MODE_ALWAYS
10
+ $Player.process_mode = Node.PROCESS_MODE_PAUSABLE
11
+ settings.visible = false
12
+ analytics.button_pressed = Telemetry.analytics_enabled()
13
+ analytics.toggled.connect(Telemetry.set_analytics_enabled)
14
+ GameState.start_game()
15
+ _refresh()
16
+
17
+ func _unhandled_input(event: InputEvent) -> void:
18
+ if event.is_action_pressed("pause"):
19
+ _toggle_settings()
20
+ get_viewport().set_input_as_handled()
21
+
22
+ func _process(_delta: float) -> void:
23
+ _refresh()
24
+
25
+ func _toggle_settings() -> void:
26
+ settings.visible = not settings.visible
27
+ get_tree().paused = settings.visible
28
+ if settings.visible:
29
+ analytics.grab_focus()
30
+
31
+ func _refresh() -> void:
32
+ hud.text = "Score %d Loops %d Esc for settings" % [GameState.score, GameState.core_loops_completed]
@@ -0,0 +1,10 @@
1
+ extends Node2D
2
+ ## A placeholder player: a shape that moves. Swap the Polygon2D for your own art.
3
+
4
+ const SPEED := 180.0
5
+
6
+ var bounds := Rect2(16.0, 16.0, 608.0, 352.0)
7
+
8
+ func _physics_process(delta: float) -> void:
9
+ var direction := Input.get_vector("move_left", "move_right", "move_up", "move_down")
10
+ position = (position + direction * SPEED * delta).clamp(bounds.position, bounds.end)
@@ -0,0 +1,16 @@
1
+ extends Node
2
+ ## Lets a playtest read the game. The driver calls _ravensight_state() on every node
3
+ ## in the ravensight_state group. Add what a player can see to this dictionary as the
4
+ ## game grows: a persona reads it, and a replay compares it.
5
+
6
+ @export var player_path: NodePath
7
+
8
+ func _ready() -> void:
9
+ add_to_group("ravensight_state")
10
+
11
+ func _ravensight_state() -> Dictionary:
12
+ var state := GameState.snapshot()
13
+ var player := get_node_or_null(player_path) as Node2D
14
+ if player != null:
15
+ state["player_position"] = [round(player.position.x), round(player.position.y)]
16
+ return state
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "godot-2d",
3
+ "version": "1.0.0",
4
+ "engine": "godot",
5
+ "min_engine": "4.3",
6
+ "sdk": {
7
+ "version": "0.3.1",
8
+ "source": "https://github.com/Reality-Software-Entertainment/ravensight-godot",
9
+ "tag": "v0.3.1",
10
+ "commit": "997d53edfd40641e88f40fd712df711dbb116f78",
11
+ "path": "addons/ravensight/Ravensight.gd",
12
+ "sha256": "e7a5dc6f5f0edb62c3265867da2e8da74f5ff3ed2145a8b162d77f2dbb69733e"
13
+ }
14
+ }
@@ -0,0 +1,19 @@
1
+ #!/bin/sh
2
+ # Writes the public release key into the project just before an export, and removes
3
+ # it after, so it is in the exported build and never in git:
4
+ #
5
+ # RAVENSIGHT_RELEASE_KEY=gt_pub_... sh tools/inject_release_key.sh godot --headless --export-release Web build/index.html
6
+ #
7
+ # Everything after the script's name is the export command. The development key is
8
+ # never exported: export_presets.cfg leaves it out by name.
9
+ set -eu
10
+ : "${RAVENSIGHT_RELEASE_KEY:?Set RAVENSIGHT_RELEASE_KEY to this game public key, the one that starts gt_pub_.}"
11
+ case "$RAVENSIGHT_RELEASE_KEY" in
12
+ gt_pub_*) ;;
13
+ *) echo "A build anyone can download needs a public key, the one that starts gt_pub_." >&2; exit 2 ;;
14
+ esac
15
+ file="ravensight_key.release.cfg"
16
+ trap 'rm -f "$file"' EXIT
17
+ umask 077
18
+ printf '[ravensight]\ningest_key="%s"\n' "$RAVENSIGHT_RELEASE_KEY" > "$file"
19
+ "$@"
@@ -0,0 +1,70 @@
1
+ extends SceneTree
2
+ ## Proves the parts of this project a game depends on, headless, with no key:
3
+ ## godot --headless --path . --script tools/selftest.gd
4
+ ## Prints one line per check and exits non-zero if any failed.
5
+
6
+ var failed := 0
7
+
8
+ func _initialize() -> void:
9
+ # Autoloads and the scene have to be ready first.
10
+ process_frame.connect(_run, CONNECT_ONE_SHOT)
11
+
12
+ func _check(name: String, passed: bool) -> void:
13
+ print("%s %s" % ["ok " if passed else "FAIL", name])
14
+ if not passed:
15
+ failed += 1
16
+
17
+ func _run() -> void:
18
+ var game_config: Node = root.get_node("GameConfig")
19
+ var telemetry: Node = root.get_node("Telemetry")
20
+ var game_state: Node = root.get_node("GameState")
21
+ var sdk: Node = root.get_node("Ravensight")
22
+
23
+ # Every kind reaches the event as rs_kind, and a bad one reads as none.
24
+ for kind in ["level_start", "level_complete", "death", "none"]:
25
+ _check("rs_kind %s" % kind, telemetry.prepare({}, kind)["rs_kind"] == kind)
26
+ _check("an unknown kind is sent as none", telemetry.prepare({}, "nonsense")["rs_kind"] == "none")
27
+ var original := {"a": 1}
28
+ telemetry.prepare(original, "none")
29
+ _check("prepare leaves the caller's data alone", not original.has("rs_kind"))
30
+
31
+ # The key loader's order, without a key on this machine being read.
32
+ var dir := OS.get_user_data_dir().path_join("selftest")
33
+ DirAccess.make_dir_recursive_absolute(dir)
34
+ var path := dir.path_join("key.cfg")
35
+ var file := ConfigFile.new()
36
+ file.set_value("ravensight", "ingest_key", "gt_live_selftest")
37
+ file.set_value("ravensight", "api_url", "http://127.0.0.1:9")
38
+ file.save(path)
39
+ var found: Dictionary = game_config.find_key({"ravensight-key-file": path}, true)
40
+ _check("a key file argument wins", found["source"] == "argument" and found["key"] == "gt_live_selftest" and found["api_url"] == "http://127.0.0.1:9")
41
+ _check("--no-key answers none", game_config.find_key({"no-key": "true", "ravensight-key-file": path}, true)["source"] == "none")
42
+ DirAccess.remove_absolute(path)
43
+
44
+ # The opt-out is saved where the SDK reads it, and a later run would honour it.
45
+ var was_on: bool = telemetry.analytics_enabled()
46
+ telemetry.set_analytics_enabled(false)
47
+ _check("opting out is saved", not telemetry.analytics_enabled())
48
+ telemetry.set_analytics_enabled(true)
49
+ _check("opting back in clears it", telemetry.analytics_enabled())
50
+ telemetry.set_analytics_enabled(was_on)
51
+
52
+ # What the playable check reads.
53
+ var before: int = game_state.core_loops_completed
54
+ game_state.complete_core_loop()
55
+ _check("a completed loop is counted", game_state.core_loops_completed == before + 1)
56
+ _check("it is reported as a level completion", telemetry.last_event["data"]["rs_kind"] == "level_complete")
57
+
58
+ # The hook a playtest reads, on the real scene.
59
+ var scene: Node = load("res://scenes/main.tscn").instantiate()
60
+ root.add_child(scene)
61
+ var hook: Node = scene.get_node("StateHook")
62
+ _check("the state node is in the group", hook.is_in_group("ravensight_state"))
63
+ var state: Dictionary = hook._ravensight_state()
64
+ _check("the state says how many loops", state.has("core_loops_completed"))
65
+ _check("the state says where the player is", state.has("player_position"))
66
+ _check("the SDK is the public one", String(sdk.VERSION).length() > 0)
67
+ scene.queue_free()
68
+
69
+ print("selftest %s" % ("failed" if failed > 0 else "passed"))
70
+ quit(1 if failed > 0 else 0)
@@ -0,0 +1,49 @@
1
+ # __GAME_NAME__
2
+
3
+ A small placeholder 3D project from the Ravensight `godot-3d` template (Alpha, a proof of
4
+ concept). Every visual is a primitive mesh, and your own models, art and audio replace
5
+ them. Ravensight does not make a 3D game from a brief: this template, Build a feature
6
+ and Validate changes are the whole of the 3D Alpha.
7
+
8
+ ## Run it
9
+
10
+ Open this folder in Godot 4, or run `godot --path .`. Move with WASD or the arrow keys
11
+ along the floor, jump with Space or E, and Esc for settings. Walk into the beacon: it
12
+ moves somewhere else, and that is one pass of the core loop. Fall off the floor and you
13
+ start again from the middle.
14
+
15
+ `godot --headless --path . --script tools/selftest.gd` checks the parts a game
16
+ depends on.
17
+
18
+ ## Analytics
19
+
20
+ The Ravensight SDK (`addons/ravensight/Ravensight.gd`, MIT, unmodified) is an autoload.
21
+ Report events with `Telemetry.track(name, data, kind)`, where `kind` is `level_start`,
22
+ `level_complete`, `death` or `none`. It always sets `rs_kind`, so an event's name is
23
+ never used to guess what it means.
24
+
25
+ - **The key.** With no key the SDK does nothing and the game plays the same. To send
26
+ events, put this game's key in `ravensight_key.cfg`, which git ignores, under
27
+ `[ravensight]` as `ingest_key="..."`. `scripts/autoload/game_config.gd` says where else
28
+ a key can come from.
29
+ - **The opt-out.** Esc opens settings, and "Share anonymous analytics" is the player's
30
+ own choice. The SDK saves it and honours it on every later run.
31
+ - **A build anyone can download.** Use a public key, the one that starts `gt_pub_`, so
32
+ it is written only into the build:
33
+
34
+ RAVENSIGHT_RELEASE_KEY=gt_pub_... sh tools/inject_release_key.sh godot --headless --export-release Web build/web/index.html
35
+
36
+ ## How a playtest reads the game
37
+
38
+ `scripts/state_hook.gd` is in the `ravensight_state` group, and its
39
+ `_ravensight_state()` answer is what a persona sees and a replay compares. Add what a
40
+ player can see to it as the game grows. Call `GameState.complete_core_loop()` once for
41
+ each pass of the core loop.
42
+
43
+ A persona plays with keys and input actions, and it can click a node by its path. It
44
+ cannot steer a camera with the mouse, so a feature that needs mouse look cannot be
45
+ played by a persona yet.
46
+
47
+ Random numbers from the global `randf()` and `randi()` are seeded by a playtest, so a
48
+ replay sees them come out the same. See the replay notes in the CLI's `docs/REPLAY.md`
49
+ for what else holds and what does not.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Reality Software Entertainment
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.