Skip to content

Instantly share code, notes, and snippets.

@ghosty141
Last active December 27, 2020 19:43
Show Gist options
  • Save ghosty141/1f6f569197467ae36049615d777bf7ad to your computer and use it in GitHub Desktop.
Save ghosty141/1f6f569197467ae36049615d777bf7ad to your computer and use it in GitHub Desktop.

Venice Unleashed Modding

Preface

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.

Dev Environment

First we need a couple of things to get started. Let's set up our development environment.

Code Editor

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.

Server

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

Programming

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:

Mod Structure

Procedural

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.

Object Oriented

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

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".

Working with GUIDS

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?

Finding the data

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.

Modifying the data

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)

Modifying instances

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment