DC Space

DC Space 1.0

No permission to download
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?
  • 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.
Push is position displacement, not damage or velocity. DC Space does not create native attack hits, inflict damage, run a launch animation, or replace native attack/body-box collision processing. Its geometry test checks a target entity's position against the push region, not the overlap of two native body boxes. The ATTACK template selects attack-appropriate targets; it does not require a native hit to have connected first.

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.
Target entities do not need their own DC Space setup merely to be pushed. Install the entity hooks on actors that own push boxes; the shared cleanup hook is installed once for the module.



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:


1792549242088.png


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.

FramesEffective slot 0
0Inactive, assuming no other declaration.
1-2Narrow version.
3-4Wide version.
5 onwardStopped.

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:

SettingLESSSAMEGREATER
X attempt-0.050.050.05
Y attempt0.00.00.0
Z attempt0.00.00.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.

ReferenceLESS meansPositive attempt means
RIGHTTarget is to the actor's left.Move right.
LEFTTarget is to the actor's right.Move left.
FACINGTarget 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.

MultiplierRequested actor reaction
-1.0Equal displacement opposite the target's attempt.
-0.5Half displacement opposite the target's attempt.
-2.0Double displacement opposite the target's attempt.
0.0No reaction for that case.
1.0Equal 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_:

GroupAlternativesATTACK presetPASSIVE preset
PlatformPLATFORM / NOT_PLATFORMBothBoth
Damage relationshipCAN_DAMAGE / CANNOT_DAMAGECAN_DAMAGEBoth
InvincibilityINVINCIBLE / NOT_INVINCIBLENOT_INVINCIBLEBoth
Airborne stateIN_AIR / NOT_IN_AIRBothNOT_IN_AIR
VulnerabilityVULNERABLE / NOT_VULNERABLEVULNERABLEBoth
Downed stateDOWN / NOT_DOWNNOT_DOWNNOT_DOWN
Actor-relative typeSAME_TYPE / DIFFERENT_TYPEDIFFERENT_TYPEDIFFERENT_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 valueInclude meaningExclude meaning
DC_SPACE_TYPES_INHERITInherit this filter.Inherit this filter.
NULL()No inclusion restriction.No exclusion restriction.
0 / TYPE_NONEPermit only type zero.Exclude only type zero.
Positive native maskRequire 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.

SettingInheritExplicitly disable or remove
Attempt/rebound valueNULL()0.0 disables that case.
X reference directionNULL()Choose a valid named mode; zero is not an off mode.
Target permission maskNULL()0 admits no targets.
Native include/exclude filterDC_SPACE_TYPES_INHERITNULL() 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.

1792551365003.png


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 constantRequired for target displacement when enabled
DC_SPACE_PUSH_CHECK_PLATFORMTarget must not be a platform. The actor's supporting platform is ignored entirely.
DC_SPACE_PUSH_CHECK_OBSTACLETarget must not carry TYPE_OBSTACLE.
DC_SPACE_PUSH_CHECK_TYPETarget must carry TYPE_PLAYER, TYPE_ENEMY, or TYPE_NPC.
DC_SPACE_PUSH_CHECK_SPEEDRuntime speed must be a decimal greater than zero.
DC_SPACE_PUSH_CHECK_ANTIGRABRuntime antigrab must be a decimal below the configured threshold, default 999.
DC_SPACE_PUSH_CHECK_WALKNative 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.
Different-type permission is not a synonym for enemy-team selection. Type inclusion is not permission to move scenery. Template selection does not set a stronger movement magnitude. Each of those controls answers a separate question.

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:
  1. Initialize the actor. Reserve its animation master directory. Detailed animation storage is created only when that animation needs it.
  2. 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.
  3. Close and prepare. Mark a writable record complete and compile the animation's known declarations into effective frame states.
  4. 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.
  5. Admit the update. The actor-local simulation window prevents repeated sweeps in the same timing window.
  6. Sweep boxes first. Resolve each active box into scalar values, reject ineligible or nonoverlapping targets, and record selections that later boxes must not repeat.
  7. Resolve movement. Attempt target displacement, then permitted reactions on its failed axes. Refresh actor geometry after actual rebound.
  8. Reuse. The next update discards local validity, not all retained arrays. Completed definitions and pooled storage remain available.
The runtime uses facts prepared at setup without freezing gameplay facts. Current type, damage relationship, state, positions, and inherited entity values are still sampled where needed.

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.

1792551930204.png


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.
Cleanup visits the owners, including inactive, disabled, unfinished, and currently unreferenced retained objects. It does not recursively free every pointer encountered in the effective frame table.

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:

FieldsMeaning
0Packed frame endpoints and completed/disabled flags.
1-3Packed X, Y, and Z bounds.
4-12Nine attempt strengths.
13-21Nine rebound multipliers.
22X reference direction.
23Target permission mask.
24-25Native 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.
The captured entity indices must remain stable across the synchronous sweeps. Reuse between updates is harmless; a callback destroying and replacing an already-scanned slot during the same update would violate the lifetime contract.

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.
Author
DCurrent
Downloads
1
Views
25
First release
Last update

Ratings

0.00 star(s) 0 ratings

More resources from DCurrent

Back
Top Bottom