Jump to content

Thinkscript

From OpenBOR
Revision as of 18:31, 18 August 2026 by Dcurrent (talk | contribs) (Created page with "<code>thinkscript</code> defines a model-level, per-entity hook synchronized with OpenBOR's scheduled decision cycle. It executes after the entity's current native action handler and native thinking routine, while animation advancement, attack collision, health-display updates, velocity accumulation, gravity, and movement still remain ahead in the logical update. Despite its historical placement in the AI system, Thinkscript is not limited to enemy artificial intelligen...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

thinkscript defines a model-level, per-entity hook synchronized with OpenBOR's scheduled decision cycle. It executes after the entity's current native action handler and native thinking routine, while animation advancement, attack collision, health-display updates, velocity accumulation, gravity, and movement still remain ahead in the logical update.

Despite its historical placement in the AI system, Thinkscript is not limited to enemy artificial intelligence. Players, NPCs, projectiles, traps, interface entities, and other model types can use it whenever their nextthink timestamp becomes due. Native AI may also be disabled while Thinkscript continues to run, allowing the hook to extend or replace scheduled decision logic.

Useful applications include custom AI layers, target selection, tactical state changes, player-input post-processing, periodic status logic, scheduled controllers, state-machine transitions, action correction, and low-frequency work that does not need to execute during every logical tick.

Syntax

thinkscript {path}

# Default
# No corresponding Thinkscript
  • {path} - Path to an OpenBOR Script source file.
  • The command belongs in a model definition.
  • OpenBOR loads and compiles the supplied source with the model.
  • The source defines a normal main() entry point.
  • The script's return value is ignored.

Example model header:

name example_enemy
type enemy

thinkscript data/scripts/example_think.c

Event code may also be embedded directly in the model:

thinkscript @script
void main()
{
    void self = getlocalvar("self");

    update_tactical_state(self);
}
@end_script

Local Variables

Local Type Value
self Entity pointer Entity whose scheduled think event is in progress.

No target, owner, player index, action result, animation index, logical time, interval, or cancellation value is supplied automatically. Retrieve additional state from self, system properties, player properties, entity variables, global variables, or project-managed data.

Runtime Timing

Thinkscript belongs to the entity's due think block. For each eligible entity, the relevant order is:

  1. Updateentityscript executes near the beginning of the entity's logical update.
  2. OpenBOR tests whether nextthink is less than or equal to the current logical time and whether end-game state permits thinking.
  3. The entity's current native action handler executes when one is assigned.
  4. When the entity has a native thinking routine, OpenBOR supplies a default next-think time if the timestamp is still due.
  5. The native thinking routine executes unless AI control is disabled.
  6. Thinkscript executes.
  7. Spawn and respawn animations that have finished may fall back to idle.
  8. OpenBOR returns to the main entity update and checks whether the entity still exists.
  9. The current animation may advance.
  10. Attack collision is evaluated.
  11. Displayed health and magic values update.
  12. Current X and Z velocity is added to pending movement.
  13. Gravity, movement, and binding resolution occur later in the entity-update process.

Thinkscript is consequently a post-decision hook inside the scheduled think event, not a callback that precedes native AI. State selected by the native action and thinking routines is already available for inspection. Script can refine or replace the resulting target, animation, direction, velocity, action state, timing, or other properties before later animation and collision stages consume them.

Returning a value does not cancel native processing. Earlier native work has already occurred, and the script's return value is ignored.

The nextthink Scheduler

nextthink is an absolute logical timestamp. Thinkscript becomes eligible when:

nextthink <= elapsed_time

Setting nextthink to 20 means logical tick 20, not 20 ticks from the present. Relative scheduling must add an interval to the current elapsed time.

New entities receive nextthink = elapsed_time + 1. The main update deliberately skips an entity during its spawn tick, so Thinkscript first becomes eligible during a later logical update. Use Onspawnscript for work that must occur immediately when the entity enters play.

Default Cadence

When an entity has a native thinking routine and its timestamp remains due after the action handler, OpenBOR sets nextthink to the current logical time plus the fixed THINK_SPEED interval. Current THINK_SPEED is 2 logical ticks. At the default 200-tick game speed, that represents 10 milliseconds.

Native action or thinking routines may replace that timestamp with a longer interval. Thinkscript executes afterward and may replace it again, giving script the final opportunity in the due block to schedule the next event.

Entities without a native thinking routine do not receive the automatic THINK_SPEED reschedule. Once their timestamp is due, Thinkscript can execute during every eligible logical tick until script or another system moves nextthink into the future. This behavior makes the hook available to models without native AI, while leaving cadence under creator control.

Custom Cadence

This compact example performs a project decision update ten times per second at any configured game speed:

void main()
{
    void self = getlocalvar("self");
    int now = openborvariant("elapsed_time");
    int game_speed = openborvariant("game_speed");

    update_project_decision(self);

    set_entity_property(
        self,
        ENTITY_PROPERTY_THINK_TIME,
        now + game_speed / 10
    );
}

Setting the timestamp to the current time or an earlier time does not recursively invoke Thinkscript again during the same entity update. The due block is entered once. Still-due timestamps make the script eligible again during the next logical update.

Execution Conditions

Condition Executes? Notes
Existing, unfrozen entity with due nextthink Yes Thinkscript must be initialized and end-game state must permit the think block.
nextthink is later than current logical time No The complete action, native-think, and Thinkscript block is skipped.
AI control is disabled Yes Native think() is suppressed, while the current action handler and Thinkscript still execute.
Entity has no native thinking routine Yes Thinkscript still executes when due. Script may need to schedule the next event explicitly.
Entity is frozen No OpenBOR skips the normal entity-update stages and expands nextthink to preserve its remaining interval.
Entity was spawned during the current logical tick No The main entity update skips fresh entities for that tick.
Entity is outside the visible viewport Yes Visibility does not gate logical thinking, though level loss rules may remove the entity first.
Game is paused No The central logical entity-update loop does not advance while pause is active.
End-game state suppresses thinking No The guard surrounds the complete action, native-think, and Thinkscript block.
Entity exists on a non-level screen using normal entity updates Yes Thinkscript itself does not require an active level. Models without native thinking should manage their own cadence.

One display refresh may process zero, one, or several logical ticks. Thinkscript follows logical time rather than display frequency, so catch-up processing can execute several due events before the next presented image.

Native Action Handler

The action handler represented internally by takeaction executes first. Native states use these handlers for attacks, pain, falls, rising, grabbing, blocking, landing, spawning, movement transitions, and other ongoing actions.

AI-disable state does not suppress this handler. Disabling native AI therefore does not automatically erase an action already in progress. Thinkscript runs after the handler and sees any animation, velocity, state, target, or timing changes it produced.

If the action handler moves nextthink into the future, the current due event still continues. Eligibility was established before the handler ran. Native thinking and Thinkscript remain part of that already-entered event.

Native Thinking and AI Disable

Native thinking executes after the action handler when the entity has a thinking routine and ENTITY_PROPERTY_AI_DISABLE is false. Thinkscript executes afterward regardless of that property.

This supports two broad designs:

  • Extension - leave native thinking enabled, then inspect and refine its result in Thinkscript.
  • Replacement - disable native thinking before the due event, then perform project-controlled decisions in Thinkscript.

AI disable must be applied before native thinking reaches its test. Setting it from Thinkscript is too late to suppress native thinking during the event already in progress, though it governs later events. Updateentityscript or Onspawnscript can establish the property earlier.

Compact setup from an earlier hook:

void disable_native_ai(void entity)
{
    set_entity_property(
        entity,
        ENTITY_PROPERTY_AI_DISABLE,
        1
    );
}

Disabling native thinking does not disable the action handler, the default timestamp update for entities that still possess a native thinking routine, or Thinkscript itself.

Relationship to Updateentityscript

Updateentityscript executes during every eligible, unfrozen logical update. Thinkscript executes later and only when the scheduled think event is due.

Feature script - Updateentityscript thinkscript
Frequency Every eligible, unfrozen logical tick. Only when nextthink is due and end-game state permits thinking.
Position Before the scheduled action and native AI block. After the current action handler and native thinking routine.
Control opportunity Can change nextthink or AI-disable state before the current due test. Can inspect the completed native decision and schedule the next event.
Typical role Continuous state preparation, movement control, and per-tick systems. Scheduled decisions, periodic work, and post-native correction.

Use Updateentityscript when work must occur every logical tick or must influence whether native thinking runs now. Use Thinkscript when work belongs to the entity's decision cadence or needs to observe the native result.

Animation, Collision, and Movement Timing

Thinkscript completes before the normal animation-advancement and attack-collision stages of the entity update.

Changes to animation, indexed animation position, attack state, collision data, or target selection can therefore influence attack evaluation later in the same logical tick. Entering an animation or indexed position may invoke Animationscript immediately through the normal animation-entry path.

Velocity and speed-multiplier changes also occur before OpenBOR adds current X and Z velocity to pending movement. Such changes affect movement accumulation during the same logical tick. Gravity and movement resolution occur after the main update pass has visited the entities.

Compact post-native correction example:

void main()
{
    void self = getlocalvar("self");

    refine_selected_target(self);
    correct_selected_action(self);
}

Project helpers can inspect the state chosen by native logic, then retain, modify, or replace it before later consumers run.

Removal During the Due Event

OpenBOR checks whether self still exists after the complete scheduled think block returns. No intermediate existence check occurs between the action handler, native thinking routine, and Thinkscript.

Native handlers that remove the entity can therefore leave Thinkscript executing later in the same due event with self already marked non-existing. Models capable of removing themselves during native action or thinking can guard project work with ENTITY_PROPERTY_EXISTS:

void main()
{
    void self = getlocalvar("self");

    if(!get_entity_property(self, ENTITY_PROPERTY_EXISTS))
    {
        return;
    }

    update_project_decision(self);
}

Removing self from Thinkscript prevents the later animation, attack, health-display, and velocity-accumulation stages from running for that entity. Returning a value without removing the entity does not cancel them.

Per-Entity Script State

OpenBOR compiles Thinkscript with the model and gives each spawned entity its own script context. Entities using the same model share compiled code, while their Thinkscript local-variable lists remain separate.

Values stored with setlocalvar() can persist between Thinkscript executions for that entity. OpenBOR supplies self before each call and clears that supplied local afterward. Other creator-managed locals remain available to the next execution.

Thinkscript locals are also separate from locals belonging to Updateentityscript, Animationscript, or other model hooks. Use entity variables when several scripts attached to the same entity must share a value.

Compact decision-counter example:

void main()
{
    void self = getlocalvar("self");
    int decision_count = getlocalvar("decision_count");

    if(isempty(decision_count))
    {
        decision_count = 0;
    }

    decision_count++;
    setlocalvar("decision_count", decision_count);

    process_decision_count(self, decision_count);
}

Entity Ordering

Due Thinkscripts execute as OpenBOR visits entities in active entity-list order. This order reflects internal entity management rather than visual depth, player number, model declaration order, or a guaranteed gameplay priority.

Earlier entities can change state observed by later entities during the same logical tick. Later entities cannot retroactively alter native decisions or Thinkscripts already completed by earlier entities.

Use explicit timestamps, ownership, unique IDs, entity variables, or project-managed queues when several entities must coordinate. Shared systems requiring complete before-and-after passes often belong in global Update.c or Updated.c logic.

Practical Uses

Pattern Use of Thinkscript
Native AI extension Inspect the target, animation, direction, or action selected by native thinking and refine the result.
Custom AI replacement Disable native thinking before the event, then run a project state machine at a creator-controlled cadence.
Scheduled target scan Search for opponents periodically instead of performing a full entity scan during every logical tick.
Player decision layer Post-process player state after native input logic while retaining the normal movement and attack pipeline.
Periodic status controller Update regeneration, hazards, tactical meters, ownership checks, or environmental reactions on a scheduled interval.
Action correction Validate or replace a native action before animation advancement and attack collision.
Non-AI controller Give projectiles, interface entities, helpers, or other models scheduled logic even when they have no native thinking routine.
Adaptive cadence Schedule frequent decisions during combat and longer intervals while idle or off-screen.

Performance Considerations

Thinkscript can run very frequently. Entities with ordinary native thinking may become eligible every two logical ticks, while entities without native thinking can remain due during every logical tick unless the script reschedules them. Catch-up processing may produce several executions before one display refresh.

Scheduled cadence can make Thinkscript an efficient home for target scans, path selection, tactical evaluation, or other work that does not need per-tick precision. Move nextthink forward deliberately after expensive work, cache stable results, and avoid repeating full-entity searches from every model when one shared calculation would suffice.

Common Mistakes

  • Treating Thinkscript as a callback that runs before native AI.
  • Assuming the hook belongs only to enemy models.
  • Expecting ENTITY_PROPERTY_AI_DISABLE to suppress Thinkscript.
  • Expecting AI disable to suppress an active native action handler.
  • Setting AI disable inside Thinkscript and expecting it to cancel native thinking that already ran.
  • Treating nextthink as a relative delay instead of an absolute timestamp.
  • Forgetting to schedule the next event for an entity without a native thinking routine.
  • Leaving nextthink due and unintentionally executing Thinkscript every logical tick.
  • Returning a value and expecting it to cancel later entity processing.
  • Assuming one Thinkscript execution corresponds to one display refresh.
  • Expecting the hook to run while the entity is frozen or during suppressed end-game thinking.
  • Assuming a fresh entity receives Thinkscript during its spawn tick.
  • Ignoring the possibility that a native handler removed self before Thinkscript began.
  • Depending on active entity-list order as a permanent cross-entity priority.

Comparison with Other Hooks

Hook Scope and timing Typical role
Onspawnscript Executes when an entity enters play. Immediate initialization before the first normal logical update.
Updateentityscript Executes near the beginning of every eligible, unfrozen entity update. Continuous preparation and per-tick control before native action and AI.
thinkscript Executes when nextthink is due, after the native action and thinking routines. Scheduled decisions, periodic systems, and post-native correction.
Animationscript Executes when an entity enters or is moved to an indexed animation position. Animation-position actions, branches, and transitions.
Ondrawscript Executes during display composition after native entity visuals enter the sprite queue. Entity-linked drawing and visual overlays.
Update.c Global pre-update script. Prepare shared logic before entity processing.
Updated.c Global post-update script. Finalize shared state and late display work.

See Also