Files
Shubham Mishra e4098f98d9 Rust: macro change name -> accessor (#4264)
## Description of Changes

This PR primarily affects the `bindings-macro` and `schema` crates to
review:

### Core changes

1. Replaces the `name` macro with `accessor` for **Tables, Views,
Procedures, and Reducers** in Rust modules.
2. Extends `RawModuleDefV10` with a new section for:

   * case conversion policies
   * explicit names
    New sections are not validated in this PR so not functional.
3. Updates index behavior:

* Index names are now always **system-generated** for clients. Which
will be fixed in follow-up PR when we start validating RawModuleDef with
explicit names.
   * The `accessor` name for an index is used only inside the module.


## Breaking changes (API/ABI)

1. **Rust modules**

   * The `name` macro must be replaced with `accessor`.

2. **Client bindings (all languages)**

* Index names are now system-generated instead of using explicitly
provided names.


**Complexity:** 3

A follow-up PR will reintroduce explicit names with support for case
conversion.

---------

Co-authored-by: rekhoff <r.ekhoff@clockworklabs.io>
Co-authored-by: clockwork-labs-bot <clockwork-labs-bot@users.noreply.github.com>
Co-authored-by: clockwork-labs-bot <bot@clockworklabs.com>
2026-02-16 15:23:50 +00:00

435 lines
12 KiB
Rust

//! STDB module used for benchmarks based on "realistic" workloads we are focusing in improving.
#![allow(clippy::too_many_arguments, unused_variables)]
use crate::Load;
use spacetimedb::{log, ReducerContext, SpacetimeType, Table};
use std::hash::{Hash, Hasher};
#[spacetimedb::table(accessor = velocity)]
pub struct Velocity {
#[primary_key]
pub entity_id: u32,
pub x: f32,
pub y: f32,
pub z: f32,
}
impl Velocity {
pub fn new(entity_id: u32, x: f32, y: f32, z: f32) -> Self {
Self { entity_id, x, y, z }
}
}
#[spacetimedb::table(accessor = position)]
pub struct Position {
#[primary_key]
pub entity_id: u32,
pub x: f32,
pub y: f32,
pub z: f32,
pub vx: f32,
pub vy: f32,
pub vz: f32,
}
impl Position {
pub fn new(entity_id: u32, x: f32, y: f32, z: f32) -> Self {
Self {
entity_id,
x,
y,
z,
vx: x + 10.0,
vy: y + 20.0,
vz: z + 30.0,
}
}
}
pub fn moment_milliseconds() -> u64 {
1
// Duration::from_micros(1000).as_millis() as u64
// or previously...
// Timestamp::from_micros_since_unix_epoch(1000)
// .duration_since(Timestamp::UNIX_EPOCH)
// .ok()
// .unwrap()
// .as_millis() as u64
}
#[derive(SpacetimeType, Debug, Clone, Copy)]
pub enum AgentAction {
Inactive,
Idle,
Evading,
Investigating,
Retreating,
Fighting,
}
#[spacetimedb::table(accessor = game_enemy_ai_agent_state)]
#[derive(Clone)]
pub struct GameEnemyAiAgentState {
#[primary_key]
pub entity_id: u64,
pub last_move_timestamps: Vec<u64>,
pub next_action_timestamp: u64,
pub action: AgentAction,
}
#[spacetimedb::table(accessor = game_targetable_state)]
#[derive(Clone)]
pub struct GameTargetableState {
#[primary_key]
pub entity_id: u64,
pub quad: i64,
}
#[spacetimedb::table(accessor = game_live_targetable_state)]
pub struct GameLiveTargetableState {
#[unique]
pub entity_id: u64,
#[index(btree)]
pub quad: i64,
}
#[spacetimedb::table(accessor = game_mobile_entity_state)]
pub struct GameMobileEntityState {
#[primary_key]
pub entity_id: u64,
#[index(btree)]
pub location_x: i32,
pub location_y: i32,
pub timestamp: u64,
}
#[spacetimedb::table(accessor = game_enemy_state)]
#[derive(Clone)]
pub struct GameEnemyState {
#[primary_key]
pub entity_id: u64,
pub herd_id: i32,
}
#[derive(SpacetimeType, Default, Copy, Clone, Debug, PartialEq)]
pub struct SmallHexTile {
pub x: i32,
pub z: i32,
pub dimension: u32,
}
#[spacetimedb::table(accessor = game_herd_cache)]
#[derive(Clone, Debug)]
pub struct GameHerdCache {
#[primary_key]
pub id: i32,
pub dimension_id: u32,
pub current_population: i32,
pub location: SmallHexTile,
pub max_population: i32,
pub spawn_eagerness: f32,
pub roaming_distance: i32,
}
fn calculate_hash<T: Hash>(t: &T) -> u64 {
let mut s = std::collections::hash_map::DefaultHasher::new();
t.hash(&mut s);
s.finish()
}
// ---------- insert bulk ----------
#[spacetimedb::reducer]
pub fn insert_bulk_position(ctx: &ReducerContext, count: u32) {
for id in 0..count {
ctx.db
.position()
.insert(Position::new(id, id as f32, (id + 5) as f32, (id * 5) as f32));
}
log::info!("INSERT POSITION: {count}");
}
#[spacetimedb::reducer]
pub fn insert_bulk_velocity(ctx: &ReducerContext, count: u32) {
for id in 0..count {
ctx.db
.velocity()
.insert(Velocity::new(id, id as f32, (id + 5) as f32, (id * 5) as f32));
}
log::info!("INSERT VELOCITY: {count}");
}
// Simulate
// ```
// UPDATE Position SET
// x = x + vx,
// y = y + vy,
// z = z + vz;
// ```
#[spacetimedb::reducer]
pub fn update_position_all(ctx: &ReducerContext, expected: u32) {
let mut count = 0;
for mut position in ctx.db.position().iter() {
position.x += position.vx;
position.y += position.vy;
position.z += position.vz;
let id = position.entity_id;
ctx.db.position().entity_id().update(position);
count += 1;
}
log::info!("UPDATE POSITION ALL: {expected}, processed: {count}");
}
// Simulate
// ```
// UPDATE Position
// SET
// x = Position.x + Velocity.x,
// y = Position.y + Velocity.y,
// z = Position.z + Velocity.z
// FROM Velocity
// WHERE Position.entity_id = Velocity.entity_id;
// ```
#[spacetimedb::reducer]
pub fn update_position_with_velocity(ctx: &ReducerContext, expected: u32) {
let mut count = 0;
for velocity in ctx.db.velocity().iter() {
let Some(mut position) = ctx.db.position().entity_id().find(velocity.entity_id) else {
continue;
};
position.x += velocity.x;
position.y += velocity.y;
position.z += velocity.z;
let id = position.entity_id;
ctx.db.position().entity_id().update(position);
count += 1;
}
log::info!("UPDATE POSITION BY VELOCITY: {expected}, processed: {count}");
}
// Simulations for a game loop
#[spacetimedb::reducer]
pub fn insert_world(ctx: &ReducerContext, players: u64) {
for (i, id) in (0..players).enumerate() {
let next_action_timestamp = if i & 2 == 2 {
moment_milliseconds() + 2000 // Check every 2secs
} else {
moment_milliseconds()
};
ctx.db.game_enemy_ai_agent_state().insert(GameEnemyAiAgentState {
entity_id: id,
next_action_timestamp,
last_move_timestamps: vec![id, 0, id * 2],
action: AgentAction::Idle,
});
ctx.db.game_live_targetable_state().insert(GameLiveTargetableState {
entity_id: id,
quad: id as i64,
});
ctx.db.game_targetable_state().insert(GameTargetableState {
entity_id: id,
quad: id as i64,
});
ctx.db.game_mobile_entity_state().insert(GameMobileEntityState {
entity_id: id,
location_x: id as i32,
location_y: id as i32,
timestamp: next_action_timestamp,
});
ctx.db.game_enemy_state().insert(GameEnemyState {
entity_id: id,
herd_id: id as i32,
});
ctx.db.game_herd_cache().insert(GameHerdCache {
id: id as i32,
dimension_id: id as u32,
current_population: id as i32 * 2,
max_population: id as i32 * 4,
spawn_eagerness: id as f32,
roaming_distance: id as i32,
location: SmallHexTile {
x: id as i32,
z: id as i32,
dimension: id as u32 * 2,
},
});
}
log::info!("INSERT WORLD PLAYERS: {players}");
}
fn get_targetables_near_quad(ctx: &ReducerContext, entity_id: u64, num_players: u64) -> Vec<GameTargetableState> {
let mut result = Vec::with_capacity(4);
for id in entity_id..num_players {
for t in ctx.db.game_live_targetable_state().quad().filter(&(id as i64)) {
result.push(
ctx.db
.game_targetable_state()
.entity_id()
.find(t.entity_id)
.expect("Identity not found"),
)
}
}
result
}
const MAX_MOVE_TIMESTAMPS: usize = 20;
fn move_agent(
ctx: &ReducerContext,
agent: &mut GameEnemyAiAgentState,
agent_coord: SmallHexTile,
current_time_ms: u64,
) {
let entity_id = agent.entity_id;
let enemy = ctx
.db
.game_enemy_state()
.entity_id()
.find(entity_id)
.expect("GameEnemyState Entity ID not found")
.clone();
ctx.db.game_enemy_state().entity_id().update(enemy);
agent.next_action_timestamp = current_time_ms + 2000;
// Keep track of the last [MAX_MOVE_TIMESTAMPS] movements
agent.last_move_timestamps.push(current_time_ms);
if agent.last_move_timestamps.len() > MAX_MOVE_TIMESTAMPS {
agent.last_move_timestamps.remove(0);
}
// Update targetable to the destination
let mut targetable = ctx
.db
.game_targetable_state()
.entity_id()
.find(entity_id)
.expect("GameTargetableState Entity ID not found");
let new_hash = calculate_hash(&targetable.quad) as i64;
targetable.quad = new_hash;
ctx.db.game_targetable_state().entity_id().update(targetable);
// If the entity is alive (which it should be),
// also update the `LiveTargetableState` used by `enemy_ai_agent_loop`.
if ctx
.db
.game_live_targetable_state()
.entity_id()
.find(entity_id)
.is_some()
{
ctx.db.game_live_targetable_state().entity_id().delete(entity_id);
ctx.db.game_live_targetable_state().insert(GameLiveTargetableState {
entity_id,
quad: new_hash,
});
}
let mobile_entity = ctx
.db
.game_mobile_entity_state()
.entity_id()
.find(entity_id)
.expect("GameMobileEntityState Entity ID not found");
let mobile_entity = GameMobileEntityState {
entity_id,
location_x: mobile_entity.location_x + 1,
location_y: mobile_entity.location_y + 1,
timestamp: agent.next_action_timestamp,
};
ctx.db.game_enemy_ai_agent_state().entity_id().update(agent.clone());
ctx.db.game_mobile_entity_state().entity_id().update(mobile_entity);
}
fn agent_loop(
ctx: &ReducerContext,
mut agent: GameEnemyAiAgentState,
agent_targetable: GameTargetableState,
surrounding_agents: &[GameTargetableState],
current_time_ms: u64,
) {
let entity_id = agent.entity_id;
let coordinates = ctx
.db
.game_mobile_entity_state()
.entity_id()
.find(entity_id)
.expect("GameMobileEntityState Entity ID not found");
let agent_entity = ctx
.db
.game_enemy_state()
.entity_id()
.find(entity_id)
.expect("GameEnemyState Entity ID not found");
let agent_herd = ctx
.db
.game_herd_cache()
.id()
.find(agent_entity.herd_id)
.expect("GameHerdCache Entity ID not found");
let agent_herd_coordinates = agent_herd.location;
move_agent(ctx, &mut agent, agent_herd_coordinates, current_time_ms);
}
// We check only for a single pass in the game loop.
#[spacetimedb::reducer]
pub fn game_loop_enemy_ia(ctx: &ReducerContext, players: u64) {
let mut count = 0;
let current_time_ms = moment_milliseconds();
for mut agent in ctx.db.game_enemy_ai_agent_state().iter() {
let agent_targetable = ctx
.db
.game_targetable_state()
.entity_id()
.find(agent.entity_id)
.expect("No TargetableState for AgentState entity");
let surrounding_agents = get_targetables_near_quad(ctx, agent_targetable.entity_id, players);
agent.action = AgentAction::Fighting;
agent_loop(ctx, agent, agent_targetable, &surrounding_agents, current_time_ms);
count += 1;
}
log::info!("ENEMY IA LOOP PLAYERS: {players}, processed: {count}");
}
#[spacetimedb::reducer]
pub fn init_game_ia_loop(ctx: &ReducerContext, initial_load: u32) {
let load = Load::new(initial_load);
insert_bulk_position(ctx, load.biggest_table);
insert_bulk_velocity(ctx, load.big_table);
update_position_all(ctx, load.biggest_table);
update_position_with_velocity(ctx, load.big_table);
insert_world(ctx, load.num_players as u64);
}
#[spacetimedb::reducer]
pub fn run_game_ia_loop(ctx: &ReducerContext, initial_load: u32) {
let load = Load::new(initial_load);
game_loop_enemy_ia(ctx, load.num_players as u64);
}