# ============================================================================= # ENGINE (FRAMEWORK FILE) VERSION 2026-07-12 # ============================================================================= # The engine uses Palix sprites to render the animated actors in a scene. # It also manages tilemaps and tilesets as well as the gamepad module. # If you know how to make your own engine, you could delete this file. # ----------------------------------------------------------------------------- module gamepad var left: bool var right: bool var up: bool var down: bool var button_a: bool var button_b: bool var button_c: bool var button_d: bool end module # ----------------------------------------------------------------------------- class tile_layer var tilemap: tilemap var tileset: tileset var io_tilemap: io_tilemap var io_tileset_addresses: int[size 1024] var x: int, y: int # If screen_space=true, then the camera does not affect this tilemap var screen_space: bool # If wrap=true, the tilemap repeats infinitely in every direction var wrap: bool # 8 for regular tiles, 16 for jumbo tiles var _tile_dimension: int var _tile_count: int # --------------------------------------------------------------------------- constructor(io_tilemap: io_tilemap, io_tileset_addresses: int[size 1024]) .io_tilemap <- io_tilemap .io_tileset_addresses <- io_tileset_addresses end constructor # --------------------------------------------------------------------------- # Use tileset=null to unload. func load_tileset(tileset: tileset) .tileset <- tileset var io_tilemap: io_tilemap io_tilemap <- .io_tilemap var io_tileset_addresses: int[size 1024] io_tileset_addresses <- .io_tileset_addresses var i: int i <- 0 var tile_count: int if tileset = null then tile_count <- 0 else if tileset.jumbo then io_tilemap.jumbo <- 1 ._tile_dimension <- 16 else io_tilemap.jumbo <- 0 ._tile_dimension <- 8 end if io_tilemap.tile_pitch <- 0 tile_count <- tileset.tiles.size if tile_count > 1024 do tile_count <- 1024 loop if i >= tile_count do drop var tile: tile tile <- tileset.tiles[i] if tile = null then io_tileset_addresses[i] <- 0 else # (+3 skips array size) io_tileset_addresses[i] <- to_address(tile.pixels) + 3 end if i <- i + 1 end loop end if # Clear the remainder; to avoid iterating the entire array every time, we # assume the tile_layer constructor received an array that was all zeros. var old_tile_count: int old_tile_count <- ._tile_count loop if i >= old_tile_count do drop io_tileset_addresses[i] <- 0 i <- i + 1 end loop ._tile_count <- tile_count end func # --------------------------------------------------------------------------- # Use tilemap=null to unload. func load_tilemap(tilemap: tilemap) .tilemap <- tilemap var io_tilemap: io_tilemap io_tilemap <- .io_tilemap if tilemap = null then io_tilemap.col_count <- 0 io_tilemap.row_count <- 0 io_tilemap.tile_codes_address <- 0 io_tilemap.edge_mode <- 0 return end if io_tilemap.col_count <- tilemap.col_count io_tilemap.row_count <- tilemap.row_count # (+3 skips array size) io_tilemap.tile_codes_address <- to_address(tilemap.tile_codes) + 3 end func # --------------------------------------------------------------------------- func _render() var io_tilemap: io_tilemap io_tilemap <- .io_tilemap if .tilemap <> null then var x: int, y: int var x_pair: pair, y_pair: pair x <- .x y <- .y if not .screen_space then x <- x - engine::camera_x y <- y - engine::camera_y end if x_pair <- to_pair(x) y_pair <- to_pair(y) var clip_edge: bool clip_edge <- not .wrap io_tilemap.edge_mode <- to_byte(clip_edge) # 0=wrap, 1=clip if clip_edge and (x_pair <> x or y_pair <> y) then # Hide the tilemap if to_pair() overflows io_tilemap.x <- -16384 else io_tilemap.x <- x_pair io_tilemap.y <- y_pair end if end if end func # --------------------------------------------------------------------------- func get_tile_at_colrow(col: int, row: int): pair var tilemap: tilemap tilemap <- .tilemap var col_count: int, row_count: int col_count <- tilemap.col_count row_count <- tilemap.row_count if col >= 0 and col < col_count and row >= 0 and row < row_count then return tilemap.tile_codes[row * col_count + col] else return -1 end if end func # --------------------------------------------------------------------------- func set_tile_at_colrow(col: int, row: int, tile_code: pair) var tilemap: tilemap tilemap <- .tilemap var col_count: int, row_count: int col_count <- tilemap.col_count row_count <- tilemap.row_count if col >= 0 and col < col_count and row >= 0 and row < row_count then tilemap.tile_codes[row * col_count + col] <- tile_code end if end func # --------------------------------------------------------------------------- func get_tile_at_xy(x: int, y: int): pair var tile_dimension: int tile_dimension <- ._tile_dimension var result: pair # Euclidean division handles negative (x,y) correctly result <- .get_tile_at_colrow(x // tile_dimension, y // tile_dimension) return result end func # --------------------------------------------------------------------------- func set_tile_at_xy(x: int, y: int, tile_code: pair) var tile_dimension: int tile_dimension <- ._tile_dimension .set_tile_at_colrow(x // tile_dimension, y // tile_dimension, tile_code) end func # --------------------------------------------------------------------------- func get_color_at_xy(x: int, y: int): byte var tile_code: pair, tx: int, ty: int var tile: tile var tileset: tileset tileset <- .tileset var tile_dimension: int tile_dimension <- ._tile_dimension tile_code <- .get_tile_at_colrow(x // tile_dimension, y // tile_dimension) # Bits 0..9 encode the tile index (0..1023) from the tileset var tile_index: pair tile_index <- to_pair(math::bit_and(tile_code, $03ff)) if tile_code >= 0 and tile_index < tileset.tiles.size then tile <- tileset.tiles[tile_index] if tile <> null then tx <- x %% tile_dimension ty <- y %% tile_dimension var color: byte color <- tile.pixels[ty * tile_dimension + tx] return color end if end if return 0 # clear color end func end class # ----------------------------------------------------------------------------- module engine var camera_x: int, camera_y: int var tile_layer_a: tile_layer var tile_layer_b: tile_layer var tile_layer_c: tile_layer var frame_counter: pair var _thinklist_head: actor var _thinklist_tail: actor # A special "fence" node that is not actually used, # but marks the start/end of the circlular list var _renderlist_fence: actor # For a given depth, the next node will be added before this one # in the renderlist var _insertion_points: actor[size 4] var _last_render_actor: actor var _actor_count: int # Avoid having to clear sprites that are unused var _used_sprites: int # 0=none, 1=thinking, 2=rendering, 3=clearing var _busy: byte # --------------------------------------------------------------------------- func init() io::paint_mode <- 1 # 1 = buffered engine::frame_counter <- io::frame_counter engine::_thinklist_head <- null engine::_thinklist_tail <- null var insertion_points: actor[size 4] insertion_points <- new actor[size 4]() engine::_insertion_points <- insertion_points var renderlist_fence: actor renderlist_fence <- new actor() # Create the circular list renderlist_fence._renderlist_prev_link <- renderlist_fence renderlist_fence._renderlist_next_link <- renderlist_fence # Wire up the pointers insertion_points[0] <- renderlist_fence insertion_points[1] <- renderlist_fence insertion_points[2] <- renderlist_fence insertion_points[3] <- renderlist_fence engine::_last_render_actor <- renderlist_fence engine::_renderlist_fence <- renderlist_fence engine::_actor_count <- 0 engine::_used_sprites <- 0 io::background_color <-- 13 # skyscape engine::tile_layer_a <- | new tile_layer(io::tilemap_a, io::tileset_a_addresses) engine::tile_layer_b <- | new tile_layer(io::tilemap_b, io::tileset_b_addresses) engine::tile_layer_c <- | new tile_layer(io::tilemap_c, io::tileset_c_addresses) end func # --------------------------------------------------------------------------- func load_actor_spot(spot: s_actor_spot): actor var actor: actor if spot.actor_factory <> null then actor <- spot.actor_factory() else actor <- new actor() end if actor.apply_spot(spot) engine::add_actor_behind(actor) return actor end func # --------------------------------------------------------------------------- func load_actor_spots(spots: s_actor_spot[]) var i, size size <- spots.size i <- 0 loop if i >= size do drop engine::load_actor_spot(spots[i]) i <- i + 1 end loop end func # --------------------------------------------------------------------------- func clear_actors() var old_busy: byte old_busy <- engine::_busy # 0=none, 1=thinking, 2=rendering, 3=clearing if old_busy = 3 do return # already clearing if old_busy = 2 do kernel::fail("Actor list is busy") engine::_busy <- 3 # The loop could simply use get_next_actor() and remove_actor(), but the # loop is optimized based on the guarantee that add_actor() can't happen var actor: actor actor <- engine::_thinklist_head # renderlist is temporarily corrupted during this loop loop if actor = null do drop if actor._renderlist_prev_link <> null then # This actor is not already marked for removal, so remove it # Remove from list actor._renderlist_prev_link <- null actor._renderlist_next_link <- null engine::_actor_count <- engine::_actor_count - 1 actor.on_removed() end if actor <- actor._thinklist_next_link end loop # Clear out the thinklist engine::_prune_thinklist() # Now reset the renderlist var renderlist_fence: actor renderlist_fence <- engine::_renderlist_fence # Empty the circular list renderlist_fence._renderlist_prev_link <- renderlist_fence renderlist_fence._renderlist_next_link <- renderlist_fence var insertion_points: actor[size 4] insertion_points <- engine::_insertion_points # Wire up the pointers insertion_points[0] <- renderlist_fence insertion_points[1] <- renderlist_fence insertion_points[2] <- renderlist_fence insertion_points[3] <- renderlist_fence engine::_last_render_actor <- renderlist_fence engine::_actor_count <- 0 engine::_busy <- old_busy end func # --------------------------------------------------------------------------- # Add the actor in front of all other actors in its depth level func add_actor(actor: actor) engine::_add_actor(actor, false) actor.on_added() end func # --------------------------------------------------------------------------- # Add the actor behind all other actors in its depth level func add_actor_behind(actor: actor) engine::_add_actor(actor, true) actor.on_added() end func # --------------------------------------------------------------------------- func _add_actor(actor: actor, behind: bool) # 0=none, 1=thinking, 2=rendering, 3=clearing if engine::_busy >= 2 do kernel::fail("Actor list is busy") if actor._renderlist_prev_link <> null do kernel::fail("Actor already added") # --- Append to thinklist if engine::_thinklist_tail = null then # the list was empty engine::_thinklist_head <- actor engine::_thinklist_tail <- actor actor._thinklist_next_link <- null elsif actor._thinklist_next_link = null | and engine::_thinklist_tail <> actor then # it's not already in thinklist, so add it engine::_thinklist_tail._thinklist_next_link <- actor engine::_thinklist_tail <- actor end if # --- Insert into renderlist var insertion_points: actor[size 4] insertion_points <- engine::_insertion_points var depth: byte depth <- actor._depth var insertion_point: actor insertion_point <- insertion_points[depth] if behind then # Find the depth+1 insertion point var next_insertion_point: actor if depth < 3 then next_insertion_point <- insertion_points[depth + 1] else next_insertion_point <- engine::_renderlist_fence end if if insertion_point = next_insertion_point then # Our depth sublist is empty, so revert to the non-behind algorithm behind <- false else # Insert before next_insertion_point, i.e. the end of our sublist insertion_point <- next_insertion_point end if end if # Add the actor immediately before the insertion_point actor._renderlist_next_link <- insertion_point actor._renderlist_prev_link <- insertion_point._renderlist_prev_link actor._renderlist_prev_link._renderlist_next_link <- actor insertion_point._renderlist_prev_link <- actor # (no fixups are needed if we inserted before next_insertion_point) if not behind then insertion_points[depth] <- actor # adjust other insertion points loop if depth = 0 do drop depth <- to_byte(depth - 1) if insertion_points[depth] = insertion_point do insertion_points[depth] <- actor end loop end if engine::_actor_count <- engine::_actor_count + 1 end func # --------------------------------------------------------------------------- func remove_actor(actor: actor) engine::_remove_actor(actor) actor.on_removed() end func # --------------------------------------------------------------------------- func _remove_actor(actor: actor) # 0=none, 1=thinking, 2=rendering, 3=clearing if engine::_busy = 2 do kernel::fail("Actor list is busy") var insertion_points: actor[size 4] var next_link: actor insertion_points <- engine::_insertion_points next_link <- actor._renderlist_next_link if next_link = null or actor = engine::_renderlist_fence do kernel::fail("Actor not added") # (We don't process thinklist at all here, because we might be # in the middle of iterating the list. Clearing _renderlist_next_link # will mark it for removal by _prune_thinklist().) # If this is an insertion_points, then move to the next one if insertion_points[0] = actor do insertion_points[0] <- next_link if insertion_points[1] = actor do insertion_points[1] <- next_link if insertion_points[2] = actor do insertion_points[2] <- next_link if insertion_points[3] = actor do insertion_points[3] <- next_link if engine::_last_render_actor = actor do engine::_last_render_actor <- actor._renderlist_prev_link # Remove from list actor._renderlist_next_link._renderlist_prev_link <- actor._renderlist_prev_link actor._renderlist_prev_link._renderlist_next_link <- actor._renderlist_next_link actor._renderlist_prev_link <- null actor._renderlist_next_link <- null engine::_actor_count <- engine::_actor_count - 1 end func # --------------------------------------------------------------------------- func get_first_actor(): actor var actor: actor actor <- engine::_thinklist_head loop if actor = null or actor._renderlist_prev_link <> null do drop # Skip over this actor because it is marked for removal actor <- actor._thinklist_next_link end loop return actor end func # --------------------------------------------------------------------------- func get_next_actor(current: actor): actor var actor: actor if current = null then actor <- null else actor <- current._thinklist_next_link loop if actor = null or actor._renderlist_prev_link <> null do drop # Skip over this actor because it is marked for removal actor <- actor._thinklist_next_link end loop end if return actor end func # --------------------------------------------------------------------------- # Remove the actors that were marked for removal func _prune_thinklist() var current: actor var previous: actor previous <- null current <- engine::_thinklist_head loop if current = null then # Reached the end if previous = null then # Previous was the head engine::_thinklist_head <- null engine::_thinklist_tail <- null else # Previous was the tail engine::_thinklist_tail <- previous previous._thinklist_next_link <- null end if drop end if var next: actor next <- current._thinklist_next_link if current._renderlist_prev_link = null then # This actor was marked for removal current._thinklist_next_link <- null # (Don't update previous, because it is the previous non-removed node.) else # This actor was not marked for removal if previous = null then engine::_thinklist_head <- current else previous._thinklist_next_link <- current end if previous <- current end if current <- next end loop end func # --------------------------------------------------------------------------- func get_camera_cx(): int # Viewport dimensions are 320 x 224, so 160 is half the width return engine::camera_x + 160 end func # --------------------------------------------------------------------------- func set_camera_cx(value: int) engine::camera_x <- value - 160 end func # --------------------------------------------------------------------------- func get_camera_cy(): int # Viewport dimensions are 320 x 224, so 112 is half the height return engine::camera_y + 112 end func # --------------------------------------------------------------------------- func set_camera_cy(value: int) engine::camera_y <- value - 112 end func # --------------------------------------------------------------------------- func render() # 0=none, 1=thinking, 2=rendering, 3=clearing if engine::_busy <> 0 do kernel::fail("Nested engine call") engine::_busy <- 2 # Update game camera engine::tile_layer_a._render() engine::tile_layer_b._render() engine::tile_layer_c._render() # Update frame counter var frame_delta: pair frame_delta <- engine::frame_counter engine::frame_counter <- io::frame_counter frame_delta <- to_pair(engine::frame_counter - frame_delta) if frame_delta < 0 do frame_delta <- 1 # overflow var renderlist_fence: actor var end_actor: actor renderlist_fence <- engine::_renderlist_fence end_actor <- engine::_last_render_actor var sprite_index: int sprite_index <- 0 if end_actor = renderlist_fence then # This situation arises after adding to an empty list; # ensure that end_actor is an actual actor end_actor <- renderlist_fence._renderlist_prev_link end if # (If we're still at the fence, then the list is empty) if end_actor <> renderlist_fence then # Count how many actors until the end var sprites_left: byte sprites_left <- 64 var actor: actor actor <- end_actor loop actor <- actor._renderlist_next_link if actor = renderlist_fence do drop if actor.visible then sprites_left <- to_byte(sprites_left - 1) if sprites_left = 0 do drop end if end loop # Start allocating sprites if sprites_left = 0 then engine::_last_render_actor <- actor else # The sequence wraps back to the head, so start rendering with the remainder # because its z is higher # actor = renderlist_fence loop actor <- actor._renderlist_next_link if actor.visible then actor.render(sprite_index, frame_delta) sprite_index <- sprite_index + 1 sprites_left <- to_byte(sprites_left - 1) if sprites_left = 0 do drop end if if actor = end_actor do drop end loop engine::_last_render_actor <- actor end if # Now do the first part again sprites_left <- 64 actor <- end_actor loop actor <- actor._renderlist_next_link if actor = renderlist_fence do drop if actor.visible then actor.render(sprite_index, frame_delta) sprite_index <- sprite_index + 1 sprites_left <- to_byte(sprites_left - 1) if sprites_left = 0 do drop end if end loop end if # Hide any remaining unused sprites var i: int i <- sprite_index loop if i >= engine::_used_sprites do drop if i >= 64 do drop # Hide this sprite io::sprites[i].pixels_address <- 0 i <- i + 1 end loop engine::_used_sprites <- sprite_index engine::_busy <- 0 end func # --------------------------------------------------------------------------- # Wait for the next video frame to get painted. # This also synchronizes the main loop to at most 30 fps. func wait_for_paint() io::irq_pending <- 1 # "write 1 to clear" irq_pending if io::paint_mode <> 0 do io::paint_mode <- 3 # in buffered mode, we need to request a paint io::irq_wake_mask <- 1 # sleep until the paint finishes kernel::sleep() end func # --------------------------------------------------------------------------- func think() # 0=none, 1=thinking, 2=rendering, 3=clearing if engine::_busy <> 0 do kernel::fail("Nested engine call") engine::_busy <- 1 var buttons: byte buttons <-- io::gamepad_0.buttons gamepad::button_a <- math::bit_and(buttons, 1) <> 0 gamepad::button_b <- math::bit_and(buttons, 2) <> 0 gamepad::button_c <- math::bit_and(buttons, 4) <> 0 gamepad::button_d <- math::bit_and(buttons, 8) <> 0 gamepad::left <- false gamepad::right <- false gamepad::up <- false gamepad::down <- false var p: pair p <- io::gamepad_0.x if p >= 512 then gamepad::right <- true elsif p <= -512 then gamepad::left <- true end if p <- io::gamepad_0.y if p >= 512 then gamepad::down <- true elsif p <= -512 then gamepad::up <- true end if var actor: actor actor <- engine::get_first_actor() loop if actor = null do drop # Don't process actors that were marked for removal if actor._renderlist_prev_link <> null do actor.think() actor <- engine::get_next_actor(actor) end loop # Now that we've finished processing, clean up any nodes that were # marked for removal by engine::remove_actor() engine::_prune_thinklist() engine::_busy <- 0 end func end module