Jump to content

Onmodelcopyscript

From OpenBOR

onmodelcopyscript defines a model-level lifecycle hook for an entity changing from one model to another through OpenBOR's model-copy system. It executes after the incoming model and its scripts have been installed, providing the live entity as self and a temporary snapshot of its previous state as old.

Weapon models are the traditional use because equipping or removing a weapon can replace the player's active model. The hook is equally useful for transformations, armor, mounts, alternate forms, stance packages, possession, power states, costume models, and other systems that change an existing entity without spawning a replacement.

Onmodelcopyscript allows the incoming model to inspect what the entity was and finish the transition deliberately. Creator logic can preserve proportional health, transfer form-specific resources, reconfigure helpers, choose a transition animation, rebuild interface state, apply effects, or normalize any property that model copying alone cannot resolve.

Syntax

onmodelcopyscript {path}

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

Example weapon or transformation model header:

name hero_armored
type player

onmodelcopyscript data/scripts/hero_armored_copy.c

Event code may also be embedded directly in the model:

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

    complete_model_transition(self, old);
}
@end_script

complete_model_transition() in this example represents a project-defined function.

Local Variables

Local Type Value
self Entity pointer Live entity after the incoming model has been applied.
old Entity pointer Temporary snapshot of the same entity immediately before the model change.

old is more than a previous model handle. It is an entity snapshot containing the earlier model reference, copied model data, current health and magic, animation state, position, relationships, combat state, and other entity members as they existed before the transition.

The live entity does not receive a new identity. self retains the same entity pointer, unique identifier, world membership, and general runtime state while its active model package changes.

The old Snapshot

OpenBOR prepares old specifically for the callback. Direct entity members represent pre-copy values, while offense and defense arrays receive independent copies so the incoming model cannot overwrite the values most commonly needed for weapon transitions.

The snapshot is temporary and valid only while Onmodelcopyscript is executing. Never store old in a global variable, entity variable, array, object relationship, binding, or another location for later use. Its memory ceases to represent a valid object when the callback returns.

Treat old as read-only transition data rather than an entity in play. It is not a second world entity and should not be moved, damaged, killed, bound, spawned from, or passed to systems that expect an active entity-list member.

The copy is intentionally shallow except for offense and defense arrays. Pointer-backed members may still refer to shared model data, scripts, objects, or live entities. Scalar properties and the embedded previous model-data copy are the safest sources for comparisons.

Useful values include:

  • ENTITY_PROPERTY_MODEL - Previous cached model pointer on old; incoming cached model pointer on self.
  • ENTITY_PROPERTY_MODEL_DATA - Previous entity model-data copy on old; incoming entity model-data copy on self.
  • ENTITY_PROPERTY_HP and ENTITY_PROPERTY_MP - Resource values before and after native limits are applied.
  • ENTITY_PROPERTY_ANIMATION_ID - Previous and resulting animation identifiers.
  • ENTITY_PROPERTY_ANIMATION_FRAME - Previous and resulting indexed animation positions.
  • ENTITY_PROPERTY_DEFENSE and ENTITY_PROPERTY_OFFENSE - Independently preserved old arrays and rebuilt new arrays.
  • ENTITY_PROPERTY_OWNER, ENTITY_PROPERTY_PARENT, and ENTITY_PROPERTY_WEAPON_ITEM - Relationship context when applicable.

The word FRAME in ENTITY_PROPERTY_ANIMATION_FRAME is the literal script identifier. Conceptually, the value is the current indexed position within the animation.

Trigger Conditions

Onmodelcopyscript belongs to OpenBOR's complete model-copy path. Current native entry points include:

  • Equipping, replacing, or removing a weapon when the operation selects another model.
  • An animation's weapon-change command.
  • Native weapon pickup, loss, level restoration, and related player weapon handling.
  • Script weapon changes routed through changeentityproperty() or changeplayerproperty().
  • Script model changes routed through changeentityproperty() with the model property.

The event does not execute when the requested incoming model is already the entity's current model. OpenBOR returns without performing a copy in that case.

The following operations do not inherently invoke Onmodelcopyscript:

  • Spawning or respawning an entity. Onspawnscript covers entity creation.
  • Directly assigning a model pointer with set_entity_property().
  • Low-level engine paths that change only the cached model reference.
  • Modifying properties on the entity's current model-data copy.
  • Loading, caching, or inheriting a model definition without changing a live entity.

Directly assigning ENTITY_PROPERTY_MODEL is not a complete model transformation. Such assignment does not install model data, copy scripts, rebuild offense and defense, apply palette state, or dispatch this event. Use the full model-change path when the entity should actually transition.

No global_model Onmodelcopyscript is executed. The callback belongs to the script set installed on the transitioning entity, normally supplied by the incoming model.

Initiating a Scripted Model Change

The current full model-change route is the model property of changeentityproperty(). Its final argument governs animation synchronization:

changeentityproperty(
    self,
    "model",
    "hero_armored",
    1
);

When the final argument is nonzero and the incoming model supplies the current animation, OpenBOR retains the animation identifier and indexed position. When synchronization is disabled or the incoming model lacks that animation, OpenBOR selects the appropriate starting state for the new model.

Weapon changes use the same internal model-copy operation. Their animation argument is interpreted through the weapon-changing wrapper, so creator code should follow the documented weapon API rather than assuming it has identical polarity to the direct model argument.

Runtime Order

The relevant transition order is:

  1. OpenBOR resolves the requested incoming model.
  2. When the incoming model is already current, the operation ends with no callback.
  3. Model-copy flags on the incoming model govern inheritance from the outgoing model.
  4. OpenBOR preserves a temporary snapshot of the live entity for old when the incoming callback is available.
  5. The live entity's cached model and model-data copy change to the incoming model.
  6. Selected compatibility properties from the outgoing entity model data are retained where native rules require them.
  7. The entity's original type is restored, preventing a weapon or form model from changing its fundamental runtime type.
  8. Instance scripts transition to the incoming model's script set.
  9. Offense and defense arrays are rebuilt from the resulting model data.
  10. The entity's current palette selection is reapplied.
  11. Onmodelcopyscript executes with the resulting entity as self and the prior snapshot as old.
  12. Control returns to the weapon or model-changing caller, which may perform additional source-specific work.

Onmodelcopyscript is therefore a post-copy hook. The incoming model, model data, scripts, offense, defense, and palette are already active. Changes made by the callback become the final transition adjustments before the initiating model-copy operation returns.

Onspawnscript does not run because the entity was not created. The entity's next ordinary update continues according to its existing lifecycle and timing.

State Preserved by the Entity

Model copying transforms an existing entity rather than replacing it. Position, direction, unique identifier, owner, parent, opponent, bindings, runtime counters, and many other entity-level values generally remain in place unless native model-copy logic or script changes them.

Current health and magic also remain, though OpenBOR clamps either value when it exceeds the incoming maximum. Missing incoming maximum values may inherit their outgoing counterparts through compatibility rules.

The entity's runtime type is explicitly preserved from the outgoing state. Weapon and transformation models do not turn a player into an item, projectile, or enemy merely because the incoming model definition uses another type.

Script instance storage is retained across the copy operation. Incoming script bytecode replaces matching initialized script slots, while the model's modelflag determines whether unmatched outgoing script slots remain available.

Animation handling depends on the synchronization argument. Successful synchronization retains the current animation identifier and indexed position against the incoming animation. Fallback handling selects a spawn, respawn, select, or idle state according to the current screen, logical time, entity type, and available animations.

Model Copy Flags

The incoming model's modelflag command controls which portions of the outgoing model may be inherited during transition.

modelflag {flags}

# Default
modelflag none

Multiple named flags may be combined on one line:

modelflag no_basic no_weapon no_script
Flag Effect
none Enables the default compatibility copy behavior.
no_basic Prevents the incoming model from inheriting the outgoing model's basic compatibility package. Current native members include movement configuration, missing movement and run values, icons, shadow and effect references, sounds, and missing animations.
no_weapon Prevents the incoming model from inheriting the outgoing model's weapon index and missing weapon list.
no_script Clears the entity's outgoing script slots before incoming model scripts are copied. Incoming scripts still install normally, including Onmodelcopyscript itself.

no_script does not disable scripts. It prevents scripts absent from the incoming model from lingering on the entity after the transition. Model scripts actually supplied by the incoming model still replace their corresponding slots.

The flags belong to the incoming model, not the outgoing model and not the individual copy call. Every transition into that model uses its declared policy.

Cached Model Inheritance

Default basic and weapon inheritance can populate missing values directly on the cached incoming model. Those inherited values may then be observed by later entities that use the same model during the process lifetime.

This behavior is useful for weapon models designed as extensions of one compatible base character. The weapon can omit shared animations and configuration, allowing the copy system to complete it from the wielder.

Models intended for unrelated source characters or fully independent transformations should define their own data and use no_basic or no_weapon as appropriate. Otherwise, the first source model copied into a shared target can influence later transitions.

Return Value and Re-entry

The script's return value is ignored. Returning 0, 1, or another value does not undo the model copy or restore the previous model.

Script may alter self, select an animation, adjust resources, change relationships, or initiate another full model change. Since the callback executes synchronously, a second model change can invoke another Onmodelcopyscript before the first operation returns.

Nested transitions enable intentional multi-stage forms, though cycles can recurse indefinitely. Guard any callback that can change models by testing the incoming and outgoing models, transition state, or another explicit condition.

Removing self is also possible, though the weapon or model-changing caller can still have source-specific work remaining after the callback. Transition logic should account for that continuation rather than treating a return value as cancellation.

Quick Examples

Preserve Health Proportion

Native copying preserves current health and clamps it to the incoming maximum. Transformations that should preserve the previous health percentage can calculate it from both embedded model-data copies:

void main()
{
    void self = getlocalvar("self");
    void old = getlocalvar("old");
    void old_model = get_entity_property(
        old,
        ENTITY_PROPERTY_MODEL_DATA
    );
    void new_model = get_entity_property(
        self,
        ENTITY_PROPERTY_MODEL_DATA
    );
    int old_hp = get_entity_property(
        old,
        ENTITY_PROPERTY_HP
    );
    int old_max = get_model_property(
        old_model,
        MODEL_PROPERTY_HP
    );
    int new_max = get_model_property(
        new_model,
        MODEL_PROPERTY_HP
    );
    int new_hp = old_hp;

    if(old_max > 0)
    {
        new_hp = old_hp * new_max / old_max;
    }

    if(old_hp > 0 && new_hp < 1)
    {
        new_hp = 1;
    }

    if(new_hp > new_max)
    {
        new_hp = new_max;
    }

    set_entity_property(
        self,
        ENTITY_PROPERTY_HP,
        new_hp
    );
}

The same pattern can migrate magic, guard points, transformation charge, armor, ammunition, or any project-defined resource.

Transition-Specific Presentation

Both model names are available during the callback:

void main()
{
    void self = getlocalvar("self");
    void old = getlocalvar("old");
    void old_model = get_entity_property(
        old,
        ENTITY_PROPERTY_MODEL_DATA
    );
    void new_model = get_entity_property(
        self,
        ENTITY_PROPERTY_MODEL_DATA
    );
    void old_name = get_model_property(
        old_model,
        MODEL_PROPERTY_NAME
    );
    void new_name = get_model_property(
        new_model,
        MODEL_PROPERTY_NAME
    );

    present_model_transition(
        self,
        old_name,
        new_name
    );
}

The project-defined function can select a flash, sound, animation, text callout, camera effect, or interface update according to the exact transition.

Preserve Action Context

Incoming logic can inspect the previous animation and indexed position even when the model-copy request did not synchronize them:

void main()
{
    void self = getlocalvar("self");
    void old = getlocalvar("old");
    int animation_id = get_entity_property(
        old,
        ENTITY_PROPERTY_ANIMATION_ID
    );
    int animation_position = get_entity_property(
        old,
        ENTITY_PROPERTY_ANIMATION_FRAME
    );

    restore_compatible_action(
        self,
        animation_id,
        animation_position
    );
}

restore_compatible_action() represents project logic that validates the incoming animation before selecting an equivalent state.

Common Uses

  • Initializing an equipped weapon model from the outgoing character state.
  • Restoring character behavior when a weapon is removed or lost.
  • Migrating resources between normal, armored, mounted, powered, or transformed forms.
  • Selecting transition effects according to both outgoing and incoming models.
  • Reconfiguring child entities, interface elements, sound ownership, or encounter registration after a form change.
  • Rebuilding project-specific offense, defense, movement, or artificial intelligence state.
  • Preserving compatible action context across models with matching animations.
  • Applying target-model rules after all native compatibility inheritance is complete.

Common Mistakes

  • Treating old as only a model pointer rather than a temporary entity snapshot.
  • Storing old for use after the callback returns.
  • Trying to manipulate old as though it were an active entity.
  • Placing Onmodelcopyscript only on the outgoing model when the incoming model should own the transition logic.
  • Expecting global_model to receive the event.
  • Expecting Onmodelcopyscript during spawn, respawn, or a request for the already-current model.
  • Directly assigning ENTITY_PROPERTY_MODEL and expecting a complete model copy.
  • Expecting the return value to cancel or reverse the transition.
  • Assuming no_script disables incoming scripts.
  • Forgetting that default basic or weapon inheritance can populate the cached incoming model.
  • Starting another model change from the callback without preventing recursive cycles.
  • Assuming model-copy animation synchronization and weapon-wrapper animation arguments use identical polarity.

See Also