Skip to content

Control Flow ​

Summary: How Enforce Script handles branching and iteration --- if/else, for, while, foreach, switch, break/continue, and the thread keyword --- including the traps that differ from C-family languages (no do...while, sibling-branch variable redeclaration errors).


if / else / else if ​

The if statement evaluates a boolean expression and executes a block of code when the result is true. You can chain conditions with else if and provide a fallback with else.

c
void CheckHealth(PlayerBase player)
{
    float health = player.GetHealth("", "Health");

    if (health > 75)
    {
        Print("Player is healthy");
    }
    else if (health > 25)
    {
        Print("Player is wounded");
    }
    else
    {
        Print("Player is critical");
    }
}

Null checks ​

In Enforce Script, object references evaluate to false when null. This is the standard way to guard against null access:

c
void ProcessItem(EntityAI item)
{
    if (!item)
        return;

    string name = item.GetType();
    Print("Processing: " + name);
}

Logical operators ​

Combine conditions with && (AND) and || (OR). Short-circuit evaluation applies: if the left side of && is false, the right side is never evaluated.

c
void CheckPlayerState(PlayerBase player)
{
    if (player && player.IsAlive())
    {
        // Safe -- player is checked for null before calling IsAlive()
        Print("Player is alive");
    }

    if (player.GetHealth("", "Blood") < 3000 || player.GetHealth("", "Health") < 25)
    {
        Print("Player is in danger");
    }
}

PITFALL: Variable redeclaration in else-if blocks ​

This is one of the most common Enforce Script errors. In most languages, variables declared inside one if branch are independent from variables in a sibling else branch. Not in Enforce Script. Declaring the same variable name in sibling if/else if/else blocks causes a multiple declaration error at compile time.

c
// WRONG -- Compile error!
void BadExample(Object obj)
{
    if (obj.IsKindOf("Car"))
    {
        Car vehicle = Car.Cast(obj);
        vehicle.GetSpeedometer();
    }
    else if (obj.IsKindOf("ItemBase"))
    {
        ItemBase item = ItemBase.Cast(obj);    // OK -- different name
        item.GetQuantity();
    }
    else
    {
        string msg = "Unknown object";         // First declaration of msg
        Print(msg);
    }
}

Wait -- that looks fine, right? The problem occurs when you use the same variable name in two branches:

c
// WRONG -- Compile error: multiple declaration of 'result'
void ProcessObject(Object obj)
{
    if (obj.IsKindOf("Car"))
    {
        string result = "It's a car";
        Print(result);
    }
    else
    {
        string result = "It's something else";  // ERROR! Same name as in the if block
        Print(result);
    }
}

The fix: Declare the variable before the if statement, or use unique names per branch.

c
// CORRECT -- Declare before the if
void ProcessObject(Object obj)
{
    string result;

    if (obj.IsKindOf("Car"))
    {
        result = "It's a car";
    }
    else
    {
        result = "It's something else";
    }

    Print(result);
}

for Loop ​

The for loop is identical to C-style syntax: initializer, condition, and increment.

c
// Print numbers 0 through 9
void CountToTen()
{
    for (int i = 0; i < 10; i++)
    {
        Print(i);
    }
}

Iterating over an array with for ​

c
void ListInventory(PlayerBase player)
{
    array<EntityAI> items = new array<EntityAI>;
    player.GetInventory().EnumerateInventory(InventoryTraversalType.PREORDER, items);

    for (int i = 0; i < items.Count(); i++)
    {
        EntityAI item = items.Get(i);
        if (item)
        {
            Print(string.Format("[%1] %2", i, item.GetType()));
        }
    }
}

Nested for loops ​

c
// Spawn a grid of objects
void SpawnGrid(vector origin, int rows, int cols, float spacing)
{
    for (int r = 0; r < rows; r++)
    {
        for (int c = 0; c < cols; c++)
        {
            vector pos = origin;
            pos[0] = pos[0] + (c * spacing);
            pos[2] = pos[2] + (r * spacing);
            pos[1] = GetGame().SurfaceY(pos[0], pos[2]);

            GetGame().CreateObject("Barrel_Green", pos, false, false, true);
        }
    }
}

Note: Do not redeclare the loop variable i if there is already a variable named i in the enclosing scope. Enforce Script treats this as a multiple declaration error, even in nested scopes.


while Loop ​

The while loop repeats a block as long as its condition is true. The condition is evaluated before each iteration.

c
// Remove all dead zombies from a tracking list
void CleanupDeadZombies(array<DayZInfected> zombieList)
{
    int i = 0;
    while (i < zombieList.Count())
    {
        EntityAI eai;
        if (Class.CastTo(eai, zombieList.Get(i)) && !eai.IsAlive())
        {
            zombieList.RemoveOrdered(i);
            // Do NOT increment i -- the next element has shifted into this index
        }
        else
        {
            i++;
        }
    }
}

WARNING: There is NO do...while in Enforce Script ​

The do...while keyword does not exist. The compiler will reject it. If you need a loop that always executes at least once, use the flag pattern described below.

c
// WRONG -- This will NOT compile
do
{
    // body
}
while (someCondition);

Simulating do...while with a Flag ​

The standard workaround is to use a bool flag that is true on the first iteration:

c
void SimulateDoWhile()
{
    bool first = true;
    int attempts = 0;
    vector spawnPos;

    while (first || !IsPositionSafe(spawnPos))
    {
        first = false;
        attempts++;
        spawnPos = GetRandomPosition();

        if (attempts > 100)
            break;
    }

    Print(string.Format("Found safe position after %1 attempts", attempts));
}

An alternative approach using break:

c
void AlternativeDoWhile()
{
    while (true)
    {
        // Body executes at least once
        DoSomething();

        // Check the exit condition at the END
        if (!ShouldContinue())
            break;
    }
}

foreach ​

The foreach statement is the cleanest way to iterate over arrays, maps, and static arrays. It comes in two forms.

Simple foreach (value only) ​

c
void AnnounceItems(array<string> itemNames)
{
    foreach (string name : itemNames)
    {
        Print("Found item: " + name);
    }
}

foreach with index ​

When iterating over arrays, the first variable receives the index:

c
void ListPlayers(array<Man> players)
{
    foreach (int idx, Man player : players)
    {
        Print(string.Format("Player #%1: %2", idx, player.GetIdentity().GetName()));
    }
}

foreach over maps ​

For maps, the first variable receives the key and the second receives the value:

c
void PrintScoreboard(map<string, int> scores)
{
    foreach (string playerName, int score : scores)
    {
        Print(string.Format("%1: %2 kills", playerName, score));
    }
}

You can also iterate over maps with just the value:

c
void SumScores(map<string, int> scores)
{
    int total = 0;
    foreach (int score : scores)
    {
        total += score;
    }
    Print("Total kills: " + total);
}

foreach over static arrays ​

c
void PrintStaticArray()
{
    int numbers[] = {10, 20, 30, 40, 50};

    foreach (int value : numbers)
    {
        Print(value);
    }
}

switch / case ​

The switch statement matches a value against a list of case labels. It works with int, string, enum values, and constants.

Important: Fall-through behavior (same as C/C++) ​

Enforce Script switch/case DOES fall through from one case to the next when break is omitted, just like C/C++. Vanilla code intentionally uses fall-through (biossessionservice.c:182 has comment "Intentionally no break, fall through to connecting"). Always use break at the end of every case unless you intentionally want fall-through.

c
void HandleCommand(string command)
{
    switch (command)
    {
        case "heal":
            HealPlayer();
            break;

        case "kill":
            KillPlayer();
            break;

        case "teleport":
            TeleportPlayer();
            break;

        default:
            Print("Unknown command: " + command);
            break;
    }
}

switch with enums ​

c
enum EDifficulty
{
    EASY = 0,
    MEDIUM,
    HARD
};

void SetDifficulty(EDifficulty difficulty)
{
    float zombieMultiplier;

    switch (difficulty)
    {
        case EDifficulty.EASY:
            zombieMultiplier = 0.5;
            break;

        case EDifficulty.MEDIUM:
            zombieMultiplier = 1.0;
            break;

        case EDifficulty.HARD:
            zombieMultiplier = 2.0;
            break;

        default:
            zombieMultiplier = 1.0;
            break;
    }

    Print(string.Format("Zombie multiplier: %1", zombieMultiplier));
}

switch with integer constants ​

c
void DescribeWeaponSlot(int slotId)
{
    const int SLOT_SHOULDER = 0;
    const int SLOT_MELEE = 1;
    const int SLOT_PISTOL = 2;

    switch (slotId)
    {
        case SLOT_SHOULDER:
            Print("Primary weapon");
            break;

        case SLOT_MELEE:
            Print("Melee weapon");
            break;

        case SLOT_PISTOL:
            Print("Sidearm");
            break;

        default:
            Print("Unknown slot");
            break;
    }
}

Note: You can stack cases to share a handler, just like in C/C++. An empty case without break falls through to the next case's handler.


break and continue ​

break ​

break exits the innermost loop (or switch case) immediately.

c
// Find the first player within 100 meters
void FindNearbyPlayer(vector origin, array<Man> players)
{
    foreach (Man player : players)
    {
        float dist = vector.Distance(origin, player.GetPosition());
        if (dist < 100)
        {
            Print("Found nearby player: " + player.GetIdentity().GetName());
            break; // Stop searching
        }
    }
}

continue ​

continue skips the rest of the current iteration and jumps to the next one.

c
// Process only alive players
void HealAllPlayers(array<Man> players)
{
    foreach (Man man : players)
    {
        PlayerBase player;
        if (!Class.CastTo(player, man))
            continue; // Not a PlayerBase, skip

        if (!player.IsAlive())
            continue; // Dead, skip

        player.SetHealth("", "Health", 100);
        Print("Healed: " + player.GetIdentity().GetName());
    }
}

Nested loops with break ​

break only exits the innermost loop. To break out of nested loops, use a flag variable:

c
void FindItemInGrid(array<array<string>> grid, string target)
{
    bool found = false;

    for (int row = 0; row < grid.Count(); row++)
    {
        for (int col = 0; col < grid.Get(row).Count(); col++)
        {
            if (grid.Get(row).Get(col) == target)
            {
                Print(string.Format("Found '%1' at [%2, %3]", target, row, col));
                found = true;
                break; // Only exits inner loop
            }
        }

        if (found)
            break; // Exits outer loop
    }
}

Thread Keyword ​

Enforce Script has a thread keyword for asynchronous execution:

c
// Declare the function normally -- there is no `thread` on the declaration
void LongOperation()
{
    Sleep(5000);  // Wait 5 seconds without blocking the caller
    Print("Done!");
}

// The keyword goes on the CALL
thread LongOperation();  // Starts without blocking the caller

thread goes on the call, not the declaration. Bohemia's keyword table describes it as "declared before the function call", and every real use follows that form: Community Framework's CF_XML.ReadAsync calls thread Process(...) at CF_XML.c:37 against the plain static void Process(...) declared at :40, and VPP Admin Tools does the same (VPPUIManager.c:177, EspToolsMenu.c:131). There are no declaration-site uses anywhere in the extraction or in the community frameworks checked. Functions & Methods states the same rule.

How parallel is it? Bohemia's own wording is that thread "runs the function on a new thread", and the engine's ScriptModule.Call doc block notes that the call "creates new thread, so it's legal to use sleep/wait" while CallFunction "do not create new thread" (1_core/proto/enscript.c:137,144). In practice script code is scheduled cooperatively and yields through Sleep(), which is why threaded routines read like coroutines — but that practical model is community understanding, not documented behaviour. The safe rule either way: never assume two pieces of script run in parallel, and never rely on it for correctness. For most mod work, prefer CallLater; it is simpler and more predictable.

Note on Sleep(): Sleep() is an engine built-in (intrinsic) --- there is no proto declaration for it anywhere in the script files, and it takes an int in milliseconds. No vanilla code calls the intrinsic (the only Sleep in the extraction is an unrelated, locally declared float Sleep(float timeS) test helper in 3_game/systems/tftests/enprofilertests.c:707, called at :237); community frameworks call the intrinsic only from inside functions started with thread (for example _Sleep in Community Online Tools' JMESPModule.c:720). Use it only in a threaded context. What happens if you call it outside one is not documented, and is not established here.

Thread vs CallLater ​

FeaturethreadCallLater
Syntaxthread MyFunc();GetGame().GetCallQueue(CALL_CATEGORY_GAMEPLAY).CallLater(this.MyFunc, delayMs, repeat);
Can sleep/yieldYes (Sleep())No (fires once or repeats at interval)
CancellableYes -- KillThread(owner, "FnName") (proto native int KillThread(Class owner, string name); in 1_core/proto/enscript.c), but it kills by owner+name, not by a handle to the specific callYes (CallQueue.Remove()), by function reference
Use caseSequential async logic with waitsDelayed or repeated callbacks

Full coverage of thread, Sleep(), and KillThread() lives in Functions & Methods.

For most DayZ modding scenarios, CallLater with a timer is the preferred approach. Reserve thread for cases where you genuinely need sequential logic with intermediate waits (e.g., a multi-step animation sequence).


Common Mistakes ​

MistakeProblemFix
Using do...whileDoes not exist in Enforce ScriptUse while with a bool first = true flag
Declaring same variable in if and else blocksMultiple declaration errorDeclare the variable before the if
Redeclaring loop variable i in nested scopeMultiple declaration errorUse different names (i, j, k) or declare outside
Forgetting break in switch casesCases fall through without break (same as C/C++)Always add break unless fall-through is intentional
Modifying array while iterating with foreachUndefined behavior, potential crashUse index-based for loop when removing elements
Infinite while loop without breakServer freeze / client hangAlways ensure the condition will eventually be false, or use break

Quick Reference ​

c
// if / else if / else
if (condition) { } else if (other) { } else { }

// for loop
for (int i = 0; i < count; i++) { }

// while loop
while (condition) { }

// Simulate do...while
bool first = true;
while (first || condition) { first = false; /* body */ }

// foreach (value only)
foreach (Type value : collection) { }

// foreach (index + value)
foreach (int i, Type value : array) { }

// foreach (key + value on map)
foreach (KeyType key, ValueType val : someMap) { }

// switch/case (falls through without break, like C/C++)
switch (value) { case X: /* ... */ break; default: break; }

// thread (async start -- keyword on the CALL, not the declaration)
void MyFunc() { Sleep(1000); }
thread MyFunc();  // non-blocking call

Released under CC BY-SA 4.0 | Code examples under MIT License