add: simulation core
This commit is contained in:
parent
8461085d8b
commit
774b812bcd
|
|
@ -3,3 +3,4 @@
|
||||||
target
|
target
|
||||||
data
|
data
|
||||||
example-wasm-agent/target
|
example-wasm-agent/target
|
||||||
|
default-core/target
|
||||||
|
|
@ -694,6 +694,15 @@ dependencies = [
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "default-core"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"extism-pdk",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "der-parser"
|
name = "der-parser"
|
||||||
version = "10.0.0"
|
version = "10.0.0"
|
||||||
|
|
@ -870,6 +879,32 @@ dependencies = [
|
||||||
"serde_json",
|
"serde_json",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "extism-pdk"
|
||||||
|
version = "1.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "352fcb5a66eb74145a1c4a01f2bd15d59c62c85be73aac8471880c65b26b798f"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"base64 0.22.1",
|
||||||
|
"extism-convert",
|
||||||
|
"extism-manifest",
|
||||||
|
"extism-pdk-derive",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "extism-pdk-derive"
|
||||||
|
version = "1.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d086daea5fd844e3c5ac69ddfe36df4a9a43e7218cf7d1f888182b089b09806c"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fallible-iterator"
|
name = "fallible-iterator"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
|
|
@ -3974,6 +4009,7 @@ dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"clap",
|
"clap",
|
||||||
|
"default-core",
|
||||||
"extism",
|
"extism",
|
||||||
"rcgen",
|
"rcgen",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,15 @@ name = "wolloseum"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
members = ["default-core"]
|
||||||
|
resolver = "3"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = { version = "0.8", features = ["ws"] }
|
axum = { version = "0.8", features = ["ws"] }
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
clap = { version = "4.6.7", features = ["derive", "env"] }
|
clap = { version = "4.6.7", features = ["derive", "env"] }
|
||||||
|
default-core = { path = "default-core" }
|
||||||
extism = { version = "1.30", default-features = false, features = ["wasmtime-default-features"] }
|
extism = { version = "1.30", default-features = false, features = ["wasmtime-default-features"] }
|
||||||
rcgen = "0.14"
|
rcgen = "0.14"
|
||||||
reqwest = { version = "0.13", default-features = false, features = ["rustls-no-provider"] }
|
reqwest = { version = "0.13", default-features = false, features = ["rustls-no-provider"] }
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ FROM rust:1-bookworm AS builder
|
||||||
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
COPY Cargo.toml Cargo.lock ./
|
COPY Cargo.toml Cargo.lock ./
|
||||||
|
COPY default-core ./default-core
|
||||||
COPY src ./src
|
COPY src ./src
|
||||||
COPY web ./web
|
COPY web ./web
|
||||||
RUN cargo build --release --bin wolloseum
|
RUN cargo build --release --bin wolloseum
|
||||||
|
|
|
||||||
14
README.md
14
README.md
|
|
@ -116,6 +116,20 @@ Agent and core WASM uploads are limited to 1 MiB and validated in the
|
||||||
sandboxed runtime before they are persisted. Existing signed request headers
|
sandboxed runtime before they are persisted. Existing signed request headers
|
||||||
remain accepted for compatibility with older clients.
|
remain accepted for compatibility with older clients.
|
||||||
|
|
||||||
|
The server uses the native Rust implementation in `default-core/` until a core
|
||||||
|
is uploaded. An uploaded core replaces it and must export an Extism `tick`
|
||||||
|
function that accepts `CoreTickInput` JSON (`world` and `actions`) and returns
|
||||||
|
`CoreTickOutput` JSON (`world`). Build the default implementation as a WASM
|
||||||
|
core with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo build --manifest-path default-core/Cargo.toml \
|
||||||
|
--target wasm32-unknown-unknown --release
|
||||||
|
```
|
||||||
|
|
||||||
|
Upload `target/wasm32-unknown-unknown/release/default_core.wasm`
|
||||||
|
from the admin console to run that same implementation through the WASM path.
|
||||||
|
|
||||||
## CLI
|
## CLI
|
||||||
|
|
||||||
The binary also provides an authenticated client. New installations use the
|
The binary also provides an authenticated client. New installations use the
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,391 @@
|
||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyhow"
|
||||||
|
version = "1.0.104"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "autocfg"
|
||||||
|
version = "1.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.22.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytemuck"
|
||||||
|
version = "1.25.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytes"
|
||||||
|
version = "1.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "default-core"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"extism-pdk",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "either"
|
||||||
|
version = "1.18.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "equivalent"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "extism-convert"
|
||||||
|
version = "1.30.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ad19858c4c462309a8f3a20abec53e8603bda1eefda26c8bfab51d5516b40cbb"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"base64",
|
||||||
|
"bytemuck",
|
||||||
|
"extism-convert-macros",
|
||||||
|
"prost",
|
||||||
|
"rmp-serde",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "extism-convert-macros"
|
||||||
|
version = "1.30.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5f2932799f6d9f9646f97b65287f6bb2addc75a0ee61e40fb24559a7540dd928"
|
||||||
|
dependencies = [
|
||||||
|
"manyhow",
|
||||||
|
"proc-macro-crate",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "extism-manifest"
|
||||||
|
version = "1.30.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e2f59c8dadb5e0bde9a48c6ed45312e6ef625cbcd5f67c28459dbc8fe8bc0383"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "extism-pdk"
|
||||||
|
version = "1.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "352fcb5a66eb74145a1c4a01f2bd15d59c62c85be73aac8471880c65b26b798f"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"base64",
|
||||||
|
"extism-convert",
|
||||||
|
"extism-manifest",
|
||||||
|
"extism-pdk-derive",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "extism-pdk-derive"
|
||||||
|
version = "1.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d086daea5fd844e3c5ac69ddfe36df4a9a43e7218cf7d1f888182b089b09806c"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.17.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indexmap"
|
||||||
|
version = "2.14.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
|
||||||
|
dependencies = [
|
||||||
|
"equivalent",
|
||||||
|
"hashbrown",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itertools"
|
||||||
|
version = "0.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "manyhow"
|
||||||
|
version = "0.11.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b33efb3ca6d3b07393750d4030418d594ab1139cee518f0dc88db70fec873587"
|
||||||
|
dependencies = [
|
||||||
|
"manyhow-macros",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "manyhow-macros"
|
||||||
|
version = "0.11.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "46fce34d199b78b6e6073abf984c9cf5fd3e9330145a93ee0738a7443e371495"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro-utils",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-traits"
|
||||||
|
version = "0.2.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro-crate"
|
||||||
|
version = "3.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
|
||||||
|
dependencies = [
|
||||||
|
"toml_edit",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro-utils"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eeaf08a13de400bc215877b5bdc088f241b12eb42f0a548d3390dc1c56bb7071"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.107"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "prost"
|
||||||
|
version = "0.14.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"prost-derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "prost-derive"
|
||||||
|
version = "0.14.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"itertools",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.47"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rmp"
|
||||||
|
version = "0.8.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rmp-serde"
|
||||||
|
version = "1.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155"
|
||||||
|
dependencies = [
|
||||||
|
"rmp",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.229"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_core"
|
||||||
|
version = "1.0.229"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.229"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.6",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json"
|
||||||
|
version = "1.0.151"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"memchr",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
"zmij",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smallvec"
|
||||||
|
version = "1.16.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.119"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "3.0.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_datetime"
|
||||||
|
version = "1.1.1+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_edit"
|
||||||
|
version = "0.25.15+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"toml_datetime",
|
||||||
|
"toml_parser",
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_parser"
|
||||||
|
version = "1.1.3+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"
|
||||||
|
dependencies = [
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.26"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zmij"
|
||||||
|
version = "1.0.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
[package]
|
||||||
|
name = "default-core"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
|
||||||
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
|
extism-pdk = "1"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
serde_json = "1"
|
||||||
|
|
@ -2,9 +2,11 @@ use std::collections::BTreeMap;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
const MAX_WORLD_CELLS: usize = 1_048_576;
|
const MAX_WORLD_CELLS: usize = 1_048_576;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
pub enum Terrain {
|
pub enum Terrain {
|
||||||
Grass,
|
Grass,
|
||||||
Water,
|
Water,
|
||||||
|
|
@ -12,7 +14,7 @@ pub enum Terrain {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[repr(i32)]
|
#[repr(i32)]
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
pub enum ObservedTerrain {
|
pub enum ObservedTerrain {
|
||||||
Grass = 0,
|
Grass = 0,
|
||||||
Water = 1,
|
Water = 1,
|
||||||
|
|
@ -20,10 +22,10 @@ pub enum ObservedTerrain {
|
||||||
Boundary = 3,
|
Boundary = 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
|
||||||
pub struct AgentId(pub u64);
|
pub struct AgentId(pub u64);
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
pub enum Direction {
|
pub enum Direction {
|
||||||
North,
|
North,
|
||||||
East,
|
East,
|
||||||
|
|
@ -31,13 +33,13 @@ pub enum Direction {
|
||||||
West,
|
West,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
pub enum Action {
|
pub enum Action {
|
||||||
Move(Direction),
|
Move(Direction),
|
||||||
Wait,
|
Wait,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
pub struct AgentObservation {
|
pub struct AgentObservation {
|
||||||
pub tick: u64,
|
pub tick: u64,
|
||||||
pub x: u16,
|
pub x: u16,
|
||||||
|
|
@ -45,7 +47,13 @@ pub struct AgentObservation {
|
||||||
pub adjacent: [ObservedTerrain; 4],
|
pub adjacent: [ObservedTerrain; 4],
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
|
pub struct AgentAction {
|
||||||
|
pub id: AgentId,
|
||||||
|
pub action: Action,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
pub struct Agent {
|
pub struct Agent {
|
||||||
id: AgentId,
|
id: AgentId,
|
||||||
x: u16,
|
x: u16,
|
||||||
|
|
@ -62,10 +70,11 @@ impl Agent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
pub enum SimulationError {
|
pub enum SimulationError {
|
||||||
InvalidDimensions,
|
InvalidDimensions,
|
||||||
WorldTooLarge,
|
WorldTooLarge,
|
||||||
|
InvalidWorldState,
|
||||||
AgentAlreadyExists(AgentId),
|
AgentAlreadyExists(AgentId),
|
||||||
NoSpawnCellAvailable,
|
NoSpawnCellAvailable,
|
||||||
UnknownAgent(AgentId),
|
UnknownAgent(AgentId),
|
||||||
|
|
@ -77,6 +86,7 @@ impl fmt::Display for SimulationError {
|
||||||
match self {
|
match self {
|
||||||
Self::InvalidDimensions => write!(formatter, "world dimensions must be nonzero"),
|
Self::InvalidDimensions => write!(formatter, "world dimensions must be nonzero"),
|
||||||
Self::WorldTooLarge => write!(formatter, "world exceeds the cell limit"),
|
Self::WorldTooLarge => write!(formatter, "world exceeds the cell limit"),
|
||||||
|
Self::InvalidWorldState => write!(formatter, "world state is invalid"),
|
||||||
Self::AgentAlreadyExists(id) => write!(formatter, "agent {} already exists", id.0),
|
Self::AgentAlreadyExists(id) => write!(formatter, "agent {} already exists", id.0),
|
||||||
Self::NoSpawnCellAvailable => {
|
Self::NoSpawnCellAvailable => {
|
||||||
write!(formatter, "no unoccupied grass cell is available")
|
write!(formatter, "no unoccupied grass cell is available")
|
||||||
|
|
@ -89,7 +99,7 @@ impl fmt::Display for SimulationError {
|
||||||
|
|
||||||
impl Error for SimulationError {}
|
impl Error for SimulationError {}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
pub struct World {
|
pub struct World {
|
||||||
seed: u64,
|
seed: u64,
|
||||||
width: u16,
|
width: u16,
|
||||||
|
|
@ -211,6 +221,10 @@ impl World {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn remove_agent(&mut self, id: AgentId) -> bool {
|
||||||
|
self.agents.remove(&id).is_some()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn step(&mut self, actions: &BTreeMap<AgentId, Action>) -> Result<(), SimulationError> {
|
pub fn step(&mut self, actions: &BTreeMap<AgentId, Action>) -> Result<(), SimulationError> {
|
||||||
let next_tick = self
|
let next_tick = self
|
||||||
.tick
|
.tick
|
||||||
|
|
@ -265,6 +279,26 @@ impl World {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn validate(&self) -> Result<(), SimulationError> {
|
||||||
|
let cell_count = usize::from(self.width)
|
||||||
|
.checked_mul(usize::from(self.height))
|
||||||
|
.filter(|count| *count > 0 && *count <= MAX_WORLD_CELLS)
|
||||||
|
.ok_or(SimulationError::InvalidWorldState)?;
|
||||||
|
if self.tiles.len() != cell_count || self.spawn_cursor >= cell_count {
|
||||||
|
return Err(SimulationError::InvalidWorldState);
|
||||||
|
}
|
||||||
|
let mut occupied = BTreeMap::new();
|
||||||
|
for (id, agent) in &self.agents {
|
||||||
|
if *id != agent.id || self.terrain_at(agent.x, agent.y) != Some(Terrain::Grass) {
|
||||||
|
return Err(SimulationError::InvalidWorldState);
|
||||||
|
}
|
||||||
|
if occupied.insert(agent.position(), ()).is_some() {
|
||||||
|
return Err(SimulationError::InvalidWorldState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn index(&self, x: u16, y: u16) -> Option<usize> {
|
fn index(&self, x: u16, y: u16) -> Option<usize> {
|
||||||
if x >= self.width || y >= self.height {
|
if x >= self.width || y >= self.height {
|
||||||
return None;
|
return None;
|
||||||
|
|
@ -286,6 +320,52 @@ impl World {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
|
pub struct CoreTickInput {
|
||||||
|
pub world: World,
|
||||||
|
pub actions: Vec<AgentAction>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
|
pub struct CoreTickOutput {
|
||||||
|
pub world: World,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum CoreError {
|
||||||
|
DuplicateAction(AgentId),
|
||||||
|
Simulation(SimulationError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for CoreError {
|
||||||
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::DuplicateAction(id) => write!(formatter, "duplicate action for agent {}", id.0),
|
||||||
|
Self::Simulation(error) => write!(formatter, "{error}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for CoreError {}
|
||||||
|
|
||||||
|
impl From<SimulationError> for CoreError {
|
||||||
|
fn from(error: SimulationError) -> Self {
|
||||||
|
Self::Simulation(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_tick(mut input: CoreTickInput) -> Result<CoreTickOutput, CoreError> {
|
||||||
|
input.world.validate()?;
|
||||||
|
let mut actions = BTreeMap::new();
|
||||||
|
for action in input.actions {
|
||||||
|
if actions.insert(action.id, action.action).is_some() {
|
||||||
|
return Err(CoreError::DuplicateAction(action.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
input.world.step(&actions)?;
|
||||||
|
Ok(CoreTickOutput { world: input.world })
|
||||||
|
}
|
||||||
|
|
||||||
struct SplitMix64 {
|
struct SplitMix64 {
|
||||||
state: u64,
|
state: u64,
|
||||||
}
|
}
|
||||||
|
|
@ -304,9 +384,23 @@ impl SplitMix64 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
use extism_pdk::*;
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
#[plugin_fn]
|
||||||
|
pub fn tick(input: Json<CoreTickInput>) -> FnResult<Json<CoreTickOutput>> {
|
||||||
|
run_tick(input.0)
|
||||||
|
.map(Json)
|
||||||
|
.map_err(|error| extism_pdk::Error::msg(error.to_string()).into())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{Action, AgentId, Direction, SimulationError, World};
|
use super::{
|
||||||
|
Action, AgentAction, AgentId, CoreError, CoreTickInput, Direction, SimulationError, World,
|
||||||
|
run_tick,
|
||||||
|
};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -339,16 +433,59 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn invalid_action_does_not_partially_advance_world() {
|
fn native_tick_advances_serializable_world() {
|
||||||
let mut world = World::generate(42, 8, 8).unwrap();
|
let mut world = World::generate(42, 8, 8).unwrap();
|
||||||
|
let id = AgentId(7);
|
||||||
|
world.spawn_agent(id).unwrap();
|
||||||
|
let input = CoreTickInput {
|
||||||
|
world,
|
||||||
|
actions: vec![AgentAction {
|
||||||
|
id,
|
||||||
|
action: Action::Wait,
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
let encoded = serde_json::to_vec(&input).unwrap();
|
||||||
|
let decoded = serde_json::from_slice(&encoded).unwrap();
|
||||||
|
let output = run_tick(decoded).unwrap();
|
||||||
|
assert_eq!(output.world.tick(), 1);
|
||||||
|
assert_eq!(output.world.agents().count(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn duplicate_actions_are_rejected() {
|
||||||
|
let mut world = World::generate(42, 8, 8).unwrap();
|
||||||
|
let id = AgentId(7);
|
||||||
|
world.spawn_agent(id).unwrap();
|
||||||
|
let input = CoreTickInput {
|
||||||
|
world,
|
||||||
|
actions: vec![
|
||||||
|
AgentAction {
|
||||||
|
id,
|
||||||
|
action: Action::Wait,
|
||||||
|
},
|
||||||
|
AgentAction {
|
||||||
|
id,
|
||||||
|
action: Action::Wait,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(run_tick(input), Err(CoreError::DuplicateAction(id)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_action_does_not_advance_world() {
|
||||||
|
let world = World::generate(42, 8, 8).unwrap();
|
||||||
let before = world.clone();
|
let before = world.clone();
|
||||||
let actions = BTreeMap::from([(AgentId(404), Action::Wait)]);
|
let actions = BTreeMap::from([(AgentId(404), Action::Wait)]);
|
||||||
|
let mut next = world;
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
world.step(&actions),
|
next.step(&actions),
|
||||||
Err(SimulationError::UnknownAgent(AgentId(404)))
|
Err(SimulationError::UnknownAgent(AgentId(404)))
|
||||||
);
|
);
|
||||||
assert_eq!(world, before);
|
assert_eq!(next, before);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
use default_core::{CoreTickInput, CoreTickOutput, World, run_tick};
|
||||||
|
use extism::{Manifest, Plugin, PluginBuilder, Wasm};
|
||||||
|
|
||||||
|
const CORE_FUEL_LIMIT: u64 = 5_000_000;
|
||||||
|
const CORE_MEMORY_PAGES: u32 = 64;
|
||||||
|
|
||||||
|
pub(crate) enum CoreRuntime {
|
||||||
|
Native,
|
||||||
|
Wasm(Plugin),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CoreRuntime {
|
||||||
|
pub(crate) fn from_wasm(bytes: &[u8]) -> Result<Self, String> {
|
||||||
|
let manifest =
|
||||||
|
Manifest::new([Wasm::data(bytes.to_vec())]).with_memory_max(CORE_MEMORY_PAGES);
|
||||||
|
let plugin = PluginBuilder::new(manifest)
|
||||||
|
.with_wasi(false)
|
||||||
|
.with_fuel_limit(CORE_FUEL_LIMIT)
|
||||||
|
.build()
|
||||||
|
.map_err(|error| error.to_string())?;
|
||||||
|
let mut runtime = Self::Wasm(plugin);
|
||||||
|
runtime.tick(CoreTickInput {
|
||||||
|
world: World::generate(0x574F4C4C, 8, 8).map_err(|error| error.to_string())?,
|
||||||
|
actions: Vec::new(),
|
||||||
|
})?;
|
||||||
|
Ok(runtime)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn tick(&mut self, input: CoreTickInput) -> Result<CoreTickOutput, String> {
|
||||||
|
let expected_tick = input
|
||||||
|
.world
|
||||||
|
.tick()
|
||||||
|
.checked_add(1)
|
||||||
|
.ok_or_else(|| "simulation tick overflow".to_string())?;
|
||||||
|
let expected_seed = input.world.seed();
|
||||||
|
let expected_dimensions = input.world.dimensions();
|
||||||
|
let output = match self {
|
||||||
|
Self::Native => run_tick(input).map_err(|error| error.to_string())?,
|
||||||
|
Self::Wasm(plugin) => {
|
||||||
|
let encoded = serde_json::to_vec(&input).map_err(|error| error.to_string())?;
|
||||||
|
let response = plugin
|
||||||
|
.call("tick", &encoded)
|
||||||
|
.map_err(|error| error.to_string())?;
|
||||||
|
serde_json::from_slice(response).map_err(|error| error.to_string())?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
output
|
||||||
|
.world
|
||||||
|
.validate()
|
||||||
|
.map_err(|error| format!("core returned invalid world state: {error}"))?;
|
||||||
|
if output.world.tick() != expected_tick
|
||||||
|
|| output.world.seed() != expected_seed
|
||||||
|
|| output.world.dimensions() != expected_dimensions
|
||||||
|
{
|
||||||
|
return Err("core returned incompatible world state".to_string());
|
||||||
|
}
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
pub mod cli;
|
pub mod cli;
|
||||||
|
mod core_runtime;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
pub mod simulation;
|
pub use default_core as simulation;
|
||||||
pub mod wasm_agent;
|
pub mod wasm_agent;
|
||||||
|
|
|
||||||
264
src/server.rs
264
src/server.rs
|
|
@ -1,11 +1,13 @@
|
||||||
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::fs::{self, OpenOptions};
|
use std::fs::{self, OpenOptions};
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::thread;
|
||||||
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
use axum::body::Bytes;
|
use axum::body::Bytes;
|
||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
|
|
@ -16,6 +18,9 @@ use axum::routing::{get, post};
|
||||||
use axum::{Json, Router};
|
use axum::{Json, Router};
|
||||||
use base64::Engine as _;
|
use base64::Engine as _;
|
||||||
use base64::engine::general_purpose::{STANDARD as BASE64, URL_SAFE_NO_PAD};
|
use base64::engine::general_purpose::{STANDARD as BASE64, URL_SAFE_NO_PAD};
|
||||||
|
use default_core::{
|
||||||
|
Action, AgentAction, AgentId, AgentObservation, CoreTickInput, ObservedTerrain, World,
|
||||||
|
};
|
||||||
use rcgen::{KeyPair, PKCS_ED25519};
|
use rcgen::{KeyPair, PKCS_ED25519};
|
||||||
use ring::rand::{SecureRandom, SystemRandom};
|
use ring::rand::{SecureRandom, SystemRandom};
|
||||||
use ring::signature::{ED25519, UnparsedPublicKey};
|
use ring::signature::{ED25519, UnparsedPublicKey};
|
||||||
|
|
@ -23,7 +28,7 @@ use rusqlite::{Connection, OptionalExtension, params};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::sync::broadcast;
|
use tokio::sync::broadcast;
|
||||||
|
|
||||||
use crate::simulation::{AgentObservation, ObservedTerrain};
|
use crate::core_runtime::CoreRuntime;
|
||||||
use crate::wasm_agent::WasmAgent;
|
use crate::wasm_agent::WasmAgent;
|
||||||
|
|
||||||
const MAX_WASM_BYTES: usize = 1_048_576;
|
const MAX_WASM_BYTES: usize = 1_048_576;
|
||||||
|
|
@ -200,10 +205,252 @@ fn bearer_token(headers: &HeaderMap) -> Result<&str, ApiError> {
|
||||||
struct AppState {
|
struct AppState {
|
||||||
database: Arc<Mutex<Connection>>,
|
database: Arc<Mutex<Connection>>,
|
||||||
game_running: Arc<AtomicBool>,
|
game_running: Arc<AtomicBool>,
|
||||||
|
simulation_tick: Arc<AtomicU64>,
|
||||||
event_sender: broadcast::Sender<StateEvent>,
|
event_sender: broadcast::Sender<StateEvent>,
|
||||||
event_sequence: Arc<Mutex<u64>>,
|
event_sequence: Arc<Mutex<u64>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct StoredAgent {
|
||||||
|
client_id: String,
|
||||||
|
wasm: Vec<u8>,
|
||||||
|
sha256: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct StoredCore {
|
||||||
|
wasm: Vec<u8>,
|
||||||
|
sha256: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ActiveAgent {
|
||||||
|
id: AgentId,
|
||||||
|
sha256: String,
|
||||||
|
runtime: WasmAgent,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SimulationLoop {
|
||||||
|
world: World,
|
||||||
|
agents: BTreeMap<String, ActiveAgent>,
|
||||||
|
next_agent_id: u64,
|
||||||
|
core: Option<CoreRuntime>,
|
||||||
|
core_sha256: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SimulationLoop {
|
||||||
|
fn new() -> Self {
|
||||||
|
eprintln!("Using native Rust simulation core");
|
||||||
|
Self {
|
||||||
|
world: World::generate(0x574F4C4C, 32, 32)
|
||||||
|
.expect("the default world dimensions are valid"),
|
||||||
|
agents: BTreeMap::new(),
|
||||||
|
next_agent_id: 1,
|
||||||
|
core: Some(CoreRuntime::Native),
|
||||||
|
core_sha256: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_core(&mut self, uploaded: Option<StoredCore>) {
|
||||||
|
match uploaded {
|
||||||
|
Some(core) if self.core_sha256.as_deref() != Some(&core.sha256) => {
|
||||||
|
self.core_sha256 = Some(core.sha256.clone());
|
||||||
|
match CoreRuntime::from_wasm(&core.wasm) {
|
||||||
|
Ok(runtime) => {
|
||||||
|
eprintln!("Using uploaded simulation core {}", core.sha256);
|
||||||
|
self.core = Some(runtime);
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
self.core = None;
|
||||||
|
eprintln!("Uploaded simulation core could not load: {error}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None if self.core_sha256.is_some() => {
|
||||||
|
self.core_sha256 = None;
|
||||||
|
self.core = Some(CoreRuntime::Native);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sync_agents(&mut self, stored_agents: Vec<StoredAgent>) {
|
||||||
|
let current_ids: BTreeSet<_> = stored_agents
|
||||||
|
.iter()
|
||||||
|
.map(|agent| agent.client_id.as_str())
|
||||||
|
.collect();
|
||||||
|
let removed: Vec<_> = self
|
||||||
|
.agents
|
||||||
|
.keys()
|
||||||
|
.filter(|client_id| !current_ids.contains(client_id.as_str()))
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
for client_id in removed {
|
||||||
|
if let Some(agent) = self.agents.remove(&client_id) {
|
||||||
|
self.world.remove_agent(agent.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for stored in stored_agents {
|
||||||
|
if self
|
||||||
|
.agents
|
||||||
|
.get(&stored.client_id)
|
||||||
|
.is_some_and(|agent| agent.sha256 == stored.sha256)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let runtime = match WasmAgent::load(&stored.wasm) {
|
||||||
|
Ok(runtime) => runtime,
|
||||||
|
Err(error) => {
|
||||||
|
eprintln!("Agent {} could not load: {error}", stored.client_id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(agent) = self.agents.get_mut(&stored.client_id) {
|
||||||
|
agent.sha256 = stored.sha256;
|
||||||
|
agent.runtime = runtime;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = AgentId(self.next_agent_id);
|
||||||
|
if let Err(error) = self.world.spawn_agent(id) {
|
||||||
|
eprintln!("Agent {} could not spawn: {error}", stored.client_id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
self.next_agent_id = self.next_agent_id.saturating_add(1);
|
||||||
|
self.agents.insert(
|
||||||
|
stored.client_id,
|
||||||
|
ActiveAgent {
|
||||||
|
id,
|
||||||
|
sha256: stored.sha256,
|
||||||
|
runtime,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn step(&mut self) -> Result<(), String> {
|
||||||
|
let actions = self
|
||||||
|
.agents
|
||||||
|
.values_mut()
|
||||||
|
.map(|agent| {
|
||||||
|
let action = match self.world.observation(agent.id) {
|
||||||
|
Ok(observation) => agent.runtime.decide(observation).unwrap_or_else(|error| {
|
||||||
|
eprintln!("Agent {} decision failed: {error}", agent.id.0);
|
||||||
|
Action::Wait
|
||||||
|
}),
|
||||||
|
Err(error) => {
|
||||||
|
eprintln!("Agent {} observation failed: {error}", agent.id.0);
|
||||||
|
Action::Wait
|
||||||
|
}
|
||||||
|
};
|
||||||
|
AgentAction {
|
||||||
|
id: agent.id,
|
||||||
|
action,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let input = CoreTickInput {
|
||||||
|
world: self.world.clone(),
|
||||||
|
actions,
|
||||||
|
};
|
||||||
|
let output = self
|
||||||
|
.core
|
||||||
|
.as_mut()
|
||||||
|
.ok_or_else(|| "uploaded simulation core is unavailable".to_string())?
|
||||||
|
.tick(input);
|
||||||
|
match output {
|
||||||
|
Ok(output) => self.world = output.world,
|
||||||
|
Err(error) => {
|
||||||
|
if self.core_sha256.is_some() {
|
||||||
|
self.core = None;
|
||||||
|
}
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_simulation_loop(state: AppState) {
|
||||||
|
let mut simulation = SimulationLoop::new();
|
||||||
|
loop {
|
||||||
|
thread::sleep(Duration::from_millis(100));
|
||||||
|
if !state.game_running.load(Ordering::SeqCst) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (agents, core) = match load_simulation_inputs(&state) {
|
||||||
|
Ok(inputs) => inputs,
|
||||||
|
Err(error) => {
|
||||||
|
eprintln!("Could not load simulation inputs: {error}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
simulation.select_core(core);
|
||||||
|
simulation.sync_agents(agents);
|
||||||
|
if simulation.core.is_none() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Err(error) = simulation.step() {
|
||||||
|
eprintln!("Simulation tick failed: {error}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let tick = simulation.world.tick();
|
||||||
|
state.simulation_tick.store(tick, Ordering::SeqCst);
|
||||||
|
let agents = simulation
|
||||||
|
.world
|
||||||
|
.agents()
|
||||||
|
.map(|agent| {
|
||||||
|
let (x, y) = agent.position();
|
||||||
|
serde_json::json!({ "id": agent.id().0, "x": x, "y": y })
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
publish_state_event(
|
||||||
|
&state,
|
||||||
|
"simulation_tick",
|
||||||
|
serde_json::json!({ "tick": tick, "agents": agents }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_simulation_inputs(
|
||||||
|
state: &AppState,
|
||||||
|
) -> Result<(Vec<StoredAgent>, Option<StoredCore>), String> {
|
||||||
|
let database = state.database.lock().map_err(|error| error.to_string())?;
|
||||||
|
let mut statement = database
|
||||||
|
.prepare(
|
||||||
|
"SELECT agents.identity_id, agents.wasm, agents.sha256
|
||||||
|
FROM agents JOIN identities ON identities.id = agents.identity_id
|
||||||
|
WHERE identities.active = 1 ORDER BY agents.identity_id",
|
||||||
|
)
|
||||||
|
.map_err(|error| error.to_string())?;
|
||||||
|
let agents = statement
|
||||||
|
.query_map([], |row| {
|
||||||
|
Ok(StoredAgent {
|
||||||
|
client_id: row.get(0)?,
|
||||||
|
wasm: row.get(1)?,
|
||||||
|
sha256: row.get(2)?,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.map_err(|error| error.to_string())?
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.map_err(|error| error.to_string())?;
|
||||||
|
let core = database
|
||||||
|
.query_row(
|
||||||
|
"SELECT wasm, sha256 FROM core_module WHERE id = 1",
|
||||||
|
[],
|
||||||
|
|row| {
|
||||||
|
Ok(StoredCore {
|
||||||
|
wasm: row.get(0)?,
|
||||||
|
sha256: row.get(1)?,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
|
.map_err(|error| error.to_string())?;
|
||||||
|
Ok((agents, core))
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
#[derive(Clone, Debug, Serialize)]
|
||||||
struct StateEvent {
|
struct StateEvent {
|
||||||
sequence: u64,
|
sequence: u64,
|
||||||
|
|
@ -271,9 +518,14 @@ pub async fn run(data_directory: PathBuf, address: SocketAddr) -> Result<(), Box
|
||||||
let state = AppState {
|
let state = AppState {
|
||||||
database: Arc::new(Mutex::new(database)),
|
database: Arc::new(Mutex::new(database)),
|
||||||
game_running: Arc::new(AtomicBool::new(true)),
|
game_running: Arc::new(AtomicBool::new(true)),
|
||||||
|
simulation_tick: Arc::new(AtomicU64::new(0)),
|
||||||
event_sender: broadcast::channel(256).0,
|
event_sender: broadcast::channel(256).0,
|
||||||
event_sequence: Arc::new(Mutex::new(0)),
|
event_sequence: Arc::new(Mutex::new(0)),
|
||||||
};
|
};
|
||||||
|
let simulation_state = state.clone();
|
||||||
|
thread::Builder::new()
|
||||||
|
.name("wolloseum-simulation".to_string())
|
||||||
|
.spawn(move || run_simulation_loop(simulation_state))?;
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/", get(user_ui))
|
.route("/", get(user_ui))
|
||||||
.route("/admin", get(admin_ui))
|
.route("/admin", get(admin_ui))
|
||||||
|
|
@ -806,7 +1058,7 @@ async fn upload_core(
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
// Sandboxed load validates the module without requiring agent-specific exports.
|
// Sandboxed load validates the module without requiring agent-specific exports.
|
||||||
WasmAgent::load(&body).map_err(|error| ApiError::bad_request(error.to_string()))?;
|
CoreRuntime::from_wasm(&body).map_err(ApiError::bad_request)?;
|
||||||
|
|
||||||
let digest = ring::digest::digest(&ring::digest::SHA256, &body);
|
let digest = ring::digest::digest(&ring::digest::SHA256, &body);
|
||||||
let sha256 = digest
|
let sha256 = digest
|
||||||
|
|
@ -905,6 +1157,7 @@ fn snapshot_event(state: &AppState) -> StateEvent {
|
||||||
event: "snapshot".to_string(),
|
event: "snapshot".to_string(),
|
||||||
data: serde_json::json!({
|
data: serde_json::json!({
|
||||||
"running": state.game_running.load(Ordering::SeqCst),
|
"running": state.game_running.load(Ordering::SeqCst),
|
||||||
|
"tick": state.simulation_tick.load(Ordering::SeqCst),
|
||||||
"sequence": sequence,
|
"sequence": sequence,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
@ -1055,7 +1308,7 @@ mod tests {
|
||||||
use ring::signature::Ed25519KeyPair;
|
use ring::signature::Ed25519KeyPair;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::atomic::AtomicBool;
|
use std::sync::atomic::{AtomicBool, AtomicU64};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
fn test_state(directory: &Path) -> (AppState, String, KeyPair) {
|
fn test_state(directory: &Path) -> (AppState, String, KeyPair) {
|
||||||
|
|
@ -1072,6 +1325,7 @@ mod tests {
|
||||||
let state = AppState {
|
let state = AppState {
|
||||||
database: Arc::new(Mutex::new(database)),
|
database: Arc::new(Mutex::new(database)),
|
||||||
game_running: Arc::new(AtomicBool::new(true)),
|
game_running: Arc::new(AtomicBool::new(true)),
|
||||||
|
simulation_tick: Arc::new(AtomicU64::new(0)),
|
||||||
event_sender: tokio::sync::broadcast::channel(256).0,
|
event_sender: tokio::sync::broadcast::channel(256).0,
|
||||||
event_sequence: Arc::new(Mutex::new(0)),
|
event_sequence: Arc::new(Mutex::new(0)),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ use std::fmt;
|
||||||
use extism::{Manifest, Plugin, PluginBuilder, Wasm};
|
use extism::{Manifest, Plugin, PluginBuilder, Wasm};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::simulation::{Action, AgentObservation, Direction, ObservedTerrain};
|
use default_core::{Action, AgentObservation, Direction, ObservedTerrain};
|
||||||
|
|
||||||
const MAX_MODULE_BYTES: usize = 1_048_576;
|
const MAX_MODULE_BYTES: usize = 1_048_576;
|
||||||
const MAX_MEMORY_PAGES: u32 = 32;
|
const MAX_MEMORY_PAGES: u32 = 32;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue