This page will show you how to set up a BF3: Reality Mod development environment in simple to follow steps and escorts you while you create your first Venice Unleashed mod to get a feeling for modding with the framework and some RM coding conventions.
Some links that may be interesting for you:
For more in-depth guides on Venice Ext we recommend also taking a look at the official guides
To prevent unecessary questions we really encourage you to take a look at the official guides
If you haven´t taken a look at the official guides by now. Do it.
%HOMEPATH%\Documents\Battlefield 3\Server (create it if it doesn´t exist)
%HOMEPATH%\Documents\Battlefield 3\ServerLua Developers:
UI Developers:
Both:
Always open Reality Mod via the workspace file, else Vua won´t get loaded.
To get you started with coding using Vua and Venice Unleashed´s API VeniceExt (Vext) we will provide a small introduction guide here which you can follow to get yourself familiar with Vua and Vext.
Download and Build Content and wait for it to download all VeniceExt types & support libraries.

Create New Project.

GettingStartedMod here.

No for now.

To use Vua after closing VSCode you will have to open the project via the created .code-workspace file - else it will not initialize!
__init__.lua is always the first lua file to be loaded on initialization of the mod.
In high to low order:
GettingStartedModContains the mod.json
GettingStartedMod/extRoot folder of the modding files. Contains the Shared, Server and Client folders.
GettingStartedMod/ext/SharedCompatible Code in this folder will be executed on Server & Client at the same time and also before any other code.
GettingStartedMod/ext/ServerCompatible Code in this folder will only be executed on the Server.
GettingStartedMod/ext/ClientCompatible Code in this folder will only be executed on the Client.
For the purpose of understanding the basics we will write a small and easy teleporter mod. This will include:
Utils folder in ext/Shared(so it will be available everywhere)UtilsEnums.lua & Settings.lua in ext/SharedShared/__init__.lua like such, we will also use this to take a look at the general class structure in Reality Mod:--- A class description
---@class SomeSharedClass
---@overload fun():SomeSharedClass
SomeSharedClass = class "SomeSharedClass"
require "__shared/Utils/Enums"
require "__shared/Utils/Settings"
require "__shared/Utils/Logger"
require "__shared/Utils/DC"
require "__shared/Utils/RealityModTimer"
-- Define the logger: <Logger("Name", "Enabled <true/false>")>
---@type Logger
local m_Logger = Logger("SomeSharedClass", false)
-- the function that gets called on initialization of the class
function SomeSharedClass:__init()
m_Logger:Write("SomeSharedClass init.")
self:RegisterVars()
self:RegisterEvents()
self:RegisterHooks()
end
function SomeSharedClass:RegisterVars()
-- register variables here
end
function SomeSharedClass:RegisterEvents()
Events:Subscribe('Engine:Update', self, self.OnEngineUpdate)
end
function SomeSharedClass:RegisterHooks()
-- register hooks here
end
function SomeSharedClass:AnyOtherFunction()
-- any other function
end
-- specifically on shared because the timer needs the engine update to work
function SomeSharedClass:OnEngineUpdate(p_DeltaTime, p_SimulationDeltaTime)
RealityModTimer:OnEngineUpdate(p_DeltaTime, p_SimulationDeltaTime)
end
return SomeSharedClass()
Server and Client. Don´t forget we don´t have to require our libraries anymore because we already made them available globally in Shared. Name your classes to something that makes sense, here you can just follow the guides naming or be creative. Also don´t subscribe to the Engine:Update event again, we won´t need it except for the RealityModTimer.You can find a more in-depth guide on Vext Events on the official guide
function ClientTeleporter:RegisterEvents()
Events:Subscribe('Player:UpdateInput', self, self.OnPlayerUpdateInput)
end
---@param p_Player Player
---@param p_DeltaTime number
function ClientTeleporter:OnPlayerUpdateInput(p_Player, p_DeltaTime)
end
You can get the parameters returned in the callback from the official VU Docs
---@param p_Player Player
---@param p_DeltaTime number
function ClientTeleporter:OnPlayerUpdateInput(p_Player, p_DeltaTime)
if InputManager:WentKeyDown(InputDeviceKeys.IDK_F) and p_Player.soldier ~= nil and not p_Player.inVehicle then
self:TeleportRaycasted()
end
end
You can find a more in-depth guide on Vext Net Events on the official guide
function ClientTeleporter:TeleportRaycasted()
-- get the client camera transform
local s_Transform = ClientUtils:GetCameraTransform()
-- add security checks
if s_Transform == nil then return end
if s_Transform.trans == Vec3.zero then -- Camera is below the ground. Creating an entity here would be useless.
return
end
-- The freecam transform is inverted. Invert it back
local s_CameraForward = Vec3(s_Transform.forward.x * -1, s_Transform.forward.y * -1, s_Transform.forward.z * -1)
-- get the position in the world to raycast to
local s_CastPosition = Vec3(
s_Transform.trans.x + (s_CameraForward.x * SETTINGS.RAYCAST_DISTANCE),
s_Transform.trans.y + (s_CameraForward.y * SETTINGS.RAYCAST_DISTANCE),
s_Transform.trans.z + (s_CameraForward.z * SETTINGS.RAYCAST_DISTANCE)
)
-- Raycast
local s_Raycast = RaycastManager:Raycast(s_Transform.trans, s_CastPosition, RayCastFlags.IsAsyncRaycast)
if s_Raycast == nil then
return
end
-- lets add a meter to the position height to prevent spawning in the ground
local s_TeleportPosition = s_Raycast.position
s_TeleportPosition.y = s_TeleportPosition.y + 1
-- send our event to the server so it can handle the teleporting
NetEvents:SendLocal(NETEVENTS.TELEPORT_TO_POSITION, s_TeleportPosition)
m_Logger:Write("Teleport Request Sent")
end
Enums.lua
EVENTS = {}
NETEVENTS = {
TELEPORT_TO_POSITION = "Teleport:ToPosition"
}
Settings.lua
SETTINGS = {
LOGGER_PRINT_ALL = false,
RAYCAST_DISTANCE = 5000
}
You can find a more in-depth guide on Vext Net Events on the official guide
function ServerTeleporter:RegisterEvents()
NetEvents:Subscribe(NETEVENTS.TELEPORT_TO_POSITION, self, self.OnTeleportToPositionRequest)
end
---@param p_Player Player
---@param p_TeleportPosition Vec3
function ServerTeleporter:OnTeleportToPositionRequest(p_Player, p_TeleportPosition)
if not p_TeleportPosition then
m_Logger:Error("Teleport Position is invalid")
return
end
-- get the player soldier to teleport
local s_Soldier = p_Player.soldier
if not s_Soldier then
m_Logger:Warning("Soldier to teleport is nil")
return
end
s_Soldier:SetPosition(p_TeleportPosition)
m_Logger:Write("Teleported Player: " .. p_Player.name .. " to Position: " .. tostring(p_TeleportPosition))
end
---@type Logger
local m_Logger = Logger("ServerTeleporter", true)
---@type RealityModTimer
local m_Timer = RealityModTimer
function ServerTeleporter:__init()
To test the mod, add it to the ModList.txt in the Server/Admin folder. This can be done by just entering the mods root folder name e.g. here GettingStartedMod. A successfully loaded mod will be displayed in the server window:

Now after you successfully tested the teleport mod you may or may have not experienced dying from teleporting. This can happen because fall damage still gets applied to the players soldier after teleporting. Lets take a look on how we can prevent that:
function ServerTeleporter:RegisterHooks()
Hooks:Install('Soldier:Damage', 1, self, self.OnSoldierDamage)
end
function ServerTeleporter:OnSoldierDamage()
-- we will continue here later
end
function ServerTeleporter:RegisterVars()
self.m_IgnoreSoldierDamageForPlayer = {}
end
---@param p_Player Player
---@param p_TeleportPosition Vec3
function ServerTeleporter:OnTeleportToPositionRequest(p_Player, p_TeleportPosition)
if not p_TeleportPosition then
m_Logger:Error("Teleport Position is invalid")
return
end
-- get the player soldier to teleport
---@type SoldierEntity|nil
local s_Soldier = p_Player.soldier
if not s_Soldier then
m_Logger:Warning("Soldier to teleport is nil")
return
end
-- Set new position of the soldier
s_Soldier:SetPosition(p_TeleportPosition)
-- Add soldier to damage protection table
table.insert(self.m_IgnoreSoldierDamageForPlayer, p_Player.name)
-- remove protection after set time
m_Timer:Simple(SETTINGS.TELEPORT_PROTECTION_IN_S, function()
if self:_CheckIfSoldierIsProtected(s_Soldier) then
m_Logger:Write("Soldier Protection Lifted")
end
end)
-- log the teleported player and the position
m_Logger:Write("Teleported Player: " .. p_Player.name .. " to Position: " .. tostring(p_TeleportPosition))
end
Settings.luaSETTINGS = {
LOGGER_PRINT_ALL = false,
RAYCAST_DISTANCE = 5000,
TELEPORT_PROTECTION_IN_S = 0.2
}
_ in the function name describes the local use in the class.---@param p_Soldier SoldierEntity|nil
function ServerTeleporter:_CheckIfSoldierIsProtected(p_Soldier)
if not p_Soldier then
return false
end
for l_Index, l_SoldierName in ipairs(self.m_IgnoreSoldierDamageForPlayer) do
if l_SoldierName == p_Soldier.player.name then
table.remove(self.m_IgnoreSoldierDamageForPlayer, l_Index)
return true
end
end
return false
end
OnSoldierDamage function and check there if the soldier is protected. If yes return nil in the hook context.function ServerTeleporter:OnSoldierDamage(p_HookCtx, p_Soldier, p_Info, p_GiverInfo)
if self:_CheckIfSoldierIsProtected(p_Soldier) then
-- disable damage
p_HookCtx:Return(nil)
end
end
We will take a quick look into DataContainer modification using the Reality Mod DC class. You can grab that from the GettingStartedMod Repository we set up for you.
Shared/__init__.lua. And initialize the DataContainers at the top of the file.-- DataContainer we want to modify
---@type DC
local m_FiringFunctionM249 = DC(Guid("F37DBC84-F61B-11DF-829C-95F94E7154E3"), Guid("7FCFC3D7-49C2-477E-8952-664FDA86B41E"))
-- Projectile we want to replace its projectile with
---@type DC
local m_SMAWHEProjectileData = DC(Guid("168F529B-17F6-11E0-8CD8-85483A75A7C5"), Guid("168F529C-17F6-11E0-8CD8-85483A75A7C5"))
---@type DC
local m_SMAWHEProjectileBlueprint = DC(Guid("168F529B-17F6-11E0-8CD8-85483A75A7C5"), Guid("90BAEBC0-C9E6-CB0B-7531-110499218677"))
function SomeSharedClass:__init()
m_Logger:Write("SomeSharedClass init.")
self:RegisterVars()
self:RegisterEvents()
self:RegisterHooks()
self:RegisterCallbacks()
end
function SomeSharedClass:RegisterCallbacks()
m_FiringFunctionM249:RegisterLoadHandler(self, self.ModifyM249FiringFunctionData)
end
---@param p_Instance FiringFunctionData
function SomeSharedClass:ModifyM249FiringFunctionData(p_FiringFunctionData)
end
p_FiringFunctionData writable first.p_FiringFunctionData:MakeWritable()
local s_ShotConfig = p_FiringFunctionData.shot
if s_ShotConfig then
s_ShotConfig.projectileData = m_SMAWHEProjectileData:GetInstance()
s_ShotConfig.projectile = m_SMAWHEProjectileBlueprint:GetInstance()
m_Logger:Write("Replaced M249 Projectile")
end
local s_FireLogic = p_FiringFunctionData.fireLogic
if s_FireLogic then
s_FireLogic.rateOfFire = 1200
m_Logger:Write("Modified M249 FireLogic")
end
local s_AmmoConfig = p_FiringFunctionData.ammo
if s_AmmoConfig then
s_AmmoConfig.magazineCapacity = 249
s_AmmoConfig.autoReplenishMagazine = true
s_AmmoConfig.autoReplenishDelay = 5.0
m_Logger:Write("Modified M249 AmmoConfig")
end