Mod – Arma Tardis

  • Find the mod on Arma Reforger Workshop here
  • Visit the Github repo here

Tardis is a simple mod for Arma Reforger which introduce an unique asset (credits here) with unique functionalities. The mod focuses on the ScriptedUserAction shipping with the main game. It affords the player three new actions, themed after Doctor Who, the popular British TV show:

  • Regeneration: the player dies and respawn (SCR_Tardis.c)
  • Teleportation: player and tardis asset are moved to a different location (SCR_Teleport.c)
  • Time Travel: the clock moves to a different time (SCR_TimeTravel.c)

To function it also relies on Arkensor’s helpers scripts (EPF_Component.c and EPF_Const.c) from his Enfusion Persistence Framework. The utility scripts allow to retrieve the entities replication components and check if the float passed are NaN. While replication is not necessary for a SP scenario such as this one, with Reforger built inherently as a multiplayer game, resolving rather then circumventing the issue proved as the best approach.

Scripts in details

//------------------------------------------------------------------------------------------------
class SCR_Tardis : ScriptedUserAction
{   
    //References
    protected PlayerManager m_PlayerManager;

    //------------------------------------------------------------------------------------------------
    override void PerformAction(IEntity pOwnerEntity, IEntity pUserEntity) 
    {
        IEntity playerEntity = pUserEntity;
        DamageManagerComponent damageManager = DamageManagerComponent.Cast(playerEntity.FindComponent(DamageManagerComponent));

        if (!damageManager || damageManager.GetState() == EDamageState.DESTROYED)
            return;
        
        damageManager.SetHealthScaled(0);                          
    }
    
    //------------------------------------------------------------------------------------------------
    override bool GetActionNameScript(out string outName)
    {
        outName = "Regenerate";
        return true;
    }    
};

The class SCR_Tardis inherits from ScriptedUserAction, so it can override specific methods to provide custom scripted user actions. In this case it’s regeneration.

We override the ScriptedUserAction’s method PerformAction and we pass two parameters, pOwnerEntity, the entity which owns the action, and pUserEntity, the one using the action. This method will define what happens when the action is performed by the user. The method returns void since it performs key actions, but the results aren’t used anywhere else

We assign pUserEntity to a local variable of type IEntity (Interface entity) which we refer as playerEntity.

Find the componet “DamageManagerComponent” on playerEntity, if the entity has the correct component safely cast them as the variable damageManager (of type DamageManagerComponent).

If damageManager results null or destroyed, the function will return early.

Otherwise use the method SetHealthScaled of the damage manager component with argument 0.

To display the name of the action, for our specific implementation, we override the method GetActionNameScript. This method needs to pass two pieces of information: bool first (Y/N) to indicate success or failure, and finally the actual out parameter outName of type string “Regenerate”. If the string is passed successfully the bool returns true.

class SCR_Teleport : ScriptedUserAction
{
	 // Indices to keep track of the current position
    private int m_CurrentIndex = 0;

    // Positions array for the player and the TARDIS	
ref	array<vector> m_PlayerPositions = {"5158.263 13.831 3959.744", "4834.332 170.396 7005.776", "4815.983 51.697 6194.626"};
ref	array<vector> m_TardisPositions = { "5178.675 14.571 3976.484", "4833.263 170.319 7015.651", "4820.329 51.507 6162.347" };

//ref array<float> m_PlayerYaws = {0, 90, 180};
//ref array<float> m_TardisYaws = {0, 270, 0}; 	
	
//------------------------------------------------------------------------------------------------
override void PerformAction(IEntity pOwnerEntity, IEntity pUserEntity)
    {
        if (m_CurrentIndex < m_PlayerPositions.Count() && m_CurrentIndex < m_TardisPositions.Count())
        {
            Teleport(pUserEntity, m_PlayerPositions[m_CurrentIndex]);
            Teleport(pOwnerEntity, m_TardisPositions[m_CurrentIndex]);
			
			//Teleport(pUserEntity, m_PlayerPositions[m_CurrentIndex], m_PlayerYaws[m_CurrentIndex]);
			//Teleport(pOwnerEntity, m_TardisPositions[m_CurrentIndex], m_TardisYaws[m_CurrentIndex]);

            Print("Player coordinates: " + pUserEntity.GetOrigin());
            Print("TARDIS coordinates: " + pOwnerEntity.GetOrigin());

            m_CurrentIndex++; // Move to the next set of coordinates
        }
        else
        {
            Print("No more coordinates to teleport to!");
        }
    }

//------------------------------------------------------------------------------------------------
override bool GetActionNameScript(out string outName)
	{
	    outName = "Teleport";
	    return true;
	}

static void Teleport(notnull IEntity entity, vector position, float yaw = "-1337.42042".ToFloat())
	{
	    vector transform[4];
	
	    if (!EPF_Const.IsUnset(yaw))
	    {
	        Math3D.AnglesToMatrix(Vector(yaw, 0, 0), transform);
	    }
	    else
	    {
	        entity.GetWorldTransform(transform);
	    }
	
	    transform[3] = position;
	    SCR_TerrainHelper.OrientToTerrain(transform);
	
	    ForceTransformEx(entity, transform);
	}

static void ForceTransformEx(notnull IEntity entity, vector transform[4])
	{
	    vector previousOrigin = entity.GetOrigin();
	
	    BaseGameEntity baseGameEntity = BaseGameEntity.Cast(entity);
	    if (baseGameEntity && !BaseVehicle.Cast(baseGameEntity))
	    {
	        baseGameEntity.Teleport(transform);
	    }
	    else
	    {
	        entity.SetWorldTransform(transform);
	    }
	
	    Physics physics = entity.GetPhysics();
	    if (physics)
	    {
	        physics.SetVelocity(vector.Zero);
	        physics.SetAngularVelocity(vector.Zero);
	    }
	
	    RplComponent replication = EPF_Component<RplComponent>.Find(entity);
	    if (replication)
	        replication.ForceNodeMovement(previousOrigin);
	
	    if (!ChimeraCharacter.Cast(entity))
	        entity.Update();
	}
};

The class SCR_Teleport also inherits from ScriptedUserAction, this time to provide a teleport action.

We declare and initialize to 0 a private integer variable m_CurrentIndex. This will work as a counter to progress through our arrays.

We need arrays to store the positions of teleportation, for both the player and the Tardis. The arrays are made of simple vectors as they store XYZ coordinates for us. Trying to find the Tardis in the surroundings every time the player teleports, is somewhat of an amusing gameplay challenge.

Just like before we override ScriptedUserAction with owner and user entities as parameters, so we can create another custom action.

There are a handful of teleport positions for player and Tardis, 3 each specifically, and since they teleport together, we will only need to teleport a total of 3 times. Every time we go through this method the index of the counter m_CurrentIndex will go up, but the if statement will allow the execution only as long as the counter index is lower than the number of either positions counted so far, i.e. until the 3rd time. If we were to add more teleport positions in the arrays we wouldn’t have to change anything in this method.

If the counter condition is satisfied the Teleport method is called, while passing the user entity (the player) and the position it teleports to, which correspond the position in the array numbered as the current step in the counter index. For the Tardis we instead pass the owner entity, since the Action component is owned by the Tardis (yet used by the player), and we also pass the corresponding position just as with the player.

We have a couple of prints for debug.

Same as the other scripts we override a method to return the name of our new action.

We get to the Teleport method itself. The parameters for this function are: an entity (ensuring that is notnull), a vector for the position and a default yaw value to make sure the float used is a valid number . Since we have to transform the entity, we create a transformation matrix which will contain all the various information and changes necessary. We interact with different portions of the matrix to validate different transformations.

If the yaw angle is set to a valid value we can convert it to the matrix, else we retrieve the current transformation matrix of the entity. We use the translation vector portion of the matrix to determine the new position. We use the method OrientToTerrain of the Reforger base script SCR_TerrainHelper with our matrix as argument to make sure that the transformation is oriented to the terrain normal. With ForceTransformEx we finally apply the transformation to the entity.

ForceTransformeEx’s parameters are known: an entity (notnull) and the transformation vector matrix. We store the entity’s original position. We try to cast such entity as baseGameEntity: if it fits the role (and if it’s not a vehicle) we use teleport on it, else we set its world transform directly.

To avoid complications we get the physics component of the entity and if there is any, we reset the velocity and the angular velocity. We use the helper script to find the replication component on the entity. If this is found, force the replication of the entity. If the entity is not a characters just manually update its state/position.

Enfusion, as other networked software solutions, uses nodes to encapsulate data and effectively replicate it across the network.

class SCR_TimeTravel : ScriptedUserAction
{
    // References
    protected TimeAndWeatherManagerEntity m_TimeManager;

    //------------------------------------------------------------------------------------------------
    override void PerformAction(IEntity pOwnerEntity, IEntity pUserEntity)
    {
        // Obtain the ChimeraWorld instance
        ChimeraWorld world = ChimeraWorld.CastFrom(GetGame().GetWorld());
        
        // Ensure world casting was successful
        if (!world)
        {
            Print("Failed to cast world to ChimeraWorld");
            return;
        }

        // Get the TimeAndWeatherManagerEntity
        m_TimeManager = world.GetTimeAndWeatherManager();
        
        // Check if m_TimeManager was successfully obtained
        if (!m_TimeManager)
        {
            Print("Failed to get TimeAndWeatherManagerEntity");
            return;
        }

        // Retrieve and manipulate the time of day
        float currentTime = m_TimeManager.GetTimeOfTheDay();
        
        // Validate currentTime before use
        if (EPF_Const.IsUnset(currentTime))
        {
            Print("Invalid current time of day");
            return;
        }
        
        float timeToAdd = 6.0;
        float newTime = currentTime + timeToAdd;

        // Ensure newTime is within valid bounds (0.0 to 24.0 for time of day)
        newTime = Clamp(newTime, 0.0, 24.0);

        // Validate newTime before setting it
        if (!EPF_Const.IsUnset(newTime))
        {
            m_TimeManager.SetTimeOfTheDay(newTime);
        }
        else
        {
            Print("New time of day is invalid");
        }
    }

    //------------------------------------------------------------------------------------------------
    override bool GetActionNameScript(out string outName)
    {
        outName = "Time Travel";
        return true;
    }
    
    //------------------------------------------------------------------------------------------------
    // Utility function to clamp values within a range
    float Clamp(float value, float min, float max)
    {
        if (value < min)
            return min;
        if (value > max)
            return max;
        return value;
    }
};

Finally we get to the ‘time travel’ action. We declare TimeAndWeatherManagerEntity as the variable m_TimeManager.

Once again we override ScriptedUserAction with owner and user entities as parameters.

We cast the current world of the current game as ChimeraWorld, and we ensure that the casting was successful.

We retrieve the time and weather manager from the world and assign it to the our declared variable. We briefly make sure this was successful. We get the current time from the manager and validate it. We add time as float to the current time (newTime), we ensure the time is within the valid bounds of a 24 hours day with the clamp function. We use the helper script to make sure the new time is a valid number, if it is we finally set the time to the new time.

Some as the other two we override a method to return the name of the action.

Chimera is the codename for Arma Reforger, most likely hinting at its moddabililty. Perhaps also hinting at 2016’s A3 performance profiling servers