Skip to content

Script TXD Issues

Seemann edited this page Sep 22, 2025 · 14 revisions

TXD & Sprite Management — Behavior, Problems, and Fixes

This document summarizes the current runtime behavior for texture dictionaries (TXDs) and sprite handling.

Typical Usage

LOAD_TEXTURE_DICTIONARY 'LD_BEAT'

LOAD_SPRITE 1 {spriteName} "DOWN"
LOAD_SPRITE 2 {spriteName} "LEFT"
LOAD_SPRITE 3 {spriteName} "UP"
LOAD_SPRITE 4 {spriteName} "RIGHT"
LOAD_SPRITE 13 {spriteName} "UP"
LOAD_SPRITE 14 {spriteName} "DOWN"
LOAD_SPRITE 10 {spriteName} "RIGHT"
LOAD_SPRITE 9 {spriteName} "LEFT"
...
DRAW_SPRITE 1 {offsetLeft} $X {offsetTop} $Y {width} 20@ {height} 19@ {r} 128 {g} 128 {b} 128 {a} 255
...
REMOVE_TEXTURE_DICTIONARY
  • LOAD_TEXTURE_DICTIONARY allocates new TXD object for the script.
    • previous TXD (if any) is leaked.
    • .cm only: appends additional record to CTheScripts::MissionCleanUp.
      • max capacity is 75 records, no bounds checks.
  • LOAD_SPRITE stores texture pointer (shallow copy) in a fixed CTheScripts::ScriptSprites array
    • max capacity is 128 sprites (1-128), no bounds checks.
    • Since CTheScripts::ScriptSprites is shared, CLEO maintains a copy of the array per script and switches the active copy on script switch.
  • DRAW_SPRITE uses the pointer stored in CTheScripts::ScriptSprites to render.
  • REMOVE_TEXTURE_DICTIONARY clears the active TXD and sprite slots
    • .cm only: removes all TXD records from CTheScripts::MissionCleanUp.
    • it DOES NOT free memory in CLEO4.

GET_TEXTURE_FROM_SPRITE

There is an additional CLEO+ command GET_TEXTURE_FROM_SPRITE which retrieves a pointer to a sprite's underlying texture data and stores that pointer in a variable. The stored value can later be used in rendering calls (for example, DRAW_TEXTURE_PLUS).

Example usage:

LOAD_TEXTURE_DICTIONARY 'LD_BEAT'
LOAD_SPRITE 1 {spriteName} "DOWN"
GET_TEXTURE_FROM_SPRITE 1 0@   // store pointer to sprite 1 in variable 0@
DRAW_TEXTURE_PLUS 0@ ...       // render via pointer stored in variable

GET_TEXTURE_FROM_SPRITE performs a shallow copy of the sprite pointer into the destination variable (no ownership transfer)

Use-after-free (UAF): because the pointer is a shallow reference into the active TXD, freeing or removing the TXD invalidates the stored pointer. This creates an easy-to-miss use-after-free when a script caches sprite pointers in variables and later calls REMOVE_TEXTURE_DICTIONARY.

UAF example:

LOAD_TEXTURE_DICTIONARY 'LD_BEAT'
LOAD_SPRITE 1 {spriteName} "DOWN"
GET_TEXTURE_FROM_SPRITE 1 0@
REMOVE_TEXTURE_DICTIONARY        // frees/clears TXD and sprite data
DRAW_TEXTURE_PLUS 0@ ...         // use-after-free: 0@ now points at freed memory

Scenarios

Safe load and draw

LOAD_TEXTURE_DICTIONARY 'LD_BEAT'
LOAD_SPRITE 1 {spriteName} "DOWN"
WHITE TRUE
   WAIT 0
   DRAW_SPRITE 1 ...
END
REMOVE_TEXTURE_DICTIONARY

✅ valid render — TXD and sprite present. Texture memory is freed via REMOVE_TEXTURE_DICTIONARY (in CLEO 5.1)

Use-after-remove (dangling pointer)

LOAD_TEXTURE_DICTIONARY 'LD_BEAT'
LOAD_SPRITE 1 {spriteName} "DOWN"
REMOVE_TEXTURE_DICTIONARY
DRAW_SPRITE 1 ...

⚠️ DRAW_SPRITE references freed memory → garbage rendering. ✅ Does not happen in CLEO 4 since REMOVE_TEXTURE_DICTIONARY does not free memory.

Use-after-remove via variable (UAF)

LOAD_TEXTURE_DICTIONARY 'LD_BEAT'
LOAD_SPRITE 1 {spriteName} "DOWN"
GET_TEXTURE_FROM_SPRITE 1 0@
REMOVE_TEXTURE_DICTIONARY        // frees/clears TXD and sprite data
DRAW_TEXTURE_PLUS 0@ ...         // use-after-free: 0@ now points at freed memory

⚠️ DRAW_TEXTURE_PLUS references freed memory → garbage rendering. ✅ Does not happen in CLEO 4 since REMOVE_TEXTURE_DICTIONARY does not free memory.

Double load leak

LOAD_TEXTURE_DICTIONARY 'LD_BEAT'
LOAD_TEXTURE_DICTIONARY 'LD_BEAT1' // leaks LD_BEAT
LOAD_TEXTURE_DICTIONARY 'LD_BEAT2' // leaks LD_BEAT1

⚠️ Each load overwrites the TXD handle; the previous allocation becomes leaked.

remove-no-free (CLEO 4 only)

WHILE TRUE
    WAIT 0
    LOAD_TEXTURE_DICTIONARY 'LD_BEAT'
    REMOVE_TEXTURE_DICTIONARY
END

⚠️ TXD memory remains allocated in CLEO4 — repeated cycles leak memory.

Index overflow / memory corruption

LOAD_SPRITE 10000 "DOWN"

⚠️ unchecked write may corrupt memory — unpredictable behavior.

CTheScripts::MissionCleanUp exhaustion (.cm)

WHILE TRUE
    WAIT 0
    LOAD_TEXTURE_DICTIONARY 'LD_BEAT'
END

⚠️ Repeated loads use remaining empty records to CTheScripts::MissionCleanUp which is fixed-size and append-only. If the list is exhausted, the mission has no room for storing more items.

Root Issues

  • LOAD_TEXTURE_DICTIONARY always allocates a new TXD → previous allocations are leaked if not freed.
  • Shallow-copy semantics: LOAD_SPRITE/GET_TEXTURE_FROM_SPRITE are pointers into TXD → freeing the TXD invalidates all copies.
  • No bounds checks for CTheScripts::ScriptSprites index writes → memory corruption
  • No duplicate checks for CTheScripts::MissionCleanUp appends → memory exhaustion.
  • REMOVE_TEXTURE_DICTIONARY does not free memory in CLEO4 → memory leaks on repeated loads.

Recommended Fixes (CLEO 5.2)

MissionCleanup Exhaustion Prevention

LOAD_TEXTURE_DICTIONARY / REMOVE_TEXTURE_DICTIONARY does not modify CTheScripts::MissionCleanUp array for .cm missions - textures unloaded on script/mission end.

TXD Array Per Script

Each script maintains its own array of loaded TXDs.

LOAD_TEXTURE_DICTIONARY pushes a new TXD to the array and points global script.txd to it (so that LOAD_SPRITE can work).

REMOVE_TEXTURE_DICTIONARY does not remove the TXD from the array. On script termination, the runtime automatically unloads all TXDs on the array.

  • Eliminates double-load leak (no overwrite semantics).
  • Guarantees no dangling TXD references when REMOVE_TEXTURE_DICTIONARY was never called.
  • Solves UAF (no frees until script end).

Loop / Memory Growth Via De-duplication

Runtime should validate duplicate loads: if the requested TXD is already on the list of loaded TXDs, skip the load.

Bounds Checks (nice-to-have)

Validate sprite indices: reject LOAD_SPRITE if index < 1 or > 128 and return an error. Validating upper bound can be problematic if array size was increased.

Final Notes

  • no behavior difference for .cs and .cs4
  • legacy scripts that rely on REMOVE_TEXTURE_DICTIONARY to not free memory will work
  • allows scripts to load and use multiple TXDs safely
  • prevents memory leaks, corruption, and use-after-free bugs.