DC Space
The DC Space library is a frame-indexed push and physical-spacing system for OpenBOR 3.0 v6391. Space boxes mimic those found in verses fighters, with many additional functions and conditions to accommodate scrolling brawlers and platformer needs.
Build attack-associated push areas, keep eligible entities from crowding a character, pull targets toward a point, and control the actor's reaction when a target cannot move. Define a box once, let it continue through the animation, and replace or stop individual box slots on the frames where their behavior changes.
What can it do?
Built to keep script work small
I've done all I can to make the library lean and mean. Its optimizations include:
Installation
Copy the bundled libraries
DC Space has a dependency library - DC Spawning, that is included in the package. Copy both complete folders from the package into your module scripts folder:
You don't need to do anything else with DC Spawning. DC Space imports it's dependencies automatically.
The remaining installation steps belong to DC Space: one global cleanup hook, plus the required hooks for each entity that owns push boxes.
If your module already contains either library, merge with a compatible copy rather than overwriting unrelated modifications blindly. Keep DC Utilities installed when other scripts in your module still use it; removing DC Space's dependency does not remove theirs.
Global lifetime hook
The shared handled-target cache outlives individual actors. Release its arrays from the global update script's destruction hook, before other code clears global variables. This is DC Space cleanup, not a DC Spawning hook.
data/scripts/update.c
If the script already exists, merge the import and cleanup call into it. Keep its existing functions and code. Do not add a second
Entity hooks
Each entity owning boxes needs spawn, update, kill, and animation-script integration. Add these model-header declarations only when equivalent hooks do not already exist:
Replace
event_spawn.c
event_update.c
event_kill.c
event_animation.c
Model and weapon changes
For models that replace their push definitions when the live entity changes model, add or merge:
Dust effect
The spawning helper and the dust effect model are separate. The package includes DC Spawning so DC Space can create the configured effect; installing the helper does not create or load an effect model for you.
Dust requires successful grounded movement and a permitted dust opportunity. Successful altitude changes do not emit friction dust. Neither the bounds template nor the setup block creates or loads a dust model for you.
Creating push boxes
Every declaration follows this pattern:
Box indices run from 0 through 31. The same index can have another version on a later frame, and every animation has its own slots. Lower indices receive processing priority when boxes compete for the same target.
The examples below are separate recipes. Replace sample sprite paths and frame numbers with your own. Code snippets that contain only setters belong inside an open setup block unless identified as entity-level calls.
1. Create an attack-associated push box
Place this before the animation frame where pushing should begin:
The box starts on that declaration frame and continues through animation end unless a later definition or stop replaces slot 0.
The six bounds are:
These are identical to the engine's native range settings, meaning they are pixel distances from the entity's offset, and X is relative to the entity's facing. For this example, right-facing X extends from 30 units behind the actor to 165 units ahead, Y extends from the actor's altitude to 40 units above it, and Z extends 15 units to either side of its depth coordinate.
Each value requires an integers from
Adjust one pair of bounds
The combined bounds call sets the complete box. These setters change just one axis on a writable declaration:
They apply the same integer and range rules but do not run admission templates. Reusing them on a completed declaration still requires the normal start-level reset to accept changes.
The ATTACK template configures target admission only. Default movement is a small horizontal separation: X strengths
For a stronger horizontal push, add this before closing:
This requests half a coordinate unit per selected interaction. If the target entity is blocked by walls or other obstructions, the rebound multiplier for that axis is applied to the actor instead. If both entities are blocked, the pushing effect is canceled.
2. Create passive physical spacing
For an idle, walking, guarding, or similar animation:
PASSIVE permits neutral physical contact without requiring the actor's
To restrict passive targets to selected native types, add an explicit include filter inside the block:
This is a native type mask supplied by the creator, not a synthetic "character" category.
Templates versus presets
The bounds function's optional seventh argument is a template shortcut that accepts these selectors:
ATTACK and PASSIVE invoke the target-permission, type-include, and type-exclude setters with their matching presets. They replace all three admission fields, including inherited type restrictions. Later setters can change individual fields again.
Omitting the selector, passing
3. Change or stop a box on another frame
Use the same box index on a later frame to create another version. No reset flag is needed merely because the slot was used earlier.
This sample animation fragment declares a narrow box at frame 1, widens it at frame 3, and stops it at frame 5:
Keep your animation's other properties and commands. The frame lines here illustrate placement; replace them with the actual frames in your module.
Other box slots continue independently. Changing slot 0 does not clear slot 1.
New versions do not copy omitted settings from the previous version. Each starts with its own default bounds and movement/admission inheritance. The example repeats the stronger X setting because it should apply to both versions. Unchanged slots, in contrast, keep their existing complete records automatically.
Use
Choose explicit frame limits
To configure a box before frame 0 but make it active only on frames 1-4:
Both endpoints are inclusive. Frame numbers begin at zero.
4. Choose push or pull on each axis
Each attempt setter takes three decimal strengths in the same order:
LESS, SAME, GREATER describe the target relative to the actor. SAME means exact match with target. Positive and negative values are signed displacement requests, not automatically "push" and "pull" labels.
Use decimal values such as
The supplied global values are:
Named constants in
Horizontal separation
With the default FACING reference:
Targets behind the actor move backward, targets in front move forward, and targets at equal X separate along facing.
Horizontal pull
Targets on either side move toward the actor. Equal X requests no horizontal movement. The request is not automatically clamped to the actor's coordinate, so choose an appropriate magnitude for small separations.
Depth separation or pull
These are alternatives. LESS means smaller world Z, GREATER means larger world Z. Positive displacement increases Z regardless of facing.
Fixed vertical or depth movement
Y is altitude. Positive Y raises the target; negative Y lowers it. Gravity and native physics continue afterward. Repeated small upward position requests do not by themselves create a launch or hovering system.
Choose X's reference direction
These are separate choices, not a sequence to copy together. Do not OR direction modes.
For an always-left request:
This direction setting affects movement selection, not the bounds' facing-based X mirroring. Passing
5. Set the reaction when movement fails
Rebound setters also take LESS, SAME, GREATER:
These values are multipliers, not independent movement strengths. The selected multiplier uses the same original relationship as the attempted movement:
Only a failed requested axis can react. Target X succeeding while Z fails does not trigger X recoil. Zero attempt makes no movement request and cannot fail.
For reduced horizontal recoil:
To suppress vertical reactions:
For different reactions by relative altitude:
This requests half opposite reaction against a target below, none at equal altitude, and full opposite reaction against a target above.
6. Choose which targets qualify
Three setters control admission independently:
The complete bounds templates are shortcuts for these calls. Individual setters remain useful for exceptions.
Permission groups are cumulative. Alternatives within a group are ORed, but every group must pass. Neither alternative allows neither state; one allows that state; both allow either.
The constants below use the prefix
Passing one group never bypasses another. Platforms must satisfy every remaining gate too. Platform admission does not authorize moving the platform.
Start with a preset and add permissions
To use attack permissions while also admitting invincible targets:
The target must still be vulnerable and pass every other attack gate. To permit either vulnerability state as well:
To let passive boxes affect the same native type too:
These functions replace their entire permission field; they do not incrementally add bits to an earlier call. Build the complete intended mask for each call.
For an air-only attack policy, remove the whole air group before supplying its intended alternative:
Use XOR with
Group shortcuts combine both alternatives:
What the relationships actually mean
CAN_DAMAGE tests the actor's native
Native include and exclude filters
Native
Each example changes only the named filter. Exclusion vetoes inclusion.
Explicit
For a passive template that should consider obstacles too:
Its remaining passive gates still apply. If an object has been retyped from obstacle to enemy, an exclusion of
Target exclusion is not environmental exemption. Obstacles ignored as direct passive targets can still block displacement of some other selected entity.
7. Set entity defaults and override only exceptions
Movement and admission settings follow:
The actor-level functions require no open box:
Here,
For an actor whose usual horizontal separation should be stronger and whose recoil should be reduced, place these after initialization:
Every box inheriting those fields uses that actor policy. Individual attacks can still override only X attempts with
Inheritance is per field, including individual members of each triplet:
LESS and GREATER inherit; SAME explicitly disables the vertical reaction. Completed boxes retain those raw choices, so later entity settings remain visible wherever the box still inherits.
Bounds and active frame windows remain properties of the box definition. They are not part of entity movement-policy inheritance.
Entity setters replace their fields when called; they do not use the box-completion gate. Call them at controlled setup or gameplay points outside an active push sweep. Model-copy reinitialization clears them, so reapply intended destination-model policy afterward.
8. Control overlapping boxes and priority
By default, the first colliding and admitted box handles a target for this actor's update. Box slots run from lowest to highest; lower slots finish their entity sweeps first.
If slot 0 selects a target, slot 1 does not get a second attempt against that target during the same update. Selection counts even when strength is zero, target movement is blocked, or only the actor reacts.
Admission or geometry rejection does not handle the target. Later boxes with different permissions or bounds can still select it.
To allow several boxes to affect the same target, configure the animation:
Place that command near the animation's beginning, before the boxes are used. This policy belongs to the animation, independently of any open box. Restore first-box-wins with:
It persists for that entity/animation until changed or cleaned up. Ordinary box declarations do not reset it.
Multiple mode uses current positions at each later test. Earlier displacement can move a target into or out of another box, and actor rebound moves subsequent box geometry. Enabling multiple mode permits repeated selections; it does not guarantee frozen-snapshot stacking.
9. Replace an existing declaration deliberately
Ordinary playback reuses completed declarations. Meaning the setup animation commands only perform their work once, and then early exit on every subsequent run. This is an optimization that removes redundant work from the engine - once a setup up pass is complete, there's usually no reason to repeat it.
To force repeat and rebuild the same entity/animation/declaration-frame/index identity:
Reset clears that version's configured geometry, policy overrides, enabled/completed state, and default window before accepting the replacement.
The fixed record is reused in place. Other authored versions and slots remain, although their effective coverage can change when the replacement's frame window changes. The same reset option exists on
Do not leave the reset flag in normal animation setup unless rebuilding on every execution is intentional. Creating a different frame's version does not require reset. No setter has a separate
10. Show boxes while testing
Add or merge a draw hook on an entity using DC Space:
The library will draw any active space boxes as a flat amber rectangle on screen.
For a log dump, call this once at a chosen debugging point after initialization and outside an open block:
The dump includes authored versions, enabled/completed state, frame-state transitions, bounds, and raw/resolved policy fields. Do not put a full database dump in every update for ordinary play. Remove the optional draw/dump calls when no longer needed.
Timing, effects, and movement protection
Push timing
As an optimization, DC Space intentionally throttles itself to
Set the window to
Dust timing
The opportunity can be spent without a particle if nothing suitable moves. Later contacts in the same window do not retry. Several qualifying moves within the permitted sweep can each emit dust. Dust scheduling is independent of the push throttle and does not cap each target to one particle.
Admission, movement eligibility, and blocking are separate
Targets can pass their box's permissions and still be protected against displacement. The supplied
These are independent compile-time policy switches, not the per-box target permission flags. Speed, antigrab, and walking-animation availability are conservative signals, not universal definitions of physical immobility.
Broadening target inclusion does not disable these protections. For example, an admitted obstacle can cause configured actor recoil without being moved. The actor's supporting platform remains an ignored contact while platform protection is enabled.
X/Z movement also stays within the library's viewport/stage limits and applicable wall/entity blocking policy. X viewport endpoints are strict; playable Z endpoints are inclusive. If both independently safe lateral components have an unsafe combined destination, the already-cleared X-only move takes priority. Previously rejected axes are never reopened by that fallback.
Y is checked at the accepted lateral destination. Downward movement respects the mover's current base and applicable terrain/platform support; upward clearance uses model height. Unsafe requests are rejected rather than searched, redirected, or partially clamped.
The vertical policy is deliberately conservative. Native nohithead platforms are treated as solid by the interval check. Model height supplies upward clearance rather than an animation-specific envelope. Elevated current base values can prevent downward requests until native physics updates support. Successful altitude changes detach platform support without rewriting velocity, gravity, base, animation, or native landing/head-hit callbacks.
This is not continuous swept-volume collision detection. Very large endpoint displacements can cross thin blockers. Configure reasonable strengths and test unusual vertical/platform situations in the target engine.
When a box appears to do nothing
Check the complete selection path before increasing strength:
The native animation handle and
Runtime then follows:
Frame entries hold state pointers directly, not state IDs requiring a search through an owner list. Out-of-range or inactive frame queries have no effective state.
Every frame in the same run shares a state. States B and C also share the unchanged box-1 record. Changing box 0 does not copy all of box 1's strengths, reactions, and admission fields.
This is why per-frame control does not require a complete set of box definitions on every frame. Actual changes cost records and boundary states; continuation mostly costs references in the preallocated frame table.
Runtime selection does not depend on a mutable "last box seen" cursor. After known definitions are prepared, visiting frames in the order
The effective box at frame 5 might have been declared at frame 2. Its existence must not prevent a new frame-5 declaration. The no-op gate therefore uses:
The gate does not eliminate argument evaluation. Functions used to calculate setter arguments still execute before the setter can return. Use reset deliberately when a completed declaration must actually store replacement values.
Boundary states compose up to 32 effective box references. Frames without a change reuse the preceding state. Older expiration events cannot disable a replacement, and a stopped or expired version does not revive its predecessor. The saving is that ordinary playback and each candidate collision do not reconstruct that timeline again.
Retained state allocations remain available when a reset makes an old boundary redundant. Repeating a reset through previously encountered boundaries can reuse them. New declarations or new boundaries can still allocate; the library does not claim that setup is allocation-free.
This avoids both repeated frees and general-purpose reference counting. Reset can reuse a fixed box record, but readers must not consume the database while that setup block is open. Publication belongs at close, not halfway through changing fields.
Optional actor fallback policy uses 22 fields, corresponding to movement and admission settings. It has no bounds or frame window.
Each authored row and each boundary state uses 33 fields: 32 record references and one mask. Each used animation reserves
The practical tradeoff is intentional: modest empty directories and retained state tables provide direct access and reuse. They do not preallocate every possible box version on every frame.
The frame word also uses two 15-bit fields. Bit 30 marks completion, and bit 31 marks a disabled declaration. The end value 32767 is reserved for animation end rather than a numeric finite endpoint.
Packing reduces stored fields and paired lookups. It also explains why bounds and frame values have explicit validation ranges: silently wrapping an out-of-range coordinate would produce a different box.
Creators use ordinary setup functions. They should not write packed fields or internal array offsets directly.
The update walks those bits and reads records only for active slots. Gaps do not require reading every unused box record. Processing remains in ascending slot order.
Thirty-two is a simultaneous-slot bound, not a whole-animation definition limit. Slot 0 can have many authored versions, while each effective frame still addresses at most one current version in each of the 32 slots.
Permission masks, active-box masks, and movement-axis masks are separate numbers with separate meanings. The fact that each uses bits does not make their constants interchangeable.
The target loop selects the appropriate LESS/SAME/GREATER values from those scalars. It does not allocate a profile array, traverse inheritance for every target, or reread the box's six bounds for every collision attempt.
Actor XYZ and facing are sampled for the update. Successful rebound refreshes only the affected coordinates and recomputes that box's corresponding world boundaries. Vertical rebound also refreshes the supporting-platform exclusion. Later targets therefore use the actor's actual new position without repacking or rereading its definition.
Target positions are sampled for the current interaction and can be read again by later boxes after movement. The cache is not a frozen whole-frame physics snapshot.
Viewport and stage limits form a separate lazy scalar snapshot. No lateral movement request means no lateral boundary acquisition; when needed, all boxes and target/rebound attempts in that update share the same limits. Their blocking results remain separate.
Five children, 32 words per child, and 32 bits per word total 5120 enumeration indices, enough to cover the engine's 5000 entity per frame limit. The address comes directly from the engine's current entity index:
For index 4999, that selects child 4, word 28, bit 7. No entity-pointer search is needed to locate its handled flag.
The root and only the needed children allocate on demand. The sweep caches the current child, validity word, and handled word in scalar variables, avoiding a separate array lookup for every candidate's membership test.
Default mode checks the captured enumeration span against the bitmap capacity before movement. Multiple-push mode does not use that bitmap and therefore does not apply its capacity restriction.
Inactive children start with zero cached word validity. Each inactive word starts with a zero cached handled value. Its first write replaces stale content before publishing the validity bits. Later writes add selections to that current word.
Allocated does not mean valid. This distinction lets another actor reuse the same storage immediately without inheriting the previous actor's handled targets.
Bookkeeping also stays lazy within one update:
Safety-dependent reads and expensive calls therefore sit behind real branches:
Compound scalar tests are still useful; the distinction is whether an operand needs the other test to suppress its execution.
Unary bitwise NOT is also avoided, as this operator is bugged prior to OpenBOR 4.x. For integer masks, XOR with
With width 3, steps
This follows game simulation progress rather than the user's outer-update setting. It also avoids repeatedly discarding timing overshoot by setting the next deadline to "now plus three" after every accepted call.
The remembered value belongs to the actor, not each box or target. Animation changes do not automatically grant another window. Cleanup clears the stamp. Missed windows are discarded rather than converted to catch-up displacement.
Dust has its own actor window and receives the same simulation sample when available. Its permission is cached for the whole sweep, not consumed separately by the first qualifying target.
Nested active push updates are rejected. Cleanup and setup cannot mutate the actor whose push sweep is running; global handled teardown cannot run during any active sweep. Open setup also excludes runtime readers until the declaration closes.
Actor cleanup releases its definitions, fallback policy, setup references, and timing history. It leaves the shared handled allocation available to future actors. The global destruction hook separately frees the shared root and every allocated child before its owner handle is lost.
The public setup and lifecycle functions preserve these responsibilities. Borrowed state and box handles must not be freed, rewritten, or kept across registry destruction. Normal creators can leave the indexing, masks, and ownership entirely to the library.
The DC Space library is a frame-indexed push and physical-spacing system for OpenBOR 3.0 v6391. Space boxes mimic those found in verses fighters, with many additional functions and conditions to accommodate scrolling brawlers and platformer needs.
Build attack-associated push areas, keep eligible entities from crowding a character, pull targets toward a point, and control the actor's reaction when a target cannot move. Define a box once, let it continue through the animation, and replace or stop individual box slots on the frames where their behavior changes.
What can it do?
- Attack push and passive spacing - Use ready-made ATTACK and PASSIVE templates, or choose exactly which targets a box may affect.
- Frame-specific boxes - Use up to 32 box slots on each frame. Change one slot's bounds or behavior later in the animation without repeating the other boxes.
- XYZ movement - Configure horizontal, vertical, and depth displacement independently. Push outward, pull inward, or request a fixed direction.
- Relationship-based strengths - Give each axis separate strengths for targets at lesser, equal, and greater coordinates. X can use right, left, or the actor's facing as its reference.
- Configurable reactions - Give each axis and relationship its own rebound multiplier. Failed pushes can produce full recoil, reduced recoil, no reaction, or movement in the attempted direction.
- Cumulative target permissions - Filter platform status, damage relationship, invincibility, airborne state, vulnerability, downed state, and same/different native type. Separate native type inclusion and exclusion filters provide additional control.
- Layered defaults - Override a setting on one box, inherit the actor's current setting, or fall back to a named constant. Typical declarations need only three commands.
- Movement protection - Check stage and viewport limits, walls, and applicable entity blockers. Resolve target movement and actor reaction independently.
- Optional debugging - Draw the current frame's boxes or log the actor's definitions, shared frame states, and inherited settings.
Built to keep script work small
I've done all I can to make the library lean and mean. Its optimizations include:
- Shared frame states - Unchanged frames share the same box-reference table. Unchanged boxes can also be shared between different states.
- Prepared definitions - Completed setup blocks become no-ops on later playback unless explicitly reset.
- Direct indexing - Native animation and frame numbers lead directly to the effective push state. The update does not search backward through animation history.
- Scalar snapshots - Bounds, strengths, reactions, and target policy are resolved into function variables before each box's target sweep. Rebound refreshes affected actor coordinates without rereading the box definition.
- Bitfields and packed values - Masks track active boxes and handled targets; packed coordinate pairs keep each definition compact.
- Reusable storage - Full animation-directory preallocation, retained boundary states, and shared handled storage avoid unnecessary allocation churn.
- Early gates - Inactive states, repeated timing windows, horizontal misses, and settled movement axes skip work that cannot affect the result.
Installation
Copy the bundled libraries
DC Space has a dependency library - DC Spawning, that is included in the package. Copy both complete folders from the package into your module scripts folder:
You don't need to do anything else with DC Spawning. DC Space imports it's dependencies automatically.
The remaining installation steps belong to DC Space: one global cleanup hook, plus the required hooks for each entity that owns push boxes.
If your module already contains either library, merge with a compatible copy rather than overwriting unrelated modifications blindly. Keep DC Utilities installed when other scripts in your module still use it; removing DC Space's dependency does not remove theirs.
Global lifetime hook
The shared handled-target cache outlives individual actors. Release its arrays from the global update script's destruction hook, before other code clears global variables. This is DC Space cleanup, not a DC Spawning hook.
data/scripts/update.c
C:
#import "data/scripts/dc_space/handled.c"
void main() {
// Existing global update work goes here.
}
void ondestroy() {
dc_space_destroy_handled_cache();
// Other global cleanup follows.
}
If the script already exists, merge the import and cleanup call into it. Keep its existing functions and code. Do not add a second
main() or ondestroy().Entity hooks
Each entity owning boxes needs spawn, update, kill, and animation-script integration. Add these model-header declarations only when equivalent hooks do not already exist:
Code:
onspawnscript data/chars/your_character/script/event_spawn.c
onkillscript data/chars/your_character/script/event_kill.c
script data/chars/your_character/script/event_update.c
animationscript data/chars/your_character/script/event_animation.c
Replace
your_character with your folder. Existing hooks should receive the includes and calls rather than be replaced.event_spawn.c
C:
#include "data/scripts/dc_space/main.c"
void main() {
void acting_entity = getlocalvar("self");
dc_space_initialize(acting_entity);
// Optional entity-level defaults go here.
}
event_update.c
C:
#include "data/scripts/dc_space/main.c"
void main() {
dc_space_update_pushes(getlocalvar("self"));
}
event_kill.c
C:
#include "data/scripts/dc_space/main.c"
void main() {
dc_space_cleanup(getlocalvar("self"));
}
event_animation.c
C:
#include "data/scripts/dc_space/main.c"
void main() {
// Existing animation-hook logic goes here.
}
Model and weapon changes
For models that replace their push definitions when the live entity changes model, add or merge:
Code:
onmodelcopyscript data/chars/your_character/script/event_model_copy.c
C:
#include "data/scripts/dc_space/main.c"
void main() {
void acting_entity = getlocalvar("self");
dc_space_reinitialize(acting_entity);
// Reapply destination-model entity defaults here when needed.
}
Dust effect
The spawning helper and the dust effect model are separate. The package includes DC Spawning so DC Space can create the configured effect; installing the helper does not create or load an effect model for you.
DC_SPACE_PUSH_DUST_MODEL_NAME in master_config.h names the friction-dust effect. Its supplied value is "po". Set it to a suitable effect model available in your module, and make sure that model and its assets are loaded through your normal model configuration.Dust requires successful grounded movement and a permitted dust opportunity. Successful altitude changes do not emit friction dust. Neither the bounds template nor the setup block creates or loads a dust model for you.
Creating push boxes
Every declaration follows this pattern:
C:
dc_space_start(0);
// Set bounds and any behavior that differs from the inherited defaults.
dc_space_end();
dc_space_start() selects a box on the current entity, animation, and declaration frame. dc_space_end() finishes configuration, not the box's active frame window. Complete both bookends on the same frame.Box indices run from 0 through 31. The same index can have another version on a later frame, and every animation has its own slots. Lower indices receive processing priority when boxes compete for the same target.
The examples below are separate recipes. Replace sample sprite paths and frame numbers with your own. Code snippets that contain only setters belong inside an open setup block unless identified as entity-level calls.
1. Create an attack-associated push box
Place this before the animation frame where pushing should begin:
Code:
@cmd dc_space_start 0
@cmd dc_space_setup_bounds -30 165 0 40 -15 15 DC_SPACE_TEMPLATE_ATTACK
@cmd dc_space_end
The box starts on that declaration frame and continues through animation end unless a later definition or stop replaces slot 0.
The six bounds are:
Code:
x_min x_max y_min y_max z_min z_max
These are identical to the engine's native range settings, meaning they are pixel distances from the entity's offset, and X is relative to the entity's facing. For this example, right-facing X extends from 30 units behind the actor to 165 units ahead, Y extends from the actor's altitude to 40 units above it, and Z extends 15 units to either side of its depth coordinate.
Each value requires an integers from
-16,384 through 16,383.Adjust one pair of bounds
The combined bounds call sets the complete box. These setters change just one axis on a writable declaration:
C:
dc_space_setup_bounds_x(-30, 165);
dc_space_setup_bounds_y(0, 40);
dc_space_setup_bounds_z(-15, 15);
They apply the same integer and range rules but do not run admission templates. Reusing them on a completed declaration still requires the normal start-level reset to accept changes.
The ATTACK template configures target admission only. Default movement is a small horizontal separation: X strengths
-0.05, 0.05, 0.05, with FACING as the reference. Y and Z strength default to 0.0; rebound multipliers all default to -1.0.For a stronger horizontal push, add this before closing:
Code:
@cmd dc_space_setup_attempt_x -0.5 0.5 0.5
This requests half a coordinate unit per selected interaction. If the target entity is blocked by walls or other obstructions, the rebound multiplier for that axis is applied to the actor instead. If both entities are blocked, the pushing effect is canceled.
2. Create passive physical spacing
For an idle, walking, guarding, or similar animation:
Code:
@cmd dc_space_start 0
@cmd dc_space_setup_bounds -15 15 0 45 -8 8 DC_SPACE_TEMPLATE_PASSIVE
@cmd dc_space_end
PASSIVE permits neutral physical contact without requiring the actor's
candamage mask to accept the target. It permits either invincibility and vulnerability state, but excludes airborne and downed targets and requires a different native type relationship. Its complete template also excludes native TYPE_OBSTACLE targets.To restrict passive targets to selected native types, add an explicit include filter inside the block:
C:
dc_space_setup_target_types_include(
openborconstant("TYPE_PLAYER")
| openborconstant("TYPE_ENEMY")
| openborconstant("TYPE_NPC"));
This is a native type mask supplied by the creator, not a synthetic "character" category.
Templates versus presets
The bounds function's optional seventh argument is a template shortcut that accepts these selectors:
C:
DC_SPACE_TEMPLATE_NONE
DC_SPACE_TEMPLATE_ATTACK
DC_SPACE_TEMPLATE_PASSIVE
ATTACK and PASSIVE invoke the target-permission, type-include, and type-exclude setters with their matching presets. They replace all three admission fields, including inherited type restrictions. Later setters can change individual fields again.
Omitting the selector, passing
NULL(), or selecting DC_SPACE_TEMPLATE_NONE changes bounds only. It preserves any admission overrides already present and leaves new fields inherited. With untouched entity/global defaults, an ordinary six-argument bounds call therefore still uses attack admission.3. Change or stop a box on another frame
Use the same box index on a later frame to create another version. No reset flag is needed merely because the slot was used earlier.
This sample animation fragment declares a narrow box at frame 1, widens it at frame 3, and stops it at frame 5:
Code:
anim attack1
delay 8
frame data/chars/your_character/attack1_0.png
@cmd dc_space_start 0
@cmd dc_space_setup_bounds -10 30 0 40 -12 12 DC_SPACE_TEMPLATE_ATTACK
@cmd dc_space_setup_attempt_x -0.5 0.5 0.5
@cmd dc_space_end
frame data/chars/your_character/attack1_1.png
frame data/chars/your_character/attack1_2.png
@cmd dc_space_start 0
@cmd dc_space_setup_bounds -10 70 0 40 -12 12 DC_SPACE_TEMPLATE_ATTACK
@cmd dc_space_setup_attempt_x -0.5 0.5 0.5
@cmd dc_space_end
frame data/chars/your_character/attack1_3.png
frame data/chars/your_character/attack1_4.png
@cmd dc_space_stop 0
frame data/chars/your_character/attack1_5.png
Keep your animation's other properties and commands. The frame lines here illustrate placement; replace them with the actual frames in your module.
| Frames | Effective slot 0 |
|---|---|
| 0 | Inactive, assuming no other declaration. |
| 1-2 | Narrow version. |
| 3-4 | Wide version. |
| 5 onward | Stopped. |
Other box slots continue independently. Changing slot 0 does not clear slot 1.
New versions do not copy omitted settings from the previous version. Each starts with its own default bounds and movement/admission inheritance. The example repeats the stronger X setting because it should apply to both versions. Unchanged slots, in contrast, keep their existing complete records automatically.
dc_space_stop(index) is a complete one-shot declaration. Do not put it inside another start/end block. The equivalent disabled declaration is:
Code:
@cmd dc_space_start 0
@cmd dc_space_setup_enabled 0
@cmd dc_space_end
Use
1 or NULL() with dc_space_setup_enabled() to enable a writable declaration. Later enabled versions can restart a stopped slot.Choose explicit frame limits
To configure a box before frame 0 but make it active only on frames 1-4:
Code:
@cmd dc_space_start 0
@cmd dc_space_setup_bounds -30 165 0 40 -15 15 DC_SPACE_TEMPLATE_ATTACK
@cmd dc_space_setup_frames 1 4
@cmd dc_space_end
Both endpoints are inclusive. Frame numbers begin at zero.
NULL() as the first argument uses the captured declaration frame; NULL() or DC_SPACE_FRAME_END_ANIMATION as the second uses animation end.
C:
// Begin where declared and stop after frame 4.
dc_space_setup_frames(NULL(), 4);
// Begin on frame 2 and continue through animation end.
dc_space_setup_frames(2, DC_SPACE_FRAME_END_ANIMATION);
4. Choose push or pull on each axis
Each attempt setter takes three decimal strengths in the same order:
C:
dc_space_setup_attempt_x(less, same, greater);
dc_space_setup_attempt_y(less, same, greater);
dc_space_setup_attempt_z(less, same, greater);
LESS, SAME, GREATER describe the target relative to the actor. SAME means exact match with target. Positive and negative values are signed displacement requests, not automatically "push" and "pull" labels.
Use decimal values such as
0.5 and 0.0. NULL() inherits the corresponding field.The supplied global values are:
| Setting | LESS | SAME | GREATER |
|---|---|---|---|
| X attempt | -0.05 | 0.05 | 0.05 |
| Y attempt | 0.0 | 0.0 | 0.0 |
| Z attempt | 0.0 | 0.0 | 0.0 |
| Rebound on each axis | -1.0 | -1.0 | -1.0 |
Named constants in
master_config.h follow patterns such as DC_SPACE_ATTEMPT_X_LESS_DEFAULT, DC_SPACE_ATTEMPT_Y_SAME_DEFAULT, and DC_SPACE_REBOUND_Z_GREATER_DEFAULT. DC_SPACE_DIRECTION_X_DEFAULT selects FACING. Entity and box overrides take precedence.Horizontal separation
With the default FACING reference:
C:
dc_space_setup_attempt_x(-0.5, 0.5, 0.5);
Targets behind the actor move backward, targets in front move forward, and targets at equal X separate along facing.
Horizontal pull
C:
dc_space_setup_attempt_x(0.5, 0.0, -0.5);
Targets on either side move toward the actor. Equal X requests no horizontal movement. The request is not automatically clamped to the actor's coordinate, so choose an appropriate magnitude for small separations.
Depth separation or pull
C:
// Separate along Z.
dc_space_setup_attempt_z(-0.25, 0.0, 0.25);
// Pull along Z instead.
dc_space_setup_attempt_z(0.25, 0.0, -0.25);
These are alternatives. LESS means smaller world Z, GREATER means larger world Z. Positive displacement increases Z regardless of facing.
Fixed vertical or depth movement
C:
// Request upward displacement in every relative-altitude case.
dc_space_setup_attempt_y(0.25, 0.25, 0.25);
// Request decreasing Z in every depth relationship.
dc_space_setup_attempt_z(-0.25, -0.25, -0.25);
Y is altitude. Positive Y raises the target; negative Y lowers it. Gravity and native physics continue afterward. Repeated small upward position requests do not by themselves create a launch or hovering system.
Choose X's reference direction
C:
dc_space_setup_direction_x(DC_SPACE_DIRECTION_RIGHT);
dc_space_setup_direction_x(DC_SPACE_DIRECTION_LEFT);
dc_space_setup_direction_x(DC_SPACE_DIRECTION_FACING);
These are separate choices, not a sequence to copy together. Do not OR direction modes.
| Reference | LESS means | Positive attempt means |
|---|---|---|
| RIGHT | Target is to the actor's left. | Move right. |
| LEFT | Target is to the actor's right. | Move left. |
| FACING | Target is behind the actor. | Move along facing. |
For an always-left request:
C:
dc_space_setup_direction_x(DC_SPACE_DIRECTION_LEFT);
dc_space_setup_attempt_x(0.5, 0.5, 0.5);
This direction setting affects movement selection, not the bounds' facing-based X mirroring. Passing
NULL() restores direction inheritance.5. Set the reaction when movement fails
Rebound setters also take LESS, SAME, GREATER:
C:
dc_space_setup_rebound_x(less, same, greater);
dc_space_setup_rebound_y(less, same, greater);
dc_space_setup_rebound_z(less, same, greater);
These values are multipliers, not independent movement strengths. The selected multiplier uses the same original relationship as the attempted movement:
Code:
actor reaction = attempted target displacement * rebound multiplier
Only a failed requested axis can react. Target X succeeding while Z fails does not trigger X recoil. Zero attempt makes no movement request and cannot fail.
| Multiplier | Requested actor reaction |
|---|---|
| -1.0 | Equal displacement opposite the target's attempt. |
| -0.5 | Half displacement opposite the target's attempt. |
| -2.0 | Double displacement opposite the target's attempt. |
| 0.0 | No reaction for that case. |
| 1.0 | Equal displacement in the attempted direction. |
For reduced horizontal recoil:
C:
dc_space_setup_rebound_x(-0.5, -0.5, -0.5);
To suppress vertical reactions:
C:
dc_space_setup_rebound_y(0.0, 0.0, 0.0);
For different reactions by relative altitude:
C:
dc_space_setup_rebound_y(-0.5, 0.0, -1.0);
This requests half opposite reaction against a target below, none at equal altitude, and full opposite reaction against a target above.
6. Choose which targets qualify
Three setters control admission independently:
C:
dc_space_setup_targets(target_flags);
dc_space_setup_target_types_include(type_mask);
dc_space_setup_target_types_exclude(type_mask);
The complete bounds templates are shortcuts for these calls. Individual setters remain useful for exceptions.
Permission groups are cumulative. Alternatives within a group are ORed, but every group must pass. Neither alternative allows neither state; one allows that state; both allow either.
The constants below use the prefix
DC_SPACE_TARGET_:| Group | Alternatives | ATTACK preset | PASSIVE preset |
|---|---|---|---|
| Platform | PLATFORM / NOT_PLATFORM | Both | Both |
| Damage relationship | CAN_DAMAGE / CANNOT_DAMAGE | CAN_DAMAGE | Both |
| Invincibility | INVINCIBLE / NOT_INVINCIBLE | NOT_INVINCIBLE | Both |
| Airborne state | IN_AIR / NOT_IN_AIR | Both | NOT_IN_AIR |
| Vulnerability | VULNERABLE / NOT_VULNERABLE | VULNERABLE | Both |
| Downed state | DOWN / NOT_DOWN | NOT_DOWN | NOT_DOWN |
| Actor-relative type | SAME_TYPE / DIFFERENT_TYPE | DIFFERENT_TYPE | DIFFERENT_TYPE |
Passing one group never bypasses another. Platforms must satisfy every remaining gate too. Platform admission does not authorize moving the platform.
Start with a preset and add permissions
To use attack permissions while also admitting invincible targets:
C:
dc_space_setup_targets(
DC_SPACE_TARGET_ATTACK | DC_SPACE_TARGET_INVINCIBLE);
The target must still be vulnerable and pass every other attack gate. To permit either vulnerability state as well:
C:
dc_space_setup_targets(
DC_SPACE_TARGET_ATTACK
| DC_SPACE_TARGET_INVINCIBLE
| DC_SPACE_TARGET_NOT_VULNERABLE);
To let passive boxes affect the same native type too:
C:
dc_space_setup_targets(
DC_SPACE_TARGET_PASSIVE | DC_SPACE_TARGET_SAME_TYPE);
These functions replace their entire permission field; they do not incrementally add bits to an earlier call. Build the complete intended mask for each call.
For an air-only attack policy, remove the whole air group before supplying its intended alternative:
C:
int permissions = DC_SPACE_TARGET_ATTACK;
permissions &= (DC_SPACE_TARGET_AIR_ANY ^ -1);
permissions |= DC_SPACE_TARGET_IN_AIR;
dc_space_setup_targets(permissions);
Use XOR with
-1 for the complement in this engine, not unary ~.Group shortcuts combine both alternatives:
C:
DC_SPACE_TARGET_PLATFORM_ANY
DC_SPACE_TARGET_DAMAGE_ANY
DC_SPACE_TARGET_INVINCIBILITY_ANY
DC_SPACE_TARGET_AIR_ANY
DC_SPACE_TARGET_VULNERABILITY_ANY
DC_SPACE_TARGET_DOWN_ANY
DC_SPACE_TARGET_TYPE_RELATION_ANY
DC_SPACE_TARGET_ALL permits both alternatives in every group. It does not bypass type filters, self/support exclusions, geometry, or movement safety. Supplying only DC_SPACE_TARGET_IN_AIR, for example, leaves the other groups empty and selects nothing.What the relationships actually mean
CAN_DAMAGE tests the actor's native
candamage type mask.Native include and exclude filters
Native
TYPE_* values belong in the include/exclude setters, not in the permission mask:
C:
// Include players and enemies.
dc_space_setup_target_types_include(
openborconstant("TYPE_PLAYER") | openborconstant("TYPE_ENEMY"));
// Exclude native obstacles and traps.
dc_space_setup_target_types_exclude(
openborconstant("TYPE_OBSTACLE") | openborconstant("TYPE_TRAP"));
Each example changes only the named filter. Exclusion vetoes inclusion.
| Supplied type-filter value | Include meaning | Exclude meaning |
|---|---|---|
| DC_SPACE_TYPES_INHERIT | Inherit this filter. | Inherit this filter. |
| NULL() | No inclusion restriction. | No exclusion restriction. |
| 0 / TYPE_NONE | Permit only type zero. | Exclude only type zero. |
| Positive native mask | Require any matching bit. | Reject any matching bit. |
Explicit
NULL() stops inheritance and removes that restriction, even when the entity imposes one.For a passive template that should consider obstacles too:
Code:
@cmd dc_space_start 0
@cmd dc_space_setup_bounds -15 15 0 45 -8 8 DC_SPACE_TEMPLATE_PASSIVE
@cmd dc_space_setup_target_types_exclude NULL()
@cmd dc_space_end
Its remaining passive gates still apply. If an object has been retyped from obstacle to enemy, an exclusion of
TYPE_OBSTACLE does not identify its former role. Filters use the current native type.Target exclusion is not environmental exemption. Obstacles ignored as direct passive targets can still block displacement of some other selected entity.
7. Set entity defaults and override only exceptions
Movement and admission settings follow:
Code:
box override -> acting-entity override -> named global constant
The actor-level functions require no open box:
C:
dc_space_set_entity_attempt_x(entity, less, same, greater);
dc_space_set_entity_attempt_y(entity, less, same, greater);
dc_space_set_entity_attempt_z(entity, less, same, greater);
dc_space_set_entity_rebound_x(entity, less, same, greater);
dc_space_set_entity_rebound_y(entity, less, same, greater);
dc_space_set_entity_rebound_z(entity, less, same, greater);
dc_space_set_entity_direction_x(entity, direction);
dc_space_set_entity_targets(entity, target_flags);
dc_space_set_entity_target_types_include(entity, type_mask);
dc_space_set_entity_target_types_exclude(entity, type_mask);
Here,
entity must be a valid actor pointer, such as the acting_entity variable in the spawn example.For an actor whose usual horizontal separation should be stronger and whose recoil should be reduced, place these after initialization:
C:
dc_space_set_entity_attempt_x(acting_entity, -0.25, 0.25, 0.25);
dc_space_set_entity_rebound_x(acting_entity, -0.5, -0.5, -0.5);
Every box inheriting those fields uses that actor policy. Individual attacks can still override only X attempts with
dc_space_setup_attempt_x(-0.75, 0.75, 0.75).Inheritance is per field, including individual members of each triplet:
C:
dc_space_setup_rebound_y(NULL(), 0.0, NULL());
LESS and GREATER inherit; SAME explicitly disables the vertical reaction. Completed boxes retain those raw choices, so later entity settings remain visible wherever the box still inherits.
| Setting | Inherit | Explicitly disable or remove |
|---|---|---|
| Attempt/rebound value | NULL() | 0.0 disables that case. |
| X reference direction | NULL() | Choose a valid named mode; zero is not an off mode. |
| Target permission mask | NULL() | 0 admits no targets. |
| Native include/exclude filter | DC_SPACE_TYPES_INHERIT | NULL() removes that filter's restriction. |
Bounds and active frame windows remain properties of the box definition. They are not part of entity movement-policy inheritance.
Entity setters replace their fields when called; they do not use the box-completion gate. Call them at controlled setup or gameplay points outside an active push sweep. Model-copy reinitialization clears them, so reapply intended destination-model policy afterward.
8. Control overlapping boxes and priority
By default, the first colliding and admitted box handles a target for this actor's update. Box slots run from lowest to highest; lower slots finish their entity sweeps first.
If slot 0 selects a target, slot 1 does not get a second attempt against that target during the same update. Selection counts even when strength is zero, target movement is blocked, or only the actor reacts.
Admission or geometry rejection does not handle the target. Later boxes with different permissions or bounds can still select it.
To allow several boxes to affect the same target, configure the animation:
Code:
@cmd dc_space_setup_animation DC_SPACE_ANIM_ALLOW_MULTIPLE_PUSHES
Place that command near the animation's beginning, before the boxes are used. This policy belongs to the animation, independently of any open box. Restore first-box-wins with:
Code:
@cmd dc_space_setup_animation 0
It persists for that entity/animation until changed or cleaned up. Ordinary box declarations do not reset it.
Multiple mode uses current positions at each later test. Earlier displacement can move a target into or out of another box, and actor rebound moves subsequent box geometry. Enabling multiple mode permits repeated selections; it does not guarantee frozen-snapshot stacking.
9. Replace an existing declaration deliberately
Ordinary playback reuses completed declarations. Meaning the setup animation commands only perform their work once, and then early exit on every subsequent run. This is an optimization that removes redundant work from the engine - once a setup up pass is complete, there's usually no reason to repeat it.
To force repeat and rebuild the same entity/animation/declaration-frame/index identity:
Code:
@cmd dc_space_start 0 DC_SPACE_SETUP_RESET
@cmd dc_space_setup_bounds -20 90 0 45 -12 12 DC_SPACE_TEMPLATE_ATTACK
@cmd dc_space_setup_attempt_x -0.75 0.75 0.75
@cmd dc_space_end
Reset clears that version's configured geometry, policy overrides, enabled/completed state, and default window before accepting the replacement.
The fixed record is reused in place. Other authored versions and slots remain, although their effective coverage can change when the replacement's frame window changes. The same reset option exists on
dc_space_stop(index, reset).Do not leave the reset flag in normal animation setup unless rebuilding on every execution is intentional. Creating a different frame's version does not require reset. No setter has a separate
force argument.10. Show boxes while testing
Add or merge a draw hook on an entity using DC Space:
Code:
ondrawscript data/chars/your_character/script/event_draw.c
C:
#include "data/scripts/dc_space/main.c"
void main() {
void acting_entity = getlocalvar("self");
if(!openborvariant("in_level")) {
return;
}
if(!acting_entity) {
return;
}
if(getentityvar(acting_entity, DC_SPACE_VARKEY_PUSH_MASTER) == NULL()) {
return;
}
dc_space_debug_draw_current_pushboxes(
acting_entity, rgbcolor(255, 102, 0));
}
The library will draw any active space boxes as a flat amber rectangle on screen.
For a log dump, call this once at a chosen debugging point after initialization and outside an open block:
C:
dc_space_debug_dump_master(getlocalvar("self"));
The dump includes authored versions, enabled/completed state, frame-state transitions, bounds, and raw/resolved policy fields. Do not put a full database dump in every update for ordinary play. Remove the optional draw/dump calls when no longer needed.
Timing, effects, and movement protection
Push timing
As an optimization, DC Space intentionally throttles itself to
DC_SPACE_PUSH_WINDOW_STEPS in master_config.h. This value is set by default to 3 logical simulation steps. At the engine 200 Hz simulation rate, that supplies approximately 66.67 executions per second. Higher outer-update settings can invoke the script more often, but calls within the actor's already-used window do not repeat its push sweep.Set the window to
DC_SPACE_PUSH_THROTTLE_DISABLED (0) to remove that push gate. Windows are fixed groups of simulation steps before DC Space executes.Dust timing
DC_SPACE_PUSH_DUST_INTERVAL defaults to 8 simulation steps and must be a positive integer. The first admitted sweep in a new dust window receives the opportunity; successful grounded target moves and actor reactions can use it.The opportunity can be spent without a particle if nothing suitable moves. Later contacts in the same window do not retry. Several qualifying moves within the permitted sweep can each emit dust. Dust scheduling is independent of the push throttle and does not cap each target to one particle.
Admission, movement eligibility, and blocking are separate
Targets can pass their box's permissions and still be protected against displacement. The supplied
DC_SPACE_PUSH_CHECKS enables these conservative movement tests:| Check constant | Required for target displacement when enabled |
|---|---|
| DC_SPACE_PUSH_CHECK_PLATFORM | Target must not be a platform. The actor's supporting platform is ignored entirely. |
| DC_SPACE_PUSH_CHECK_OBSTACLE | Target must not carry TYPE_OBSTACLE. |
| DC_SPACE_PUSH_CHECK_TYPE | Target must carry TYPE_PLAYER, TYPE_ENEMY, or TYPE_NPC. |
| DC_SPACE_PUSH_CHECK_SPEED | Runtime speed must be a decimal greater than zero. |
| DC_SPACE_PUSH_CHECK_ANTIGRAB | Runtime antigrab must be a decimal below the configured threshold, default 999. |
| DC_SPACE_PUSH_CHECK_WALK | Native ANI_WALK must be valid. |
These are independent compile-time policy switches, not the per-box target permission flags. Speed, antigrab, and walking-animation availability are conservative signals, not universal definitions of physical immobility.
Broadening target inclusion does not disable these protections. For example, an admitted obstacle can cause configured actor recoil without being moved. The actor's supporting platform remains an ignored contact while platform protection is enabled.
X/Z movement also stays within the library's viewport/stage limits and applicable wall/entity blocking policy. X viewport endpoints are strict; playable Z endpoints are inclusive. If both independently safe lateral components have an unsafe combined destination, the already-cleared X-only move takes priority. Previously rejected axes are never reopened by that fallback.
Y is checked at the accepted lateral destination. Downward movement respects the mover's current base and applicable terrain/platform support; upward clearance uses model height. Unsafe requests are rejected rather than searched, redirected, or partially clamped.
The vertical policy is deliberately conservative. Native nohithead platforms are treated as solid by the interval check. Model height supplies upward clearance rather than an animation-specific envelope. Elevated current base values can prevent downward requests until native physics updates support. Successful altitude changes detach platform support without rewriting velocity, gravity, base, animation, or native landing/head-hit callbacks.
This is not continuous swept-volume collision detection. Very large endpoint displacements can cross thin blockers. Configure reasonable strengths and test unusual vertical/platform situations in the target engine.
When a box appears to do nothing
Check the complete selection path before increasing strength:
- Setup and frames - The initialization and update hooks must run, the start/end block must close, and the declaration must already have executed. Confirm the current frame is active and the slot has not been superseded or stopped.
- Geometry - The target's position must fit all three axes. Supply min/max endpoints, not native bbox offset/size arguments. The debug rectangle alone does not establish Z overlap.
- Admission - Every permission group must have an allowed alternative, and the target must pass include/exclude filters. ATTACK still requires damage-mask compatibility, non-invincibility, vulnerability, not-down, and a different type relationship.
- Selection and movement - Earlier boxes may already have handled the target. Zero selected strength, immovability checks, stage limits, walls, and blockers can then prevent displacement even after selection. Zero rebound multipliers suppress reactions independently.
Optional appendix: Under the hood
You can use DC Space without reading this section. This is the tour of how frame-specific definitions, shared states, masks, and reusable storage keep ordinary playback from repeating setup work.From declaration to displacement
The main workflow is:- Initialize the actor. Reserve its animation master directory. Detailed animation storage is created only when that animation needs it.
- Open a declaration. Select its exact animation/declaration-frame/box-index identity. New or reset records accept configuration; completed records remain read-only to ordinary setters.
- Close and prepare. Mark a writable record complete and compile the animation's known declarations into effective frame states.
- Select current work. Use the actor's animation and frame to find one state and its active-box mask. No active boxes means no target sweep.
- Admit the update. The actor-local simulation window prevents repeated sweeps in the same timing window.
- Sweep boxes first. Resolve each active box into scalar values, reject ineligible or nonoverlapping targets, and record selections that later boxes must not repeat.
- Resolve movement. Attempt target displacement, then permitted reactions on its failed axes. Refresh actor geometry after actual rebound.
- Reuse. The next update discards local validity, not all retained arrays. Completed definitions and pooled storage remain available.
Native indexing supplies the lookup key
The actor owns a master indexed by native animation ID. Each used animation has a child containing five fields:
Code:
entity master[animation_id]
|
+-- frame_table[frame_count] effective state references
+-- declarations[frame_count] authored box ownership
+-- state_pool[frame_count] retained state ownership
+-- animation selection flags
+-- cached native frame count
The native animation handle and
ANI_PROP_NUMFRAMES supply the actual frame count during animation-storage creation. The three directories reserve that count once. Native frame-count queries do not run during target sweeps.Runtime then follows:
Code:
current animation -> frame table[current frame] -> state -> box[0..31]
Frame entries hold state pointers directly, not state IDs requiring a search through an owner list. Out-of-range or inactive frame queries have no effective state.
Shared states are not duplicate frame definitions
Suppose box 0 starts at frame 2. At frame 5 it changes and box 1 is added. Box 0 stops at frame 8, and box 1 stops at frame 10:
Code:
Frames 0-1 -> NULL
Frames 2-4 -> State A
box[0] -> Box 0 version 1
Frames 5-7 -> State B
box[0] -> Box 0 version 2
box[1] -> Box 1 version 1
Frames 8-9 -> State C
box[1] -> the SAME Box 1 version 1
Frames 10+ -> NULL
Every frame in the same run shares a state. States B and C also share the unchanged box-1 record. Changing box 0 does not copy all of box 1's strengths, reactions, and admission fields.
This is why per-frame control does not require a complete set of box definitions on every frame. Actual changes cost records and boundary states; continuation mostly costs references in the preallocated frame table.
Runtime selection does not depend on a mutable "last box seen" cursor. After known definitions are prepared, visiting frames in the order
2 -> 7 -> 3 -> 5 selects each frame's proper state rather than carrying frame 7's geometry backward into frame 3.Authored identity keeps setup gating possible
The declaration directory answers a different question from the frame table:
Code:
Frame table:
Which completed box is effective here?
Declaration directory:
Was this exact box slot explicitly declared on this frame?
The effective box at frame 5 might have been declared at frame 2. Its existence must not prevent a new frame-5 declaration. The no-op gate therefore uses:
Code:
entity / animation / declaration frame / box index
dc_space_end() marks that exact version complete and publishes it. On another playback, the same identity can skip its setters without confusing inherited effective data with authored setup.The gate does not eliminate argument evaluation. Functions used to calculate setter arguments still execute before the setter can return. Use reset deliberately when a completed declaration must actually store replacement values.
Compile changes during setup, not during every target test
Closing a new or reset declaration recompiles the animation's known information. The builder stages effective starts, stops, and finite-end boundaries, then walks frames in numerical order.Boundary states compose up to 32 effective box references. Frames without a change reuse the preceding state. Older expiration events cannot disable a replacement, and a stopped or expired version does not revive its predecessor. The saving is that ordinary playback and each candidate collision do not reconstruct that timeline again.
Retained state allocations remain available when a reset makes an old boundary redundant. Repeating a reset through previously encountered boundaries can reuse them. New declarations or new boundaries can still allocate; the library does not claim that setup is allocation-free.
Ownership matters when references are shared
There are separate owning and borrowed links:- Declaration rows own box records. Each distinct authored version is freed once through its row.
- The state pool owns state tables. Repeated frame references never free those tables individually.
- Frame tables and state box slots borrow references. Their pointers identify effective data without taking another ownership share.
This avoids both repeated frees and general-purpose reference counting. Reset can reuse a fixed box record, but readers must not consume the database while that setup block is open. Publication belongs at close, not halfway through changing fields.
Fixed records and real storage costs
Each box uses 26 script fields, roughly 312 bytes of RAM:| Fields | Meaning |
|---|---|
| 0 | Packed frame endpoints and completed/disabled flags. |
| 1-3 | Packed X, Y, and Z bounds. |
| 4-12 | Nine attempt strengths. |
| 13-21 | Nine rebound multipliers. |
| 22 | X reference direction. |
| 23 | Target permission mask. |
| 24-25 | Native include and exclude filters. |
Optional actor fallback policy uses 22 fields, corresponding to movement and admission settings. It has no bounds or frame window.
Each authored row and each boundary state uses 33 fields: 32 record references and one mask. Each used animation reserves
3 * frame_count directory fields plus its five-field child. The actor's master retains full animation-ID capacity rather than resizing whenever another animation is used - this avoids memory churn.The practical tradeoff is intentional: modest empty directories and retained state tables provide direct access and reuse. They do not preallocate every possible box version on every frame.
Packed bounds and frame flags
One integer stores each axis's minimum and maximum as signed 15-bit values. Masking and shifting recover them; sign decoding restores negative coordinates. Its two upper bits remain reserved for axis metadata.The frame word also uses two 15-bit fields. Bit 30 marks completion, and bit 31 marks a disabled declaration. The end value 32767 is reserved for animation end rather than a numeric finite endpoint.
Packing reduces stored fields and paired lookups. It also explains why bounds and frame values have explicit validation ranges: silently wrapping an out-of-range coordinate would produce a different box.
Creators use ordinary setup functions. They should not write packed fields or internal array offsets directly.
Box masks and the meaning of 32
Each state has one 32-bit active mask. Bit N says whether box slot N participates on that frame:
Code:
mask 00000101 -> boxes 0 and 2 are active
The update walks those bits and reads records only for active slots. Gaps do not require reading every unused box record. Processing remains in ascending slot order.
Thirty-two is a simultaneous-slot bound, not a whole-animation definition limit. Slot 0 can have many authored versions, while each effective frame still addresses at most one current version in each of the 32 slots.
Permission masks, active-box masks, and movement-axis masks are separate numbers with separate meanings. The fact that each uses bits does not make their constants interchangeable.
Scalar caches keep array and property reads out of the inner loop
Each box's packed bounds are read and unpacked once before its sweep. Its movement/admission settings resolve through box, actor, and named defaults into scalar function variables.The target loop selects the appropriate LESS/SAME/GREATER values from those scalars. It does not allocate a profile array, traverse inheritance for every target, or reread the box's six bounds for every collision attempt.
Actor XYZ and facing are sampled for the update. Successful rebound refreshes only the affected coordinates and recomputes that box's corresponding world boundaries. Vertical rebound also refreshes the supporting-platform exclusion. Later targets therefore use the actor's actual new position without repacking or rereading its definition.
Target positions are sampled for the current interaction and can be read again by later boxes after movement. The cache is not a frozen whole-frame physics snapshot.
Viewport and stage limits form a separate lazy scalar snapshot. No lateral movement request means no lateral boundary acquisition; when needed, all boxes and target/rebound attempts in that update share the same limits. Their blocking results remain separate.
First-box-wins without stamping target variables
Default mode needs to remember which entity indices earlier boxes selected. DC Space uses one globally shared hierarchical bitmap, not a variable written onto each target.
Code:
root[0..4]
|
+-- optional child[0..31] : handled-bit words
child[32] : word-validity mask
function-local root mask : children valid in this invocation
Five children, 32 words per child, and 32 bits per word total 5120 enumeration indices, enough to cover the engine's 5000 entity per frame limit. The address comes directly from the engine's current entity index:
C:
child_index = entity_index >> 10;
word_index = (entity_index >> 5) & 31;
entity_bit = 1 << (entity_index & 31);
For index 4999, that selects child 4, word 28, bit 7. No entity-pointer search is needed to locate its handled flag.
The root and only the needed children allocate on demand. The sweep caches the current child, validity word, and handled word in scalar variables, avoiding a separate array lookup for every candidate's membership test.
Default mode checks the captured enumeration span against the bitmap capacity before movement. Multiple-push mode does not use that bitmap and therefore does not apply its capacity restriction.
Lazy validity replaces bulk clearing
Each admitted update begins with a zero local root-validity mask. That makes every retained child and word logically stale without physically clearing their arrays.Inactive children start with zero cached word validity. Each inactive word starts with a zero cached handled value. Its first write replaces stale content before publishing the validity bits. Later writes add selections to that current word.
Allocated does not mean valid. This distinction lets another actor reuse the same storage immediately without inheriting the previous actor's handled targets.
Bookkeeping also stays lazy within one update:
- No earlier-box history: the whole current box sweep skips membership checks. It prepares insertion addresses only for collisions that must be remembered.
- No later active box: new selections need no recording writes, although existing history must still be checked.
- Single active box, no collisions, or multiple mode: no handled-storage acquisition is needed.
Explicit gates are important in this interpreter
Build 6391 evaluates both sides of&& and ||. Expressions such as pointer && read(pointer) do not safely skip the read when the pointer is absent. Likewise, an expensive probe beside a false scalar condition still runs.Safety-dependent reads and expensive calls therefore sit behind real branches:
C:
if(pointer != NULL()) {
value = get(pointer, index);
}
Compound scalar tests are still useful; the distinction is whether an operand needs the other test to suppress its execution.
Unary bitwise NOT is also avoided, as this operator is bugged prior to OpenBOR 4.x. For integer masks, XOR with
-1 supplies the complement without the target parser's broken unary operation:
C:
pending_axes = requested_axes & (blocked_axes ^ -1);
Simulation windows prevent outer-update multiplication
The push gate divides simulation time into fixed integer windows:
C:
simulation_window = simulation_time / window_steps;
With width 3, steps
0..2 share window 0, 3..5 share window 1, and so on. Each actor stores its last admitted window. Repeated outer calls in that window return before geometry, policy sweeps, and handled work.This follows game simulation progress rather than the user's outer-update setting. It also avoids repeatedly discarding timing overshoot by setting the next deadline to "now plus three" after every accepted call.
The remembered value belongs to the actor, not each box or target. Animation changes do not automatically grant another window. Cleanup clears the stamp. Missed windows are discarded rather than converted to catch-up displacement.
Dust has its own actor window and receives the same simulation sample when available. Its permission is cached for the whole sweep, not consumed separately by the first qualifying target.
Sharing storage safety - Avoiding RACE
Ordinary script calls are synchronous, but movement-related spawning or custom callbacks can enter other scripts before the caller returns. Shared scratch storage therefore has an active-update guard.Nested active push updates are rejected. Cleanup and setup cannot mutate the actor whose push sweep is running; global handled teardown cannot run during any active sweep. Open setup also excludes runtime readers until the declaration closes.
Actor cleanup releases its definitions, fallback policy, setup references, and timing history. It leaves the shared handled allocation available to future actors. The global destruction hook separately frees the shared root and every allocated child before its owner handle is lost.
The public setup and lifecycle functions preserve these responsibilities. Borrowed state and box handles must not be freed, rewritten, or kept across registry destruction. Normal creators can leave the indexing, masks, and ownership entirely to the library.