If you want to develop your own VU mod(s) I think it's best if you've already programmed either in lua or other scripting languages. You should be familiar with basic concepts like classes, if, for, while etc. Basic knowledge of object oriented programming is also useful but not needed.
Most of the difficulty lies in knowing where and how to change things or use certain VU functions. The official VU documentation will be your best friend as well as the Discord's "modding-help" channel.
First we need a couple of things to get started. Let's set up our development environment.
I recommend VS Code or Notepad++. It does not really matter which one you use since functions like autocompletion are not really needed for writing VU mods, so any editor that displays line numbers and has decent syntax highlighting will do.
We will also need a server to test our mod on, to set one up follow the
official guide: https://docs.veniceunleashed.net/hosting/setup-win/.
After this open the Startup.txt
file and add an RCON password:
admin.password "<password>"
Next download and install the Procon client
(download here).
Any other RCON client will work as well as long as it offers a way to issue
RCON commands. This is not strictly needed but very useful since you can send
the command modList.ReloadExtensions
which reloads all mods on the
server. Without this command you have to restart the server everytime you
make changes to your mod.
If your server is now running, start Procon and create a new connection with the following properties and connect to the server:
Username: <empty>
Password: <your password>
IP: 127.0.0.1
Port: 47200
Now it's time that you do the official "Your First Mod" guide that covers the very basics of how to print a simple message and also touches on the concept of Client, Shared and Server.
In general, the __init__.lua
file gives you total freedom of how you
structure the code of your mod. The two common approaches are:
The "Your First Mod" guide follows this approach also simple mods that just change some data values are mostly done this way.
Let's take a look at this simple mod that changes some properties of the vehicle spawns. (pulled from the EmulatorNexus Github):
Events:Subscribe('Partition:Loaded', function(partition)
if partition == nil then
return
end
local instances = partition.instances
for _, instance in pairs(instances) do
if instance:Is("VehicleSpawnReferenceObjectData") then
instance = VehicleSpawnReferenceObjectData(instance)
instance:MakeWritable()
instance.applyDamageToAbandonedVehicles = false
...
instance.initialSpawnDelay = 0
end
end
end)
All it does is subscribe to the Partition:Loaded
event. This event
passes the partition
being loaded to the callback function. There the code checks if the instance
is a VehicleSpawnReferenceObjectData which then gets casted.
instance = VehicleSpawnReferenceObjectData(instance)
Then the partitions values are modified to make the vehicle spawns infinite.
You will find this pattern, subscribing to Partition:Loaded
and iterating
over the instanes quite often, when it comes to modify map or entity data.
The more complex your mod gets the better it is if it follows a common structure. I've created a sample mod that uses it:
class 'SampleMod'
function SampleMod:__init()
print("Initializing SampleMod")
self:RegisterVars()
self:RegisterEvents()
end
function SampleMod:RegisterVars()
self.isLevelLoaded = false
end
function SampleMod:RegisterEvents()
self.levelLoaded = Events:Subscribe('Level:Loaded', self, self.OnLevelLoaded)
end
function SampleMod:OnLevelLoaded()
print('Level has loaded!')
self.isLevelLoaded = true
end
SampleMod()
Let's go over the code and look at its functions. First off, you probably
noticed that this is a class with the name "SampleMod". The class has a
constructor (the __init()
method) that gets called when an instance of
the class gets created (see the last line of code). This is the first
method that is called.
The init method does 3 things, first it prints out a message and then calls
two methods.
These two methods can be found in many mods, they help with readability and
keeping a common style that makes it easier to find certain parts of the code.
For example, the Event:Subscribe()
calls are commonly done in the
RegisterEvents()
method.
The RegisterVar()
method on the other hand is used to define instance
variables to store certain values that have to be used across various methods.
In this sample mod it just stores a bool if the level has loaded or not.
Events are described in-depth in the official guide.
To subscribe to events the Events:Subscribe()
function is used which
should be called in the RegisterEvents()
method. It takes 2 or 3
parameters, the first is always the name of the event, the second is either
the function or the instance of the object the method is part of (self
)
followed by the method (self.OnLevelLoaded
in this example).
The naming convention for the function/method passed to the
Event:Subscribe()
function is "On".
If your mod changes properties of some already existing data you often have to work with instanceGuids and partitionGuids. These are both properties of the DataContainer class. This means, all classes that inherit from DataContainer will have their own GUIDs.
Lets say you want to modify some properties of the Glock 18. Where does the game store them and how can you access them?
The EBX dump is where you have to look, there is the text version on Github as well as this more interactive EBX viewer made by Powback.
Now open the EBX viewer and you will see that there is a folder called "Weapons" which contains another folder, "Glock18". If you open that folder you'll find multiple .json files, we're interested in the Glock18.json file, lets click on it and we'll see the contents of it in the Ebx Viewer window.
Depending on what you want to change about the weapon you have to locate that property in a class. Let's say we want to change a property of SoldierWeaponData. In the Ebx Viewer it shows an orange field "SoldierWeaponData", perfect! Open by clicking on it and there you will find the instanceGuid and partitionGuid by which this data is identified by. Remember those, we will need them in the next step.
The tricky part about all this is, knowing how and where to get access to what you want to change. Most of the time you will need Events and Hooks.
Since partitions are essentially the EBX files, partition events or hooks are what you need. The Data is is stored in the partitions instance field and can be iterated over.
Here is a simple example of how this would roughly look like.
Events:Subscribe('Partition:Loaded', function(partition)
local instances = partition.instances
for _, instance in pairs(instances) do
if instance.instanceGuid == Guid('3EC4D98C-E7A2-3679-2C2D-9E6C16F126A9') then
local data = SoldierWeaponData(instance)
data:MakeWritable()
-- change data
end
end
end)
Lets look at the code from the previous example again:
for _, instance in pairs(instances) do
if instance:Is("VehicleSpawnReferenceObjectData") then
instance = VehicleSpawnReferenceObjectData(instance)
instance:MakeWritable()
instance.applyDamageToAbandonedVehicles = false
...
instance.initialSpawnDelay = 0
end
end
If you're trying to modify an instance's properties you will have to
construct an object based on it first:
instance = VehicleSpawnReferenceObjectData(instance)
To make this object now writable you need to call the aptly named method:
instance:MakeWritable()
MakeWritable() is a method that every class that inherits from DataContainer has.