# AGENTS.md ## 项目约束 - 代码需要有完善的中文注释 - 在落地工程修改前检查是否有详细指导本次落地的文档,若没有文档或文档的完善程度仍有落地过程中编码级别的歧义优先优化文档后落地工程迭代。 - 对工程的修改不仅要落地到代码更面,还要更改对应文档,若没有生成新的文档,文档统一存在doc目录中 - 不要擅自把现有中文文案、注释、剧情、文档改写成英文,除非用户明确要求翻译。 - 看到中文乱码时,不要直接沿用乱码文本,也不要用英文替换;先确认文件真实编码,再决定是否修改。 - 在 PowerShell 5.1 中读取或写入文本时,必须显式使用 UTF-8;如果终端输出疑似乱码,要用 `Get-Content -Encoding UTF8`、Python 或 Node 再次核对原文。 - 非必要不要整文件重写,尤其是包含中文的文件;优先做局部补丁,避免把未改动的中文内容重新编码。 - 修改包含中文的文件后,优先运行仓库里的编码检查,确保没有把文本写坏。 - UI面板中不要默认写一些规则描述文案,清爽一些,按照游戏UI设计规范设计即可。 - UI设计需要兼顾网页端、移动端双端的使用体验,确保在不同设备上都能正常显示和操作,移动端优先考虑。 - 不要在gitignore中添加.env.local文件。 - 严格遵循简洁的代码风格 - 前端只负责做表现,所有的逻辑、数据都放到Express后端进行运算和存储。 - 请默认保持系统的简洁性,能复用、修改、扩展现有系统、页面就不新建新系统新页面。 - 禁止将功能说明描述类的文本默认写入UI界面中。 - prd文档中每个模块的描述要落地设计到可以精准编码到位,不能出现需求落地漂移。 - 点击按钮弹出独立的面板的设计不要实现成在当前面板下面显示内容。 ## 文档图谱 ```text docs/ ├─ README.md ├─ audits/ │ ├─ README.md │ ├─ FUNCTION_DESIGN_AUDIT_2026-04-03.md │ ├─ ITEM_AND_BUILD_PRD_AUDIT_2026-04-05.md │ ├─ engineering/ │ │ ├─ README.md │ │ ├─ ENGINEERING_OPTIMIZATION_REVIEW_2026-03-29.md │ │ ├─ ENGINEERING_OPTIMIZATION_REVIEW_2026-03-30.md │ │ ├─ ENGINEERING_OPTIMIZATION_REVIEW_2026-04-01.md │ │ └─ MONSTER_NPC_UNIFICATION_AUDIT_2026-04-06.md │ └─ text/ │ ├─ README.md │ ├─ CHINESE_MOJIBAKE_INVENTORY.md │ ├─ EDITOR_GAME_PRESET_TEXT_AUDIT_2026-03-25.md │ ├─ GAME_EDITOR_PRESET_TEXT_AUDIT_2026-03-29.md │ ├─ GAME_EDITOR_PRESET_TEXT_AUDIT_2026-03-30.md │ ├─ GAME_UI_PRESET_EDITOR_NPC_PROMPT_TEXT_AUDIT_2026-04-02_DEEP_SCAN.md │ ├─ GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-03-30_CONTINUED.md │ ├─ GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-03-31.md │ ├─ GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-04-01.md │ └─ GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-04-02.md ├─ design/ │ ├─ README.md │ ├─ AI_NATIVE_RUNTIME_ITEM_SYSTEM_REDESIGN_2026-04-02.md │ ├─ COMPANION_FIRST_CONTACT_RELATIONSHIP_AND_PRIVATE_CHAT_DESIGN_2026-04-04.md │ ├─ CUSTOM_WORLD_CREATOR_INPUT_AND_AI_BOUNDARY_DESIGN_2026-04-06.md │ ├─ EQUIPMENT_BUILD_AND_FORGE_LOOP_SYSTEM_DESIGN.md │ └─ npc-conversation-situation-draft.md ├─ experience/ │ ├─ README.md │ ├─ ADVENTURE_RUNTIME_DEV_EXPERIENCE.md │ ├─ AGENT_UI_CHANGELOG.md │ ├─ CODEX_IMPLEMENTATION_EXPERIENCE_2026-03-24.md │ ├─ CODEX_PAST_WORK_EXPERIENCE_SUMMARY.md │ ├─ MOBILE_UI_DEV_EXPERIENCE.md │ ├─ PROJECT_DEVELOPMENT_EXPERIENCE.md │ └─ PROJECT_WORK_EXPERIENCE_PLAYBOOK.md ├─ planning/ │ ├─ README.md │ └─ CURRENT_GAME_ITERATION_PRIORITIES_2026-04-03.md ├─ prd/ │ ├─ AI_CHARACTER_VISUAL_ANIMATION_MVP_PRD_2026-04-04.md │ ├─ AI_NATIVE_CLASSIC_RPG_EXPERIENCE_BENCHMARK_PRD_2026-04-06.md │ ├─ AI_NATIVE_CROSS_GENRE_STORY_ENGINE_PRD_2026-04-06.md │ ├─ AI_NATIVE_CUSTOM_WORLD_CREATION_FLOW_OPTIMIZATION_PRD_2026-04-06.md │ ├─ AI_NATIVE_NARRATIVE_THREAD_ITEM_AND_WORLD_NPC_PRD_2026-04-06.md │ ├─ AI_NATIVE_QUEST_SYSTEM_PRD_2026-04-02.md │ ├─ AI_NATIVE_RUNTIME_ITEM_GENERATION_DESIGN.md │ ├─ AI_NATIVE_STORY_ENGINE_PHASE1_IMPLEMENTATION_PLAN_2026-04-06.md │ ├─ AI_NATIVE_STORY_ENGINE_PHASE2_IMPLEMENTATION_PLAN_2026-04-06.md │ ├─ AI_NATIVE_STORY_ENGINE_PHASE3_IMPLEMENTATION_PLAN_2026-04-06.md │ ├─ AI_NATIVE_STORY_ENGINE_PHASE4_IMPLEMENTATION_PLAN_2026-04-06.md │ ├─ AI_NATIVE_STORY_ENGINE_PHASE5_IMPLEMENTATION_PLAN_2026-04-06.md │ ├─ AI_NATIVE_STORY_ENGINE_PHASE6_IMPLEMENTATION_PLAN_2026-04-06.md │ ├─ AI_NATIVE_UNIFIED_ROLE_ATTRIBUTE_SYSTEM_PRD_2026-04-02.md │ ├─ BUILD_SYSTEM_ATTRIBUTE_SIMILARITY_PRD_2026-04-02.md │ └─ RUNTIME_ITEM_GENERATION_CURRENT_SYSTEM_DESIGN.md ├─ reference/[QwenSpriteSheetTool.tsx](src/tools/QwenSpriteSheetTool.tsx) │ ├─ README.md │ └─ FUNCTION_SCRIPT_CATALOG_2026-04-04.md └─ technical/ ├─ README.md ├─ AI_CHARACTER_ANIMATION_TECHNICAL_SOLUTION_2026-04-04.md ├─ GO_SERVER_RUNTIME_INTEGRATION_2026-04-07.md ├─ GO_SERVER_TASKLIST_2026-04-08.md ├─ NODE_SERVER_KNOWLEDGE_GRAPH_2026-04-08.md ├─ PIXELMOTION_TECHNICAL_BREAKDOWN_2026-04-04.md └─ SERVER_DEPLOYMENT_AND_CORS_TECHNICAL_SOLUTION_2026-04-05.md ``` # SpacetimeDB Rules (All Languages) ## Migrating from 1.0 to 2.0? **If you are migrating existing SpacetimeDB 1.0 code to 2.0, apply `spacetimedb-migration-2.0.mdc` first.** It documents breaking changes (reducer callbacks → event tables, `name`→`accessor`, `sender()` method, etc.) and should be considered before other rules. --- ## Language-Specific Rules | Language | Rule File | |----------|-----------| | **TypeScript/React** | `spacetimedb-typescript.mdc` (MANDATORY) | | **Rust** | `spacetimedb-rust.mdc` (MANDATORY) | | **C#** | `spacetimedb-csharp.mdc` (MANDATORY) | | **Migrating 1.0 → 2.0** | `spacetimedb-migration-2.0.mdc` | --- ## Core Concepts 1. **Reducers are transactional** — they do not return data to callers 2. **Reducers must be deterministic** — no filesystem, network, timers, or random 3. **Read data via tables/subscriptions** — not reducer return values 4. **Auto-increment IDs are not sequential** — gaps are normal, don't use for ordering 5. **`ctx.sender` is the authenticated principal** — never trust identity args --- ## Feature Implementation Checklist When implementing a feature that spans backend and client: 1. **Backend:** Define table(s) to store the data 2. **Backend:** Define reducer(s) to mutate the data 3. **Client:** Subscribe to the table(s) 4. **Client:** Call the reducer(s) from UI — **don't forget this step!** 5. **Client:** Render the data from the table(s) **Common mistake:** Building backend tables/reducers but forgetting to wire up the client to call them. --- ## Index System SpacetimeDB automatically creates indexes for: - Primary key columns - Columns marked as unique You can add explicit indexes on non-unique columns for query performance. **Index names must be unique across your entire module (all tables).** If two tables have indexes with the same declared name → conflict error. **Schema ↔ Code coupling:** - Your query code references indexes by name - If you add/remove/rename an index in the schema, update all code that uses it - Removing an index without updating queries causes runtime errors --- ## Commands ```bash # Login to allow remote database deployment e.g. to maincloud spacetime login # Start local SpacetimeDB spacetime start # Publish module spacetime publish --module-path # Clear and republish spacetime publish --clear-database -y --module-path # Generate client bindings spacetime generate --lang --out-dir --module-path # View logs spacetime logs ``` --- ## Deployment - Maincloud is the spacetimedb hosted cloud and the default location for module publishing - The default server marked by *** in `spacetime server list` should be used when publishing - If the default server is maincloud you should publish to maincloud - Publishing to maincloud is free of charge - When publishing to maincloud the database dashboard will be at the url: https://spacetimedb.com/@/ - The database owner can view utilization and performance metrics on the dashboard --- ## Debugging Checklist 1. Is SpacetimeDB server running? (`spacetime start`) 2. Is the module published? (`spacetime publish`) 3. Are client bindings generated? (`spacetime generate`) 4. Check server logs for errors (`spacetime logs `) 5. **Is the reducer actually being called from the client?** --- ## Editing Behavior - Make the smallest change necessary - Do NOT touch unrelated files, configs, or dependencies - Do NOT invent new SpacetimeDB APIs — use only what exists in docs or this repo - Do NOT add restrictions the prompt didn't ask for — if "users can do X", implement X for all users # SpacetimeDB Rust SDK ## ⛔ COMMON MISTAKES — LLM HALLUCINATIONS These are **actual errors** observed when LLMs generate SpacetimeDB Rust code: ### 1. Wrong Crate for Server vs Client ```rust // ❌ WRONG — using client crate for server module use spacetimedb_sdk::*; // This is for CLIENTS only! // ✅ CORRECT — use spacetimedb for server modules use spacetimedb::{table, reducer, Table, ReducerContext, Identity, Timestamp}; ``` ### 2. Wrong Table Macro Syntax ```rust // ❌ WRONG — using attribute-style like C# #[spacetimedb::table] #[primary_key] pub struct User { ... } // ❌ WRONG — SpacetimeType on tables (causes conflicts!) #[derive(SpacetimeType)] #[table(accessor = my_table)] pub struct MyTable { ... } // ✅ CORRECT — use #[table(...)] macro with options, NO SpacetimeType #[table(accessor = user, public)] pub struct User { #[primary_key] identity: Identity, name: Option, } ``` ### 3. Wrong Table Access Pattern ```rust // ❌ WRONG — using ctx.Db or ctx.db() method or field access ctx.Db.user.Insert(...); ctx.db().user().insert(...); ctx.db.player; // Field access // ✅ CORRECT — ctx.db is a field, table names are methods with parentheses ctx.db.user().insert(User { ... }); ctx.db.user().identity().find(ctx.sender); ctx.db.player().id().find(&player_id); ``` ### 4. Wrong Update Pattern ```rust // ❌ WRONG — partial update or using .update() directly on table ctx.db.user().update(User { name: Some("new".into()), ..Default::default() }); // ✅ CORRECT — find existing, spread it, update via primary key accessor if let Some(user) = ctx.db.user().identity().find(ctx.sender) { ctx.db.user().identity().update(User { name: Some("new".into()), ..user }); } ``` ### 5. Wrong Reducer Return Type ```rust // ❌ WRONG — returning data from reducer #[reducer] pub fn get_user(ctx: &ReducerContext, id: Identity) -> Option { ... } // ❌ WRONG — mutable context pub fn my_reducer(ctx: &mut ReducerContext, ...) { } // ✅ CORRECT — reducers return Result<(), String> or nothing, immutable context #[reducer] pub fn do_something(ctx: &ReducerContext, value: String) -> Result<(), String> { if value.is_empty() { return Err("Value cannot be empty".to_string()); } Ok(()) } ``` ### 6. Wrong Client Connection Pattern ```rust // ❌ WRONG — subscribing before connected let conn = DbConnection::builder().build()?; conn.subscription_builder().subscribe_to_all_tables(); // NOT CONNECTED YET! // ✅ CORRECT — subscribe in on_connect callback DbConnection::builder() .on_connect(|conn, identity, token| { conn.subscription_builder() .on_applied(|ctx| println!("Ready!")) .subscribe_to_all_tables(); }) .build()?; ``` ### 7. Forgetting to Advance the Connection ```rust // ❌ WRONG — connection never processes messages let conn = DbConnection::builder().build()?; // ... callbacks never fire ... // ✅ CORRECT — must call one of these to process messages conn.run_threaded(); // Spawn background thread // OR conn.run_async().await; // Async task // OR (in game loop) conn.frame_tick()?; // Manual polling ``` ### 8. Missing Table Trait Import ```rust // ❌ WRONG — "no method named `insert` found" use spacetimedb::{table, reducer, ReducerContext}; ctx.db.user().insert(...); // ERROR! // ✅ CORRECT — import Table trait for table methods use spacetimedb::{table, reducer, Table, ReducerContext}; ctx.db.user().insert(...); // Works! ``` ### 9. Wrong ScheduleAt Variant ```rust // ❌ WRONG — At variant doesn't exist scheduled_at: ScheduleAt::At(future_time), // ✅ CORRECT — use Time variant scheduled_at: ScheduleAt::Time(future_time), ``` ### 10. Identity to String Conversion ```rust // ❌ WRONG — to_hex() returns HexString<32>, not String let id: String = identity.to_hex(); // Type mismatch! // ✅ CORRECT — chain .to_string() let id: String = identity.to_hex().to_string(); ``` ### 11. Timestamp Duration Extraction ```rust // ❌ WRONG — returns Result, not Duration directly let micros = ctx.timestamp.to_duration_since_unix_epoch().as_micros(); // ✅ CORRECT — unwrap the Result let micros = ctx.timestamp.to_duration_since_unix_epoch() .unwrap_or_default() .as_micros(); ``` ### 12. Borrow After Move ```rust // ❌ WRONG — `tool` moved into struct, then borrowed ctx.db.stroke().insert(Stroke { tool, color, ... }); if tool == "eraser" { ... } // ERROR: value moved! // ✅ CORRECT — check before move, or use clone let is_eraser = tool == "eraser"; ctx.db.stroke().insert(Stroke { tool, color, ... }); if is_eraser { ... } ``` ### 13. Client SDK Uses Blocking I/O The SpacetimeDB Rust client SDK uses blocking I/O. If mixing with async runtimes (Tokio, async-std), use `spawn_blocking` or run the SDK on a dedicated thread to avoid blocking the async executor. ### 14. Wrong Schedule Syntax ```rust // ❌ WRONG — `schedule` is not a valid table type #[table(name = tick_timer, schedule(reducer = tick, column = scheduled_at))] // ✅ CORRECT — `scheduled` is a valid table type #[table(name = tick_timer, scheduled(reducer = tick, column = scheduled_at))] ``` --- ## 1) Common Mistakes Table ### Server-side errors | Wrong | Right | Error | |-------|-------|-------| | `#[derive(SpacetimeType)]` on `#[table]` | Remove it — macro handles this | Conflicting derive macros | | `ctx.db.player` (field access) | `ctx.db.player()` (method) | "no field `player` on type" | | `ctx.db.player().find(id)` | `ctx.db.player().id().find(&id)` | Must access via index | | `&mut ReducerContext` | `&ReducerContext` | Wrong context type | | Missing `use spacetimedb::Table;` | Add import | "no method named `insert`" | | `#[table(accessor = "my_table")]` | `#[table(accessor = my_table)]` | String literals not allowed | | Missing `public` on table | Add `public` flag | Clients can't subscribe | | `#[spacetimedb::reducer]` | `#[reducer]` after import | Wrong attribute path | | Network/filesystem in reducer | Use procedures instead | Sandbox violation | | Panic for expected errors | Return `Result<(), String>` | WASM instance destroyed | --- ## 2) Table Definition (CRITICAL) **Tables use `#[table(...)]` macro on `pub struct`. DO NOT derive `SpacetimeType` on tables!** > ⚠️ **CRITICAL:** Always import `Table` trait — required for `.insert()`, `.iter()`, `.find()`, etc. ```rust use spacetimedb::{table, reducer, Table, ReducerContext, Identity, Timestamp}; // ❌ WRONG — DO NOT derive SpacetimeType on tables! #[derive(SpacetimeType)] // REMOVE THIS! #[table(accessor = task)] pub struct Task { ... } // ✅ CORRECT — just the #[table] attribute #[table(accessor = user, public)] pub struct User { #[primary_key] identity: Identity, #[unique] username: Option, online: bool, } #[table(accessor = message, public)] pub struct Message { #[primary_key] #[auto_inc] id: u64, sender: Identity, text: String, sent: Timestamp, } // With multi-column index #[table(accessor = task, public, index(name = by_owner, btree(columns = [owner_id])))] pub struct Task { #[primary_key] #[auto_inc] pub id: u64, pub owner_id: Identity, pub title: String, } ``` ### Table Options ```rust #[table(accessor = my_table)] // Private table (default) #[table(accessor = my_table, public)] // Public table - clients can subscribe ``` ### Column Attributes ```rust #[primary_key] // Primary key (auto-indexed, enables .find()) #[auto_inc] // Auto-increment (use with #[primary_key]) #[unique] // Unique constraint (auto-indexed) #[index(btree)] // B-Tree index for queries ``` ### Insert returns ROW, not ID ```rust let row = ctx.db.task().insert(Task { id: 0, // auto-inc placeholder owner_id: ctx.sender, title: "New task".to_string(), created_at: ctx.timestamp, }); let new_id = row.id; // Get the actual ID ``` --- ## 3) Reducers ### Definition Syntax ```rust use spacetimedb::{reducer, ReducerContext, Table}; #[reducer] pub fn send_message(ctx: &ReducerContext, text: String) -> Result<(), String> { // Validate input if text.is_empty() { return Err("Message cannot be empty".to_string()); } // Insert returns the inserted row let row = ctx.db.message().insert(Message { id: 0, // auto-inc placeholder sender: ctx.sender, text, sent: ctx.timestamp, }); log::info!("Message {} sent by {:?}", row.id, ctx.sender); Ok(()) } ``` ### Update Pattern (CRITICAL) ```rust #[reducer] pub fn set_name(ctx: &ReducerContext, name: String) -> Result<(), String> { // Find existing row let user = ctx.db.user().identity().find(ctx.sender) .ok_or("User not found")?; // ✅ CORRECT — spread existing row, override specific fields ctx.db.user().identity().update(User { name: Some(name), ..user // Preserves identity, online, etc. }); Ok(()) } // ❌ WRONG — partial update nulls out other fields! // ctx.db.user().identity().update(User { identity: ctx.sender, name: Some(name), ..Default::default() }); ``` ### Delete Pattern ```rust #[reducer] pub fn delete_message(ctx: &ReducerContext, message_id: u64) -> Result<(), String> { ctx.db.message().id().delete(&message_id); Ok(()) } ``` ### Lifecycle Hooks ```rust #[reducer(init)] pub fn init(ctx: &ReducerContext) { // Called when module is first published } #[reducer(client_connected)] pub fn client_connected(ctx: &ReducerContext) { // ctx.sender is the connecting identity if let Some(user) = ctx.db.user().identity().find(ctx.sender) { ctx.db.user().identity().update(User { online: true, ..user }); } else { ctx.db.user().insert(User { identity: ctx.sender, username: None, online: true, }); } } #[reducer(client_disconnected)] pub fn client_disconnected(ctx: &ReducerContext) { if let Some(user) = ctx.db.user().identity().find(ctx.sender) { ctx.db.user().identity().update(User { online: false, ..user }); } } ``` ### ReducerContext fields ```rust ctx.sender // Identity of the caller ctx.timestamp // Current timestamp ctx.db // Database access ctx.rng // Deterministic RNG (use instead of rand) ``` --- ## 4) Index Access ### Primary Key / Unique — `.find()` returns `Option` ```rust // Primary key lookup let user = ctx.db.user().identity().find(ctx.sender); // Unique column lookup let user = ctx.db.user().username().find(&"alice".to_string()); if let Some(user) = user { // Found } ``` ### BTree Index — `.filter()` returns iterator ```rust #[table(accessor = message, public)] pub struct Message { #[primary_key] #[auto_inc] id: u64, #[index(btree)] room_id: u64, text: String, } // Filter by indexed column for msg in ctx.db.message().room_id().filter(&room_id) { // Process each message in room } ``` ### No Index — `.iter()` + manual filter ```rust // Full table scan for user in ctx.db.user().iter() { if user.online { // Process online users } } ``` --- ## 5) Custom Types **Use `#[derive(SpacetimeType)]` ONLY for custom structs/enums used as fields or parameters.** ```rust use spacetimedb::SpacetimeType; // Custom struct for table fields #[derive(SpacetimeType, Clone, Debug, PartialEq)] pub struct Position { pub x: i32, pub y: i32, } // Custom enum #[derive(SpacetimeType, Clone, Debug, PartialEq)] pub enum PlayerStatus { Idle, Walking(Position), Fighting(Identity), } // Use in table (DO NOT derive SpacetimeType on the table!) #[table(accessor = player, public)] pub struct Player { #[primary_key] pub id: Identity, pub position: Position, pub status: PlayerStatus, } ``` --- ## 6) Scheduled Tables ```rust use spacetimedb::{table, reducer, ReducerContext, ScheduleAt, Timestamp}; #[table(accessor = cleanup_job, scheduled(cleanup_expired))] pub struct CleanupJob { #[primary_key] #[auto_inc] scheduled_id: u64, scheduled_at: ScheduleAt, target_id: u64, } #[reducer] pub fn cleanup_expired(ctx: &ReducerContext, job: CleanupJob) { // Job row is auto-deleted after reducer completes log::info!("Cleaning up: {}", job.target_id); } // Schedule a job #[reducer] pub fn schedule_cleanup(ctx: &ReducerContext, target_id: u64, delay_ms: u64) { let future_time = ctx.timestamp + std::time::Duration::from_millis(delay_ms); ctx.db.cleanup_job().insert(CleanupJob { scheduled_id: 0, // auto-inc placeholder scheduled_at: ScheduleAt::Time(future_time), target_id, }); } // Cancel by deleting the row #[reducer] pub fn cancel_cleanup(ctx: &ReducerContext, job_id: u64) { ctx.db.cleanup_job().scheduled_id().delete(&job_id); } ``` --- ## 7) Client SDK ```rust // Connection pattern let conn = DbConnection::builder() .with_uri("http://localhost:3000") .with_module_name("my-module") .with_token(load_saved_token()) // None for first connection .on_connect(on_connected) .build() .expect("Failed to connect"); // Subscribe in on_connect callback, NOT before! fn on_connected(conn: &DbConnection, identity: Identity, token: &str) { conn.subscription_builder() .on_applied(|ctx| println!("Ready!")) .subscribe_to_all_tables(); } ``` ### ⚠️ CRITICAL: Advance the Connection **You MUST call one of these** — without it, no callbacks fire: ```rust conn.run_threaded(); // Background thread (simplest) conn.run_async().await; // Async task conn.frame_tick()?; // Manual polling (game loops) ``` ### Table Access & Callbacks ```rust // Iterate for user in ctx.db.user().iter() { ... } // Find by primary key if let Some(user) = ctx.db.user().identity().find(&identity) { ... } // Row callbacks ctx.db.user().on_insert(|ctx, user| { ... }); ctx.db.user().on_update(|ctx, old, new| { ... }); ctx.db.user().on_delete(|ctx, user| { ... }); // Call reducers ctx.reducers.set_name("Alice".to_string()).unwrap(); ``` --- ## 8) Procedures (Beta) **Procedures are for side effects (HTTP, filesystem) that reducers can't do.** ⚠️ Procedures are currently in beta. API may change. ```rust use spacetimedb::{procedure, ProcedureContext}; // Simple procedure #[procedure] fn add_numbers(_ctx: &mut ProcedureContext, a: u32, b: u32) -> u64 { a as u64 + b as u64 } // Procedure with database access #[procedure] fn save_external_data(ctx: &mut ProcedureContext, url: String) -> Result<(), String> { // HTTP request (allowed in procedures, not reducers) let data = fetch_from_url(&url)?; // Database access requires explicit transaction ctx.try_with_tx(|tx| { tx.db.external_data().insert(ExternalData { id: 0, content: data, }); Ok(()) })?; Ok(()) } ``` ### Key differences from reducers | Reducers | Procedures | |----------|------------| | `&ReducerContext` (immutable) | `&mut ProcedureContext` (mutable) | | Direct `ctx.db` access | Must use `ctx.with_tx()` | | No HTTP/network | HTTP allowed | | No return values | Can return data | --- ## 9) Logging ```rust use spacetimedb::log; log::trace!("Detailed trace"); log::debug!("Debug info"); log::info!("Information"); log::warn!("Warning"); log::error!("Error occurred"); ``` --- ## 10) Commands ```bash # Start local server spacetime start # Publish module spacetime publish --module-path # Clear database and republish spacetime publish --clear-database -y --module-path # Generate bindings spacetime generate --lang rust --out-dir /src/module_bindings --module-path # View logs spacetime logs ``` --- ## 11) Hard Requirements **Rust-specific:** 1. **DO NOT derive `SpacetimeType` on `#[table]` structs** — the macro handles this 2. **Import `Table` trait** — `use spacetimedb::Table;` required for `.insert()`, `.iter()`, etc. 3. **Use `&ReducerContext`** — not `&mut ReducerContext` 4. **Tables are methods** — `ctx.db.table()` not `ctx.db.table` 5. **Server modules use `spacetimedb` crate** — clients use `spacetimedb-sdk` 6. **Reducers must be deterministic** — no filesystem, network, timers, or external RNG 7. **Use `ctx.rng`** — not `rand` crate for random numbers 8. **Use `ctx.timestamp`** — never `std::time::SystemTime::now()` in reducers 9. **Client MUST advance connection** — call `run_threaded()`, `run_async()`, or `frame_tick()` 10. **Subscribe in `on_connect` callback** — not before connection is established 11. **Update requires full row** — spread existing row with `..existing` 12. **DO NOT edit generated bindings** — regenerate with `spacetime generate` 13. **Identity to String needs `.to_string()`** — `identity.to_hex().to_string()` 14. **Client SDK is blocking** — use `spawn_blocking` or dedicated thread if mixing with async runtimes