Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions .github/check-datascripts.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,12 @@
# 3. `lyracore packages check` verifies every committed artifact's Build Identity against this
# checkout: source, typings, authoring library and toolchain pins.
#
# What this job does NOT do: re-emit a Datascript. That needs a Base Snapshot, which is the
# Operator's own client-derived data and does not exist on a CI runner — see
# `docs/agents/cross-repo-cli.md` in LyraCore and `packages check`'s own handling of a missing
# snapshot. It never writes a regenerated artifact back to this checkout either; a diff here is a
# failure to fix by hand and re-commit, never something CI commits on the collection's behalf.
# example-data is also built in a temporary directory against Core's synthetic Base Snapshot.
# No client-derived Base Snapshot is needed, and no Delta is written into the collection.

set -euo pipefail

collection_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
core_root=${1:-}
if [[ -z "$core_root" || ! -f "$core_root/module/Cargo.toml" ]]; then
echo "usage: $0 /path/to/LyraCore" >&2
Expand Down Expand Up @@ -59,5 +57,8 @@ echo "reproducible."
echo "== installing the pinned Datascript dependencies and typechecking"
(cd datascripts && bun install --frozen-lockfile && bun ./node_modules/typescript/bin/tsc --noEmit)

echo "== checking example-data with a synthetic Base Snapshot"
python3 "$collection_root/.github/check-example-data.py" "$core_root"

echo "== lyracore packages check"
./lyracore packages check
52 changes: 52 additions & 0 deletions .github/check-example-data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#!/usr/bin/env python3
"""Build example-data against Core's synthetic Base Snapshot, outside the collection."""

import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile


def check(core: Path) -> None:
collection = Path(__file__).resolve().parent.parent
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
(root / "datascripts").symlink_to(core / "datascripts", target_is_directory=True)
package = root / "packages/example-data"
shutil.copytree(collection / "example-data/datascripts", package / "datascripts")
source = package / "datascripts/welcome.ts"
with tempfile.NamedTemporaryFile(
mode="w", suffix=".json", prefix=".example-data-", dir=core / "datascripts"
) as config:
json.dump({"extends": "./tsconfig.json", "files": [str(source)]}, config)
config.flush()
subprocess.run(
["bun", "./node_modules/typescript/bin/tsc", "--noEmit", "--project", config.name],
cwd=core / "datascripts", check=True,
)
subprocess.run(
["bun", "run", str(source)], check=True,
env={**os.environ,
"LYRACORE_BASE_SNAPSHOT": str(core / "datascripts/tests/fixtures/base-snapshot.json"),
"LYRACORE_PACKAGES_ROOT": str(root / "packages")},
)
artifact = package / "data/.generated/spell.json"
delta = json.loads(artifact.read_text())
assert delta["package"] == "example-data"
spells = [claim for claim in delta["claims"] if claim["table"] == "game_spell"]
assert len(spells) == 1
assert spells[0]["operation"] == "insert"
assert spells[0]["key"] == {"spell_id": 6000300}
assert spells[0]["fields"]["name"]["value"] == "A Warm Welcome"
subprocess.run(
["cargo", "run", "--locked", "--quiet", "-p", "lyracore-package-delta",
"--bin", "lyracore-delta-check", "--", str(artifact)],
cwd=core, check=True,
)


if __name__ == "__main__":
check(Path(sys.argv[1]).resolve())
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ Provenance Stamp records the exact collection commit it came from.

## Packages

Start with these Reference Packages. Each adds a small welcome at one level of the Package API.

| Package | What to read |
| --- | --- |
| [`example-script`](example-script/) | TypeScript on login and Lua on level-up, with one chat line each |
| [`example-client`](example-client/) | An addon and a UI Transform |
| [`example-data`](example-data/) | A Datascript that clones one spell, with a Delta built locally |
| [`example-rust`](example-rust/) | A Rust hook, Package Config and a Package test |
| [`example-all`](example-all/) | Rust asks a Runtime Script, with Package Config as the fallback |

- [`dungeons`](dungeons/) holds scripted dungeon choreography, one submodule per dungeon.
Deadmines is its first dungeon.
- [`playerbots`](playerbots/) fields a population of session-less Characters a player can group
Expand Down
15 changes: 15 additions & 0 deletions example-all/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Rust asks a Runtime Script

On login, Rust calls `ask()` for Package Event `example-all.welcome`. `scripts/welcome.lua`
answers `1`, so Rust sends a welcome through `actor::system_message`. Change the Script Answer
to `0` to suppress it.

When no script answers, Rust reads Package Config key `welcome`. `false` suppresses the greeting;
`true`, a missing value, or an invalid value sends it. The default is `true`. Package tests run
the authored Lua in the Runtime Script Host and check the fallback.

Install with `./lyracore packages add example-all`, then publish through the normal realm
update. Apply its Script Artifact with `./lyracore packages replay` on your development topology.
After editing the Runtime Script, run `./lyracore packages build` and commit its Script
Artifact and Build Identity together. Its Script ID is 100302. Give copies distinct IDs before
installing them together.
16 changes: 16 additions & 0 deletions example-all/data/.generated/example-all.script.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"kind": "script",
"version": 1,
"package": "example-all",
"source_hash": "bd88e41c6f9976c287287db30ed0ce0b07d0b64fca7398d1093f5987dacc9dfb",
"scripts": [
{
"script_id": 100302,
"name": "example-all.welcome",
"event": "example-all.welcome",
"priority": 0,
"enabled": true,
"source": "-- @event example-all.welcome\n-- @id 100302\n\n-- A positive Script Answer sends the welcome. Zero suppresses it.\nreturn 1\n"
}
]
}
1 change: 1 addition & 0 deletions example-all/data/.generated/script.identity
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"artifact_hash":"sha256-v1:7e6c882d9086c81c9bb49d1a676b8d96cd7f2c08d9dc5e4662d72eeb95f9a816","bun_lock_hash":"sha256-v1:a3dc4c9ef1ff2b0b3502de7fc828c237f16df8a817e14a3412a08f8b052c0252","bun_version":"1.3.7","source_hash":"sha256-script-sources-v1:bd88e41c6f9976c287287db30ed0ce0b07d0b64fca7398d1093f5987dacc9dfb","toolchain_hash":"sha256-dir-v1:f53d4056ac988e2966186bcc407b34a9ed525b385e53cc7135d2fa866dab69f4","version":1}
5 changes: 5 additions & 0 deletions example-all/scripts/welcome.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- @event example-all.welcome
-- @id 100302

-- A positive Script Answer sends the welcome. Zero suppresses it.
return 1
30 changes: 30 additions & 0 deletions example-all/src/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use crate::package_config::{ensure_package_config_default, game_package_config};

const PACKAGE: &str = "example-all";
const WELCOME_EVENT: &str = "example-all.welcome";

crate::game_hook!(on_login, fn example_all_on_login(ctx, payload) {
ensure_package_config_default(ctx, PACKAGE, "welcome", "true");
let fallback = ctx.db.game_package_config().by_package_key()
.filter((PACKAGE, "welcome")).next().map(|row| row.value);
let answer = crate::script_binding::ask(ctx, WELCOME_EVENT, payload.character_guid, 0);
if should_welcome(answer, fallback.as_deref()) {
if let Err(reason) = crate::actor::system_message(
ctx, payload.character_guid, "Welcome from example-all!".to_string(),
) {
spacetimedb::log::warn!("{PACKAGE}: welcome failed: {reason}");
}
}
});

fn should_welcome(answer: Option<f64>, fallback: Option<&str>) -> bool {
match answer {
Some(number) => number > 0.0,
None => fallback
.and_then(|text| text.trim().parse().ok())
.unwrap_or(true),
}
}

#[cfg(test)]
mod tests;
28 changes: 28 additions & 0 deletions example-all/src/tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#![cfg(test)]

use crate::package_test::{ask_offline, RuntimeScript};

#[test]
fn the_script_answer_overrides_config() {
let answer = ask_offline(
super::WELCOME_EVENT,
None,
None,
&[RuntimeScript {
name: "example-all.welcome",
source: include_str!("../scripts/welcome.lua"),
}],
)
.unwrap();
assert_eq!(answer, Some(1.0));
assert!(super::should_welcome(answer, Some("false")));
assert!(!super::should_welcome(Some(0.0), Some("true")));
}

#[test]
fn no_script_uses_config() {
let answer = ask_offline(super::WELCOME_EVENT, None, None, &[]).unwrap();
assert!(!super::should_welcome(answer, Some("false")));
assert!(super::should_welcome(answer, Some("true")));
assert!(super::should_welcome(answer, Some("invalid")));
}
10 changes: 10 additions & 0 deletions example-client/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Welcome from the client

The addon prints one line when it loads. The UI Transform adds a second welcome to the default
chat frame on world entry. It inserts code after `ChatFrame_OnEvent` in the client's own
`Interface/FrameXML/ChatFrame.lua`.

Install with `./lyracore packages add example-client`, run `./lyracore client sync`, then restart
the client. This Package needs no Module change. The UI Transform requires a 1.12.1 Baseline with
one matching anchor. It ships only the insertion, never the Baseline. `client pack` refuses a
distributable Client Pack that contains a UI Transform's derived output.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DEFAULT_CHAT_FRAME:AddMessage("Welcome from the example-client addon!")
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
## Interface: 11200
## Title: example-client
## Notes: A welcome from a client Package.
example-client.lua
7 changes: 7 additions & 0 deletions example-client/client/ui-transforms.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
{
"path": "Interface/FrameXML/ChatFrame.lua",
"after": "function ChatFrame_OnEvent(event)",
"insert": "\n\tif event == \"PLAYER_ENTERING_WORLD\" and this == DEFAULT_CHAT_FRAME then\n\t\tthis:AddMessage(\"Welcome from the example-client UI Transform!\")\n\tend\n"
}
]
12 changes: 12 additions & 0 deletions example-data/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# A warm welcome spell

`datascripts/welcome.ts` clones Fireball, spell 133, into Package Spell 6000300 and names it
`A Warm Welcome`. Its other columns and effects stay the same.

Install with `./lyracore packages add example-data`. With a Base Snapshot from your own client
data, run `./lyracore packages build`, then use the normal base import to apply the Package Delta.
The spell has Fireball's effect when cast. An unmodified client has no tooltip for a Package Spell.

Keep the Datascript as source. The generated Delta and its Build Identity stay local under
`data/.generated/` and must not be committed. A copied Package must choose a different Package
Spell ID before it can be installed beside this one.
1 change: 1 addition & 0 deletions example-data/data/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.generated/
5 changes: 5 additions & 0 deletions example-data/datascripts/welcome.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { run } from "../../../datascripts/lib/index.ts";

await run("example-data", (data) => {
data.spell(133).clone(6_000_300).set("name", "A Warm Welcome");
});
9 changes: 9 additions & 0 deletions example-rust/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Welcome from Rust

One `on_login` hook sends a System Message through `actor::system_message`. It seeds Package
Config key `greeting` on first login and reads it on each login. A blank value uses the default
welcome. The Package test covers that fallback.

Install with `./lyracore packages add example-rust`, then publish through the normal realm
update. On your development topology, `./lyracore packages config example-rust greeting "Hello!"`
changes the next welcome without another publish.
29 changes: 29 additions & 0 deletions example-rust/src/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
use crate::package_config::{ensure_package_config_default, game_package_config};

const PACKAGE: &str = "example-rust";
const DEFAULT_GREETING: &str = "Welcome from example-rust!";

crate::game_hook!(on_login, fn example_rust_on_login(ctx, payload) {
ensure_package_config_default(ctx, PACKAGE, "greeting", DEFAULT_GREETING);
let configured = ctx.db.game_package_config().by_package_key()
.filter((PACKAGE, "greeting")).next().map(|row| row.value);
let message = greeting(configured.as_deref()).to_string();
if let Err(reason) = crate::actor::system_message(ctx, payload.character_guid, message) {
spacetimedb::log::warn!("{PACKAGE}: welcome failed: {reason}");
}
});

fn greeting(configured: Option<&str>) -> &str {
configured
.map(str::trim)
.filter(|text| !text.is_empty())
.unwrap_or(DEFAULT_GREETING)
}

#[cfg(test)]
mod tests {
#[test]
fn blank_config_keeps_the_default_greeting() {
assert_eq!(super::greeting(Some(" ")), "Welcome from example-rust!");
}
}
10 changes: 10 additions & 0 deletions example-script/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Welcome with Runtime Scripts

`welcome.ts` sends one chat line on login. `ding.lua` sends one on level-up. Both use
`send_chat` on the Character carried by the event. This Package needs no Rust.

Install with `./lyracore packages add example-script`, then apply the committed Script Artifact
with `./lyracore packages replay` on your development topology. After editing either source, run
`./lyracore packages build` and commit the Script Artifact and its Build Identity together.

The Script IDs are 100300 and 100301. Give copies distinct IDs before installing them together.
24 changes: 24 additions & 0 deletions example-script/data/.generated/example-script.script.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"kind": "script",
"version": 1,
"package": "example-script",
"source_hash": "24b601f2db72799e6c3879c59b1bfd03d4ee92e10dde5ebc94920fb91cea9f72",
"scripts": [
{
"script_id": 100300,
"name": "example-script.welcome",
"event": "on_login",
"priority": 0,
"enabled": true,
"source": "local function ____tbl(t)\n return t\nend\nlocal function script()\n if event.actor and event.actor.is_player then\n send_chat(event.actor, \"Welcome from example-script!\")\n end\nend\nreturn script()\n"
},
{
"script_id": 100301,
"name": "example-script.ding",
"event": "on_levelup",
"priority": 0,
"enabled": true,
"source": "-- @event on_levelup\n-- @id 100301\n\nif event.actor and event.actor.is_player then\n send_chat(event.actor, \"Welcome to your new level from example-script!\")\nend\n"
}
]
}
1 change: 1 addition & 0 deletions example-script/data/.generated/script.identity
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"artifact_hash":"sha256-v1:8f8a8b9d8d9b029091c7478912c35d1b7a0a74bda9b9fbbb8c8ed9672fd35a07","bun_lock_hash":"sha256-v1:a3dc4c9ef1ff2b0b3502de7fc828c237f16df8a817e14a3412a08f8b052c0252","bun_version":"1.3.7","source_hash":"sha256-script-sources-v1:24b601f2db72799e6c3879c59b1bfd03d4ee92e10dde5ebc94920fb91cea9f72","toolchain_hash":"sha256-dir-v1:f53d4056ac988e2966186bcc407b34a9ed525b385e53cc7135d2fa866dab69f4","version":1}
6 changes: 6 additions & 0 deletions example-script/scripts/ding.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- @event on_levelup
-- @id 100301

if event.actor and event.actor.is_player then
send_chat(event.actor, "Welcome to your new level from example-script!")
end
8 changes: 8 additions & 0 deletions example-script/scripts/welcome.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// @event on_login
// @id 100300

function script(): void {
if (event.actor && event.actor.is_player) {
send_chat(event.actor, "Welcome from example-script!");
}
}
Loading