Skip to main content

[ENGINE]

# =============================================================================
# ENGINE (FRAMEWORK FILE) VERSION 2025-11-29
# =============================================================================
# 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

# ---------------------------------------------------------------------------
FUNC NEW(IO_TILEMAP: IO_TILEMAP, IO_TILESET_ADDRESSES: INT[SIZE 1024])
IO_TILEMAP -> .IO_TILEMAP
IO_TILESET_ADDRESSES -> .IO_TILESET_ADDRESSES
END FUNC

# ---------------------------------------------------------------------------
# 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

IF TILESET = NULL THEN
0 -> IO_TILEMAP.JUMBO
0 -> IO_TILEMAP.TILE_COUNT
RETURN
END IF

VAR OLD_TILE_COUNT: PAIR
IO_TILEMAP.TILE_COUNT -> OLD_TILE_COUNT

VAR TILE_COUNT: PAIR, P: PAIR

IF TILESET.JUMBO THEN
1 -> IO_TILEMAP.JUMBO
16 -> ._TILE_DIMENSION
ELSE
0 -> IO_TILEMAP.JUMBO
8 -> ._TILE_DIMENSION
END IF

TO_PAIR(TILESET.TILES.SIZE) -> TILE_COUNT
IF TILE_COUNT > 1024
DO 1024 -> TILE_COUNT
TILE_COUNT -> IO_TILEMAP.TILE_COUNT

0 -> P
LOOP
IF P >= TILE_COUNT
DO DROP

VAR TILE: TILE
TILESET.TILES[P] -> TILE
IF TILE <> NULL THEN
# (+3 SKIPS ARRAY SIZE)
TO_ADDRESS(TILE.PIXELS) + 3 -> IO_TILESET_ADDRESSES[P]
END IF

TO_PAIR(P + 1) -> P
END LOOP

# CLEAR THE REMAINDER
LOOP
IF P >= OLD_TILE_COUNT
DO DROP

0 -> IO_TILESET_ADDRESSES[P]
TO_PAIR(P + 1) -> P
END LOOP
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
0 -> IO_TILEMAP.COL_COUNT
0 -> IO_TILEMAP.ROW_COUNT
0 -> IO_TILEMAP.TILE_CODES_ADDRESS
0 -> IO_TILEMAP.EDGE_MODE
RETURN
END IF

TILEMAP.COL_COUNT -> IO_TILEMAP.COL_COUNT
TILEMAP.ROW_COUNT -> IO_TILEMAP.ROW_COUNT

# (+3 SKIPS ARRAY SIZE)
TO_ADDRESS(TILEMAP.TILE_CODES) + 3 -> IO_TILEMAP.TILE_CODES_ADDRESS
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 - ENGINE::CAMERA_X -> X
Y - ENGINE::CAMERA_Y -> Y
END IF

TO_PAIR(X) -> X_PAIR
TO_PAIR(Y) -> Y_PAIR

VAR CLIP_EDGE: BOOL
NOT .WRAP -> CLIP_EDGE

TO_BYTE(CLIP_EDGE) -> IO_TILEMAP.EDGE_MODE # 0=WRAP, 1=CLIP

IF CLIP_EDGE AND (X_PAIR <> X OR Y_PAIR <> Y) THEN
# HIDE THE TILEMAP IF TO_PAIR() OVERFLOWS
-16384 -> IO_TILEMAP.X
ELSE
X_PAIR -> IO_TILEMAP.X
Y_PAIR -> IO_TILEMAP.Y
END IF
END IF
END FUNC

# ---------------------------------------------------------------------------
FUNC GET_TILE_AT_COLROW(COL: INT, ROW: INT): PAIR
VAR TILEMAP: TILEMAP
.TILEMAP -> TILEMAP

VAR TILE_CODE: PAIR
VAR COL_COUNT: INT, ROW_COUNT: INT
TILEMAP.COL_COUNT -> COL_COUNT
TILEMAP.ROW_COUNT -> 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
ELSE
-1 -> TILE_CODE
END IF

RETURN TILE_CODE
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
TILEMAP.COL_COUNT -> COL_COUNT
TILEMAP.ROW_COUNT -> ROW_COUNT

IF COL >= 0 AND COL < COL_COUNT AND ROW >= 0 AND ROW < ROW_COUNT THEN
TILE_CODE -> TILEMAP.TILE_CODES[ROW * COL_COUNT + COL]
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
.GET_TILE_AT_COLROW(X / TILE_DIMENSION, Y / TILE_DIMENSION) -> RESULT
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, COLOR: BYTE

VAR TILESET: TILESET
.TILESET -> TILESET

VAR TILE_DIMENSION: INT
._TILE_DIMENSION -> TILE_DIMENSION

.GET_TILE_AT_COLROW(X / TILE_DIMENSION, Y / TILE_DIMENSION) -> TILE_CODE

# BITS 0..9 ENCODE THE TILE INDEX (0..1023) FROM THE TILESET
VAR TILE_INDEX: PAIR
TO_PAIR(MATH::BIT_AND(TILE_CODE, $03FF)) -> TILE_INDEX

IF TILE_CODE >= 0 AND TILE_INDEX < TILESET.TILES.SIZE THEN
TILESET.TILES[TILE_INDEX] -> TILE

IF TILE <> NULL THEN
X % TILE_DIMENSION -> TX
Y % TILE_DIMENSION -> TY
TILE.PIXELS[TY * TILE_DIMENSION + TX] -> COLOR
RETURN COLOR
END IF
END IF

0 -> COLOR # CLEAR COLOR
RETURN 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()
0 -> IO::PAINT_MODE
IO::FRAME_COUNTER -> ENGINE::FRAME_COUNTER

NULL -> ENGINE::_THINKLIST_HEAD
NULL -> ENGINE::_THINKLIST_TAIL

VAR INSERTION_POINTS: ACTOR[SIZE 4]
NEW ACTOR[SIZE 4]() -> INSERTION_POINTS

INSERTION_POINTS -> ENGINE::_INSERTION_POINTS

VAR RENDERLIST_FENCE: ACTOR
NEW ACTOR() -> RENDERLIST_FENCE

# CREATE THE CIRCULAR LIST
RENDERLIST_FENCE -> RENDERLIST_FENCE._RENDERLIST_PREV_LINK
RENDERLIST_FENCE -> RENDERLIST_FENCE._RENDERLIST_NEXT_LINK

# WIRE UP THE POINTERS
RENDERLIST_FENCE -> 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

0 -> ENGINE::_ACTOR_COUNT
0 -> ENGINE::_USED_SPRITES

IO::BACKGROUND_COLOR <-- 13 # SKYSCAPE

NEW TILE_LAYER(IO::TILEMAP_A, IO::TILESET_A_ADDRESSES)
| -> ENGINE::TILE_LAYER_A
NEW TILE_LAYER(IO::TILEMAP_B, IO::TILESET_B_ADDRESSES)
| -> ENGINE::TILE_LAYER_B
NEW TILE_LAYER(IO::TILEMAP_C, IO::TILESET_C_ADDRESSES)
| -> ENGINE::TILE_LAYER_C
END FUNC

# ---------------------------------------------------------------------------
FUNC LOAD_ACTOR_SPOT(SPOT: S_ACTOR_SPOT): ACTOR
VAR ACTOR: ACTOR

IF SPOT.ACTOR_FACTORY <> NULL THEN
SPOT.ACTOR_FACTORY() -> ACTOR
ELSE
NEW ACTOR() -> 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
SPOTS.SIZE -> SIZE
0 -> I
LOOP
IF I >= SIZE
DO DROP
ENGINE::LOAD_ACTOR_SPOT(SPOTS[I])
I + 1 -> I
END LOOP
END FUNC

# ---------------------------------------------------------------------------
FUNC CLEAR_ACTORS()
VAR OLD_BUSY: BYTE
ENGINE::_BUSY -> OLD_BUSY

# 0=NONE, 1=THINKING, 2=RENDERING, 3=CLEARING
IF OLD_BUSY = 3
DO RETURN # ALREADY CLEARING

IF OLD_BUSY = 2
DO KERNEL::FAIL("OPERATION IS NOT ALLOWED")

3 -> ENGINE::_BUSY

# 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
ENGINE::_THINKLIST_HEAD -> ACTOR

# 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
NULL -> ACTOR._RENDERLIST_PREV_LINK
NULL -> ACTOR._RENDERLIST_NEXT_LINK

ENGINE::_ACTOR_COUNT - 1 -> ENGINE::_ACTOR_COUNT

ACTOR.ON_REMOVED()
END IF

ACTOR._THINKLIST_NEXT_LINK -> ACTOR
END LOOP

# CLEAR OUT THE THINKLIST
ENGINE::_PRUNE_THINKLIST()

# NOW RESET THE RENDERLIST
VAR RENDERLIST_FENCE: ACTOR
ENGINE::_RENDERLIST_FENCE -> RENDERLIST_FENCE

# EMPTY THE CIRCULAR LIST
RENDERLIST_FENCE -> RENDERLIST_FENCE._RENDERLIST_PREV_LINK
RENDERLIST_FENCE -> RENDERLIST_FENCE._RENDERLIST_NEXT_LINK

VAR INSERTION_POINTS: ACTOR[SIZE 4]
ENGINE::_INSERTION_POINTS -> INSERTION_POINTS

# WIRE UP THE POINTERS
RENDERLIST_FENCE -> INSERTION_POINTS[0]
RENDERLIST_FENCE -> INSERTION_POINTS[1]
RENDERLIST_FENCE -> INSERTION_POINTS[2]
RENDERLIST_FENCE -> INSERTION_POINTS[3]
RENDERLIST_FENCE -> ENGINE::_LAST_RENDER_ACTOR

0 -> ENGINE::_ACTOR_COUNT

OLD_BUSY -> ENGINE::_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("OPERATION IS NOT ALLOWED")

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
ACTOR -> ENGINE::_THINKLIST_HEAD
ACTOR -> ENGINE::_THINKLIST_TAIL
NULL -> ACTOR._THINKLIST_NEXT_LINK
ELSIF ACTOR._THINKLIST_NEXT_LINK = NULL
| AND ENGINE::_THINKLIST_TAIL <> ACTOR THEN

# IT'S NOT ALREADY IN THINKLIST, SO ADD IT
ACTOR -> ENGINE::_THINKLIST_TAIL._THINKLIST_NEXT_LINK
ACTOR -> ENGINE::_THINKLIST_TAIL
END IF

# --- INSERT INTO RENDERLIST
VAR INSERTION_POINTS: ACTOR[SIZE 4]
ENGINE::_INSERTION_POINTS -> INSERTION_POINTS
VAR DEPTH: BYTE
ACTOR._DEPTH -> DEPTH

VAR INSERTION_POINT: ACTOR
INSERTION_POINTS[DEPTH] -> INSERTION_POINT

IF BEHIND THEN
# FIND THE DEPTH+1 INSERTION POINT
VAR NEXT_INSERTION_POINT: ACTOR
IF DEPTH < 3 THEN
INSERTION_POINTS[DEPTH + 1] -> NEXT_INSERTION_POINT
ELSE
ENGINE::_RENDERLIST_FENCE -> NEXT_INSERTION_POINT
END IF
IF INSERTION_POINT = NEXT_INSERTION_POINT THEN
# OUR DEPTH SUBLIST IS EMPTY, SO REVERT TO THE NON-BEHIND ALGORITHM
FALSE -> BEHIND
ELSE
# INSERT BEFORE NEXT_INSERTION_POINT, I.E. THE END OF OUR SUBLIST
NEXT_INSERTION_POINT -> INSERTION_POINT
END IF
END IF

# ADD THE ACTOR IMMEDIATELY BEFORE THE INSERTION_POINT
INSERTION_POINT -> ACTOR._RENDERLIST_NEXT_LINK
INSERTION_POINT._RENDERLIST_PREV_LINK -> ACTOR._RENDERLIST_PREV_LINK

ACTOR -> ACTOR._RENDERLIST_PREV_LINK._RENDERLIST_NEXT_LINK
ACTOR -> INSERTION_POINT._RENDERLIST_PREV_LINK

# (NO FIXUPS ARE NEEDED IF WE INSERTED BEFORE NEXT_INSERTION_POINT)
IF NOT BEHIND THEN
ACTOR -> INSERTION_POINTS[DEPTH]

# ADJUST OTHER INSERTION POINTS
LOOP
IF DEPTH = 0
DO DROP
TO_BYTE(DEPTH - 1) -> DEPTH

IF INSERTION_POINTS[DEPTH] = INSERTION_POINT
DO ACTOR -> INSERTION_POINTS[DEPTH]
END LOOP
END IF

ENGINE::_ACTOR_COUNT + 1 -> ENGINE::_ACTOR_COUNT
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("OPERATION IS NOT ALLOWED")

VAR INSERTION_POINTS: ACTOR[SIZE 4]
VAR NEXT_LINK: ACTOR

ENGINE::_INSERTION_POINTS -> INSERTION_POINTS
ACTOR._RENDERLIST_NEXT_LINK -> 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 NEXT_LINK -> INSERTION_POINTS[0]

IF INSERTION_POINTS[1] = ACTOR
DO NEXT_LINK -> INSERTION_POINTS[1]

IF INSERTION_POINTS[2] = ACTOR
DO NEXT_LINK -> INSERTION_POINTS[2]

IF INSERTION_POINTS[3] = ACTOR
DO NEXT_LINK -> INSERTION_POINTS[3]

IF ENGINE::_LAST_RENDER_ACTOR = ACTOR
DO ACTOR._RENDERLIST_PREV_LINK -> ENGINE::_LAST_RENDER_ACTOR

# REMOVE FROM LIST
ACTOR._RENDERLIST_PREV_LINK -> ACTOR._RENDERLIST_NEXT_LINK._RENDERLIST_PREV_LINK
ACTOR._RENDERLIST_NEXT_LINK -> ACTOR._RENDERLIST_PREV_LINK._RENDERLIST_NEXT_LINK
NULL -> ACTOR._RENDERLIST_PREV_LINK
NULL -> ACTOR._RENDERLIST_NEXT_LINK

ENGINE::_ACTOR_COUNT - 1 -> ENGINE::_ACTOR_COUNT
END FUNC

# ---------------------------------------------------------------------------
FUNC GET_FIRST_ACTOR(): ACTOR
VAR ACTOR: ACTOR
ENGINE::_THINKLIST_HEAD -> ACTOR

LOOP
IF ACTOR = NULL OR ACTOR._RENDERLIST_PREV_LINK <> NULL
DO DROP

# SKIP OVER THIS ACTOR BECAUSE IT IS MARKED FOR REMOVAL
ACTOR._THINKLIST_NEXT_LINK -> ACTOR
END LOOP

RETURN ACTOR
END FUNC

# ---------------------------------------------------------------------------
FUNC GET_NEXT_ACTOR(CURRENT: ACTOR): ACTOR
VAR ACTOR: ACTOR

IF CURRENT = NULL THEN
NULL -> ACTOR
ELSE
CURRENT._THINKLIST_NEXT_LINK -> ACTOR
LOOP
IF ACTOR = NULL OR ACTOR._RENDERLIST_PREV_LINK <> NULL
DO DROP

# SKIP OVER THIS ACTOR BECAUSE IT IS MARKED FOR REMOVAL
ACTOR._THINKLIST_NEXT_LINK -> ACTOR
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
NULL -> PREVIOUS
ENGINE::_THINKLIST_HEAD -> CURRENT
LOOP
IF CURRENT = NULL THEN
# REACHED THE END
IF PREVIOUS = NULL THEN
# PREVIOUS WAS THE HEAD
NULL -> ENGINE::_THINKLIST_HEAD
NULL -> ENGINE::_THINKLIST_TAIL
ELSE
# PREVIOUS WAS THE TAIL
PREVIOUS -> ENGINE::_THINKLIST_TAIL
NULL -> PREVIOUS._THINKLIST_NEXT_LINK
END IF

DROP
END IF

VAR NEXT: ACTOR
CURRENT._THINKLIST_NEXT_LINK -> NEXT

IF CURRENT._RENDERLIST_PREV_LINK = NULL THEN
# THIS ACTOR WAS MARKED FOR REMOVAL
NULL -> CURRENT._THINKLIST_NEXT_LINK

# (DON'T UPDATE PREVIOUS, BECAUSE IT IS THE PREVIOUS NON-REMOVED NODE.)
ELSE
# THIS ACTOR WAS NOT MARKED FOR REMOVAL
IF PREVIOUS = NULL THEN
CURRENT -> ENGINE::_THINKLIST_HEAD
ELSE
CURRENT -> PREVIOUS._THINKLIST_NEXT_LINK
END IF

CURRENT -> PREVIOUS
END IF

NEXT -> CURRENT
END LOOP
END FUNC

# ---------------------------------------------------------------------------
FUNC GET_CAMERA_CX(): INT
VAR RESULT
# VIEWPORT DIMENSIONS ARE 320 X 224, SO 160 IS HALF THE WIDTH
ENGINE::CAMERA_X + 160 -> RESULT
RETURN RESULT
END FUNC

# ---------------------------------------------------------------------------
FUNC SET_CAMERA_CX(VALUE: INT)
VALUE - 160 -> ENGINE::CAMERA_X
END FUNC

# ---------------------------------------------------------------------------
FUNC GET_CAMERA_CY(): INT
VAR RESULT
# VIEWPORT DIMENSIONS ARE 320 X 224, SO 112 IS HALF THE HEIGHT
ENGINE::CAMERA_Y + 112 -> RESULT
RETURN RESULT
END FUNC

# ---------------------------------------------------------------------------
FUNC SET_CAMERA_CY(VALUE: INT)
VALUE - 112 -> ENGINE::CAMERA_Y
END FUNC

# ---------------------------------------------------------------------------
FUNC RENDER()
# 0=NONE, 1=THINKING, 2=RENDERING, 3=CLEARING
IF ENGINE::_BUSY <> 0
DO KERNEL::FAIL("OPERATION IS NOT ALLOWED")

2 -> ENGINE::_BUSY

# 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
ENGINE::FRAME_COUNTER -> FRAME_DELTA
IO::FRAME_COUNTER -> ENGINE::FRAME_COUNTER
TO_PAIR(ENGINE::FRAME_COUNTER - FRAME_DELTA) -> FRAME_DELTA

IF FRAME_DELTA < 0
DO 1 -> FRAME_DELTA # OVERFLOW

VAR RENDERLIST_FENCE: ACTOR
VAR END_ACTOR: ACTOR
ENGINE::_RENDERLIST_FENCE -> RENDERLIST_FENCE
ENGINE::_LAST_RENDER_ACTOR -> END_ACTOR

VAR SPRITE_INDEX: INT
0 -> SPRITE_INDEX

IF END_ACTOR = RENDERLIST_FENCE THEN
# THIS SITUATION ARISES AFTER ADDING TO AN EMPTY LIST;
# ENSURE THAT END_ACTOR IS AN ACTUAL ACTOR
RENDERLIST_FENCE._RENDERLIST_PREV_LINK -> END_ACTOR
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
64 -> SPRITES_LEFT

VAR ACTOR: ACTOR
END_ACTOR -> ACTOR
LOOP
ACTOR._RENDERLIST_NEXT_LINK -> ACTOR

IF ACTOR = RENDERLIST_FENCE
DO DROP

IF ACTOR.VISIBLE THEN
TO_BYTE(SPRITES_LEFT - 1) -> SPRITES_LEFT
IF SPRITES_LEFT = 0
DO DROP
END IF
END LOOP

# START ALLOCATING SPRITES

IF SPRITES_LEFT = 0 THEN
ACTOR -> ENGINE::_LAST_RENDER_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._RENDERLIST_NEXT_LINK -> ACTOR

IF ACTOR.VISIBLE THEN
ACTOR.RENDER(SPRITE_INDEX, FRAME_DELTA)
SPRITE_INDEX + 1 -> SPRITE_INDEX

TO_BYTE(SPRITES_LEFT - 1) -> SPRITES_LEFT
IF SPRITES_LEFT = 0
DO DROP
END IF

IF ACTOR = END_ACTOR
DO DROP
END LOOP

ACTOR -> ENGINE::_LAST_RENDER_ACTOR
END IF

# NOW DO THE FIRST PART AGAIN
64 -> SPRITES_LEFT
END_ACTOR -> ACTOR
LOOP
ACTOR._RENDERLIST_NEXT_LINK -> ACTOR

IF ACTOR = RENDERLIST_FENCE
DO DROP

IF ACTOR.VISIBLE THEN
ACTOR.RENDER(SPRITE_INDEX, FRAME_DELTA)
SPRITE_INDEX + 1 -> SPRITE_INDEX

TO_BYTE(SPRITES_LEFT - 1) -> SPRITES_LEFT
IF SPRITES_LEFT = 0
DO DROP
END IF
END LOOP
END IF

# HIDE ANY REMAINING UNUSED SPRITES
VAR I: INT
SPRITE_INDEX -> I
LOOP
IF I >= ENGINE::_USED_SPRITES
DO DROP
IF I >= 64
DO DROP

# HIDE THIS SPRITE
0 -> IO::SPRITES[I].PIXELS_ADDRESS

I + 1 -> I
END LOOP
SPRITE_INDEX -> ENGINE::_USED_SPRITES

0 -> ENGINE::_BUSY
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()
1 -> IO::IRQ_PENDING # "WRITE 1 TO CLEAR" IRQ_PENDING
3 -> IO::PAINT_MODE # REQUEST PAINT
1 -> IO::IRQ_WAKE_MASK # 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("OPERATION IS NOT ALLOWED")

1 -> ENGINE::_BUSY

VAR BUTTONS: BYTE
IO::GAMEPAD_0.BUTTONS --> BUTTONS
MATH::BIT_AND(BUTTONS, 1) <> 0 -> GAMEPAD::BUTTON_A
MATH::BIT_AND(BUTTONS, 2) <> 0 -> GAMEPAD::BUTTON_B
MATH::BIT_AND(BUTTONS, 4) <> 0 -> GAMEPAD::BUTTON_C
MATH::BIT_AND(BUTTONS, 8) <> 0 -> GAMEPAD::BUTTON_D

FALSE -> GAMEPAD::LEFT
FALSE -> GAMEPAD::RIGHT
FALSE -> GAMEPAD::UP
FALSE -> GAMEPAD::DOWN

VAR P: PAIR
IO::GAMEPAD_0.X -> P
IF P >= 512 THEN
TRUE -> GAMEPAD::RIGHT
ELSIF P <= -512 THEN
TRUE -> GAMEPAD::LEFT
END IF

IO::GAMEPAD_0.Y -> P
IF P >= 512 THEN
TRUE -> GAMEPAD::DOWN
ELSIF P <= -512 THEN
TRUE -> GAMEPAD::UP
END IF

VAR ACTOR: ACTOR
ENGINE::GET_FIRST_ACTOR() -> 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()

ENGINE::GET_NEXT_ACTOR(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()

0 -> ENGINE::_BUSY
END FUNC
END MODULE