-
Notifications
You must be signed in to change notification settings - Fork 5
EnScript (Enforce Script) Style Guide
EnScript (Enforce Script) is the object-oriented scripting language used by the Enfusion engine in DayZ Standalone. This style guide is based on conventions observed in the official Bohemia Interactive DayZ Script Diff and the DayZ Expansion mod codebase.
- Use
.cextension for all EnScript files - File names should match the primary class name when possible
Scripts are typically organized by layer inside a root folder called Scripts/:
-
1_Core/- Core systems -
2_GameLib/- Game library -
3_Game/- Game-specific code -
4_World/- World entities -
5_Mission/- Mission logic
PascalCase for all class names:
class ExpansionGarageSettings
class PlayerBase
class ItemBase
class ExpansionAIPatrolBasePrefixes:
- Module/mod-specific prefixes are common (e.g.,
Expansion,SCR_) - Base classes often end with
Base - Abstract classes may start with
Abstract
Modded Classes:
- ALWAYS prefix member variables in modded classes or classes inheriting from vanilla/other mods:
// ✅ CORRECT - Prefixed members in modded class
modded class ItemBase
{
bool m_Expansion_CustomFlag;
int m_Expansion_CustomValue;
}
// ✅ CORRECT - Prefixed members in inherited class
class ExpansionSomeItem : ItemBase
{
bool m_Expansion_SpecialProperty;
}This prevents name conflicts with vanilla code or other mods.
Member Variables:
- Use
m_prefix for private/protected member variables - Use PascalCase after the prefix
bool m_IsLoaded;
ExpansionMarkerModule m_MarkerModule;
ref MapMenu m_Expansion_MapMenu;Static Variables:
- Use
s_prefix for static member variables - Use PascalCase after the prefix
static ref array<EffectArea> s_Expansion_DangerousAreas;Local Variables:
- Use camelCase for local variables
- No prefix required
int bitmask;
string lootingBehavior;
vector hitPosition;PascalCase for method names:
void SetDefaultLootingBehaviour()
int GetLootingBehaviour()
void OnSend(ParamsWriteContext ctx)
bool OnRecieve(ParamsReadContext ctx)Override Methods:
- Always use the
overridekeyword when overriding parent methods
override void OnSend(ParamsWriteContext ctx)
override bool OnRecieve(ParamsReadContext ctx)Proto Methods vs Custom Methods:
When available, prefer using proto (native engine) methods over custom script implementations when the native method performs expensive logic. Proto methods are faster for complex operations as they run in native code.
// ✅ PREFERRED - Proto method for expensive operations
proto native vector GetPosition();
// ❌ AVOID - Custom reimplementation of expensive native logic
vector GetPosition()
{
// Custom expensive calculation
}if someone is writing a mod that will likely be modded/overridden, suggest using the Obsolete attribute on a method/field/variable instead of removing them altogether. The workbench will show a warning when used.
[Obsolete()]
vector GetCustomPosition()
{
// Custom expensive calculation
}Specific use note 1: If at all possible, avoid using GetObjectsAtPosition and GetObjectsAtPosition3D as much as possible. static arrays on the class you're looking for, triggers, or the GetScene methods are all much better options available.
Specific use note 2: If at all possible, avoid g_Game.SurfaceIsPond(), SurfaceIsSea() or SurfaceRoadY as much as possible. they're remarkably slow for their intended purpose.
recommend using if (g_Game.GetWaterDepth(vectorpos) <= 0) for checking if vector is in water, and something similar to the following for surface y calculating.
vector GetSurfacePosition(float x, float z)
{
return Vector(x, g_Game.SurfaceY(x, z), z);
}Important Note: There is overhead from calling native methods from script. For simple operations, custom script methods may actually be faster. This is why proto native CGame GetGame() was replaced with DayZGame GetGame() { return g_Game; } - the script version is faster for simple global access.
Rule: Use proto methods when they perform complex/expensive operations. For simple operations (like returning a global), script implementations are often faster.
Use camelCase for parameters:
void Log(string msg)
void SetValue(int value, bool isEnabled)
static vector GetPlacementPosition(vector pos)Constants:
Use const for class-specific constants, or enums for grouped constants:
class VehicleManager
{
const int MAX_VEHICLES = 10;
const float SEARCH_RADIUS = 100.0;
}Enums:
Use enums to group related constants - avoid creating many similarly named global constants:
// ✅ PREFERRED - Grouped with enum
enum EVehicleType
{
Car,
Truck,
Helicopter
}
// ✅ ALSO ACCEPTABLE - PASCAL_CASE enums
enum EVehicleType
{
CAR,
TRUCK,
HELICOPTER
}
// ❌ AVOID - Multiple global constants
const int VEHICLE_TYPE_CAR = 0;
const int VEHICLE_TYPE_TRUCK = 1;
const int VEHICLE_TYPE_HELICOPTER = 2;Enum Naming:
-
CamelCase is generally nicer to read:
enum EStatus { Idle, Running, Stopped } -
PASCAL_CASE can be used for clarity in some cases:
enum EStatus { IDLE, RUNNING, STOPPED } - Choose one style and be consistent within your codebase
Preprocessor Defines:
Use UPPERCASE for preprocessor defines:
#define EXPANSION_MARKER_VIS_WORLD
#define EXPANSIONMODVEHICLEConfig Class Name Defines (DayZ 1.26+):
As of DayZ 1.26, config CfgMods class names are automatically defined in script. To add custom defines, use the defines[] array in your config.cpp:
class CfgMods
{
class YourMod
{
defines[] =
{
"YOURMOD",
"YOURMOD_FEATURE_X"
};
};
}These defines can then be used in script:
#ifdef YOURMOD
// Mod-specific code
#endifRule: Declare new global constants (and globals in general) sparingly to avoid polluting the global namespace. Use class-specific constants, enums, and config-based defines instead.
class ClassName : ParentClass
{
// Member variables (private/protected first)
private bool m_PrivateVar;
protected int m_ProtectedVar;
// Public member variables
bool PublicEnabled;
int MaxStorableVehicles;
float VehicleSearchRadius;
// Non-serialized members
[NonSerialized()]
bool m_IsLoaded;
// Constructor
void ClassName()
{
// Initialization
}
// Deconstructor
void~ClassName()
{
// Object deletion
}
// Methods
void MethodName()
{
// Implementation
}
override void ParentMethod()
{
super.ParentMethod();
// Additional implementation
}
}Tabs vs Spaces:
- Use tabs for indentation (observed standard in both repositories)
Braces:
- Opening brace on the same line as declaration
- Closing brace on new line, aligned with declaration
void Method()
{
if (condition)
{
// code
}
else
{
// code
}
}Spacing:
- Space after keywords:
if (,for (,while ( - Space around operators:
x = y + z - No space before semicolons
- Space after commas:
Method(param1, param2)
if (isEnabled && hasPermission)
{
for (int i = 0; i < count; i++)
{
array.Insert(value);
}
}bool isEnabled;
int count;
float radius;
string name;
vector position;IMPORTANT: EnScript has specific terminology that differs from typical OOP:
// `c` is NOT a class - it's an instance
SomeClass c = new SomeClass();
// `typename` represents the actual class
typename t = c.Type(); // Returns the class type
// Type checking methods
c.Type() // Returns typename (the class)
c.Type().ToString() // Returns class name as string
c.ClassName() // Same as Type().ToString() - prefer this
c.GetType() // Returns config.cpp class name for entities if defined (PREFERRED)
// NOTE: Will differ from script class name if no script declaration existsBest Practice: Use GetType() over ClassName() for entities, as it returns the actual config class name which may differ from the script class name.
Shorthand Array Initialization (PREFERRED):
// ✅ PREFERRED - Shorthand initialization
array<string> stringArray = {};
array<int> numbers = {1, 2, 3};
// ✅ ACCEPTABLE - Explicit initialization
array<string> stringArray = new array<string>;Type Definitions:
TStringArray entityWhitelist; // typedef array<string>
autoptr TVectorArray waypoints; // autoptr for managed arraysUse ref or autoptr for managed references on member variables:
ref MapMenu m_Expansion_MapMenu;
autoptr TVectorArray Waypoints;Understanding ref vs autoptr:
-
ref: Instance is deleted when the last reference to it is removed (reference counting) -
autoptr: Instance is deleted when the parent instance is deleted (ownership semantics)
class Example
{
ref MyClass m_RefInstance; // Deleted when all references gone
autoptr MyClass m_AutoInstance; // Deleted when Example is deleted
}Use ClassName.Cast() for safe casting:
PlayerBase player = PlayerBase.Cast(g_Game.GetPlayer());
ItemBase itemBs = ItemBase.Cast(primary);Alternative: Class.CastTo()
// Using Class.CastTo (inline check)
PlayerBase player;
if (Class.CastTo(player, g_Game.GetPlayer()))
{
// player is valid, use it
}
// Using ClassName.Cast (separate check)
PlayerBase player = PlayerBase.Cast(g_Game.GetPlayer());
if (player)
{
// player is valid, use it
}Both methods are valid - CastTo combines the cast and check in one line, while Cast is more explicit.
Use new keyword (with or without trailing parentheses for constructors without arguments):
EntityWhitelist = new TStringArray; // ✅ Valid
m_MarkerModule = new ExpansionMarkerModule(); // ✅ ValidBoth forms are acceptable - choose one style and be consistent.
Inherit from Managed for automatic reference counting:
class MyClass : Managed
{
// No manual deletion needed
}Note: Anything in 3_Game and up is automatically managed - no need to explicitly inherit from Managed.
Understanding EnScript Garbage Collection:
- Everything (except
Managed) is a weak reference by default - The garbage collector is very aggressive - instances in function scope are destroyed immediately when scope ends
- All instances (Managed or not) are garbage collected when reference count reaches zero
STRONGLY DISCOURAGED: Avoid using the delete keyword in almost all cases.
// ❌ AVOID - Using delete keyword
delete myObject;
// ✅ PREFERRED - Null the reference instead
myObject = null;Why avoid delete?
- Can cause segfaults if object is still referenced elsewhere
- NEVER use for entities - will cause crashes
- Should only be used in very specific, rare cases
- Nulling references is safer and lets garbage collection handle cleanup
Rule: Let the garbage collector handle object cleanup by nulling references.
ref keyword causes crashes and unexpected behavior!
The ref keyword creates strong references and must be used carefully to avoid memory management issues, reference counting problems, and crashes that affect your mod and others.
Member Variables:
class MyClass
{
ref MyOtherClass m_Member; // ✅ Safe - member variable
autoptr array<string> m_Items; // ✅ Safe - member variable with autoptr
void MyClass()
{
m_Member = new MyOtherClass();
}
}Note: Do NOT use ref in typedefs:
// ❌ WRONG
typedef ref MyClass MyClassRef;
// ✅ CORRECT
typedef MyClass MyClassRef;DO NOT use ref in:
- Method Parameters:
// ❌ WRONG - Causes memory issues
ref MyClass MyMethod(ref MyClass1 a, ref MyClass2 b)
{
// ...
}
// ✅ CORRECT - No ref on parameters
MyClass MyMethod(MyClass1 a, MyClass2 b)
{
// ...
}- Return Types:
// ❌ WRONG - Causes reference counting issues
ref MyClass GetObject()
{
ref MyClass obj = new MyClass();
return obj;
}
// ✅ CORRECT - Return without ref
MyClass GetObject()
{
MyClass obj = new MyClass();
return obj;
}- Local Variables:
void MyMethod()
{
// ❌ WRONG - Unnecessary ref on local variable
ref MyClass3 myClass3 = new ref MyClass3();
// ✅ CORRECT - No ref on local variables
MyClass3 myClass3 = new MyClass3();
}newKeyword:
// ❌ WRONG - Never use ref with new
ref MyClass obj = new ref MyClass();
// ✅ CORRECT
MyClass obj = new MyClass();
// ✅ ALSO CORRECT - For member variables
m_Member = new MyClass();1. Minimize Strong References:
Hold only ONE explicit strong reference (using ref keyword) when possible:
class VehicleManager
{
// ✅ GOOD - Single strong reference on member
ref array<EntityAI> m_Vehicles;
void AddVehicle(EntityAI vehicle)
{
// ✅ CORRECT - No ref on parameter or local var
if (!m_Vehicles)
m_Vehicles = new array<EntityAI>;
m_Vehicles.Insert(vehicle);
}
}2. Avoid Creating Multiple Strong References:
// ❌ BAD - Multiple strong refs to same object
class BadExample
{
ref MyClass m_Object1;
ref MyClass m_Object2;
void BadExample()
{
MyClass obj = new MyClass();
m_Object1 = obj;
m_Object2 = obj; // ❌ Two strong refs to same object!
}
}
// ✅ GOOD - Single strong reference
class GoodExample
{
ref MyClass m_Object;
MyClass m_WeakRef; // Weak reference (no ref keyword)
void GoodExample()
{
m_Object = new MyClass();
m_WeakRef = m_Object; // ✅ One strong, one weak
}
}3. Let EnScript Handle References:
EnScript's reference counting works best when you don't over-specify references:
// ✅ PREFERRED - Let the engine manage it
array<EntityAI> GetVehicles()
{
array<EntityAI> result = {};
// populate result
return result;
}
// ❌ AVOID - Over-specified refs
ref array<EntityAI> GetVehicles()
{
ref array<EntityAI> result = new ref array<EntityAI>;
return result;
}- Crashes - Reference counting errors lead to premature deletion or memory leaks
- Null pointer exceptions - Objects deleted unexpectedly
- Cross-mod interference - Your ref issues affect other mods
- Unpredictable behavior - Objects existing longer than intended
- Memory leaks - Objects never getting cleaned up
- ✅ USE
ref(orautoptr) for: Member variables ONLY - ❌ NEVER use
reffor: Method parameters, return types, local variables, typedefs - ❌ NEVER use:
new ref ClassName() - ✅ ALWAYS use:
new ClassName() - 🎯 Goal: One strong reference per object when possible
- ❌ NEVER use
delete- let garbage collection handle cleanup ⚠️ Don't mix: Use eitherrefORautoptr, not both in the same codebase
When in doubt, omit the ref keyword and let EnScript's automatic reference counting handle it.
if (condition)
{
// Single condition
}
if (condition1 && condition2)
{
// Multiple conditions
}
else if (condition3)
{
// Else-if branch
}
else
{
// Else branch
}EnScript follows C/C++ operator precedence where comparison comes before bitwise operations. Always use parentheses:
int a = 1;
int b = 2;
// ❌ WRONG - Parsed as: a & (b == b)
if (a & b == b)
// ✅ CORRECT - Explicit parentheses
if ((a & b) == b)For Loop:
for (int i = 0; i < count; i++)
{
// Loop body
}
foreach (string item : collection)
{
// For-each loop
}foreach (string lootingBehavior : lootingBehaviors)
{
lootingBehavior.TrimInPlace();
int value = typename.StringToEnum(eAILootingBehavior, lootingBehavior);
// Process value
}foreach should not be used directly on iterables returned by getter methods. You should assign to a variable first:
class MyClass
{
autoptr TStringArray m_Test = {"A", "B"};
TStringArray GetTest()
{
return m_Test;
}
void TestBad()
{
// ❌ getter is called every foreach
foreach (string t : GetTest())
{
Print(t);
}
}
void TestGood()
{
// ✅ CORRECT - Assign to variable first
TStringArray test = GetTest();
foreach (string t : test)
{
Print(t);
}
}
void TestAlsoGood()
{
// ✅ CORRECT - Direct member access works fine
foreach (string t : m_Test)
{
Print(t);
}
}
}While Loop:
while (condition)
{
// Loop body
}EnScript requires explicit return after switch even with default case:
// ❌ PROBLEMATIC - Compiler complains about missing return
string Test(string what)
{
switch (what)
{
case "A":
case "B":
return "X";
default:
return "Y";
}
// Compiler error: function must return a value
}
// ✅ WORKAROUND - Move last return outside switch
string Test(string what)
{
switch (what)
{
case "A":
case "B":
return "X";
}
return "Y"; // Default case moved outside
}Always check array bounds before accessing elements:
array<int> list = {0, 1, 2};
// ✅ CORRECT - Index-based bounds check
if (index >= 0 && index < list.Count())
{
int value = list[index];
}
// ⚠️ NOTE: This is valid but has different semantics
// Returns true if entry is null OR out of bounds
if (!list[1]) // Evaluates to true for both null and out-of-boundsBest Practice: Use explicit index bounds checking: if (index >= 0 && index < array.Count())
#ifdef EXPANSIONMODGROUPS
// Group-specific code
#endif
#ifdef SERVER
// Server-only code
#endif
#ifndef DIAG_DEVELOPER
// Non-developer build code
#endifEmpty #ifdef/#ifndef blocks cause segfaults, even if they only contain comments:
// ❌ WRONG - Causes segfault
#ifdef FOO
// This will crash
#endif
// ✅ CORRECT - Either remove or add actual code
#ifdef FOO
int dummy; // At least one statement
#endif
// ✅ BETTER - Remove if not needed
// #ifdef FOO
// #endifCommon Directives:
-
#define- Define preprocessor macros (not constants) -
#ifdef/#ifndef- Conditional compilation -
#endif- End conditional block
Naming:
- Use UPPERCASE for defines
- Module-specific prefixes recommended
#define EXPANSION_VEHICLE_TOWING
#define EXPANSIONMODMARKETNote: #define creates preprocessor macros, not constants. For actual constants, use const or enums.
Use // for single-line comments:
// This is a single-line comment
int value = 5; // Inline commentUse /* */ for multi-line comments:
/*
* This is a multi-line comment
* explaining complex logic
*/Use //! for documentation comments:
//! Enable/Disable garage market mod features
bool EnableMarketFeatures;
//! @note can't check inheritance against dangerous area types on client
void CheckTypes()For class and method documentation:
/**
* @class ExpansionGarageSettings
* @brief Settings for the garage system
*/
class ExpansionGarageSettings- Be descriptive but concise
- Explain why, not just what
- Update comments when code changes
- Use comments to mark sections of code
//! Map Menu
if (input.LocalPress("UAExpansionMapToggle", false) && !viewMenu)
{
ToggleMapMenu(player);
}
//! GPS
if (input.LocalPress("UAExpansionGPSToggle", false))
{
// Implementation
}Use modded keyword to extend vanilla classes:
modded class MissionGameplay
{
// Additional members and methods
override void Expansion_OnUpdate(float timeslice, PlayerBase player)
{
super.Expansion_OnUpdate(timeslice, player);
// Additional implementation
}
}When using the modded keyword, NEVER add inheritance (: ParentClass). The modded class automatically inherits from the original class definition.
// ❌ WRONG - Adding inheritance to modded class
modded class PlayerBase : ManBase
{
// Inheritance declaration is ignored!
}
// ✅ CORRECT - No inheritance declaration
modded class PlayerBase
{
// This properly extends the original PlayerBase
}Why This Matters:
- Adding inheritance to a
moddedclass is ignored by the compiler - You cannot change the class hierarchy by modding an existing class
- It's misleading to readers and should be omitted
- The modded class already IS the class you're modding - you're just adding to it
Rule: modded class ClassName never has : ParentClass - you cannot change its inheritance.
Modded classes in the same file can access private members:
// Original class
class VanillaClass
{
private bool imPrivate = 0;
private void DoSomething()
{
Print("Vanilla method");
}
}
// Access in modded class
modded class VanillaClass
{
void AccessPvt()
{
Print(imPrivate);
DoSomething();
}
}Use super to call parent implementations:
override void OnSend(ParamsWriteContext ctx)
{
super.OnSend(ctx);
// Additional serialization
}Note: It should be remembered in most situations, you should be calling super on methods and doing your custom logic in the order you wish for the logic to apply. (pre or post super call)
// original method
void foo() {
for (int i = 0; i < 9999999; i++) { someExpensiveCall(); }
m_someSideEffect = true;
}
// modded method, adding logic after all the original method's individual logics with super calls are ran.
override void foo() {
super.foo();
for (int i = 0; i < 9999999; i++) { myModLogic(); }
}Do not pollute the global namespace with new global variables or functions when not needed.
// ❌ BAD - Global function and separate class
class MyClass { }
MyClass GetMyClassInstance() { return new MyClass(); }
// ✅ GOOD - Singleton pattern on the class itself
class MyClass
{
private static ref MyClass s_Instance;
static MyClass GetInstance()
{
if (!s_Instance)
s_Instance = new MyClass();
return s_Instance;
}
private void MyClass() { } // Private constructor
}
// ✅ ALSO GOOD - Public static instance
class MyClass
{
static ref MyClass Instance;
private void MyClass()
{
Instance = this;
}
}Prefer accessing global variables directly instead of using global getter functions:
// ✅ PREFERRED - Direct global access
g_Game.GetPlayer();
// ❌ LESS PREFERRED - Getter function
GetGame().GetPlayer();Note: While using getters is common in vanilla code, direct access is often clearer and more efficient. This is why proto native CGame GetGame() was replaced with DayZGame GetGame() { return g_Game; } - the script version is faster for simple global access due to the overhead of calling native methods.
Prefer direct access to public properties over getter methods that just return the value:
class Example
{
int PublicValue;
int GetPublicValue() // Unnecessary getter
{
return PublicValue;
}
}
// ✅ PREFERRED - Direct access
int value = instance.PublicValue;
// ❌ LESS PREFERRED - Unnecessary getter
int value = instance.GetPublicValue();Use getters only when:
- The property needs to be private/protected
- Additional logic is required (validation, lazy initialization, etc.)
- The value is computed rather than stored
override void OnSend(ParamsWriteContext ctx)
{
ctx.Write(Enabled);
ctx.Write(GarageMode);
ctx.Write(MaxStorableVehicles);
#ifdef EXPANSIONMODGROUPS
ctx.Write(EnableGroupFeatures);
#endif
}
override bool OnRecieve(ParamsReadContext ctx)
{
ctx.Read(Enabled);
ctx.Read(GarageMode);
ctx.Read(MaxStorableVehicles);
#ifdef EXPANSIONMODGROUPS
ctx.Read(EnableGroupFeatures);
#endif
return true;
}if (input.LocalPress("UAExpansionMapToggle", false) && !viewMenu)
{
ToggleMapMenu(player);
}CF_Modules<ExpansionMarkerModule>.Get(m_MarkerModule);ExpansionSettings.SI_Garage.Invoke();// Using EXTrace (preferred - minimal overhead when disabled)
auto trace = EXTrace.Start(EXTrace.GENERAL, this, "MethodName");
// Alternative (older pattern with overhead)
#ifdef EXPANSIONTRACE
auto trace = CF_Trace_0(ExpansionTracing.SETTINGS, this, "MethodName");
#endifNote: EXTrace.Start is the better implementation with much less overhead when tracing is disabled (essentially a no-op). It doesn't need to be #ifdef'd unless in performance-critical code sections.
EXError.Error(this, "Invalid looting behaviour " + lootingBehavior, {});
EXTrace.Print(EXTrace.AI, this, "Debug message");Exception logs are named crash_<date>_<time>.log, but they contain exceptions, not actual crashes (segfaults). Actual segfaults create different crash dumps.
//#define EXPANSION_CARKEY_LOGGING
//#define EXPANSION_VEHICLE_SKIN_LOGGINGComment out defines that should not be in production code.
- Existence/Null checks should be done where you know how to handle such cases;
propagate the non-nullness guarantee to other methods with
notnull
void Foo()
{
EntityAI entity = FindSomeEntitySomehow();
Print(IsWearingSunglasses(entity));
}
bool IsWearingSunglasses(EntityAI entity)
{
if (entity == null)
{
// ❌ WRONG: we don't know how to handle null variables
return false;
}
return true;
}void Foo()
{
EntityAI entity = FindSomeEntitySomehow();
if (entity)
{
// ✅ CORRECT: we do know what to do with a null value
Print("entity not found");
return;
}
Print(IsWearingSunglasses(entity));
}- Use null checks when a null value is semantically valid
bool IsWearingSunglasses(notnull EntityAI entity)
{
GameInventory inventory = entity.GetInventory();
if (!inventory)
{
// ✅ CORRECT: `GetInventory` returning null means that the entity
// has no inventory at all, hence it isn't wearing sunglasses
return false;
}
return true;
}- Avoid unnecessary checks
bool IsPlayerWearingSunglasses(notnull PlayerBase player)
{
GameInventory inventory = player.GetInventory();
if (!inventory)
{
// ❌ AVOID: an instance of a player will always have an inventory
// If for some reasons the player inventory is null, there probably are some other serious issues
return false;
}
return true;
}Important: if (something) does not just check for null - it checks whether the value evaluates to true. For objects, this means checking existence.
int value = typename.StringToEnum(eAILootingBehavior, lootingBehavior);
if (value == -1)
{
EXError.Error(this, "Invalid looting behaviour " + lootingBehavior, {});
}
else
{
bitmask |= value;
}The IsClient() and IsServer() methods behave unexpectedly during game load:
// ❌ WRONG - Returns false on client during load
if (GetGame().IsClient())
// ✅ CORRECT - Reliable client check
if (!g_Game.IsDedicatedServer())
// ❌ WRONG - Returns true on client during load
if (GetGame().IsServer())
// ✅ CORRECT - Reliable server check (dedicated only)
if (g_Game.IsDedicatedServer())
// ✅ CORRECT - Server check including offline/singleplayer
if (g_Game.IsServer() && !g_Game.IsMultiplayer())Rule: Use IsDedicatedServer() for reliable client/server detection, especially in initialization code.
// Good
float VehicleSearchRadius;
bool CanStoreWithCargo;
// Avoid
float vsr;
bool cswc;Each method should do one thing well:
void SetDefaultLootingBehaviour()
{
if (Behaviour == "ROAMING")
LootingBehaviour = "ALL";
else
LootingBehaviour = "DEFAULT";
}// Good
const int DEFAULT_MAX_VEHICLES = 10;
MaxStorableVehicles = DEFAULT_MAX_VEHICLES;
// Avoid
MaxStorableVehicles = 10;Initialize in constructor or at declaration:
void ExpansionGarageSettings()
{
EntityWhitelist = new TStringArray;
m_IsLoaded = false;
}//! Toggle between hidden and previously set visibility state for each marker category
int visibility;
int previousVisibility;
visibility |= m_MarkerModule.GetVisibility(ExpansionMapMarkerType.SERVER);
previousVisibility |= m_MarkerModule.GetPreviousVisibility(ExpansionMapMarkerType.SERVER);layerMask |= PhxInteractionLayers.BUILDING;
layerMask |= PhxInteractionLayers.VEHICLE;
layerMask |= PhxInteractionLayers.TERRAIN;
visibility &= EXPANSION_MARKER_VIS_WORLD;// Use enums
eAILootingBehavior behavior;
// Use proper casting
PlayerBase player = PlayerBase.Cast(entity);
if (player)
{
// Safe to use player
}Some language constructs that work in other languages cause segfaults in EnScript:
class MyClass
{
bool m_IsInside[3];
// ❌ WRONG - Causes segfault
void TestBad(int index, vector a, vector b, float distSq)
{
m_IsInside[index] = vector.DistanceSq(a, b) <= distSq;
}
// ✅ CORRECT - Use intermediate variable
void TestGood(int index, vector a, vector b, float distSq)
{
bool isInside = vector.DistanceSq(a, b) <= distSq;
m_IsInside[index] = isInside;
}
}Rule: For array assignments with complex expressions, use an intermediate variable.
// int.MIN = -2147483648
1 < int.MIN; // ⚠️ Returns TRUE in EnScript!
1 < -2147483647; // ⚠️ Also returns TRUE!
// Always be careful with MIN/MAX integer comparisonsCompile errors involving:
- Undefined classes (missing addon not loaded)
- Variable name conflicts
Will NOT show the correct filename and line number. Instead, they show the last successfully parsed .c file at EOF, which is misleading and unhelpful.
What to do:
- If error location seems wrong, search for the class/variable name in your entire codebase
- Check that all required addons are loaded
- Look for name conflicts across files
-
Exception logs:
crash_<date>_<time>.log- Contains exceptions (NOT crashes) - Actual crashes: Segfaults create different crash dumps
- Check both log types when debugging issues
- Complex array assignments - Use intermediate variables
- Empty preprocessor blocks - Must contain at least one statement
-
Misused
deletekeyword - Let garbage collection handle it -
Over-specified
refusage - Only use on members/typedefs
- Aggressive garbage collection destroys function-scope instances immediately
- Use
ref/autoptron class members to prevent premature collection - Avoid creating temporary objects in tight loops
This style guide reflects the conventions used in professional DayZ modding. Consistency with these patterns will make your code more maintainable and easier to integrate with existing mods and the vanilla codebase.
- Use PascalCase for classes and methods
- Use
m_prefix for member variables,s_for static - Use tabs for indentation
- Always use
overridekeyword when overriding - Check for null before using objects
- Document complex logic with comments
- Use preprocessor directives for conditional compilation
- Follow the modded class pattern for extending vanilla code
- where possible, use proto methods over custom methods for expensive logic.
- cfgmod classnames are defines in script as of 1.26
-
NEVER use
refin method parameters, return types, or local variables - only on member variables and templates -
NEVER add inheritance (
: ParentClass) tomoddedclasses - they already inherit from the original class