From f4902226553ec1db5d3d806ffbacd2b061f3992b Mon Sep 17 00:00:00 2001 From: Larko <59736843+Larkooo@users.noreply.github.com> Date: Thu, 1 Aug 2024 17:11:09 -0400 Subject: [PATCH] feat(torii-core): store update member (#2182) * feat(torii-core): store update member * feat; add set_model_member for updating specific member * chore: format * chore: update log to include member nname * fmt * refactor: clean code & add is event message * fmt * chore * feat: emit the store member update from the world * fix: ensure event messages are emitted with the correct selector * fix: cairo fmt * fix: fix test typo * wip: * fix: simple types model memebr update * chore; pass memmber type to func * chore: clean code * fmt * wip * fix: upsert to avoid constraint CHECK failing * fmt * fix: figure out way to fix upsert issue to reuse func * refactor: refactor recursive set to handle store update member * fix: fix world typo and merge * fix: struct from ty as mutable * fix: compilable torii * feat: wrap up store update member * fmt * fix fmt * fmt * cairo fmt * fix: update example to set array with new model API * fix: handle arrays * fix: correctly handle arrays * chore * fix: test --------- Co-authored-by: glihm --- Cargo.lock | 41 ++++++- bin/torii/src/main.rs | 2 + .../dojo-core/src/world/world_contract.cairo | 23 +++- .../manifests/dev/base/abis/dojo-world.json | 32 ++++++ .../manifests/dev/base/dojo-world.toml | 4 +- crates/dojo-world/src/contracts/abi/world.rs | 32 ++++++ crates/sozo/ops/src/events.rs | 2 +- crates/sozo/ops/src/tests/model.rs | 2 +- crates/torii/core/src/processors/mod.rs | 1 + .../src/processors/store_update_member.rs | 108 ++++++++++++++++++ crates/torii/core/src/sql.rs | 94 +++++++++++---- examples/spawn-and-move/Scarb.toml | 2 +- .../dojo_examples-actions-40b6994c.json | 24 +++- .../manifests/dev/base/abis/dojo-world.json | 32 ++++++ .../dojo_examples-actions-40b6994c.toml | 4 +- .../manifests/dev/base/dojo-world.toml | 4 +- .../dojo_examples-actions-40b6994c.json | 24 +++- .../dev/deployment/abis/dojo-world.json | 32 ++++++ .../manifests/dev/deployment/manifest.json | 76 +++++++++--- .../manifests/dev/deployment/manifest.toml | 20 ++-- .../dojo_examples-actions-40b6994c.json | 24 +++- .../release/base/abis/dojo-world.json | 32 ++++++ .../dojo_examples-actions-40b6994c.toml | 4 +- .../manifests/release/base/dojo-world.toml | 4 +- examples/spawn-and-move/src/actions.cairo | 21 ++-- examples/spawn-and-move/src/models.cairo | 2 +- 26 files changed, 564 insertions(+), 82 deletions(-) create mode 100644 crates/torii/core/src/processors/store_update_member.rs diff --git a/Cargo.lock b/Cargo.lock index 5340c8fcd7..7e9c750137 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4141,7 +4141,16 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8" dependencies = [ - "derive_builder_macro", + "derive_builder_macro 0.12.0", +] + +[[package]] +name = "derive_builder" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0350b5cb0331628a5916d6c5c0b72e97393b8b6b03b47a9284f4e7f5a405ffd7" +dependencies = [ + "derive_builder_macro 0.20.0", ] [[package]] @@ -4156,16 +4165,38 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive_builder_core" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d48cda787f839151732d396ac69e3473923d54312c070ee21e9effcaa8ca0b1d" +dependencies = [ + "darling 0.20.10", + "proc-macro2", + "quote", + "syn 2.0.71", +] + [[package]] name = "derive_builder_macro" version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e" dependencies = [ - "derive_builder_core", + "derive_builder_core 0.12.0", "syn 1.0.109", ] +[[package]] +name = "derive_builder_macro" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b" +dependencies = [ + "derive_builder_core 0.20.0", + "syn 2.0.71", +] + [[package]] name = "derive_more" version = "0.99.18" @@ -11879,7 +11910,7 @@ dependencies = [ "create-output-dir", "data-encoding", "deno_task_shell", - "derive_builder", + "derive_builder 0.20.0", "directories", "dunce", "fs4", @@ -11955,7 +11986,7 @@ version = "1.12.0" source = "git+https://github.com/software-mansion/scarb?tag=v2.7.0-rc.4#88bf93564b905336e6f34e0243e30ce7ce8859ec" dependencies = [ "camino", - "derive_builder", + "derive_builder 0.20.0", "semver 1.0.23", "serde", "serde_json", @@ -15310,7 +15341,7 @@ source = "git+https://github.com/cartridge-gg/wasm-webauthn?rev=972693f#972693fd dependencies = [ "ciborium", "coset", - "derive_builder", + "derive_builder 0.12.0", "js-sys", "serde", "serde-wasm-bindgen", diff --git a/bin/torii/src/main.rs b/bin/torii/src/main.rs index 27731e05a4..d9035fc612 100644 --- a/bin/torii/src/main.rs +++ b/bin/torii/src/main.rs @@ -33,6 +33,7 @@ use torii_core::processors::register_model::RegisterModelProcessor; use torii_core::processors::store_del_record::StoreDelRecordProcessor; use torii_core::processors::store_set_record::StoreSetRecordProcessor; use torii_core::processors::store_transaction::StoreTransactionProcessor; +use torii_core::processors::store_update_member::StoreUpdateMemberProcessor; use torii_core::processors::store_update_record::StoreUpdateRecordProcessor; use torii_core::simple_broker::SimpleBroker; use torii_core::sql::Sql; @@ -172,6 +173,7 @@ async fn main() -> anyhow::Result<()> { Box::new(StoreDelRecordProcessor), Box::new(EventMessageProcessor), Box::new(StoreUpdateRecordProcessor), + Box::new(StoreUpdateMemberProcessor), ], transaction: vec![Box::new(StoreTransactionProcessor)], ..Processors::default() diff --git a/crates/dojo-core/src/world/world_contract.cairo b/crates/dojo-core/src/world/world_contract.cairo index 8270e8eca2..a84d4e7754 100644 --- a/crates/dojo-core/src/world/world_contract.cairo +++ b/crates/dojo-core/src/world/world_contract.cairo @@ -145,6 +145,7 @@ pub mod world { ModelRegistered: ModelRegistered, StoreSetRecord: StoreSetRecord, StoreUpdateRecord: StoreUpdateRecord, + StoreUpdateMember: StoreUpdateMember, StoreDelRecord: StoreDelRecord, WriterUpdated: WriterUpdated, OwnerUpdated: OwnerUpdated, @@ -219,6 +220,14 @@ pub mod world { pub values: Span, } + #[derive(Drop, starknet::Event)] + pub struct StoreUpdateMember { + pub table: felt252, + pub entity_id: felt252, + pub member_selector: felt252, + pub values: Span, + } + #[derive(Drop, starknet::Event)] pub struct StoreDelRecord { pub table: felt252, @@ -821,10 +830,18 @@ pub mod world { ); }, ModelIndex::MemberId(( - entity_id, member_id + entity_id, member_selector )) => { - self.write_model_member(model_selector, entity_id, member_id, values, layout); - // TODO: here we need a new event update and see how Torii can process that. + self + .write_model_member( + model_selector, entity_id, member_selector, values, layout + ); + EventEmitter::emit( + ref self, + StoreUpdateMember { + table: model_selector, entity_id, member_selector, values + } + ); } } } diff --git a/crates/dojo-lang/src/manifest_test_data/compiler_cairo/manifests/dev/base/abis/dojo-world.json b/crates/dojo-lang/src/manifest_test_data/compiler_cairo/manifests/dev/base/abis/dojo-world.json index b12806373b..62ff4b6bfa 100644 --- a/crates/dojo-lang/src/manifest_test_data/compiler_cairo/manifests/dev/base/abis/dojo-world.json +++ b/crates/dojo-lang/src/manifest_test_data/compiler_cairo/manifests/dev/base/abis/dojo-world.json @@ -975,6 +975,33 @@ } ] }, + { + "type": "event", + "name": "dojo::world::world_contract::world::StoreUpdateMember", + "kind": "struct", + "members": [ + { + "name": "table", + "type": "core::felt252", + "kind": "data" + }, + { + "name": "entity_id", + "type": "core::felt252", + "kind": "data" + }, + { + "name": "member_selector", + "type": "core::felt252", + "kind": "data" + }, + { + "name": "values", + "type": "core::array::Span::", + "kind": "data" + } + ] + }, { "type": "event", "name": "dojo::world::world_contract::world::StoreDelRecord", @@ -1156,6 +1183,11 @@ "type": "dojo::world::world_contract::world::StoreUpdateRecord", "kind": "nested" }, + { + "name": "StoreUpdateMember", + "type": "dojo::world::world_contract::world::StoreUpdateMember", + "kind": "nested" + }, { "name": "StoreDelRecord", "type": "dojo::world::world_contract::world::StoreDelRecord", diff --git a/crates/dojo-lang/src/manifest_test_data/compiler_cairo/manifests/dev/base/dojo-world.toml b/crates/dojo-lang/src/manifest_test_data/compiler_cairo/manifests/dev/base/dojo-world.toml index 3390095088..59d4719031 100644 --- a/crates/dojo-lang/src/manifest_test_data/compiler_cairo/manifests/dev/base/dojo-world.toml +++ b/crates/dojo-lang/src/manifest_test_data/compiler_cairo/manifests/dev/base/dojo-world.toml @@ -1,6 +1,6 @@ kind = "Class" -class_hash = "0x4c90da98d2bad157dec1d7b9b9c8c5861828a7ec1b323425d84fa6c3071303f" -original_class_hash = "0x4c90da98d2bad157dec1d7b9b9c8c5861828a7ec1b323425d84fa6c3071303f" +class_hash = "0x2178527e9556d1aa21d3c2961d28f9114fcfed81b4c3674ed591c50ce46cc9d" +original_class_hash = "0x2178527e9556d1aa21d3c2961d28f9114fcfed81b4c3674ed591c50ce46cc9d" abi = "manifests/dev/base/abis/dojo-world.json" tag = "dojo-world" manifest_name = "dojo-world" diff --git a/crates/dojo-world/src/contracts/abi/world.rs b/crates/dojo-world/src/contracts/abi/world.rs index bed453d1cc..afca797f35 100644 --- a/crates/dojo-world/src/contracts/abi/world.rs +++ b/crates/dojo-world/src/contracts/abi/world.rs @@ -981,6 +981,33 @@ abigen!( } ] }, + { + "type": "event", + "name": "dojo::world::world_contract::world::StoreUpdateMember", + "kind": "struct", + "members": [ + { + "name": "table", + "type": "core::felt252", + "kind": "data" + }, + { + "name": "entity_id", + "type": "core::felt252", + "kind": "data" + }, + { + "name": "member_selector", + "type": "core::felt252", + "kind": "data" + }, + { + "name": "values", + "type": "core::array::Span::", + "kind": "data" + } + ] + }, { "type": "event", "name": "dojo::world::world_contract::world::StoreDelRecord", @@ -1162,6 +1189,11 @@ abigen!( "type": "dojo::world::world_contract::world::StoreUpdateRecord", "kind": "nested" }, + { + "name": "StoreUpdateMember", + "type": "dojo::world::world_contract::world::StoreUpdateMember", + "kind": "nested" + }, { "name": "StoreDelRecord", "type": "dojo::world::world_contract::world::StoreDelRecord", diff --git a/crates/sozo/ops/src/events.rs b/crates/sozo/ops/src/events.rs index d6b3729277..9e48af7583 100644 --- a/crates/sozo/ops/src/events.rs +++ b/crates/sozo/ops/src/events.rs @@ -291,7 +291,7 @@ mod tests { let result = extract_events(&manifest, &project_dir, &target_dir).unwrap(); // we are just collecting all events from manifest file so just verifying count should work - assert_eq!(result.len(), 17); + assert_eq!(result.len(), 18); } #[test] diff --git a/crates/sozo/ops/src/tests/model.rs b/crates/sozo/ops/src/tests/model.rs index 2a2bb94cf2..fb7da33f5e 100644 --- a/crates/sozo/ops/src/tests/model.rs +++ b/crates/sozo/ops/src/tests/model.rs @@ -37,7 +37,7 @@ async fn test_model_ops() { ) .await .unwrap(), - Felt::from_hex("0x2b14bbb22e6a21cc949e06a187436c96aeab0e0290b3a8d91fb357ed2e6d973") + Felt::from_hex("0x4eddd8563a17c7d256b35e3cb0decdfcdfe122dd72593ebc572cfc535941ac2") .unwrap() ); diff --git a/crates/torii/core/src/processors/mod.rs b/crates/torii/core/src/processors/mod.rs index 6f53b567dc..e2b22e4d75 100644 --- a/crates/torii/core/src/processors/mod.rs +++ b/crates/torii/core/src/processors/mod.rs @@ -12,6 +12,7 @@ pub mod register_model; pub mod store_del_record; pub mod store_set_record; pub mod store_transaction; +pub mod store_update_member; pub mod store_update_record; const MODEL_INDEX: usize = 0; diff --git a/crates/torii/core/src/processors/store_update_member.rs b/crates/torii/core/src/processors/store_update_member.rs new file mode 100644 index 0000000000..a7ddcab839 --- /dev/null +++ b/crates/torii/core/src/processors/store_update_member.rs @@ -0,0 +1,108 @@ +use anyhow::{Context, Error, Result}; +use async_trait::async_trait; +use dojo_world::contracts::model::ModelReader; +use dojo_world::contracts::naming; +use dojo_world::contracts::world::WorldContractReader; +use num_traits::ToPrimitive; +use starknet::core::types::{Event, TransactionReceiptWithBlockInfo}; +use starknet::core::utils::get_selector_from_name; +use starknet::providers::Provider; +use tracing::{info, warn}; + +use super::EventProcessor; +use crate::processors::{ENTITY_ID_INDEX, MODEL_INDEX}; +use crate::sql::Sql; + +pub(crate) const LOG_TARGET: &str = "torii_core::processors::store_update_member"; + +const MEMBER_INDEX: usize = 2; + +#[derive(Default, Debug)] +pub struct StoreUpdateMemberProcessor; + +#[async_trait] +impl

EventProcessor

for StoreUpdateMemberProcessor +where + P: Provider + Send + Sync + std::fmt::Debug, +{ + fn event_key(&self) -> String { + "StoreUpdateMember".to_string() + } + + fn validate(&self, event: &Event) -> bool { + if event.keys.len() > 1 { + info!( + target: LOG_TARGET, + event_key = %>::event_key(self), + invalid_keys = %>::event_keys_as_string(self, event), + "Invalid event keys." + ); + return false; + } + true + } + + async fn process( + &self, + _world: &WorldContractReader

, + db: &mut Sql, + _block_number: u64, + block_timestamp: u64, + _transaction_receipt: &TransactionReceiptWithBlockInfo, + event_id: &str, + event: &Event, + ) -> Result<(), Error> { + let selector = event.data[MODEL_INDEX]; + let entity_id = event.data[ENTITY_ID_INDEX]; + let member_selector = event.data[MEMBER_INDEX]; + + let model = db.model(selector).await?; + let schema = model.schema().await?; + + let mut member = schema + .as_struct() + .expect("model schema must be a struct") + .children + .iter() + .find(|c| { + get_selector_from_name(&c.name).expect("invalid selector for member name") + == member_selector + }) + .context("member not found")? + .clone(); + + info!( + target: LOG_TARGET, + name = %model.name(), + entity_id = format!("{:#x}", entity_id), + member = %member.name, + "Store update member.", + ); + + let values_start = MEMBER_INDEX + 1; + let values_end: usize = + values_start + event.data[values_start].to_usize().context("invalid usize")?; + + // Skip the length to only get the values as they will be deserialized. + let mut values = event.data[values_start + 1..=values_end].to_vec(); + + let tag = naming::get_tag(model.namespace(), model.name()); + + if !db.does_entity_exist(tag.clone(), entity_id).await? { + warn!( + target: LOG_TARGET, + tag, + entity_id = format!("{:#x}", entity_id), + "Entity not found, must be set before updating a member.", + ); + + return Ok(()); + } + + member.ty.deserialize(&mut values)?; + + db.set_model_member(&schema.name(), entity_id, false, &member, event_id, block_timestamp) + .await?; + Ok(()) + } +} diff --git a/crates/torii/core/src/sql.rs b/crates/torii/core/src/sql.rs index 69edbc3d70..776f3754ea 100644 --- a/crates/torii/core/src/sql.rs +++ b/crates/torii/core/src/sql.rs @@ -4,7 +4,7 @@ use std::str::FromStr; use anyhow::{anyhow, Result}; use chrono::Utc; use dojo_types::primitive::Primitive; -use dojo_types::schema::{EnumOption, Member, Ty}; +use dojo_types::schema::{EnumOption, Member, Struct, Ty}; use dojo_world::contracts::abi::model::Layout; use dojo_world::contracts::naming::compute_selector_from_names; use dojo_world::metadata::WorldMetadata; @@ -23,6 +23,9 @@ use crate::types::{ }; use crate::utils::{must_utc_datetime_from_timestamp, utc_dt_string_from_timestamp}; +type IsEventMessage = bool; +type IsStoreUpdateMember = bool; + pub const FELT_DELIMITER: &str = "/"; #[cfg(test)] @@ -201,7 +204,7 @@ impl Sql { path, event_id, (&entity_id, false), - &entity, + (&entity, false), block_timestamp, &vec![], ); @@ -260,7 +263,7 @@ impl Sql { path, event_id, (&entity_id, true), - &entity, + (&entity, false), block_timestamp, &vec![], ); @@ -271,6 +274,35 @@ impl Sql { Ok(()) } + pub async fn set_model_member( + &mut self, + model_tag: &str, + entity_id: Felt, + is_event_message: bool, + member: &Member, + event_id: &str, + block_timestamp: u64, + ) -> Result<()> { + let entity_id = format!("{:#x}", entity_id); + let path = vec![model_tag.to_string()]; + + let wrapped_ty = + Ty::Struct(Struct { name: model_tag.to_string(), children: vec![member.clone()] }); + + // update model member + self.build_set_entity_queries_recursive( + path, + event_id, + (&entity_id, is_event_message), + (&wrapped_ty, true), + block_timestamp, + &vec![], + ); + self.query_queue.execute_all().await?; + + Ok(()) + } + pub async fn delete_entity(&mut self, entity_id: Felt, entity: Ty) -> Result<()> { let entity_id = format!("{:#x}", entity_id); let path = vec![entity.name()]; @@ -383,14 +415,13 @@ impl Sql { Ok(keys) } - pub async fn entity(&self, model: String, key: Felt) -> Result> { - let query = sqlx::query_as::<_, (i32, String, String)>("SELECT * FROM ? WHERE id = ?") - .bind(model) - .bind(format!("{:#x}", key)); + pub async fn does_entity_exist(&self, model: String, key: Felt) -> Result { + let sql = format!("SELECT COUNT(*) FROM [{model}] WHERE id = ?"); - let mut conn: PoolConnection = self.pool.acquire().await?; - let row: (i32, String, String) = query.fetch_one(&mut *conn).await?; - Ok(serde_json::from_str(&row.2).unwrap()) + let count: i64 = + sqlx::query_scalar(&sql).bind(format!("{:#x}", key)).fetch_one(&self.pool).await?; + + Ok(count > 0) } pub async fn entities(&self, model: String) -> Result>> { @@ -561,12 +592,13 @@ impl Sql { path: Vec, event_id: &str, // The id of the entity and if the entity is an event message - entity_id: (&str, bool), - entity: &Ty, + entity_id: (&str, IsEventMessage), + entity: (&Ty, IsStoreUpdateMember), block_timestamp: u64, indexes: &Vec, ) { let (entity_id, is_event_message) = entity_id; + let (entity, is_store_update_member) = entity; let update_members = |members: &[Member], query_queue: &mut QueryQueue, indexes: &Vec| { @@ -629,11 +661,31 @@ impl Sql { } let placeholders: Vec<&str> = arguments.iter().map(|_| "?").collect(); - let statement = format!( - "INSERT OR REPLACE INTO [{table_id}] ({}) VALUES ({})", - columns.join(","), - placeholders.join(",") - ); + let statement = if is_store_update_member && indexes.is_empty() { + arguments.push(Argument::String(if is_event_message { + "event:".to_string() + entity_id + } else { + entity_id.to_string() + })); + + // row has to exist. update it directly + format!( + "UPDATE [{table_id}] SET {updates} WHERE id = ?", + table_id = table_id, + updates = columns + .iter() + .zip(placeholders.iter()) + .map(|(column, placeholder)| format!("{} = {}", column, placeholder)) + .collect::>() + .join(", ") + ) + } else { + format!( + "INSERT OR REPLACE INTO [{table_id}] ({}) VALUES ({})", + columns.join(","), + placeholders.join(",") + ) + }; query_queue.enqueue(statement, arguments); }; @@ -649,7 +701,7 @@ impl Sql { path_clone, event_id, (entity_id, is_event_message), - &member.ty, + (&member.ty, is_store_update_member), block_timestamp, indexes, ); @@ -685,7 +737,7 @@ impl Sql { path_clone, event_id, (entity_id, is_event_message), - &option.ty, + (&option.ty, is_store_update_member), block_timestamp, indexes, ); @@ -714,7 +766,7 @@ impl Sql { path_clone, event_id, (entity_id, is_event_message), - member, + (member, is_store_update_member), block_timestamp, indexes, ); @@ -752,7 +804,7 @@ impl Sql { path_clone, event_id, (entity_id, is_event_message), - member, + (member, is_store_update_member), block_timestamp, &indexes, ); diff --git a/examples/spawn-and-move/Scarb.toml b/examples/spawn-and-move/Scarb.toml index c6cc3f9fd5..4489b63feb 100644 --- a/examples/spawn-and-move/Scarb.toml +++ b/examples/spawn-and-move/Scarb.toml @@ -40,7 +40,7 @@ rpc_url = "http://localhost:5050/" # Default account for katana with seed = 0 account_address = "0x6162896d1d7ab204c7ccac6dd5f8e9e7c25ecd5ae4fcb4ad32e57786bb46e03" private_key = "0x1800000000300000180000000000030000000000003006001800006600" -world_address = "0x74c73d35df54ddc53bcf34aab5e0dbb09c447e99e01f4d69535441253c9571a" +world_address = "0x25e74888e786245ec7aa93d846b2cc9e4b49a5244209860bbf4b384f654521b" [profile.release.tool.dojo] # for more info on how `merge-strategy` works see: diff --git a/examples/spawn-and-move/manifests/dev/base/abis/contracts/dojo_examples-actions-40b6994c.json b/examples/spawn-and-move/manifests/dev/base/abis/contracts/dojo_examples-actions-40b6994c.json index 3d21bef6a6..ac6c7ed557 100644 --- a/examples/spawn-and-move/manifests/dev/base/abis/contracts/dojo_examples-actions-40b6994c.json +++ b/examples/spawn-and-move/manifests/dev/base/abis/contracts/dojo_examples-actions-40b6994c.json @@ -228,6 +228,24 @@ } ] }, + { + "type": "struct", + "name": "dojo_examples::models::PlayerItem", + "members": [ + { + "name": "item_id", + "type": "core::integer::u32" + }, + { + "name": "quantity", + "type": "core::integer::u32" + }, + { + "name": "score", + "type": "core::integer::i32" + } + ] + }, { "type": "interface", "name": "dojo_examples::actions::IActions", @@ -288,11 +306,11 @@ }, { "type": "function", - "name": "update_player_name_value", + "name": "update_player_items", "inputs": [ { - "name": "name", - "type": "core::byte_array::ByteArray" + "name": "items", + "type": "core::array::Array::" } ], "outputs": [], diff --git a/examples/spawn-and-move/manifests/dev/base/abis/dojo-world.json b/examples/spawn-and-move/manifests/dev/base/abis/dojo-world.json index b12806373b..62ff4b6bfa 100644 --- a/examples/spawn-and-move/manifests/dev/base/abis/dojo-world.json +++ b/examples/spawn-and-move/manifests/dev/base/abis/dojo-world.json @@ -975,6 +975,33 @@ } ] }, + { + "type": "event", + "name": "dojo::world::world_contract::world::StoreUpdateMember", + "kind": "struct", + "members": [ + { + "name": "table", + "type": "core::felt252", + "kind": "data" + }, + { + "name": "entity_id", + "type": "core::felt252", + "kind": "data" + }, + { + "name": "member_selector", + "type": "core::felt252", + "kind": "data" + }, + { + "name": "values", + "type": "core::array::Span::", + "kind": "data" + } + ] + }, { "type": "event", "name": "dojo::world::world_contract::world::StoreDelRecord", @@ -1156,6 +1183,11 @@ "type": "dojo::world::world_contract::world::StoreUpdateRecord", "kind": "nested" }, + { + "name": "StoreUpdateMember", + "type": "dojo::world::world_contract::world::StoreUpdateMember", + "kind": "nested" + }, { "name": "StoreDelRecord", "type": "dojo::world::world_contract::world::StoreDelRecord", diff --git a/examples/spawn-and-move/manifests/dev/base/contracts/dojo_examples-actions-40b6994c.toml b/examples/spawn-and-move/manifests/dev/base/contracts/dojo_examples-actions-40b6994c.toml index 36e30d588b..d6647ceeca 100644 --- a/examples/spawn-and-move/manifests/dev/base/contracts/dojo_examples-actions-40b6994c.toml +++ b/examples/spawn-and-move/manifests/dev/base/contracts/dojo_examples-actions-40b6994c.toml @@ -1,6 +1,6 @@ kind = "DojoContract" -class_hash = "0x5d3ffcdabbf541ac1ce9df26f275d6abaec2c260d7a698e1c94a4bd1a0482b8" -original_class_hash = "0x5d3ffcdabbf541ac1ce9df26f275d6abaec2c260d7a698e1c94a4bd1a0482b8" +class_hash = "0x2a8de224c28cae3049e23b352e7fce6f26cc99884331e5f2a6aea261398a27a" +original_class_hash = "0x2a8de224c28cae3049e23b352e7fce6f26cc99884331e5f2a6aea261398a27a" base_class_hash = "0x0" abi = "manifests/dev/base/abis/contracts/dojo_examples-actions-40b6994c.json" reads = [] diff --git a/examples/spawn-and-move/manifests/dev/base/dojo-world.toml b/examples/spawn-and-move/manifests/dev/base/dojo-world.toml index 3390095088..59d4719031 100644 --- a/examples/spawn-and-move/manifests/dev/base/dojo-world.toml +++ b/examples/spawn-and-move/manifests/dev/base/dojo-world.toml @@ -1,6 +1,6 @@ kind = "Class" -class_hash = "0x4c90da98d2bad157dec1d7b9b9c8c5861828a7ec1b323425d84fa6c3071303f" -original_class_hash = "0x4c90da98d2bad157dec1d7b9b9c8c5861828a7ec1b323425d84fa6c3071303f" +class_hash = "0x2178527e9556d1aa21d3c2961d28f9114fcfed81b4c3674ed591c50ce46cc9d" +original_class_hash = "0x2178527e9556d1aa21d3c2961d28f9114fcfed81b4c3674ed591c50ce46cc9d" abi = "manifests/dev/base/abis/dojo-world.json" tag = "dojo-world" manifest_name = "dojo-world" diff --git a/examples/spawn-and-move/manifests/dev/deployment/abis/contracts/dojo_examples-actions-40b6994c.json b/examples/spawn-and-move/manifests/dev/deployment/abis/contracts/dojo_examples-actions-40b6994c.json index 3d21bef6a6..ac6c7ed557 100644 --- a/examples/spawn-and-move/manifests/dev/deployment/abis/contracts/dojo_examples-actions-40b6994c.json +++ b/examples/spawn-and-move/manifests/dev/deployment/abis/contracts/dojo_examples-actions-40b6994c.json @@ -228,6 +228,24 @@ } ] }, + { + "type": "struct", + "name": "dojo_examples::models::PlayerItem", + "members": [ + { + "name": "item_id", + "type": "core::integer::u32" + }, + { + "name": "quantity", + "type": "core::integer::u32" + }, + { + "name": "score", + "type": "core::integer::i32" + } + ] + }, { "type": "interface", "name": "dojo_examples::actions::IActions", @@ -288,11 +306,11 @@ }, { "type": "function", - "name": "update_player_name_value", + "name": "update_player_items", "inputs": [ { - "name": "name", - "type": "core::byte_array::ByteArray" + "name": "items", + "type": "core::array::Array::" } ], "outputs": [], diff --git a/examples/spawn-and-move/manifests/dev/deployment/abis/dojo-world.json b/examples/spawn-and-move/manifests/dev/deployment/abis/dojo-world.json index b12806373b..62ff4b6bfa 100644 --- a/examples/spawn-and-move/manifests/dev/deployment/abis/dojo-world.json +++ b/examples/spawn-and-move/manifests/dev/deployment/abis/dojo-world.json @@ -975,6 +975,33 @@ } ] }, + { + "type": "event", + "name": "dojo::world::world_contract::world::StoreUpdateMember", + "kind": "struct", + "members": [ + { + "name": "table", + "type": "core::felt252", + "kind": "data" + }, + { + "name": "entity_id", + "type": "core::felt252", + "kind": "data" + }, + { + "name": "member_selector", + "type": "core::felt252", + "kind": "data" + }, + { + "name": "values", + "type": "core::array::Span::", + "kind": "data" + } + ] + }, { "type": "event", "name": "dojo::world::world_contract::world::StoreDelRecord", @@ -1156,6 +1183,11 @@ "type": "dojo::world::world_contract::world::StoreUpdateRecord", "kind": "nested" }, + { + "name": "StoreUpdateMember", + "type": "dojo::world::world_contract::world::StoreUpdateMember", + "kind": "nested" + }, { "name": "StoreDelRecord", "type": "dojo::world::world_contract::world::StoreDelRecord", diff --git a/examples/spawn-and-move/manifests/dev/deployment/manifest.json b/examples/spawn-and-move/manifests/dev/deployment/manifest.json index 7b64c57f07..c7d42bb7df 100644 --- a/examples/spawn-and-move/manifests/dev/deployment/manifest.json +++ b/examples/spawn-and-move/manifests/dev/deployment/manifest.json @@ -1,8 +1,8 @@ { "world": { "kind": "WorldContract", - "class_hash": "0x4c90da98d2bad157dec1d7b9b9c8c5861828a7ec1b323425d84fa6c3071303f", - "original_class_hash": "0x4c90da98d2bad157dec1d7b9b9c8c5861828a7ec1b323425d84fa6c3071303f", + "class_hash": "0x2178527e9556d1aa21d3c2961d28f9114fcfed81b4c3674ed591c50ce46cc9d", + "original_class_hash": "0x2178527e9556d1aa21d3c2961d28f9114fcfed81b4c3674ed591c50ce46cc9d", "abi": [ { "type": "impl", @@ -980,6 +980,33 @@ } ] }, + { + "type": "event", + "name": "dojo::world::world_contract::world::StoreUpdateMember", + "kind": "struct", + "members": [ + { + "name": "table", + "type": "core::felt252", + "kind": "data" + }, + { + "name": "entity_id", + "type": "core::felt252", + "kind": "data" + }, + { + "name": "member_selector", + "type": "core::felt252", + "kind": "data" + }, + { + "name": "values", + "type": "core::array::Span::", + "kind": "data" + } + ] + }, { "type": "event", "name": "dojo::world::world_contract::world::StoreDelRecord", @@ -1161,6 +1188,11 @@ "type": "dojo::world::world_contract::world::StoreUpdateRecord", "kind": "nested" }, + { + "name": "StoreUpdateMember", + "type": "dojo::world::world_contract::world::StoreUpdateMember", + "kind": "nested" + }, { "name": "StoreDelRecord", "type": "dojo::world::world_contract::world::StoreDelRecord", @@ -1189,8 +1221,8 @@ ] } ], - "address": "0x74c73d35df54ddc53bcf34aab5e0dbb09c447e99e01f4d69535441253c9571a", - "transaction_hash": "0x34ee5ea074c7c5c2b1cfbcb96b49758d18552139a0463d5f0f6758a403c29cc", + "address": "0x25e74888e786245ec7aa93d846b2cc9e4b49a5244209860bbf4b384f654521b", + "transaction_hash": "0x1606f5a86180d90027d33915cf0d62e117f778f8837df3c1375af3d3bc2951e", "block_number": 3, "seed": "dojo_examples", "metadata": { @@ -1210,9 +1242,9 @@ "contracts": [ { "kind": "DojoContract", - "address": "0x2d24481107b55ecd73c4d1b62f6bfe8c42a224447b71db7dcec2eab484d53cd", - "class_hash": "0x5d3ffcdabbf541ac1ce9df26f275d6abaec2c260d7a698e1c94a4bd1a0482b8", - "original_class_hash": "0x5d3ffcdabbf541ac1ce9df26f275d6abaec2c260d7a698e1c94a4bd1a0482b8", + "address": "0x24d926d75cd84104c3bd24f0f79e95c273d6a99ed449f3c8b83114857020332", + "class_hash": "0x2a8de224c28cae3049e23b352e7fce6f26cc99884331e5f2a6aea261398a27a", + "original_class_hash": "0x2a8de224c28cae3049e23b352e7fce6f26cc99884331e5f2a6aea261398a27a", "base_class_hash": "0x2427dd10a58850ac9a5ca6ce04b7771b05330fd18f2e481831ad903b969e6b2", "abi": [ { @@ -1444,6 +1476,24 @@ } ] }, + { + "type": "struct", + "name": "dojo_examples::models::PlayerItem", + "members": [ + { + "name": "item_id", + "type": "core::integer::u32" + }, + { + "name": "quantity", + "type": "core::integer::u32" + }, + { + "name": "score", + "type": "core::integer::i32" + } + ] + }, { "type": "interface", "name": "dojo_examples::actions::IActions", @@ -1504,11 +1554,11 @@ }, { "type": "function", - "name": "update_player_name_value", + "name": "update_player_items", "inputs": [ { - "name": "name", - "type": "core::byte_array::ByteArray" + "name": "items", + "type": "core::array::Array::" } ], "outputs": [], @@ -1641,7 +1691,7 @@ }, { "kind": "DojoContract", - "address": "0x454e4731e29aad869794ce03040f1bd866556132b0e633a376918ee17801f5e", + "address": "0x31a348061c348eb84d7d9f1658244432170822cdb1a97a586805b565c168bcf", "class_hash": "0x45ad5a298db270a9c3ee439b7a7a008e3219b70937cfaa144589866442f3908", "original_class_hash": "0x45ad5a298db270a9c3ee439b7a7a008e3219b70937cfaa144589866442f3908", "base_class_hash": "0x2427dd10a58850ac9a5ca6ce04b7771b05330fd18f2e481831ad903b969e6b2", @@ -1878,7 +1928,7 @@ }, { "kind": "DojoContract", - "address": "0x57d20e85621372042af6b626884361c1c64c701b0b7db985d10faf92aa0dedc", + "address": "0x4aee4238ae328cfe7ad116057e3e9ef083b75f13c1914a8f93ef32549ebe808", "class_hash": "0x3daab1621bba5f43f0d84f8f68ee7a5dfa4d83f98f746e2e9dcd3a848233e35", "original_class_hash": "0x3daab1621bba5f43f0d84f8f68ee7a5dfa4d83f98f746e2e9dcd3a848233e35", "base_class_hash": "0x2427dd10a58850ac9a5ca6ce04b7771b05330fd18f2e481831ad903b969e6b2", @@ -2097,7 +2147,7 @@ }, { "kind": "DojoContract", - "address": "0x52da0b3df1cb3f0627dbe75960ae5ebad647b6ade1930dc9a499c0475168754", + "address": "0x1c6c32fc58a703d08ba0080ebf840e9c8021e86c37b239d28ce1a4576709ddc", "class_hash": "0x647fc1b2d2e902e6304e127b36995d8f57fe45c38e38e15d8860db508dbf24a", "original_class_hash": "0x647fc1b2d2e902e6304e127b36995d8f57fe45c38e38e15d8860db508dbf24a", "base_class_hash": "0x2427dd10a58850ac9a5ca6ce04b7771b05330fd18f2e481831ad903b969e6b2", diff --git a/examples/spawn-and-move/manifests/dev/deployment/manifest.toml b/examples/spawn-and-move/manifests/dev/deployment/manifest.toml index 5a398195aa..064976b859 100644 --- a/examples/spawn-and-move/manifests/dev/deployment/manifest.toml +++ b/examples/spawn-and-move/manifests/dev/deployment/manifest.toml @@ -1,10 +1,10 @@ [world] kind = "WorldContract" -class_hash = "0x4c90da98d2bad157dec1d7b9b9c8c5861828a7ec1b323425d84fa6c3071303f" -original_class_hash = "0x4c90da98d2bad157dec1d7b9b9c8c5861828a7ec1b323425d84fa6c3071303f" +class_hash = "0x2178527e9556d1aa21d3c2961d28f9114fcfed81b4c3674ed591c50ce46cc9d" +original_class_hash = "0x2178527e9556d1aa21d3c2961d28f9114fcfed81b4c3674ed591c50ce46cc9d" abi = "manifests/dev/deployment/abis/dojo-world.json" -address = "0x74c73d35df54ddc53bcf34aab5e0dbb09c447e99e01f4d69535441253c9571a" -transaction_hash = "0x34ee5ea074c7c5c2b1cfbcb96b49758d18552139a0463d5f0f6758a403c29cc" +address = "0x25e74888e786245ec7aa93d846b2cc9e4b49a5244209860bbf4b384f654521b" +transaction_hash = "0x1606f5a86180d90027d33915cf0d62e117f778f8837df3c1375af3d3bc2951e" block_number = 3 seed = "dojo_examples" manifest_name = "dojo-world" @@ -23,9 +23,9 @@ manifest_name = "dojo-base" [[contracts]] kind = "DojoContract" -address = "0x2d24481107b55ecd73c4d1b62f6bfe8c42a224447b71db7dcec2eab484d53cd" -class_hash = "0x5d3ffcdabbf541ac1ce9df26f275d6abaec2c260d7a698e1c94a4bd1a0482b8" -original_class_hash = "0x5d3ffcdabbf541ac1ce9df26f275d6abaec2c260d7a698e1c94a4bd1a0482b8" +address = "0x24d926d75cd84104c3bd24f0f79e95c273d6a99ed449f3c8b83114857020332" +class_hash = "0x2a8de224c28cae3049e23b352e7fce6f26cc99884331e5f2a6aea261398a27a" +original_class_hash = "0x2a8de224c28cae3049e23b352e7fce6f26cc99884331e5f2a6aea261398a27a" base_class_hash = "0x2427dd10a58850ac9a5ca6ce04b7771b05330fd18f2e481831ad903b969e6b2" abi = "manifests/dev/deployment/abis/contracts/dojo_examples-actions-40b6994c.json" reads = [] @@ -40,7 +40,7 @@ manifest_name = "dojo_examples-actions-40b6994c" [[contracts]] kind = "DojoContract" -address = "0x454e4731e29aad869794ce03040f1bd866556132b0e633a376918ee17801f5e" +address = "0x31a348061c348eb84d7d9f1658244432170822cdb1a97a586805b565c168bcf" class_hash = "0x45ad5a298db270a9c3ee439b7a7a008e3219b70937cfaa144589866442f3908" original_class_hash = "0x45ad5a298db270a9c3ee439b7a7a008e3219b70937cfaa144589866442f3908" base_class_hash = "0x2427dd10a58850ac9a5ca6ce04b7771b05330fd18f2e481831ad903b969e6b2" @@ -54,7 +54,7 @@ manifest_name = "dojo_examples-dungeon-6620e0e6" [[contracts]] kind = "DojoContract" -address = "0x57d20e85621372042af6b626884361c1c64c701b0b7db985d10faf92aa0dedc" +address = "0x4aee4238ae328cfe7ad116057e3e9ef083b75f13c1914a8f93ef32549ebe808" class_hash = "0x3daab1621bba5f43f0d84f8f68ee7a5dfa4d83f98f746e2e9dcd3a848233e35" original_class_hash = "0x3daab1621bba5f43f0d84f8f68ee7a5dfa4d83f98f746e2e9dcd3a848233e35" base_class_hash = "0x2427dd10a58850ac9a5ca6ce04b7771b05330fd18f2e481831ad903b969e6b2" @@ -68,7 +68,7 @@ manifest_name = "dojo_examples-mock_token-31599eb2" [[contracts]] kind = "DojoContract" -address = "0x52da0b3df1cb3f0627dbe75960ae5ebad647b6ade1930dc9a499c0475168754" +address = "0x1c6c32fc58a703d08ba0080ebf840e9c8021e86c37b239d28ce1a4576709ddc" class_hash = "0x647fc1b2d2e902e6304e127b36995d8f57fe45c38e38e15d8860db508dbf24a" original_class_hash = "0x647fc1b2d2e902e6304e127b36995d8f57fe45c38e38e15d8860db508dbf24a" base_class_hash = "0x2427dd10a58850ac9a5ca6ce04b7771b05330fd18f2e481831ad903b969e6b2" diff --git a/examples/spawn-and-move/manifests/release/base/abis/contracts/dojo_examples-actions-40b6994c.json b/examples/spawn-and-move/manifests/release/base/abis/contracts/dojo_examples-actions-40b6994c.json index 3d21bef6a6..ac6c7ed557 100644 --- a/examples/spawn-and-move/manifests/release/base/abis/contracts/dojo_examples-actions-40b6994c.json +++ b/examples/spawn-and-move/manifests/release/base/abis/contracts/dojo_examples-actions-40b6994c.json @@ -228,6 +228,24 @@ } ] }, + { + "type": "struct", + "name": "dojo_examples::models::PlayerItem", + "members": [ + { + "name": "item_id", + "type": "core::integer::u32" + }, + { + "name": "quantity", + "type": "core::integer::u32" + }, + { + "name": "score", + "type": "core::integer::i32" + } + ] + }, { "type": "interface", "name": "dojo_examples::actions::IActions", @@ -288,11 +306,11 @@ }, { "type": "function", - "name": "update_player_name_value", + "name": "update_player_items", "inputs": [ { - "name": "name", - "type": "core::byte_array::ByteArray" + "name": "items", + "type": "core::array::Array::" } ], "outputs": [], diff --git a/examples/spawn-and-move/manifests/release/base/abis/dojo-world.json b/examples/spawn-and-move/manifests/release/base/abis/dojo-world.json index b12806373b..62ff4b6bfa 100644 --- a/examples/spawn-and-move/manifests/release/base/abis/dojo-world.json +++ b/examples/spawn-and-move/manifests/release/base/abis/dojo-world.json @@ -975,6 +975,33 @@ } ] }, + { + "type": "event", + "name": "dojo::world::world_contract::world::StoreUpdateMember", + "kind": "struct", + "members": [ + { + "name": "table", + "type": "core::felt252", + "kind": "data" + }, + { + "name": "entity_id", + "type": "core::felt252", + "kind": "data" + }, + { + "name": "member_selector", + "type": "core::felt252", + "kind": "data" + }, + { + "name": "values", + "type": "core::array::Span::", + "kind": "data" + } + ] + }, { "type": "event", "name": "dojo::world::world_contract::world::StoreDelRecord", @@ -1156,6 +1183,11 @@ "type": "dojo::world::world_contract::world::StoreUpdateRecord", "kind": "nested" }, + { + "name": "StoreUpdateMember", + "type": "dojo::world::world_contract::world::StoreUpdateMember", + "kind": "nested" + }, { "name": "StoreDelRecord", "type": "dojo::world::world_contract::world::StoreDelRecord", diff --git a/examples/spawn-and-move/manifests/release/base/contracts/dojo_examples-actions-40b6994c.toml b/examples/spawn-and-move/manifests/release/base/contracts/dojo_examples-actions-40b6994c.toml index 6c912bbe36..a12c96a2e4 100644 --- a/examples/spawn-and-move/manifests/release/base/contracts/dojo_examples-actions-40b6994c.toml +++ b/examples/spawn-and-move/manifests/release/base/contracts/dojo_examples-actions-40b6994c.toml @@ -1,6 +1,6 @@ kind = "DojoContract" -class_hash = "0x5d3ffcdabbf541ac1ce9df26f275d6abaec2c260d7a698e1c94a4bd1a0482b8" -original_class_hash = "0x5d3ffcdabbf541ac1ce9df26f275d6abaec2c260d7a698e1c94a4bd1a0482b8" +class_hash = "0x2a8de224c28cae3049e23b352e7fce6f26cc99884331e5f2a6aea261398a27a" +original_class_hash = "0x2a8de224c28cae3049e23b352e7fce6f26cc99884331e5f2a6aea261398a27a" base_class_hash = "0x0" abi = "manifests/release/base/abis/contracts/dojo_examples-actions-40b6994c.json" reads = [] diff --git a/examples/spawn-and-move/manifests/release/base/dojo-world.toml b/examples/spawn-and-move/manifests/release/base/dojo-world.toml index b984a39658..cc1f0eda6a 100644 --- a/examples/spawn-and-move/manifests/release/base/dojo-world.toml +++ b/examples/spawn-and-move/manifests/release/base/dojo-world.toml @@ -1,6 +1,6 @@ kind = "Class" -class_hash = "0x4c90da98d2bad157dec1d7b9b9c8c5861828a7ec1b323425d84fa6c3071303f" -original_class_hash = "0x4c90da98d2bad157dec1d7b9b9c8c5861828a7ec1b323425d84fa6c3071303f" +class_hash = "0x2178527e9556d1aa21d3c2961d28f9114fcfed81b4c3674ed591c50ce46cc9d" +original_class_hash = "0x2178527e9556d1aa21d3c2961d28f9114fcfed81b4c3674ed591c50ce46cc9d" abi = "manifests/release/base/abis/dojo-world.json" tag = "dojo-world" manifest_name = "dojo-world" diff --git a/examples/spawn-and-move/src/actions.cairo b/examples/spawn-and-move/src/actions.cairo index b966441e49..64248db5d2 100644 --- a/examples/spawn-and-move/src/actions.cairo +++ b/examples/spawn-and-move/src/actions.cairo @@ -1,4 +1,4 @@ -use dojo_examples::models::{Direction, Position, Vec2}; +use dojo_examples::models::{Direction, Position, Vec2, PlayerItem}; #[dojo::interface] pub trait IActions { @@ -7,7 +7,7 @@ pub trait IActions { fn set_player_config(ref world: IWorldDispatcher, name: ByteArray); fn get_player_position(world: @IWorldDispatcher) -> Position; fn update_player_name(ref world: IWorldDispatcher, name: ByteArray); - fn update_player_name_value(ref world: IWorldDispatcher, name: ByteArray); + fn update_player_items(ref world: IWorldDispatcher, items: Array); fn reset_player_config(ref world: IWorldDispatcher); fn set_player_server_profile(ref world: IWorldDispatcher, server_id: u32, name: ByteArray); #[cfg(feature: 'dungeon')] @@ -28,7 +28,7 @@ pub mod actions { use starknet::{ContractAddress, get_caller_address}; use dojo_examples::models::{ Position, Moves, Direction, Vec2, PlayerConfig, PlayerItem, ServerProfile, PositionStore, - MovesStore, MovesEntityStore, PlayerConfigStore, PlayerConfigEntityStore + MovesStore, MovesEntityStore, PlayerConfigStore, PlayerConfigEntityStore, }; use dojo_examples::utils::next_position; @@ -177,15 +177,22 @@ pub mod actions { assert(new_name == name, 'unable to change name'); } - fn update_player_name_value(ref world: IWorldDispatcher, name: ByteArray) { + fn update_player_items(ref world: IWorldDispatcher, items: Array) { let player = get_caller_address(); let config_id = PlayerConfigStore::entity_id_from_keys(player); + let items_clone = items.clone(); + let config = PlayerConfigEntityStore::get(world, config_id); - config.set_name(world, name.clone()); + config.set_items(world, items); - let new_name = PlayerConfigEntityStore::get_name(world, config_id); - assert(new_name == name, 'unable to change name'); + let new_items = PlayerConfigEntityStore::get_items(world, config_id); + let mut size = items_clone.len(); + + while size > 0 { + assert(new_items.at(size - 1) == items_clone.at(size - 1), 'item not found'); + size -= 1; + } } } diff --git a/examples/spawn-and-move/src/models.cairo b/examples/spawn-and-move/src/models.cairo index ddd61efb92..094ec84eab 100644 --- a/examples/spawn-and-move/src/models.cairo +++ b/examples/spawn-and-move/src/models.cairo @@ -70,7 +70,7 @@ pub struct Position { // Every field inside a model must derive `Introspect` or `IntrospectPacked`. // `IntrospectPacked` can also be used into models that are only using `Introspect`. -#[derive(Copy, Drop, Serde, Introspect)] +#[derive(Copy, Drop, Serde, Introspect, PartialEq)] pub struct PlayerItem { pub item_id: u32, pub quantity: u32,