Questions about Python <--> Lua Interaction

Hi there,

Thank you so much for your continuous support and your great API! I have some question about how does the python ←→lua integration works.

So previously, by scanning through the github issue page, I was able to read the wheel speed of each individual wheel on the fly to collect wheel speed data. With the following function:

def setup_mod():
    import platform
    """Setup the vehicle engine mod by packaging the Lua files into a zip."""
    if platform.system() == "Linux":
        USERPATH = Path.home() / '.local/share/BeamNG.drive'
    else:
        USERPATH = Path(os.getenv("LOCALAPPDATA")) / 'BeamNG.tech'

    userpath = Path(USERPATH, "0.35")
    
    # setting up mod
    myModPath = userpath / "mods" / "genericresearchmod.zip"
    myModPath.parent.mkdir(parents=True, exist_ok=True)
    
    veCode = "vehicleEngineCode.lua"
    zipVEpath = str(Path("lua") / "vehicle" / "extensions" / veCode)
    
    with zipfile.ZipFile(str(myModPath), "w") as ziph:
        ziph.write("/home/guxunjia/masters_project/BeamNGpy/examples/modInterface/vehicleEngineCode.lua", arcname=zipVEpath)
    
    print(f"Mod packaged and installed at: {myModPath}")

And then I do:

def getCustomVehicleData(veh):
    data = dict(type='GetCustomVehicleData')
    response = veh.connection.send(data)
    return response.recv()

vehicle = Vehicle('ego_vehicle', model='etk800', licence='DATA01', extensions=['vehicleEngineCode'])
custom_data = getCustomVehicleData(vehicle)

to load this mod and read the wheel data.

Now I have created a custom mod, but it is not acting on the vehicle, it is for changing the surface friction on the fly of the simulation.

I have uploaded it to my drive: https://drive.google.com/file/d/1-LwmyJCq44gUHZpGl_A-F1wrnt7jmPjt/view?usp=sharing

It basically has a small GUI, that has several buttons on it and tell you which surface you want to drive. You click on that button and your current surface will be swapped (but just the friction condition, not the visual appearance).

I am wondering whether there is a similar way to connect this with the python API? Like for example, I want to have some python loop, and at a certain point my python file will somehow trigger the surface condition swapping, and the lua will be called to do this, similar to how we read the wheel speed as above.

I gave it a try but could not make it work. I am wondering whether you have any suggestions on this? Thank you so much!

Hi @alfredgu001324,

If you need wheel speed information you can easily get it via the Electrics sensor.

My recommendation is for you to import the AdasUltrasonicApi (from beamngpy.vehicle.adas_ultrasonic import AdasUltrasonicApi) and go to its definition. You can use it as an example of how to write a class that communicate with the same file from your other question <game_home>/lua/vehicle/extensions/tech/techCore.lua. This file is where many Lua functionalities are exposed. The last step would be to have your mod’s functions be called in your new handler/s in the techCore.lua.

Hope this helps, have a nice day!

1 Like

Hi @iskren_rusimov, thank you so much for your prompt reply!

Your reference for the ultrasonic example was extremely helpful! I took a deeper look at it and made it work.

Just to share with the community in case anyone is also interested:

First I have this surface_toggler.lua file that handles the surface swapping logic:

-- Minimal Surface Toggler Extension
-- Toggles terrain groundmodels for paved and gravel paints

local M = {}

M.dependencies = {"ui_imgui"}
local ui = ui_imgui

-- State
local terrainBlock = nil
local hasTerrain = false
local showUI = false

-- unified scope: apply to all terrain paints

-- Detection helpers
local pavedExact = {
	GROUNDMODEL_ASPHALT1 = true,
	ICE = true,
	ASPHALT = true,
	ASPHALT_OLD = true,
	ASPHALT_WET2 = true,
	ASPHALT_WET3 = true,
	ASPHALT_WET_MKDW = true,
	CONCRETE2 = true,
	CONCRETE = true,
	ASPHALT_WET = true,
	GROUNDMODEL_ASPHALT_OLD = true,
	ASPHALT_ICY = true,
	ASPHALT_ICY_MKDW = true,
	SLIPPERY = true
}

local function isPaved(gmName)
	if not gmName then return false end
	local name = tostring(gmName):upper()
	if pavedExact[name] then return true end
	if pavedSubstringMatch[0] then
		if name:find("ASPHALT", 1, true) or name:find("CONCRETE", 1, true) then return true end
	end
	return false
end

local function isGravel(gmName)
	if not gmName then return false end
	local name = tostring(gmName):upper()
	if name == "GRAVEL" or name == "GRAVEL_WET" then return true end
	if gravelSubstringMatch[0] then
		if name:find("GRAVEL", 1, true) then return true end
	end
	return false
end

-- Core apply routine
local function apply(target)
	if not hasTerrain then
		guihooks.trigger('toastrMsg', {type = "error", title = "Surface Toggler", msg = "No TerrainBlock in this level.", config = {timeOut = 5000}})
		return
	end
	local changed = 0
	for index = 0, (terrainBlock:getMaterialCount() - 1) do
		local mtl = terrainBlock:getMaterial(index)
		local gmName = mtl.groundmodelName
		if gmName ~= target then
			print(tostring(gmName) .. " -> " .. tostring(target))
			mtl.groundmodelName = target
			changed = changed + 1
		end
	end
	be:reloadCollision()
	guihooks.trigger('toastrMsg', {type = "success", title = "Surface Toggler", msg = ("Applied %s to %d materials"):format(target, changed), config = {timeOut = 4000}})
end

-- UI
local function renderGui()
	ui.Begin("Surface Toggler", nil, ui.WindowFlags_AlwaysAutoResize)
	if not hasTerrain then
		ui.TextColored(ui.ImVec4(1,0.3,0.2,1), "No TerrainBlock found in this level.")
		ui.End()
		return
	end

	ui.TextColored(ui.ImVec4(1,0.5,0,1), "Apply to ALL terrain paints")
	if ui.Button("Dry (ASPHALT)") then apply("ASPHALT") end
	ui.SameLine()
	if ui.Button("Wet (ASPHALT_WET)") then apply("ASPHALT_WET") end
	ui.SameLine()
	if ui.Button("Slippery") then apply("SLIPPERY") end
	if ui.Button("Ice") then apply("ICE") end
	ui.SameLine()
	if ui.Button("Snow") then apply("SNOW") end

	ui.Separator()
	ui.TextColored(ui.ImVec4(1,0.5,0,1), "Gravel variants (also applied to ALL paints)")
	if ui.Button("Dry (GRAVEL)") then apply("GRAVEL") end
	ui.SameLine()
	if ui.Button("Wet (GRAVEL_WET)") then apply("GRAVEL_WET") end


	ui.End()
end

-- Lifecycle
local function onExtensionLoaded()
	local terrainObjs = scenetree.findSubClassObjects('TerrainBlock')
	if terrainObjs and terrainObjs[1] then
		terrainBlock = scenetree.findObject(terrainObjs[1])
		hasTerrain = terrainBlock ~= nil
	else
		hasTerrain = false
	end
end

local function onExtensionUnloaded()
end

local function onUpdate(dt)
	if showUI then renderGui() end
end

-- Public API function for swapping surface (called by techCore.lua)
local function swap(surfaceType)
	if not hasTerrain then
		return false, 'No TerrainBlock in this level.'
	end
	
	local typeMap = {
		ice = "ICE",
		asphalt = "ASPHALT",
		asphalt_wet = "ASPHALT_WET",
		slippery = "SLIPPERY",
		snow = "SNOW",
		gravel = "GRAVEL",
		gravel_wet = "GRAVEL_WET"
	}
	
	local target = typeMap[surfaceType]
	if not target then
		return false, 'Unknown surface type: ' .. tostring(surfaceType)
	end
	
	apply(target)
	return true, nil
end

-- Public API
M.onExtensionLoaded = onExtensionLoaded
M.onExtensionUnloaded = onExtensionUnloaded
M.onUpdate = onUpdate
M.swap = swap
M.toggleGui = function() showUI = not showUI end

return M

Then we need to add this to the BeamNG.tech.v0.37.6.0/lua/ge/extensions/tech/techCore.lua. Note that we need to add this to the GE techCore instead of the vehicle techCore.

M.handleSwapSurfaceFriction = function(request)
  local surfaceType = request['surfaceType']
  
  if not extensions.util_surfaceToggler then
    extensions.load('util/surfaceToggler')
  end
  
  local success, errorMsg = extensions.util_surfaceToggler.swap(surfaceType)
  
  if success then
    request:sendACK('SurfaceFrictionSwapped')
  else
    request:sendBNGError(errorMsg or 'Failed to swap surface friction')
  end
end

which routes the python messages to the extension.

And finally, in the python script:

We need to first load it:

# Pre-load the surface toggler extension and give it time to initialize
beamng_instance.control.queue_lua_command("extensions.load('util/surfaceToggler')")
sleep(0.5)  # Give extension time to load before we try to use it
print("Pre-loaded surface toggler GE extension")

Then define a function that passes the type of surface we want to swap:

def swapSurfaceFriction(beamng_instance, surface_type):
    data = dict(type='SwapSurfaceFriction', surfaceType=surface_type)
    beamng_instance.connection.send(data).ack('SurfaceFrictionSwapped')
    print(f"Surface friction swapped to: {surface_type}")

Then you can just call swapSurfaceFriction(beamng_instance, 'gravel_wet') to change the surface friction.

You can also add additional friction types if you want, it is a simple edit.

Hope this helps!

And thank you @iskren_rusimov for the great support! You have been extremely helpful. Really appreciate it!

1 Like