Skip to content

[Scripting] DayZ EnforceScript Pitfalls

lava76 edited this page Aug 11, 2022 · 1 revision

This page was born out of my frustrations with DayZ EnforceScript and has nothing to do with the Expansion Mod.

It is a work in progress and by no means complete.

Naming conventions

Class c is not a class, but an instance

typename t is a class

Naming consistency (or lack thereof)

<instance>.Type() returns a typename (class)

<some_object>.GetType() returns the config.cpp entry class name for the object as string (not necessarily the same as <instance>.ClassName(), and GetType() should be preferred over it in most cases)

<instance>.Type().ToString() is the same as <instance>.ClassName(), so use the latter

ref, Managed and delete

Everything (except Managed) is a weak reference in Enforce by default. The garbage collector is very aggressive (e.g. instances in function scope are destroyed immediately when scope ends).

Documentation on Managed vs 'unmanaged' class and delete keyword is misleading or plain wrong in parts (https://community.bistudio.com/wiki/DayZ:Enforce_Script_Syntax, the section on automatic reference counting, the part on ref keyword is correct though)

TLDR:

ref (or autoptr - don't use both!) is needed for any complex non-Managed class member that should not be garbage collected until the containing instance is destroyed (and ONLY there, do not use ref anywhere else! E.g. not for functions or function parameters, or inside functions). (See this post by MarioE on the Enfusion Modders Discord for more details)

There is no need to use the delete keyword, ever. All instances, Managed or not, will be garbage collected when the reference count reaches zero (you can add prints to destructors to see this in action). Using delete on an object that is still used can segfault.

Correct use of ref:

class MyClass
{
    ref map<string, ref TStringArray> m_Example = new map<string, ref TStringArray>;

    map<string, ref TStringArray> Get()
    {
        return m_Example;
    }
}

Wrong use of ref:

class MyClass
{
    ref TIntArray Example(ref TIntArray oh_god)  //! Bad refs in method declaration
    {
        ref TIntArray myArray = new ref TIntArray();  //! Bad ref in variable declaration and instantiation
        return myArray;
    }
}

Annoyances

Logs

Exception logs are named crash_<date>_<time>.log, despite not having anything to do with actual crashes (segfaults, see further below).

Useless compile error messages

Compile error messages involving either a class that was never defined (e.g. because a required addon is not loaded) or a variable name that conflicts, never include the correct filename and line number, the filename and line number shown in the error is of the last successfully parsed .c file at EOF, which is misleading and not helpful at all.

Functions working differently during game load

GetGame().IsClient() returns false on client during load, use !GetGame().IsDedicatedServer() instead.

GetGame().IsServer() returns true on client during load, use GetGame().IsDedicatedServer() instead (unless you want to support offline/singleplayer mode as well).

Quirks

foreach cannot be used directly on iterables returned by getters, you have to assign to a variable and iterate over that.

class MyClass
{
    autoptr TStringArray m_Test = {"A", "B"};

    TSTringArray GetTest()
    {
        return m_Test;
    }

    void Test()
    {
        foreach (string t: GetTest())
        {
            // will throw NULL pointer exception on 2nd item
        }
    }

    void Test2()
    {
        foreach (string t: m_Test)
        {
            // fine
        }
    }

    void Test3()
    {
        TStringArray test = GetTest();
        foreach (string t: test)
        {
            // fine
        }
    }
}

Bugs

No words.

// int.MIN = -2147483648

1 < int.MIN; //! Yes, this is true in EnForce
1 < -2147483647;  //! This as well

Compile errors that shouldn't exist

//! array type not important, in this case array<int>
if (!list[1])  //! doesn't compile
if (list[1] == 0)  //! ok
//! even though this function is guaranteed to always return a value due to default case,
//! Enforce complains that it needs to return a value. Workaround is to not have default case and move
//! last return statement out of switch
string Test(string what)
{
	switch (what)
	{
		case "A":
		case "B":
			return "X";
		default:
			return "Y";
	}
}

Segfaults

Segfault on numerous language constructs that would be perfectly fine in any other programming language.

Example:

class MyClass
{
    bool m_IsInside[3];

    void Test(int index, vector a, vector b, float distSq)
    {
        m_IsInside[index] = vector.DistanceSq(a, b) <= distSq;  // segfault
    }

    void Test(int index, vector a, vector b, float distSq)
    {
        bool isInside = vector.DistanceSq(a, b) <= distSq;
        m_IsInside[index] = isInside;  // fine
    }
}

Empty #ifdef/#ifndef blocks. This is also true if the only contents are comments.

#ifdef FOO
// segfault
#endif

Things to be aware of if you're coming from other languages that are not C-like

Bracketing is needed for conditions involving binary operations, as it follows C/C++ operator precedence (comparison before bitwise). This can get you if you're used to Python (like me) where bitwise come before comparison operators.

int a = 1;
int b = 2;
a & b == b;  // EnforceScript will see this as a & (b == b)
(a & b) == b;
Clone this wiki locally