diff --git a/.gitignore b/.gitignore index 47ab42b..228c46e 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ web/public/assets/placeholder/pixel-state.png contracts/out contracts/manifests contracts/target +.aider* diff --git a/contracts/Scarb.lock b/contracts/Scarb.lock index cc7d2e9..020ee45 100644 --- a/contracts/Scarb.lock +++ b/contracts/Scarb.lock @@ -16,7 +16,7 @@ source = "git+https://github.com/dojoengine/dojo?rev=f15def33#f15def330c0d099e79 [[package]] name = "pixelaw" -version = "0.3.40" +version = "0.3.50" dependencies = [ "dojo", ] diff --git a/contracts/src/apps/paint/app.cairo b/contracts/src/apps/paint/app.cairo index c9204ea..ff33153 100644 --- a/contracts/src/apps/paint/app.cairo +++ b/contracts/src/apps/paint/app.cairo @@ -3,125 +3,163 @@ use pixelaw::core::models::pixel::{Pixel, PixelUpdate}; use pixelaw::core::utils::{get_core_actions, Direction, Position, DefaultParameters}; use starknet::{get_caller_address, get_contract_address, get_execution_info, ContractAddress}; - #[dojo::interface] trait IPaintActions { + /// Initializes the Paint App. + /// + /// # Arguments + /// + /// * `world` - A reference to the world dispatcher. fn init(ref world: IWorldDispatcher); + + /// Interacts with a pixel based on default parameters. + /// + /// # Arguments + /// + /// * `world` - A reference to the world dispatcher. + /// * `default_params` - The default parameters including position and color. fn interact(ref world: IWorldDispatcher, default_params: DefaultParameters); + + /// Applies a color to a specified position. + /// + /// # Arguments + /// + /// * `world` - A reference to the world dispatcher. + /// * `default_params` - The default parameters including position and color. fn put_color(ref world: IWorldDispatcher, default_params: DefaultParameters); + + /// Initiates the fading process for a pixel. + /// + /// # Arguments + /// + /// * `world` - A reference to the world dispatcher. + /// * `default_params` - The default parameters including position and color. fn fade(ref world: IWorldDispatcher, default_params: DefaultParameters); + + /// Updates a row of pixels with provided image data. + /// + /// # Arguments + /// + /// * `world` - A reference to the world dispatcher. + /// * `default_params` - The default parameters including position. + /// * `image_data` - A span of felt252 representing the image data. fn pixel_row( - ref world: IWorldDispatcher, default_params: DefaultParameters, image_data: Span + ref world: IWorldDispatcher, default_params: DefaultParameters, image_data: Span, ); } -const APP_KEY: felt252 = 'paint'; +pub const APP_KEY: felt252 = 'paint'; const APP_ICON: felt252 = 'U+1F58C'; const PIXELS_PER_FELT: u32 = 7; - -/// BASE means using the server's default manifest.json handler const APP_MANIFEST: felt252 = 'BASE/manifests/paint'; #[dojo::contract(namespace: "pixelaw", nomapping: true)] mod paint_actions { use starknet::{ get_tx_info, get_caller_address, get_contract_address, get_execution_info, ContractAddress, - contract_address_const + contract_address_const, }; use super::IPaintActions; use pixelaw::core::models::pixel::{Pixel, PixelUpdate}; - use pixelaw::core::models::permissions::{Permission}; + use pixelaw::core::models::permissions::Permission; use pixelaw::core::actions::{ IActionsDispatcher as ICoreActionsDispatcher, - IActionsDispatcherTrait as ICoreActionsDispatcherTrait + IActionsDispatcherTrait as ICoreActionsDispatcherTrait, }; use super::{APP_KEY, APP_ICON, APP_MANIFEST, PIXELS_PER_FELT}; use pixelaw::core::utils::{ - get_core_actions, decode_color, encode_color, subu8, Direction, Position, DefaultParameters + get_core_actions, decode_color, encode_color, subu8, Direction, Position, DefaultParameters, }; use pixelaw::core::traits::IInteroperability; use pixelaw::core::models::registry::App; - #[abi(embed_v0)] impl ActionsInteroperability of IInteroperability { + /// Hook called before a pixel update. + /// + /// # Arguments + /// + /// * `world` - A reference to the world dispatcher. + /// * `pixel_update` - The proposed update to the pixel. + /// * `app_caller` - The app initiating the update. + /// * `player_caller` - The player initiating the update. fn on_pre_update( ref world: IWorldDispatcher, pixel_update: PixelUpdate, app_caller: App, - player_caller: ContractAddress + player_caller: ContractAddress, ) { - // do nothing + // Do nothing let _world = world; } + /// Hook called after a pixel update. + /// + /// # Arguments + /// + /// * `world` - A reference to the world dispatcher. + /// * `pixel_update` - The update that was applied to the pixel. + /// * `app_caller` - The app that performed the update. + /// * `player_caller` - The player that performed the update. fn on_post_update( ref world: IWorldDispatcher, pixel_update: PixelUpdate, app_caller: App, - player_caller: ContractAddress + player_caller: ContractAddress, ) { - // do nothing + // Do nothing let _world = world; } } - // impl: implement functions specified in trait #[abi(embed_v0)] impl ActionsImpl of IPaintActions { - /// Initialize the Paint App (TODO I think, do we need this??) + /// Initializes the Paint App. + /// + /// This function registers the app with core actions and sets up initial permissions. + /// + /// # Arguments + /// + /// * `world` - A reference to the world dispatcher. fn init(ref world: IWorldDispatcher) { let core_actions = pixelaw::core::utils::get_core_actions(world); - core_actions.update_app(APP_KEY, APP_ICON, APP_MANIFEST); - - // TODO: replace this with proper granting of permission - - core_actions - .update_permission( - 'snake', - Permission { - app: true, - color: true, - owner: false, - text: true, - timestamp: false, - action: false - } - ); + core_actions.new_app(contract_address_const::<0>(), APP_KEY, APP_ICON); + + // // TODO: Replace this with proper granting of permission + // core_actions + // .update_permission( + // 'snake', + // Permission { + // app: true, + // color: true, + // owner: false, + // text: true, + // timestamp: false, + // action: false, + // }, + // ); } - /// Put color on a certain position + /// Interacts with a pixel based on default parameters. + /// + /// If the pixel's current color matches the desired color, it initiates a fade. + /// Otherwise, it applies the new color. /// /// # Arguments /// - /// * `position` - Position of the pixel. - /// * `new_color` - Color to set the pixel to. + /// * `world` - A reference to the world dispatcher. + /// * `default_params` - The default parameters including position and color. fn interact(ref world: IWorldDispatcher, default_params: DefaultParameters) { println!("interact"); - // let core_actions = get_core_actions(world); let position = default_params.position; - // let player = core_actions.get_player_address(default_params.for_player); // Load the Pixel let mut pixel = get!(world, (position.x, position.y), (Pixel)); - // TODO: Load Paint App Settings like the fade steptime - // For example for the Cooldown feature - // let COOLDOWN_SECS = 5; - - // Check if 5 seconds have passed or if the sender is the owner - // TODO error message confusing, have to split this - // assert( - // pixel.owner == ContractAddress(Felt::ZERO) || (pixel.owner) == player || - // starknet::get_block_timestamp() - // - pixel.timestamp < COOLDOWN_SECS, - // 'Cooldown not over' - // ); - if pixel.color == default_params.color { self.fade(default_params); } else { @@ -129,17 +167,18 @@ mod paint_actions { } } - /// Put color on a certain position + /// Applies a color to a specified position. + /// + /// Checks for cooldown and ownership before applying the color. /// /// # Arguments /// - /// * `position` - Position of the pixel. - /// * `new_color` - Color to set the pixel to. + /// * `world` - A reference to the world dispatcher. + /// * `default_params` - The default parameters including position and color. fn put_color(ref world: IWorldDispatcher, default_params: DefaultParameters) { println!("put_color"); // Load important variables - let core_actions = get_core_actions(world); let position = default_params.position; let player = core_actions.get_player_address(default_params.for_player); @@ -148,21 +187,20 @@ mod paint_actions { // Load the Pixel let mut pixel = get!(world, (position.x, position.y), (Pixel)); - // TODO: Load Paint App Settings like the fade steptime - // For example for the Cooldown feature + // TODO: Load Paint App Settings like the fade step time + // For example for the cooldown feature let COOLDOWN_SECS = 5; // Check if 5 seconds have passed or if the sender is the owner - // TODO error message confusing, have to split this - assert( + assert!( pixel.owner == contract_address_const::<0>() - || (pixel.owner) == player + || pixel.owner == player || starknet::get_block_timestamp() - - pixel.timestamp < COOLDOWN_SECS, - 'Cooldown not over' + - pixel.timestamp >= COOLDOWN_SECS, + "Cooldown not over" ); - // We can now update color of the pixel + // Update color of the pixel core_actions .update_pixel( player, @@ -175,27 +213,36 @@ mod paint_actions { text: Option::None, app: Option::Some(system), owner: Option::Some(player), - action: Option::None // Not using this feature for paint - } + action: Option::None, // Not using this feature for paint + }, ); println!("put_color DONE"); } - + /// Updates a row of pixels with provided image data. + /// + /// Processes the image data and updates each pixel accordingly. + /// + /// # Arguments + /// + /// * `world` - A reference to the world dispatcher. + /// * `default_params` - The default parameters including position. + /// * `image_data` - A span of felt252 representing the image data. fn pixel_row( ref world: IWorldDispatcher, default_params: DefaultParameters, - image_data: Span + image_data: Span, ) { // row_length determines how many pixels are in a row - // row_offset determines how far to the right the position started. next row will + // row_offset determines how far to the right the position started. Next row will // continue (x - offset) to the left - if (image_data.is_empty()) { + if image_data.is_empty() { println!("image_data empty"); return; } + let core_actions = get_core_actions(world); let position = default_params.position; let player = core_actions.get_player_address(default_params.for_player); @@ -206,11 +253,10 @@ mod paint_actions { let mut felt: u256 = (*image_data.at(felt_index)).into(); let mut stop = false; println!("first felt: {}", felt); + while !stop { // Each felt contains 7 pixels of 4 bytes each, so 224 bits. The leftmost 28 bits // are 0 padded. - // TODO this can be optimized, maybe use the leftmost byte for processing - // instructions? // We unpack 4 bytes at a time and use them core_actions @@ -227,8 +273,8 @@ mod paint_actions { text: Option::None, app: Option::Some(system), owner: Option::Some(player), - action: Option::None // Not using this feature for paint - } + action: Option::None, // Not using this feature for paint + }, ); pixel_index += 1; @@ -248,13 +294,14 @@ mod paint_actions { } } - - /// Put color on a certain position + /// Initiates the fading process for a pixel. + /// + /// Decreases the RGB values by a fade step and schedules the next fade if necessary. /// /// # Arguments /// - /// * `position` - Position of the pixel. - /// * `new_color` - Color to set the pixel to. + /// * `world` - A reference to the world dispatcher. + /// * `default_params` - The default parameters including position and color. fn fade(ref world: IWorldDispatcher, default_params: DefaultParameters) { println!("fade"); @@ -268,7 +315,7 @@ mod paint_actions { let (r, g, b, a) = decode_color(pixel.color); - // If the color is 0,0,0 , let's stop the process, fading is done. + // If the color is 0,0,0, fading is done. if r == 0 && g == 0 && b == 0 { println!("fading is done"); delete!(world, (pixel)); @@ -280,10 +327,10 @@ mod paint_actions { println!("encode_color"); let new_color = encode_color( - subu8(r, FADE_STEP), subu8(g, FADE_STEP), subu8(b, FADE_STEP), a + subu8(r, FADE_STEP), subu8(g, FADE_STEP), subu8(b, FADE_STEP), a, ); - // We can now update color of the pixel + // Update color of the pixel core_actions .update_pixel( player, @@ -296,37 +343,38 @@ mod paint_actions { text: Option::None, app: Option::Some(system), owner: Option::Some(player), - action: Option::None // Not using this feature for paint - } + action: Option::None, // Not using this feature for paint + }, ); let FADE_SECONDS = 4; - // We implement fading by scheduling a new put_fading_color + // Implement fading by scheduling a new fade call let queue_timestamp = starknet::get_block_timestamp() + FADE_SECONDS; let mut calldata: Array = ArrayTrait::new(); let THIS_CONTRACT_ADDRESS = get_contract_address(); + // Prepare calldata // Calldata[0]: Calling player calldata.append(player.into()); // Calldata[1]: Calling system calldata.append(THIS_CONTRACT_ADDRESS.into()); - // Calldata[2,3] : Position[x,y] + // Calldata[2,3]: Position[x,y] calldata.append(position.x.into()); calldata.append(position.y.into()); - // Calldata[4] : Color + // Calldata[4]: Color calldata.append(new_color.into()); core_actions .schedule_queue( queue_timestamp, // When to fade next THIS_CONTRACT_ADDRESS, // This contract address - 0x89ce6748d77414b79f2312bb20f6e67d3aa4a9430933a0f461fedc92983084, // This selector - calldata.span() // The calldata prepared + 0x89ce6748d77414b79f2312bb20f6e67d3aa4a9430933a0f461fedc92983084, // Selector for fade + calldata.span(), // The prepared calldata ); println!("put_fading_color DONE"); } @@ -342,23 +390,36 @@ mod paint_actions { const TWO_POW_192: u256 = 0x1000000000000000000000000000000000000000000000000; const TWO_POW_224: u256 = 0x100000000000000000000000000000000000000000000000000000000; + /// Extracts a 32-bit value from a felt at a specified index. + /// + /// Each felt represents multiple 32-bit values; this function extracts one of them. + /// + /// # Arguments + /// + /// * `felt` - The felt from which to extract the value. + /// * `index` - The index of the value to extract (0 to 6). + /// + /// # Returns + /// + /// * `u32` - The extracted 32-bit value. fn extract(felt: u256, index: u32) -> u32 { - let mut result: u32 = 0; - if index == 0 { - result = (felt / TWO_POW_192).try_into().unwrap(); + let result: u32 = if index == 0 { + (felt / TWO_POW_192).try_into().unwrap() } else if index == 1 { - result = ((felt / TWO_POW_160) & MASK_32).try_into().unwrap(); + ((felt / TWO_POW_160) & MASK_32).try_into().unwrap() } else if index == 2 { - result = ((felt / TWO_POW_128) & MASK_32).try_into().unwrap(); + ((felt / TWO_POW_128) & MASK_32).try_into().unwrap() } else if index == 3 { - result = ((felt / TWO_POW_096) & MASK_32).try_into().unwrap(); + ((felt / TWO_POW_096) & MASK_32).try_into().unwrap() } else if index == 4 { - result = ((felt / TWO_POW_064) & MASK_32).try_into().unwrap(); + ((felt / TWO_POW_064) & MASK_32).try_into().unwrap() } else if index == 5 { - result = ((felt / TWO_POW_032) & MASK_32).try_into().unwrap(); + ((felt / TWO_POW_032) & MASK_32).try_into().unwrap() } else if index == 6 { - result = (felt & MASK_32).try_into().unwrap(); - } + (felt & MASK_32).try_into().unwrap() + } else { + 0 + }; println!("{}", result); result } diff --git a/contracts/src/apps/snake/app.cairo b/contracts/src/apps/snake/app.cairo index ae0145a..3a7bc83 100644 --- a/contracts/src/apps/snake/app.cairo +++ b/contracts/src/apps/snake/app.cairo @@ -2,24 +2,50 @@ use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; use starknet::{ContractAddress, ClassHash}; use pixelaw::core::utils::{Direction, Position, DefaultParameters, starknet_keccak}; +/// Calculates the next position based on the current coordinates and direction. +/// +/// # Arguments +/// +/// * `x` - Current x-coordinate. +/// * `y` - Current y-coordinate. +/// * `direction` - Direction to move in. +/// +/// # Returns +/// +/// * `Option<(u32, u32)>` - The next position as an `Option`. Returns `None` if the move is +/// invalid. fn next_position(x: u32, y: u32, direction: Direction) -> Option<(u32, u32)> { match direction { - Direction::None(()) => { Option::Some((x, y)) }, + Direction::None(()) => Option::Some((x, y)), Direction::Left(()) => { if x == 0 { Option::None } else { Option::Some((x - 1, y)) } }, - Direction::Right(()) => { Option::Some((x + 1, y)) }, + Direction::Right(()) => Option::Some((x + 1, y)), Direction::Up(()) => { if y == 0 { Option::None } else { Option::Some((x, y - 1)) } }, - Direction::Down(()) => { Option::Some((x, y + 1)) }, + Direction::Down(()) => Option::Some((x, y + 1)), } } +/// Represents a Snake in the game. +/// +/// Each snake has an owner, length, and a linked list of segments. +/// +/// Fields: +/// +/// * `owner` - The owner of the snake. +/// * `length` - The length of the snake. +/// * `first_segment_id` - The ID of the first segment (head). +/// * `last_segment_id` - The ID of the last segment (tail). +/// * `direction` - The current direction of the snake. +/// * `color` - The color of the snake. +/// * `text` - Any text associated with the snake. +/// * `is_dying` - A flag indicating whether the snake is dying. #[derive(Copy, Drop, Serde)] #[dojo::model(namespace: "pixelaw", nomapping: true)] pub struct Snake { @@ -31,9 +57,21 @@ pub struct Snake { pub direction: Direction, pub color: u32, pub text: felt252, - pub is_dying: bool + pub is_dying: bool, } +/// Represents a segment of a Snake. +/// +/// Fields: +/// +/// * `id` - The unique identifier of the segment. +/// * `previous_id` - The ID of the previous segment. +/// * `next_id` - The ID of the next segment. +/// * `x` - The x-coordinate of the segment. +/// * `y` - The y-coordinate of the segment. +/// * `pixel_original_color` - The original color of the pixel before the segment occupied it. +/// * `pixel_original_text` - The original text of the pixel. +/// * `pixel_original_app` - The original app associated with the pixel. #[derive(Copy, Drop, Serde)] #[dojo::model(namespace: "pixelaw", nomapping: true)] pub struct SnakeSegment { @@ -45,38 +83,65 @@ pub struct SnakeSegment { pub y: u32, pub pixel_original_color: u32, pub pixel_original_text: felt252, - pub pixel_original_app: ContractAddress + pub pixel_original_app: ContractAddress, } +pub const APP_KEY: felt252 = 'snake'; +pub const APP_ICON: felt252 = 'U+1F40D'; +/// Interface for Snake actions. #[dojo::interface] trait ISnakeActions { + /// Initializes the Snake App. + /// + /// # Arguments + /// + /// * `world` - A reference to the world dispatcher. fn init(ref world: IWorldDispatcher); + + /// Starts or interacts with a snake. + /// + /// # Arguments + /// + /// * `world` - A reference to the world dispatcher. + /// * `default_params` - Default parameters including position and color. + /// * `direction` - The direction to move the snake. + /// + /// # Returns + /// + /// * `u32` - The ID of the snake's first segment. fn interact( - ref world: IWorldDispatcher, default_params: DefaultParameters, direction: Direction + ref world: IWorldDispatcher, default_params: DefaultParameters, direction: Direction, ) -> u32; + + /// Moves the snake owned by the specified owner. + /// + /// # Arguments + /// + /// * `world` - A reference to the world dispatcher. + /// * `owner` - The contract address of the snake's owner. fn move(ref world: IWorldDispatcher, owner: ContractAddress); } - #[dojo::contract(namespace: "pixelaw", nomapping: true)] mod snake_actions { use starknet::{ ContractAddress, get_caller_address, get_contract_address, get_execution_info, - contract_address_const + contract_address_const, }; use pixelaw::core::models::pixel::{Pixel, PixelUpdate}; + use super::{APP_KEY, APP_ICON}; use super::{Snake, SnakeSegment}; use pixelaw::core::utils::{ get_core_actions, Direction, Position, DefaultParameters, starknet_keccak, - get_core_actions_address + get_core_actions_address, }; use super::next_position; use super::ISnakeActions; use pixelaw::core::actions::{ IActionsDispatcher as ICoreActionsDispatcher, - IActionsDispatcherTrait as ICoreActionsDispatcherTrait + IActionsDispatcherTrait as ICoreActionsDispatcherTrait, }; use pixelaw::apps::paint::app::{IPaintActionsDispatcher, IPaintActionsDispatcherTrait}; use pixelaw::core::traits::IInteroperability; @@ -88,67 +153,78 @@ mod snake_actions { #[derive(Drop, starknet::Event)] enum Event { Moved: Moved, - // Longer: Longer, - // Shorter: Shorter, - Died: Died + Died: Died, } - #[derive(Drop, starknet::Event)] struct Died { owner: ContractAddress, x: u32, - y: u32 + y: u32, } #[derive(Drop, starknet::Event)] struct Moved { owner: ContractAddress, - direction: Direction + direction: Direction, } const SNAKE_MAX_LENGTH: u8 = 255; - const APP_KEY: felt252 = 'snake'; - const APP_ICON: felt252 = 'U+1F40D'; - - /// BASE means using the server's default manifest.json handler - const APP_MANIFEST: felt252 = 'BASE/manifests/snake'; + /// Implementation of interoperability hooks for the Snake actions. #[abi(embed_v0)] impl ActionsInteroperability of IInteroperability { + /// Hook called before a pixel update. + /// + /// # Arguments + /// + /// * `world` - A reference to the world dispatcher. + /// * `pixel_update` - The proposed update to the pixel. + /// * `app_caller` - The app initiating the update. + /// * `player_caller` - The player initiating the update. fn on_pre_update( ref world: IWorldDispatcher, pixel_update: PixelUpdate, app_caller: App, - player_caller: ContractAddress + player_caller: ContractAddress, ) { - // do nothing + // Do nothing let _world = world; } + /// Hook called after a pixel update. + /// + /// If the snake is reverting and the previous app was 'paint', it calls the fade function. + /// + /// # Arguments + /// + /// * `world` - A reference to the world dispatcher. + /// * `pixel_update` - The update that was applied to the pixel. + /// * `app_caller` - The app that performed the update. + /// * `player_caller` - The player that performed the update. fn on_post_update( ref world: IWorldDispatcher, pixel_update: PixelUpdate, app_caller: App, - player_caller: ContractAddress + player_caller: ContractAddress, ) { let core_actions_address = get_core_actions_address(world); - assert(core_actions_address == get_caller_address(), 'caller is not core_actions'); + assert!(core_actions_address == get_caller_address(), "caller is not core_actions"); - // when the snake is reverting + // When the snake is reverting if pixel_update.app.is_some() && app_caller.system == get_contract_address() { let old_app = pixel_update.app.unwrap(); let old_app = get!(world, old_app, (App)); if old_app.name == 'paint' { let pixel = get!(world, (pixel_update.x, pixel_update.y), (Pixel)); let paint_actions = IPaintActionsDispatcher { - contract_address: old_app.system + contract_address: old_app.system, }; let params = DefaultParameters { for_player: pixel.owner, for_system: old_app.system, - position: Position { x: pixel_update.x, y: pixel_update.y }, - color: pixel_update.color.unwrap() + position: Position { x: pixel_update.x, y: pixel_update.y, }, + color: pixel_update.color.unwrap(), }; paint_actions.fade(params); } @@ -156,26 +232,41 @@ mod snake_actions { } } - - // impl: implement functions specified in trait + /// Implementation of the Snake actions. #[abi(embed_v0)] impl ActionsImpl of ISnakeActions { + /// Initializes the Snake App. + /// + /// Registers the app with core actions and sets up initial instructions. + /// + /// # Arguments + /// + /// * `world` - A reference to the world dispatcher. fn init(ref world: IWorldDispatcher) { let core_actions = pixelaw::core::utils::get_core_actions(world); - core_actions.update_app(APP_KEY, APP_ICON, APP_MANIFEST); + core_actions.new_app(contract_address_const::<0>(), APP_KEY, APP_ICON); - // TODO should use something like: starknet_keccak(array!['interact'].span()) + // TODO: Should use something like: starknet_keccak(array!['interact'].span()) let INTERACT_SELECTOR = 0x476d5e1b17fd9d508bd621909241c5eb4c67380f3651f54873c5c1f2b891f4; let INTERACT_INSTRUCTION = 'select direction for snake'; core_actions.set_instruction(INTERACT_SELECTOR, INTERACT_INSTRUCTION); } - - // A new snake starts + /// Starts a new snake or changes the direction of an existing snake. + /// + /// # Arguments + /// + /// * `world` - A reference to the world dispatcher. + /// * `default_params` - Default parameters including position and color. + /// * `direction` - The direction to move the snake. + /// + /// # Returns + /// + /// * `u32` - The ID of the snake's first segment. fn interact( - ref world: IWorldDispatcher, default_params: DefaultParameters, direction: Direction + ref world: IWorldDispatcher, default_params: DefaultParameters, direction: Direction, ) -> u32 { println!("snake: interact"); @@ -189,14 +280,14 @@ mod snake_actions { let pixel = get!(world, (position.x, position.y), Pixel); let mut snake = get!(world, player, Snake); - // change direction if snake already exists + // Change direction if snake already exists if snake.length > 0 { snake.direction = direction; set!(world, (snake)); return snake.first_segment_id; } - // TODO check if the pixel is unowned or player owned + // TODO: Check if the pixel is unowned or player owned let mut id = world.uuid(); if id == 0 { @@ -204,7 +295,7 @@ mod snake_actions { } let color = default_params.color; - let text = ''; //TODO + let text = ''; // TODO // Initialize the Snake model snake = Snake { @@ -215,7 +306,7 @@ mod snake_actions { direction: direction, color, text, - is_dying: false + is_dying: false, }; // Initialize the first SnakeSegment model (the head) @@ -227,7 +318,7 @@ mod snake_actions { y: position.y, pixel_original_color: pixel.color, pixel_original_text: pixel.text, - pixel_original_app: pixel.app + pixel_original_app: pixel.app, }; // Store the dojo model for the Snake @@ -246,8 +337,8 @@ mod snake_actions { text: Option::Some(text), app: Option::Some(get_contract_address()), owner: Option::None, - action: Option::None // Not using this feature for snake - } + action: Option::None, // Not using this feature for snake + }, ); let MOVE_SECONDS = 0; @@ -255,24 +346,32 @@ mod snake_actions { let mut calldata: Array = ArrayTrait::new(); let THIS_CONTRACT_ADDRESS = get_contract_address(); - // Calldata[0] : owner address + // Calldata[0]: Owner address calldata.append(player.into()); - // TODO should use something like: starknet_keccak(array!['move'].span()) + // TODO: Should use something like: starknet_keccak(array!['move'].span()) let MOVE_SELECTOR = 0x239e4c8fbd11b680d7214cfc26d1780d5c099453f0832beb15fd040aebd4ebb; // Schedule the next move core_actions .schedule_queue( - queue_timestamp, // When to fade next + queue_timestamp, // When to move next THIS_CONTRACT_ADDRESS, // This contract address MOVE_SELECTOR, // The move function - calldata.span() // The calldata prepared + calldata.span(), // The prepared calldata ); id } + /// Moves the snake owned by the specified owner. + /// + /// Handles the movement logic including moving, growing, shrinking, or dying. + /// + /// # Arguments + /// + /// * `world` - A reference to the world dispatcher. + /// * `owner` - The contract address of the snake's owner. fn move(ref world: IWorldDispatcher, owner: ContractAddress) { println!("snake: move"); @@ -281,7 +380,7 @@ mod snake_actions { // Load the Snake let mut snake = get!(world, (owner), (Snake)); - assert(snake.length > 0, 'no snake'); + assert!(snake.length > 0, "no snake"); let first_segment = get!(world, (snake.first_segment_id), SnakeSegment); // If the snake is dying, handle that @@ -292,36 +391,13 @@ mod snake_actions { if snake.length == 0 { println!("snake is dead: deleting"); - let position = Position { x: first_segment.x, y: first_segment.y }; + let position = Position { x: first_segment.x, y: first_segment.y, }; core_actions.alert_player(position, snake.owner, 'Snake died here'); emit!( world, Died { owner: snake.owner, x: first_segment.x, y: first_segment.y } ); - // TODO Properly use the delete functionality of Dojo. - // set!( - // world, - // (Snake { - // owner: snake.owner, - // length: 0, - // first_segment_id: 0, - // last_segment_id: 0, - // direction: Direction::None, - // color: 0x000000FF, - // text: Zeroable::zero(), - // is_dying: false - // }) - // ); - - // // According to answer on - // // - // https://discord.com/channels/1062934010722005042/1062934060898459678/1182202590260363344 - // // This is the right approach, but it doesnt seem to work. - // let snake_owner_felt: felt252 = snake.owner.into(); - // let mut layout = array![]; - // Introspect::::layout(ref layout); - // world.delete_entity('Snake'.into(), array![snake_owner_felt.into()].span(), - // layout.span()); + // Delete the snake delete!(world, (snake)); return; } @@ -349,24 +425,25 @@ mod snake_actions { text: Option::Some(snake.text), app: Option::Some(get_contract_address()), owner: Option::None, - action: Option::None // Not using this feature for snake - } + action: Option::None, // Not using this feature for snake + }, ); // Determine what happens to the snake // MOVE, GROW, SHRINK, DIE - if next_pixel.owner == contract_address_const::<0>() { // Snake just moves + if next_pixel.owner == contract_address_const::<0>() { + // Snake just moves println!("snake moves"); // Add a new segment on the next pixel and update the snake snake .first_segment_id = create_new_segment( - world, core_actions, next_pixel, snake, first_segment + world, core_actions, next_pixel, snake, first_segment, ); snake.last_segment_id = remove_last_segment(world, core_actions, snake); } else if !has_write_access { println!("snake will die"); - // Snake hit a pixel that is not allowing anyting: DIE + // Snake hit a pixel that is not allowing anything: DIE snake.is_dying = true; } else if next_pixel.owner == snake.owner { println!("snake grows"); @@ -376,7 +453,7 @@ mod snake_actions { snake .first_segment_id = create_new_segment( - world, core_actions, next_pixel, snake, first_segment + world, core_actions, next_pixel, snake, first_segment, ); // No growth if max length was reached @@ -384,10 +461,9 @@ mod snake_actions { // Revert last segment pixel snake.last_segment_id = remove_last_segment(world, core_actions, snake); } else { - snake.length = snake.length + 1; + snake.length += 1; } // We leave the tail as is - } else { println!("snake shrinks"); // Next pixel is not owned but can be used temporarily @@ -396,7 +472,7 @@ mod snake_actions { snake.is_dying = true; } else { // Add a new segment - create_new_segment(world, core_actions, next_pixel, snake, first_segment); + create_new_segment(world, core_actions, next_pixel, snake, first_segment,); // Remove last segment (this is normal for "moving") snake.last_segment_id = remove_last_segment(world, core_actions, snake); @@ -407,7 +483,7 @@ mod snake_actions { } } else { println!("snake will die"); - // Snake hit a pixel that is not allowing anyting: DIE + // Snake hit a pixel that is not allowing anything: DIE snake.is_dying = true; } @@ -420,7 +496,7 @@ mod snake_actions { let mut calldata: Array = ArrayTrait::new(); let THIS_CONTRACT_ADDRESS = get_contract_address(); - // Calldata[0] : owner + // Calldata[0]: Owner calldata.append(snake.owner.into()); // Schedule the next move @@ -429,15 +505,24 @@ mod snake_actions { queue_timestamp, // When to move next THIS_CONTRACT_ADDRESS, // This contract address get_execution_info().unbox().entry_point_selector, // This selector - calldata.span() // The calldata prepared + calldata.span(), // The prepared calldata ); } } - - // Removes the last segment of the snake and reverts the pixel + /// Removes the last segment of the snake and reverts the pixel to its original state. + /// + /// # Arguments + /// + /// * `world` - A reference to the world dispatcher. + /// * `core_actions` - A reference to the core actions dispatcher. + /// * `snake` - The snake from which to remove the last segment. + /// + /// # Returns + /// + /// * `u32` - The new `last_segment_id` for the snake. fn remove_last_segment( - world: IWorldDispatcher, core_actions: ICoreActionsDispatcher, snake: Snake + world: IWorldDispatcher, core_actions: ICoreActionsDispatcher, snake: Snake, ) -> u32 { let last_segment = get!(world, (snake.last_segment_id), SnakeSegment); let pixel = get!(world, (last_segment.x, last_segment.y), Pixel); @@ -455,39 +540,37 @@ mod snake_actions { text: Option::Some(last_segment.pixel_original_text), app: Option::Some(last_segment.pixel_original_app), owner: Option::None, - action: Option::None // Not using this feature for snake - } + action: Option::None, // Not using this feature for snake + }, ); let result = last_segment.previous_id; delete!(world, (last_segment)); - // set!( - // world, - // (SnakeSegment { - // id: snake.last_segment_id, - // previous_id: 0, - // next_id: 0, - // x: 0, - // y: 0, - // pixel_original_color: 0, - // pixel_original_text: 0, - // pixel_original_app: starknet::contract_address_const::<0x0>() - // }) - // ); - // Return the new last_segment_id for the snake result } - // Creates a new Segment on the given pixel + /// Creates a new segment on the given pixel and updates the snake. + /// + /// # Arguments + /// + /// * `world` - A reference to the world dispatcher. + /// * `core_actions` - A reference to the core actions dispatcher. + /// * `pixel` - The pixel where the new segment will be placed. + /// * `snake` - The snake to which the new segment will be added. + /// * `existing_segment` - The existing segment (head) that will be updated. + /// + /// # Returns + /// + /// * `u32` - The ID of the new segment created. fn create_new_segment( world: IWorldDispatcher, core_actions: ICoreActionsDispatcher, pixel: Pixel, snake: Snake, - mut existing_segment: SnakeSegment + mut existing_segment: SnakeSegment, ) -> u32 { let id = world.uuid(); @@ -501,13 +584,13 @@ mod snake_actions { world, SnakeSegment { id, - previous_id: id, // The first segment has no previous, so its itself + previous_id: id, // The first segment has no previous, so it's itself next_id: existing_segment.id, x: pixel.x, y: pixel.y, pixel_original_color: pixel.color, pixel_original_text: pixel.text, - pixel_original_app: pixel.app + pixel_original_app: pixel.app, } ); @@ -524,8 +607,8 @@ mod snake_actions { text: Option::Some(snake.text), app: Option::Some(get_contract_address()), owner: Option::None, - action: Option::None // Not using this feature for snake - } + action: Option::None, // Not using this feature for snake + }, ); id } diff --git a/contracts/src/core.cairo b/contracts/src/core.cairo index 39ee4a3..4cd8aab 100644 --- a/contracts/src/core.cairo +++ b/contracts/src/core.cairo @@ -2,4 +2,6 @@ pub mod models; pub mod actions; pub mod traits; pub mod utils; + +#[cfg(test)] mod tests; diff --git a/contracts/src/core/actions.cairo b/contracts/src/core/actions.cairo index c6a0ed1..eab4566 100644 --- a/contracts/src/core/actions.cairo +++ b/contracts/src/core/actions.cairo @@ -9,9 +9,37 @@ pub const CORE_ACTIONS_KEY: felt252 = 'core_actions'; #[dojo::interface] pub trait IActions { + /// Initializes the Pixelaw actions model. + /// + /// # Arguments + /// + /// * `world` - A reference to the world dispatcher. fn init(ref world: IWorldDispatcher); - fn update_permission(ref world: IWorldDispatcher, for_system: felt252, permission: Permission); - fn update_app(ref world: IWorldDispatcher, name: felt252, icon: felt252, manifest: felt252); + + /// Updates the permissions for a specified system. + /// + /// # Arguments + /// + /// * `world` - A reference to the world dispatcher. + /// * `app_key` - The key of the app (example: 'paint') to update permissions for. + /// * `permission` - The permission to set for the system. + fn update_permission(ref world: IWorldDispatcher, app_key: felt252, permission: Permission); + + // fn update_app(ref world: IWorldDispatcher, name: felt252, icon: felt252); + + /// Checks if a player or system has write access to a pixel. + /// + /// # Arguments + /// + /// * `world` - A reference to the world dispatcher. + /// * `for_player` - The player contract address. + /// * `for_system` - The system contract address. + /// * `pixel` - The pixel to check access for. + /// * `pixel_update` - The proposed update to the pixel. + /// + /// # Returns + /// + /// * `bool` - True if access is granted, false otherwise. fn has_write_access( ref world: IWorldDispatcher, for_player: ContractAddress, @@ -19,49 +47,127 @@ pub trait IActions { pixel: Pixel, pixel_update: PixelUpdate, ) -> bool; + + /// Processes a scheduled queue item. + /// + /// # Arguments + /// + /// * `world` - A reference to the world dispatcher. + /// * `id` - The unique identifier of the queue item. + /// * `timestamp` - The timestamp when the queue item was scheduled. + /// * `called_system` - The system contract address to call. + /// * `selector` - The function selector to call in the system. + /// * `calldata` - The calldata to pass to the function. fn process_queue( ref world: IWorldDispatcher, id: felt252, timestamp: u64, called_system: ContractAddress, selector: felt252, - calldata: Span + calldata: Span, ); + + /// Schedules a queue item to be processed at a specified timestamp. + /// + /// # Arguments + /// + /// * `world` - A reference to the world dispatcher. + /// * `timestamp` - The timestamp when the queue item should be processed. + /// * `called_system` - The system contract address to call. + /// * `selector` - The function selector to call in the system. + /// * `calldata` - The calldata to pass to the function. fn schedule_queue( ref world: IWorldDispatcher, timestamp: u64, called_system: ContractAddress, selector: felt252, - calldata: Span + calldata: Span, ); + + /// Updates a pixel with the provided updates. + /// + /// # Arguments + /// + /// * `world` - A reference to the world dispatcher. + /// * `for_player` - The player making the update. + /// * `for_system` - The system making the update. + /// * `pixel_update` - The updates to apply to the pixel. fn update_pixel( ref world: IWorldDispatcher, for_player: ContractAddress, for_system: ContractAddress, - pixel_update: PixelUpdate + pixel_update: PixelUpdate, ); + + /// Registers a new app. + /// + /// # Arguments + /// + /// * `world` - A reference to the world dispatcher. + /// * `system` - Contract address of the app's systems or zero to use the caller. + /// * `name` - Name of the app. + /// * `icon` - Unicode hex of the icon of the app. + /// + /// # Returns + /// + /// * `App` - Struct containing the contract address and name fields. fn new_app( ref world: IWorldDispatcher, system: ContractAddress, name: felt252, icon: felt252, - manifest: felt252 ) -> App; + + /// Retrieves the system address. + /// + /// # Arguments + /// + /// * `for_system` - The system contract address. If zero, returns the caller's address. + /// + /// # Returns + /// + /// * `ContractAddress` - The system address. fn get_system_address(for_system: ContractAddress) -> ContractAddress; + + /// Retrieves the player address. + /// + /// # Arguments + /// + /// * `for_player` - The player contract address. If zero, returns the caller's account address. + /// + /// # Returns + /// + /// * `ContractAddress` - The player address. fn get_player_address(for_player: ContractAddress) -> ContractAddress; + + /// Sends an alert to a player. + /// + /// # Arguments + /// + /// * `world` - A reference to the world dispatcher. + /// * `position` - The position associated with the alert. + /// * `player` - The player to alert. + /// * `message` - The message to send. fn alert_player( - ref world: IWorldDispatcher, position: Position, player: ContractAddress, message: felt252 + ref world: IWorldDispatcher, position: Position, player: ContractAddress, message: felt252, ); + + /// Sets an instruction for a given selector in a system. + /// + /// # Arguments + /// + /// * `world` - A reference to the world dispatcher. + /// * `selector` - The function selector. + /// * `instruction` - The instruction to set. fn set_instruction(ref world: IWorldDispatcher, selector: felt252, instruction: felt252); } - #[dojo::contract(namespace: "pixelaw", nomapping: true)] pub mod actions { use core::poseidon::poseidon_hash_span; use starknet::{ ContractAddress, get_caller_address, get_contract_address, get_tx_info, - contract_address_const, syscalls::{call_contract_syscall} + contract_address_const, syscalls::{call_contract_syscall}, }; use super::IActions; @@ -69,54 +175,54 @@ pub mod actions { use pixelaw::core::models::registry::{App, AppName, CoreActionsAddress, Instruction}; use pixelaw::core::models::permissions::{Permission, Permissions}; use pixelaw::core::models::pixel::{Pixel, PixelUpdate}; - use pixelaw::core::models::queue::{QueueItem}; + use pixelaw::core::models::queue::QueueItem; use pixelaw::core::utils::{get_core_actions_address, Position}; use pixelaw::core::traits::{IInteroperabilityDispatcher, IInteroperabilityDispatcherTrait}; - #[derive(Drop, starknet::Event)] struct QueueScheduled { id: felt252, timestamp: u64, called_system: ContractAddress, selector: felt252, - calldata: Span + calldata: Span, } #[derive(Drop, starknet::Event)] struct QueueProcessed { - id: felt252 + id: felt252, } #[derive(Drop, starknet::Event)] struct AppNameUpdated { app: App, - caller: felt252 + caller: felt252, } - #[derive(Drop, starknet::Event)] - struct Alert { - position: Position, - caller: ContractAddress, - player: ContractAddress, - message: felt252, - timestamp: u64 + #[derive(Debug, Drop, Serde, starknet::Event, PartialEq)] + pub struct Alert { + pub position: Position, + pub caller: ContractAddress, + pub player: ContractAddress, + pub message: felt252, + pub timestamp: u64, } #[event] #[derive(Drop, starknet::Event)] - enum Event { + pub enum Event { QueueScheduled: QueueScheduled, QueueProcessed: QueueProcessed, AppNameUpdated: AppNameUpdated, - Alert: Alert + Alert: Alert, } - // impl: implement functions specified in trait #[abi(embed_v0)] impl ActionsImpl of IActions { - /// Initializes the Pixelaw actions model + /// Initializes the Pixelaw actions model. + /// + /// One World has one CoreActions model that can be discovered by anyone. fn init(ref world: IWorldDispatcher) { set!( world, @@ -124,58 +230,66 @@ pub mod actions { ); } - /// not performing checks because it's only granting permissions to a system by the caller - /// it is in the app's responsibility to handle update_permission responsibly + /// Updates the permissions for a specified system. + /// + /// # Arguments + /// + /// * `world` - A reference to the world dispatcher. + /// * `app_key` - The key of the app (example: 'paint') to update permissions for. + /// * `permission` - The permission to set for the system. + /// + /// # Remarks + /// + /// This function grants permissions to a system by the caller. + /// It is the app's responsibility to handle `update_permission` responsibly. fn update_permission( - ref world: IWorldDispatcher, for_system: felt252, permission: Permission + ref world: IWorldDispatcher, app_key: felt252, permission: Permission, ) { let caller_address = get_caller_address(); - // Retrieve the App of the for_system - let allowed_app = get!(world, for_system, (AppName)); + // TODO maybe check that the caller is indeed an app? + + // Retrieve the App of the `for_system` + let allowed_app = get!(world, app_key, (AppName)); let allowed_app = allowed_app.system; + println!("appkey: {:?}", app_key); + println!("caller_address: {:?}", caller_address); + println!("allowed_app: {:?}", allowed_app); + set!(world, Permissions { allowing_app: caller_address, allowed_app, permission }); } - /// Updates the name of an app in the registry + /// Schedules a queue item to be processed at a specified timestamp. /// /// # Arguments /// - /// * `name` - The new name of the app - /// * `icon` - unicode hex of the icon of the app - /// * `manifest` - url to the system's manifest.json - fn update_app( - ref world: IWorldDispatcher, name: felt252, icon: felt252, manifest: felt252 - ) { - let system = get_caller_address(); - let app = self.new_app(system, name, icon, manifest); - emit!(world, (Event::AppNameUpdated(AppNameUpdated { app, caller: system.into() }))); - } - + /// * `world` - A reference to the world dispatcher. + /// * `timestamp` - The timestamp when the queue item should be processed. + /// * `called_system` - The system contract address to call. + /// * `selector` - The function selector to call in the system. + /// * `calldata` - The calldata to pass to the function. + /// + /// # Remarks + /// + /// This function emits an event that external schedulers can pick up. fn schedule_queue( ref world: IWorldDispatcher, timestamp: u64, called_system: ContractAddress, selector: felt252, - calldata: Span + calldata: Span, ) { println!("schedule_queue"); - // TODO Review security - - // Retrieve the caller system from the address. - // This prevents non-system addresses to schedule queue - // let caller_system = get!(world, caller_address, (App)).system; - - // let calldata_span = calldata.span(); + // TODO: Review security // hash the call and store the hash for verification let id = poseidon_hash_span( array![ timestamp.into(), called_system.into(), selector, poseidon_hash_span(calldata) ] - .span() + .span(), ); // Emit the event, so an external scheduler can pick it up @@ -188,44 +302,43 @@ pub mod actions { println!("schedule_queue DONE"); } + /// Processes a scheduled queue item. + /// + /// # Arguments + /// + /// * `world` - A reference to the world dispatcher. + /// * `id` - The unique identifier of the queue item. + /// * `timestamp` - The timestamp when the queue item was scheduled. + /// * `called_system` - The system contract address to call. + /// * `selector` - The function selector to call in the system. + /// * `calldata` - The calldata to pass to the function. + /// + /// # Remarks + /// + /// This function verifies the integrity of the queue item before processing it. fn process_queue( ref world: IWorldDispatcher, id: felt252, timestamp: u64, called_system: ContractAddress, selector: felt252, - calldata: Span + calldata: Span, ) { println!("process_queue"); - // A quick check on the timestamp so we know its not too early for this one - assert(timestamp <= starknet::get_block_timestamp(), 'timestamp still in the future'); - - // TODO Do we need a mechanism to ensure that Queued items are really coming from a - // schedule? - // In theory someone can just call this action directly with whatever, as long as the ID - // is correct it will be executed. - // It is only possible to call Apps though, so as long as the security of the Apps is - // okay, it should be fine? - // And we could add some rate limiting to prevent griefing? - // - // The only way i can think of doing "authentication" of a QueueItem would be to store - // the ID (hash) onchain, but that gets expensive soon? - - // TODO processQueue should never revert. + // A quick check on the timestamp so we know it's not too early for this one + assert!(timestamp <= starknet::get_block_timestamp(), "timestamp still in the future"); // Recreate the id to check the integrity let calculated_id = poseidon_hash_span( array![ timestamp.into(), called_system.into(), selector, poseidon_hash_span(calldata) ] - .span() + .span(), ); - // TODO check if id exists onchain - // Only valid when the queue item was found by the hash - assert(calculated_id == id, 'Invalid Id'); + assert!(calculated_id == id, "Invalid Id"); // Make the call itself let _result = call_contract_syscall(called_system, selector, calldata); @@ -235,12 +348,30 @@ pub mod actions { println!("process_queue DONE"); } + /// Checks if a player or system has write access to a pixel. + /// + /// # Arguments + /// + /// * `world` - A reference to the world dispatcher. + /// * `for_player` - The player contract address. + /// * `for_system` - The system contract address. + /// * `pixel` - The pixel to check access for. + /// * `pixel_update` - The proposed update to the pixel. + /// + /// # Returns + /// + /// * `bool` - True if access is granted, false otherwise. + /// + /// # Remarks + /// + /// This function verifies whether the caller has the necessary permissions to update the + /// pixel. fn has_write_access( ref world: IWorldDispatcher, for_player: ContractAddress, for_system: ContractAddress, pixel: Pixel, - pixel_update: PixelUpdate + pixel_update: PixelUpdate, ) -> bool { // The originator of the transaction let caller_account = get_tx_info().unbox().account_contract_address; @@ -256,17 +387,16 @@ pub mod actions { // The caller is not a System, and not owner, so no reason to keep looking. return false; } - // Deal with Scheduler calling - // The caller_address is a System, let's see if it has access + // The `caller_address` is a System, let's see if it has access // Retrieve the App of the calling System let caller_app = get!(world, caller_address, (App)); - // TODO decide whether an App by default has write on a pixel with same App? + // TODO: Decide whether an App by default has write on a pixel with same App - // If its the same app, always allow. + // If it's the same app, always allow. // It's the responsibility of the App developer to ensure separation of ownership if pixel.app == caller_app.system { return true; @@ -297,18 +427,30 @@ pub mod actions { true } + /// Updates a pixel with the provided updates. + /// + /// # Arguments + /// + /// * `world` - A reference to the world dispatcher. + /// * `for_player` - The player making the update. + /// * `for_system` - The system making the update. + /// * `pixel_update` - The updates to apply to the pixel. + /// + /// # Remarks + /// + /// This function applies the updates to the pixel if the caller has write access. fn update_pixel( ref world: IWorldDispatcher, for_player: ContractAddress, for_system: ContractAddress, - pixel_update: PixelUpdate + pixel_update: PixelUpdate, ) { println!("update_pixel"); let mut pixel = get!(world, (pixel_update.x, pixel_update.y), (Pixel)); - assert( - self.has_write_access(for_player, for_system, pixel, pixel_update), 'No access!' + assert!( + self.has_write_access(for_player, for_system, pixel, pixel_update), "No access!" ); let old_pixel_app = pixel.app; @@ -319,7 +461,7 @@ pub mod actions { contract_address: old_pixel_app }; let app_caller = get!(world, for_system, (App)); - interoperable_app.on_pre_update(pixel_update, app_caller, for_player) + interoperable_app.on_pre_update(pixel_update, app_caller, for_player); } // If the pixel has no owner set yet, do that now. @@ -362,12 +504,22 @@ pub mod actions { contract_address: old_pixel_app }; let app_caller = get!(world, for_system, (App)); - interoperable_app.on_post_update(pixel_update, app_caller, for_player) + interoperable_app.on_post_update(pixel_update, app_caller, for_player); } println!("update_pixel DONE"); } + /// Retrieves the player address. + /// + /// # Arguments + /// + /// * `for_player` - The player contract address. If zero, returns the caller's account + /// address. + /// + /// # Returns + /// + /// * `ContractAddress` - The player address. fn get_player_address(for_player: ContractAddress) -> ContractAddress { if for_player == contract_address_const::<0>() { println!("get_player_address.zero"); @@ -377,20 +529,28 @@ pub mod actions { return result; } else { println!("get_player_address.nonzero"); - // TODO: check if getter is a system or the core actions contract + // TODO: Check if getter is a system or the core actions contract - // Return the for_player + // Return the `for_player` return for_player; } } + /// Retrieves the system address. + /// + /// # Arguments + /// + /// * `for_system` - The system contract address. If zero, returns the caller's address. + /// + /// # Returns + /// + /// * `ContractAddress` - The system address. fn get_system_address(for_system: ContractAddress) -> ContractAddress { if for_system != contract_address_const::<0>() { - // TODO - // Check that the caller is the CoreActions contract - // Otherwise, it should be 0 (if caller not core_actions) + // TODO: Check that the caller is the CoreActions contract + // Otherwise, it should be zero (if caller not core_actions) - // Return the for_player + // Return the `for_system` return for_system; } else { // Return the caller account from the transaction (the end user) @@ -398,43 +558,46 @@ pub mod actions { } } - /// Registers an App + /// Registers a new app. /// /// # Arguments /// - /// * `system` - Contract address of the app's systems - /// * `name` - Name of the app - /// * `icon` - unicode hex of the icon of the app - /// * `manifest` - url to the system's manifest.json + /// * `world` - A reference to the world dispatcher. + /// * `system` - Contract address of the app's systems or zero to use the caller. + /// * `name` - Name of the app. + /// * `icon` - Unicode hex of the icon of the app. /// /// # Returns /// - /// * `App` - Struct with contractaddress and name fields + /// * `App` - Struct containing the contract address and name fields. fn new_app( ref world: IWorldDispatcher, system: ContractAddress, name: felt252, icon: felt252, - manifest: felt252 ) -> App { + let mut app_system = system; + // If the system is not given, use the caller for this. + // This is expected to be called from the `app.init()` function + if system == contract_address_const::<0>() { + app_system = get_caller_address(); + } + // Load app - let mut app = get!(world, system, (App)); + let mut app = get!(world, app_system, (App)); // Load app_name let mut app_name = get!(world, name, (AppName)); // Ensure neither contract nor name have been registered - assert( - app.name == 0 && app_name.system == contract_address_const::<0>(), 'app already set' + assert!( + app.name == 0 && app_name.system == contract_address_const::<0>(), "app already set" ); // Associate system with name app.name = name; - app.icon = icon; - app.manifest = manifest; - // Associate name with system app_name.system = system; @@ -445,15 +608,27 @@ pub mod actions { app } + /// Sends an alert to a player. + /// + /// # Arguments + /// + /// * `world` - A reference to the world dispatcher. + /// * `position` - The position associated with the alert. + /// * `player` - The player to alert. + /// * `message` - The message to send. + /// + /// # Remarks + /// + /// Only callable by registered apps. fn alert_player( ref world: IWorldDispatcher, position: Position, player: ContractAddress, - message: felt252 + message: felt252, ) { let caller = get_caller_address(); let app = get!(world, caller, (App)); - assert(app.name != '', 'cannot be called by a non-app'); + assert!(app.name != '', "cannot be called by a non-app"); emit!( world, (Event::Alert( @@ -468,10 +643,21 @@ pub mod actions { ); } + /// Sets an instruction for a given selector in a system. + /// + /// # Arguments + /// + /// * `world` - A reference to the world dispatcher. + /// * `selector` - The function selector. + /// * `instruction` - The instruction to set. + /// + /// # Remarks + /// + /// Only callable by registered apps. fn set_instruction(ref world: IWorldDispatcher, selector: felt252, instruction: felt252) { let system = get_caller_address(); let app = get!(world, system, (App)); - assert(app.name != '', 'cannot be called by a non-app'); + assert!(app.name != '', "cannot be called by a non-app"); set!(world, (Instruction { system, selector, instruction })) } } diff --git a/contracts/src/core/models/permissions.cairo b/contracts/src/core/models/permissions.cairo index 06ef037..c02b323 100644 --- a/contracts/src/core/models/permissions.cairo +++ b/contracts/src/core/models/permissions.cairo @@ -1,7 +1,7 @@ use starknet::{ContractAddress, ClassHash}; // TODO is this using packing? If not, try to use bitmasking approach -#[derive(Copy, Drop, Serde, Introspect)] +#[derive(Copy, Drop, Serde, Introspect, PartialEq)] pub struct Permission { pub app: bool, pub color: bool, diff --git a/contracts/src/core/models/pixel.cairo b/contracts/src/core/models/pixel.cairo index 2d7337e..16d03f7 100644 --- a/contracts/src/core/models/pixel.cairo +++ b/contracts/src/core/models/pixel.cairo @@ -12,7 +12,7 @@ pub struct PixelUpdate { pub action: Option } -#[derive(Copy, Drop, Serde)] +#[derive(Copy, Drop, Serde, PartialEq)] #[dojo::model(namespace: "pixelaw", nomapping: true)] pub struct Pixel { // System properties diff --git a/contracts/src/core/models/registry.cairo b/contracts/src/core/models/registry.cairo index 41d3337..6fd94c7 100644 --- a/contracts/src/core/models/registry.cairo +++ b/contracts/src/core/models/registry.cairo @@ -12,8 +12,6 @@ pub struct App { #[key] pub system: ContractAddress, pub name: felt252, - // ipfs link to the contract's manifest.json - pub manifest: felt252, pub icon: felt252, // Default action for the UI (a function in the system) pub action: felt252 diff --git a/contracts/src/core/tests.cairo b/contracts/src/core/tests.cairo index a7d564e..10ec92e 100644 --- a/contracts/src/core/tests.cairo +++ b/contracts/src/core/tests.cairo @@ -1,72 +1,4 @@ -#[cfg(test)] -mod tests { - use starknet::class_hash::{ClassHash}; - - use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; - - use pixelaw::core::models::registry::{app, app_name, core_actions_address}; - - use pixelaw::core::models::pixel::{Pixel, PixelUpdate}; - use pixelaw::core::models::pixel::{pixel}; - use pixelaw::core::models::permissions::{permissions}; - use pixelaw::core::utils::{get_core_actions, Direction, Position, DefaultParameters}; - use pixelaw::core::actions::{actions, IActionsDispatcher, IActionsDispatcherTrait}; - use dojo::utils::test::{spawn_test_world, deploy_contract}; - use core::poseidon::poseidon_hash_span; - - use core::traits::TryInto; - - - const SPAWN_PIXEL_ENTRYPOINT: felt252 = - 0x01c199924ae2ed5de296007a1ac8aa672140ef2a973769e4ad1089829f77875a; - - #[test] - #[available_gas(30000000)] - fn test_process_queue() { - let mut models = array![ - pixel::TEST_CLASS_HASH, - app::TEST_CLASS_HASH, - app_name::TEST_CLASS_HASH, - core_actions_address::TEST_CLASS_HASH, - permissions::TEST_CLASS_HASH, - ]; - let world = spawn_test_world(["pixelaw"].span(), models.span()); - - let core_actions_address = world - .deploy_contract('salt1', actions::TEST_CLASS_HASH.try_into().unwrap()); - - let core_actions = IActionsDispatcher { contract_address: core_actions_address }; - - let position = Position { x: 0, y: 0 }; - - let mut calldata: Array = ArrayTrait::new(); - calldata.append('snake'); - position.serialize(ref calldata); - calldata.append('snake'); - calldata.append(0); - let id = poseidon_hash_span( - array![ - 0.into(), - core_actions_address.into(), - SPAWN_PIXEL_ENTRYPOINT.into(), - poseidon_hash_span(calldata.span()) - ] - .span() - ); - - core_actions - .process_queue(id, 0, core_actions_address, SPAWN_PIXEL_ENTRYPOINT, calldata.span()); - - let pixel = get!(world, (position).into(), (Pixel)); - - // check timestamp - assert( - pixel.created_at == starknet::get_block_timestamp(), 'incorrect timestamp.created_at' - ); - assert( - pixel.updated_at == starknet::get_block_timestamp(), 'incorrect timestamp.updated_at' - ); - assert(pixel.x == position.x, 'incorrect timestamp.x'); - assert(pixel.y == position.y, 'incorrect timestamp.y'); - } -} +pub mod base; +pub mod interop; +pub mod queue; +pub mod helpers; diff --git a/contracts/src/core/tests/base.cairo b/contracts/src/core/tests/base.cairo new file mode 100644 index 0000000..a74f3f0 --- /dev/null +++ b/contracts/src/core/tests/base.cairo @@ -0,0 +1,287 @@ +use core::fmt::Display; +use starknet::{ + get_block_timestamp, contract_address_const, ClassHash, ContractAddress, + testing::{set_block_timestamp, set_account_contract_address, set_caller_address}, +}; + +use core::{traits::TryInto, poseidon::poseidon_hash_span}; + +use dojo::{ + utils::test::{spawn_test_world, deploy_contract}, + world::{IWorldDispatcher, IWorldDispatcherTrait} +}; + +use pixelaw::core::{ + models::{ + registry::{App, AppName, app, app_name, core_actions_address, CoreActionsAddress}, + pixel::{Pixel, PixelUpdate, pixel}, permissions::{permissions, Permission, Permissions} + }, + actions::{actions, IActionsDispatcher, IActionsDispatcherTrait, CORE_ACTIONS_KEY}, + utils::{get_core_actions, Direction, Position, DefaultParameters}, + tests::helpers::{ + setup_core, setup_core_initialized, setup_apps, setup_apps_initialized, ZERO_ADDRESS, + set_caller, drop_all_events, TEST_POSITION, WHITE_COLOR, RED_COLOR, PERMISSION_ALL, + PERMISSION_NONE + } +}; + +use pixelaw::{ + apps::{ + paint::app::{ + paint_actions, IPaintActionsDispatcher, IPaintActionsDispatcherTrait, + APP_KEY as PAINT_APP_KEY + }, + snake::app::{ + snake, Snake, snake_segment, SnakeSegment, snake_actions, ISnakeActionsDispatcher, + ISnakeActionsDispatcherTrait, APP_KEY as SNAKE_APP_KEY + } + } +}; + + +#[test] +fn test_init_core_actions() { + let (world, core_actions, _player_1, _player_2) = setup_core(); + let core_address = get!(world, CORE_ACTIONS_KEY, (CoreActionsAddress)); + assert(core_address.value == ZERO_ADDRESS(), 'should be 0'); + + core_actions.init(); + + let core_address = get!(world, CORE_ACTIONS_KEY, (CoreActionsAddress)); + assert(core_address.value != ZERO_ADDRESS(), 'should not be 0'); +} + +#[test] +fn test_register_new_app() { + let (world, core_actions, _player_1, _player_2) = setup_core(); + let app_name = 'myname'; + let mock_app1_system = contract_address_const::<0xBEAD>(); + let _new_app1: App = core_actions.new_app(mock_app1_system, app_name, ''); + // TODO check return values + + let loaded_app1_name = get!(world, app_name, (AppName)); + let loaded_app1 = get!(world, loaded_app1_name.system, (App)); + assert(loaded_app1.name == app_name, 'App name incorrect'); + assert(loaded_app1.system == mock_app1_system, 'App system incorrect'); +} + + +#[test] +fn test_paint_interaction() { + let (world, _core_actions, _player_1, _player_2) = setup_core_initialized(); + let (paint_actions, _snake_actions) = setup_apps_initialized(world); + + paint_actions + .interact( + DefaultParameters { + for_player: ZERO_ADDRESS(), // Leave this 0 if not processing the Queue + for_system: ZERO_ADDRESS(), // Leave this 0 if not processing the Queue + position: TEST_POSITION, + color: RED_COLOR + } + ); +} + +#[test] +fn test_update_permission() { + let (world, core_actions, player_1, _player_2) = setup_core_initialized(); + + let permissioning_system = contract_address_const::<0xBEEF01>(); + let permissioned_system = contract_address_const::<0xDEAD01>(); + + set_caller(player_1); + + // Setup PermissioningApp + let permissioning: App = core_actions.new_app(permissioning_system, 'permissioning', ''); + + // Setup PermissionedApp + let permissioned: App = core_actions.new_app(permissioned_system, 'permissioned', ''); + + // Check that existing permissions are NONE + let current_permissions = get!(world, (permissioning.system, permissioned.system), Permissions); + assert(current_permissions.permission == PERMISSION_NONE, 'permissions not none'); + + // Update the permissions, as caller + set_caller(permissioning.system); + core_actions.update_permission(permissioned.name, PERMISSION_ALL); + + // Check that existing permissions are ALL + let new_permissions = get!(world, (permissioning.system, permissioned.system), Permissions); + + assert(new_permissions.permission == PERMISSION_ALL, 'permissions not all'); +} + + +#[test] +fn test_has_write_access() { + let (world, core_actions, player_1, player_2) = setup_core_initialized(); + let (paint_actions, _snake_actions) = setup_apps_initialized(world); + + // Scenario: + // Check if Player2 can change Player1's pixel + + let position = Position { x: 12, y: 12 }; + let color = 0xFF0000FF; + // Setup Pixel + set_caller(player_1); + paint_actions + .put_color( + DefaultParameters { + for_player: ZERO_ADDRESS(), for_system: ZERO_ADDRESS(), position, color + } + ); + + // Setup PixelUpdate + let pixel_update = PixelUpdate { + x: 12, + y: 12, + color: Option::Some(0xFF00FFFF), + owner: Option::Some(player_1), + app: Option::Some(paint_actions.contract_address), + text: Option::None, + timestamp: Option::None, + action: Option::None + }; + + set_caller(player_2); + let pixel = get!(world, (position.x, position.y), Pixel); + + let has_access = core_actions + .has_write_access(ZERO_ADDRESS(), ZERO_ADDRESS(), pixel, pixel_update); + + assert(has_access == false, 'should not have access'); + + set_caller(player_1); + + let has_access = core_actions + .has_write_access(ZERO_ADDRESS(), ZERO_ADDRESS(), pixel, pixel_update); + + assert(has_access == true, 'should have access'); +} + + +#[test] +fn test_update_pixel() { + let (world, core_actions, player_1, _player_2) = setup_core_initialized(); + + set_caller(player_1); + + let x = 22; + let y = 23; + let color: u32 = 0xFF00FFFF; + let app = contract_address_const::<0xBEEFDEAD>(); + let owner = player_1; + let text = 'mytext'; + let timestamp: u64 = 123123; + let action = 'myaction'; + + let empty_pixel = Pixel { + x, + y, + color: 0, + app: ZERO_ADDRESS(), + owner: ZERO_ADDRESS(), + text: 0, + timestamp: 0, + action: 0, + created_at: 0, + updated_at: 0 + }; + + let mut changed_pixel = Pixel { + x, y, color, app, owner, text, timestamp, action, created_at: 0, updated_at: 0 + }; + + let pixel_update = PixelUpdate { + x, + y, + color: Option::Some(color), + owner: Option::Some(owner), + app: Option::Some(app), + text: Option::Some(text), + timestamp: Option::Some(timestamp), + action: Option::Some(action) + }; + + let pixel = get!(world, (x, y), Pixel); + + assert(pixel == empty_pixel, 'pixel not empty'); + + core_actions.update_pixel(ZERO_ADDRESS(), ZERO_ADDRESS(), pixel_update); + + let pixel = get!(world, (x, y), Pixel); + + // TODO properly test created_at and updated_at (if we even keep them like this) + changed_pixel.created_at = pixel.created_at; + changed_pixel.updated_at = pixel.updated_at; + + assert(pixel == changed_pixel, 'pixel was not changed'); +} + + +#[test] +fn test_get_player_address() { + let (_world, core_actions, player_1, player_2) = setup_core_initialized(); + + // Test with 0 address, we expect the caller + set_account_contract_address(player_1); + + let addr = core_actions.get_player_address(ZERO_ADDRESS()); + assert(addr == player_1, 'should return player1'); + + let addr = core_actions.get_player_address(player_2); + assert(addr == player_2, 'should return player2'); +} + + +#[test] +fn test_get_system_address() { + let (world, core_actions, _player_1, _player_2) = setup_core_initialized(); + let (paint_actions, snake_actions) = setup_apps_initialized(world); + + set_caller(paint_actions.contract_address); + + let addr = core_actions.get_system_address(ZERO_ADDRESS()); + assert(addr == paint_actions.contract_address, 'should return paint_contract'); + + let addr = core_actions.get_system_address(snake_actions.contract_address); + assert(addr == snake_actions.contract_address, 'should return snake_contract'); +} + +// TODO Try alerting with a nonexisting appkey (should panic) + +#[test] +fn test_alert_player() { + let (world, core_actions, player_1, _player_2) = setup_core_initialized(); + let (paint_actions, _snake_actions) = setup_apps_initialized(world); + + // Prep params + let position = Position { x: 12, y: 12 }; + let message = 'testme'; + let caller = paint_actions.contract_address; + let player = player_1; + + set_caller(caller); + + // Pop all the previous events from the log so only the following one will be there + drop_all_events(world.contract_address); + + // Call the action + core_actions.alert_player(position, player, message); + + // Assert that the correct event was emitted + assert_eq!( + starknet::testing::pop_log(world.contract_address), + Option::Some( + pixelaw::core::actions::actions::Alert { + position, caller, player, message, timestamp: get_block_timestamp() + } + ) + ); +} + +fn test_set_instruction(world: IWorldDispatcher, player: ContractAddress, instruction: felt252) { + // Implementation for setting an instruction for the player + println!("Setting instruction for player {:?}: {}", player, instruction); +} + diff --git a/contracts/src/core/tests/helpers.cairo b/contracts/src/core/tests/helpers.cairo new file mode 100644 index 0000000..c9015de --- /dev/null +++ b/contracts/src/core/tests/helpers.cairo @@ -0,0 +1,132 @@ +use starknet::{ + get_block_timestamp, contract_address_const, ClassHash, ContractAddress, + testing::{set_block_timestamp, set_account_contract_address}, +}; + +use core::{traits::TryInto, poseidon::poseidon_hash_span}; + +use dojo::{ + utils::test::{spawn_test_world, deploy_contract}, + world::{IWorldDispatcher, IWorldDispatcherTrait} +}; + +use pixelaw::core::{ + models::{ + registry::{App, app, app_name, core_actions_address, CoreActionsAddress, Instruction, instruction}, + pixel::{Pixel, PixelUpdate, pixel}, permissions::{permissions, Permission, Permissions} + }, + actions::{actions, IActionsDispatcher, IActionsDispatcherTrait, CORE_ACTIONS_KEY}, + utils::{get_core_actions, Direction, Position, DefaultParameters} +}; + +use pixelaw::{ + apps::{ + paint::app::{paint_actions, IPaintActionsDispatcher, IPaintActionsDispatcherTrait}, + snake::app::{ + snake, Snake, snake_segment, SnakeSegment, snake_actions, ISnakeActionsDispatcher, + ISnakeActionsDispatcherTrait + } + } +}; + + +pub const TEST_POSITION: Position = Position { x: 1, y: 1 }; +pub const WHITE_COLOR: u32 = 0xFFFFFFFF; +pub const RED_COLOR: u32 = 0xFF0000FF; + + +pub const PERMISSION_ALL: Permission = + Permission { app: true, color: true, owner: true, text: true, timestamp: true, action: true }; + +pub const PERMISSION_NONE: Permission = + Permission { + app: false, color: false, owner: false, text: false, timestamp: false, action: false + }; + + +pub fn set_caller(caller: ContractAddress) { + starknet::testing::set_account_contract_address(caller); + starknet::testing::set_contract_address(caller); +} + +pub fn ZERO_ADDRESS() -> ContractAddress { + contract_address_const::<0x0>() +} + +pub fn setup_core_initialized() -> (IWorldDispatcher, IActionsDispatcher, ContractAddress, ContractAddress) { + let (world, core_actions, player_1, player_2) = setup_core(); + + core_actions.init(); + + (world, core_actions, player_1, player_2) +} + +pub fn setup_core() -> (IWorldDispatcher, IActionsDispatcher, ContractAddress, ContractAddress) { + let mut models = array![ + pixel::TEST_CLASS_HASH, + app::TEST_CLASS_HASH, + app_name::TEST_CLASS_HASH, + core_actions_address::TEST_CLASS_HASH, + permissions::TEST_CLASS_HASH, + instruction::TEST_CLASS_HASH, + ]; + let world = spawn_test_world(["pixelaw"].span(), models.span()); + + let core_actions_address = world + .deploy_contract('salt1', actions::TEST_CLASS_HASH.try_into().unwrap()); + let core_actions = IActionsDispatcher { contract_address: core_actions_address }; + + // Setup permissions + world.grant_writer(selector_from_tag!("pixelaw-App"), core_actions_address); + world.grant_writer(selector_from_tag!("pixelaw-AppName"), core_actions_address); + world.grant_writer(selector_from_tag!("pixelaw-CoreActionsAddress"), core_actions_address); + world.grant_writer(selector_from_tag!("pixelaw-Pixel"), core_actions_address); + world.grant_writer(selector_from_tag!("pixelaw-Permissions"), core_actions_address); + world.grant_writer(selector_from_tag!("pixelaw-Instruction"), core_actions_address); + + // Setup players + let player_1 = contract_address_const::<0x1337>(); + let player_2 = contract_address_const::<0x42>(); + + (world, core_actions, player_1, player_2) +} + + +pub fn setup_apps_initialized(world: IWorldDispatcher) -> (IPaintActionsDispatcher, ISnakeActionsDispatcher) { + let (paint_actions, snake_actions) = setup_apps(world); + + paint_actions.init(); + snake_actions.init(); + + (paint_actions, snake_actions) +} + +pub fn setup_apps(world: IWorldDispatcher) -> (IPaintActionsDispatcher, ISnakeActionsDispatcher) { + let core_address = get!(world, CORE_ACTIONS_KEY, (CoreActionsAddress)); + + world.register_model((snake::TEST_CLASS_HASH).try_into().unwrap()); + world.register_model((snake_segment::TEST_CLASS_HASH).try_into().unwrap()); + + let paint_actions_address = world + .deploy_contract('salt3', paint_actions::TEST_CLASS_HASH.try_into().unwrap()); + let paint_actions = IPaintActionsDispatcher { contract_address: paint_actions_address }; + + let snake_actions_address = world + .deploy_contract('salt4', snake_actions::TEST_CLASS_HASH.try_into().unwrap()); + let snake_actions = ISnakeActionsDispatcher { contract_address: snake_actions_address }; + + // Setup permissions + world.grant_writer(selector_from_tag!("pixelaw-Snake"), core_address.value); + world.grant_writer(selector_from_tag!("pixelaw-SnakeSegment"), core_address.value); + + (paint_actions, snake_actions) +} + +pub fn drop_all_events(address: ContractAddress) { + loop { + match starknet::testing::pop_log_raw(address) { + core::option::Option::Some(_) => {}, + core::option::Option::None => { break; }, + }; + } +} diff --git a/contracts/src/core/tests/interop.cairo b/contracts/src/core/tests/interop.cairo new file mode 100644 index 0000000..e69de29 diff --git a/contracts/src/core/tests/queue.cairo b/contracts/src/core/tests/queue.cairo new file mode 100644 index 0000000..5a7b11e --- /dev/null +++ b/contracts/src/core/tests/queue.cairo @@ -0,0 +1,59 @@ +use starknet::{ + get_block_timestamp, contract_address_const, ClassHash, ContractAddress, + testing::{set_block_timestamp, set_account_contract_address}, +}; + +use core::{traits::TryInto, poseidon::poseidon_hash_span}; + +use dojo::{ + utils::test::{spawn_test_world, deploy_contract}, + world::{IWorldDispatcher, IWorldDispatcherTrait} +}; + +use pixelaw::core::{ + models::{ + registry::{app, app_name, core_actions_address}, pixel::{Pixel, PixelUpdate, pixel}, + permissions::{permissions} + }, + actions::{actions, IActionsDispatcher, IActionsDispatcherTrait}, + utils::{get_core_actions, Direction, Position, DefaultParameters}, tests::helpers::setup_core_initialized +}; + + +const SPAWN_PIXEL_ENTRYPOINT: felt252 = + 0x01c199924ae2ed5de296007a1ac8aa672140ef2a973769e4ad1089829f77875a; + +#[test] +#[available_gas(30000000)] +fn test_process_queue() { + let (world, core_actions, _player_1, _player_2) = pixelaw::core::tests::helpers::setup_core_initialized(); + let position = Position { x: 0, y: 0 }; + + let mut calldata: Array = ArrayTrait::new(); + calldata.append('snake'); + position.serialize(ref calldata); + calldata.append('snake'); + calldata.append(0); + let id = poseidon_hash_span( + array![ + 0.into(), + core_actions.contract_address.into(), + SPAWN_PIXEL_ENTRYPOINT.into(), + poseidon_hash_span(calldata.span()) + ] + .span() + ); + + core_actions + .process_queue( + id, 0, core_actions.contract_address.into(), SPAWN_PIXEL_ENTRYPOINT, calldata.span() + ); + + let pixel = get!(world, (position).into(), (Pixel)); + + // check timestamp + assert(pixel.created_at == starknet::get_block_timestamp(), 'incorrect timestamp.created_at'); + assert(pixel.updated_at == starknet::get_block_timestamp(), 'incorrect timestamp.updated_at'); + assert(pixel.x == position.x, 'incorrect timestamp.x'); + assert(pixel.y == position.y, 'incorrect timestamp.y'); +} diff --git a/contracts/src/core/utils.cairo b/contracts/src/core/utils.cairo index d00a8f0..cf84ac0 100644 --- a/contracts/src/core/utils.cairo +++ b/contracts/src/core/utils.cairo @@ -1,5 +1,6 @@ use starknet::{ContractAddress, get_caller_address, ClassHash, get_contract_address}; use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; +use pixelaw::core::models::pixel::{Pixel}; #[derive(Serde, Copy, Drop, Introspect)] pub enum Direction { @@ -10,7 +11,7 @@ pub enum Direction { Down: (), } -#[derive(Copy, Drop, Serde, Introspect)] +#[derive(Debug, Copy, Drop, Serde, Introspect, PartialEq)] pub struct Position { pub x: u32, pub y: u32 @@ -159,3 +160,8 @@ pub fn decode_color(color: u32) -> (u8, u8, u8, u8) { (r, g, b, a) } + +pub fn is_pixel_color(world: IWorldDispatcher, position: Position, color: u32) -> bool { + let pixel: Pixel = get!(world, (position.x, position.y), (Pixel)); + pixel.color == color +}