I'd like to keep a race scenario running until the player reaches the final waypoint, no matter if the AI arrived there first. The Field run scenario works like this as the AI can arrive at the final waypoint while the player can still drive. Using some of the lua functions from (Field run) I added variables for lap racing for multiple opponents. Unfortunately the AI still triggers the scenario ending if they arrive at their waypoint before the player. Is there something wrong in the logic/lua amd why do AI cars trigger the scenario end? The function onBeamNGTrigger(data) sould only set AI to aiFinished = true, for the function onRaceWaypointReached(data) to check if players arrives beofer or after the AI to trigger the success messages. Here are the main relevant lua functions: Spoiler: Player reaches final waypoint Code: local function onRaceWaypointReached(data) --log('I', logTag,'onRaceWaypointReached called ') --dump(data) -- The player wins if he crosses the final waypoint while still being close enough to the AI (distance implied by the scenario failing if AI finishes far enough from player) if data.vehicleName == playerInstance and data.waypointName == finalWaypointName then lapsCompleted[data.vehicleName] = lapsCompleted[data.vehicleName] + 1 dump(data) if lapsCompleted['scenario_player0'] == noOfLaps then if aiFinished then dump(data) playerWon = true else --Display special message if player reaches final waypoint before AI playerSpecialWin = true end onRaceResult() end end end Spoiler: AI reaches their waypoint (final trigger) Code: local function onBeamNGTrigger(data) if data.triggerName == finalTriggerName and data.event == 'enter' then if data.subjectName == aiInstance1 or data.subjectName == aiInstance2 or data.subjectName == aiInstance3 or data.subjectName == aiInstance4 or data.subjectName == aiInstance5 or data.subjectName == aiInstance6 or data.subjectName == aiInstance7 or data.subjectName == aiInstance8 or data.subjectName == aiInstance9 then lapsCompleted[data.subjectName] = lapsCompleted[data.subjectName] + 1 dump(data) if lapsCompleted[data.subjectName] == noOfLaps then dump(data) scenetree.findObject(data.subjectName):queueLuaCommand('ai.stopFollowing()') aiFinished = true end end end end Spoiler: Full lua code of my race scenario Code: local M = {} local helper = require('scenario/scenariohelper') local logTag = 'hirochiF8races94' local finalWaypointName = 'derby_wp14' local finalTriggerName = 'trigger_finish1' local playerInstance = 'scenario_player0' local aiInstance1 = 'scenario_opponent1' local aiInstance2 = 'scenario_opponent2' local aiInstance3 = 'scenario_opponent3' local aiInstance4 = 'scenario_opponent4' local aiInstance5 = 'scenario_opponent5' local aiInstance6 = 'scenario_opponent6' local aiInstance7 = 'scenario_opponent7' local aiInstance8 = 'scenario_opponent8' local aiInstance9 = 'scenario_opponent9' local playerWon = false local playerSpecialWin = false local aiFinished = false local running = false local noOfLaps = 2 local lapsCompleted = {} local function reset() lapsCompleted['scenario_player0'] = 0 lapsCompleted['scenario_opponent1'] = 0 lapsCompleted['scenario_opponent2'] = 0 lapsCompleted['scenario_opponent3'] = 0 lapsCompleted['scenario_opponent4'] = 0 lapsCompleted['scenario_opponent5'] = 0 lapsCompleted['scenario_opponent6'] = 0 lapsCompleted['scenario_opponent7'] = 0 lapsCompleted['scenario_opponent8'] = 0 lapsCompleted['scenario_opponent9'] = 0 running = false playerWon = false playerSpecialWin = false aiFinished = false end local function fail(reason) scenario_scenarios.finish({failed = reason}) reset() end local function success(reason) scenario_scenarios.finish({msg = reason}) reset() end local racers = {"scenario_opponent1", "scenario_opponent2", "scenario_opponent3", "scenario_opponent4", "scenario_opponent5", "scenario_opponent6", "scenario_opponent7", "scenario_opponent8", "scenario_opponent9"} local filenames = {"f8mc-1sunburstofforad", "f8mc-2pickupmarauder", "f8mc-3etki300m", "f8mc-4legranluxev6", "f8mc-5fullsizedriftmissile", "f8mc-6pessimazxblue", "f8mc-7coupe", "f8mc-8covetgreenrally", "f8mc-9bluebucklila"} local function onRaceResult(outcome) dump(outcome) scenetree.findObject(aiInstance1):queueLuaCommand('ai.stopFollowing()') if playerWon then success('scenarios.italy.crestRoadAutobello.win.msg') end if playerSpecialWin then success('scenarios.italy.crestRoadAutobello.win.overtake.msg') end end local function onCountdownStarted() reset() -- Start the ai local recordings = {} for i, v in ipairs(racers) do local id = scenetree.findObject(racers[i]):getId() local filename = "/replays/scriptai/tracks/"..filenames[i]..".track.json" local scriptData = jsonReadFile(filename) --dump(scriptData) recordings[id] = scriptData.recording scenetree.findObject(racers[i]):queueLuaCommand("ai.startFollowing("..serialize(recordings[id])..")") end end local function onRaceStart() -- log('I', logTag,'onRaceStart called') running = true end local function onRaceWaypointReached(data) --log('I', logTag,'onRaceWaypointReached called ') --dump(data) -- The player wins if he crosses the final waypoint while still being close enough to the AI (distance implied by the scenario failing if AI finishes far enough from player) if data.vehicleName == playerInstance and data.waypointName == finalWaypointName then lapsCompleted[data.vehicleName] = lapsCompleted[data.vehicleName] + 1 dump(data) if lapsCompleted['scenario_player0'] == noOfLaps then if aiFinished then dump(data) playerWon = true else --Display special message if player reaches final waypoint before AI playerSpecialWin = true end onRaceResult() end end end local function onBeamNGTrigger(data) if data.triggerName == finalTriggerName and data.event == 'enter' then if data.subjectName == aiInstance1 or data.subjectName == aiInstance2 or data.subjectName == aiInstance3 or data.subjectName == aiInstance4 or data.subjectName == aiInstance5 or data.subjectName == aiInstance6 or data.subjectName == aiInstance7 or data.subjectName == aiInstance8 or data.subjectName == aiInstance9 then lapsCompleted[data.subjectName] = lapsCompleted[data.subjectName] + 1 dump(data) if lapsCompleted[data.subjectName] == noOfLaps then dump(data) scenetree.findObject(data.subjectName):queueLuaCommand('ai.stopFollowing()') aiFinished = true end end end end M.onCountdownStarted = onCountdownStarted M.onRaceStart = onRaceStart M.onRaceWaypointReached = onRaceWaypointReached M.onBeamNGTrigger = onBeamNGTrigger M.onRaceResult = onRaceResult return M Spoiler: Field run full lua Code: local M = {} local helper = require('scenario/scenariohelper') local logTag = 'crestRoadAutobello' local finalWaypointName = 'scenario_wpEnd' local finalTriggerName = 'finishTrigger' local playerInstance = 'scenario_player0' local aiInstance = 'scenario_ai0' local playerWon = false local playerSpecialWin = false local aiWon = false local aiFinished = false local damageFail = false local running = false local distance = 0 local playerDamage = 0 local aiDamage = 0 local messageText = "" local lastMessage = "" local function reset() running = false playerWon = false playerSpecialWin = false aiWon = false damageFail = false aiFinished = false playerDamage = 0 aiDamage = 0 distance = 0 lastMessage = "" end local function fail(reason) scenario_scenarios.finish({failed = reason}) reset() end local function success(reason) scenario_scenarios.finish({msg = reason}) reset() end local function onRaceResult(outcome) scenetree.findObject(aiInstance):queueLuaCommand('ai.stopFollowing()') if aiWon then fail('scenarios.italy.crestRoadAutobello.fail.msg') end if damageFail then fail('scenarios.italy.crestRoadAutobello.damage.msg') end if playerWon then success('scenarios.italy.crestRoadAutobello.win.msg') end if playerSpecialWin then success('scenarios.italy.crestRoadAutobello.win.overtake.msg') end end local function onCountdownStarted() reset() -- Start the ai local aiData = jsonReadFile('levels/italy/scenarios/crestRoadAutobello.track.json') scenetree.findObject(aiInstance):queueLuaCommand("ai.startFollowing("..serialize(aiData.recording)..")") end local function onRaceStart() -- log('I', logTag,'onRaceStart called') running = true end local function onPreRender(dt) -- Gets the distance between the 2 cars if running == true then local playerVehicle = scenetree.findObject(playerInstance) local playerVehicleData = map.objects[playerVehicle:getID()] playerDamage = playerVehicleData.damage local playerVehiclePos = playerVehicle:getPosition() local aiVehicle = scenetree.findObject(aiInstance) local aiVehicleData = map.objects[aiVehicle:getID()] aiDamage = aiVehicleData.damage local aiVehiclePos = aiVehicle:getPosition() distance = (playerVehiclePos - aiVehiclePos):len() end -- Fail scenario if player or ai is damaged if playerDamage > 10000 or aiDamage > 10000 then onRaceResult() damageFail = true end -- Warn player if he's too far behind the ai if distance > 50 then messageText = "scenarios.italy.crestRoadAutobello.far.msg" else messageText = "" end if messageText ~= lastMessage then helper.realTimeUiDisplay(messageText) lastMessage = messageText end end local function onRaceWaypointReached(data) --log('I', logTag,'onRaceWaypointReached called ') --dump(data) -- The player wins if he crosses the final waypoint while still being close enough to the AI (distance implied by the scenario failing if AI finishes far enough from player) if data.vehicleName == playerInstance and data.waypointName == finalWaypointName then if aiFinished then playerWon = true else --Display special message if player reaches final waypoint before AI playerSpecialWin = true end onRaceResult() end end local function onBeamNGTrigger(data) --log('I', logTag,'onBeamNGTrigger called ') --dump(data) if data.subjectName == aiInstance and data.triggerName == finalTriggerName and data.event == 'enter' then aiFinished = true if distance > 50 then -- The player loses if the AI reaches the end of the scenario and is far enough from the player aiWon = true onRaceResult() end end end M.onCountdownStarted = onCountdownStarted M.onRaceStart = onRaceStart M.onPreRender = onPreRender M.onRaceWaypointReached = onRaceWaypointReached M.onBeamNGTrigger = onBeamNGTrigger M.onRaceResult = onRaceResult return M
Hey, sorry for the late reply. I spent some time figuring this out and typing this long reply, as it wasn't as easy as I thought. This is worth the trouble, though. There are several steps. Edited to show correct solution. Next, in your .prefab file, set "isAIControlled" to "0" for all AI vehicles. Code: new BeamNGVehicle(scenario_opponent1) { JBeam = "sunburst"; color = "0.729412019 0.101961002 0.0588234998 1.13199997"; colorPalette0 = "0.301083654 0.728916347 0.353139341 0.890999973"; colorPalette1 = "0.0974876434 0.185390681 0.224512354 1.704"; partConfig = "levels/hirochi_raceway/scenarios/1sun.pc"; renderDistance = "500"; renderFade = "0.100000001"; datablock = "default_vehicle"; position = "165.180527 -6.35728979 20.3994751"; scale = "1 1 1"; rotationMatrix = "0.618084133 -0.776483297 -0.12266434 0.779909849 0.625258744 -0.0281519536 0.0985564366 -0.0782668442 0.992048919"; canSave = "1"; canSaveDynamicFields = "1"; persistentId = "5172466a-9291-4424-9b64-ea33b83f1ad3"; mode = "Ignore"; annotation = "CAR"; cloneOrigin = "scenario_player0"; isAIControlled = "0"; licenseFormats = "[\"30-15\"]"; licenseText = "SUNKISS"; }; Now, your .lua file should run things as intended. Looking at your code, everything should hopefully work. Here's something cool you can try: Code: local aiFinishCount = 0 local finishRanks = {'1st', '2nd', '3rd', '4th', '5th', '6th', '7th', '8th', '9th', '10th'} local function onRaceWaypointReached(data) if data.waypointName == finalWaypointName then lapsCompleted[data.vehicleName] = lapsCompleted[data.vehicleName] + 1 if lapsCompleted[data.vehicleName] == noOfLaps then if not aiFinished then playerWon = true end end end end local function onBeamNGTrigger(data) if data.triggerName == finalTriggerName and data.event == 'enter' then if data.subjectName ~= playerInstance then lapsCompleted[data.subjectName] = lapsCompleted[data.subjectName] + 1 if lapsCompleted[data.subjectName] == noOfLaps then aiFinished = true aiFinishCount = aiFinishCount + 1 end end end end local function onRaceResult() local text = 'You finished the race in '..finishRanks[aiFinishCount + 1]..' place. ' if playerWon then text = text..'Congratulations!' success(text) else fail(text) end end Now the scenario can tell the player which place it ended up in when they hit the finish line. I didn't test this, but it should work. Just remember to put the local variables from this excerpt at the top of your .lua file, and perhaps add aiFinishCount = 0 to the reset() function.
Wow, first of all thanks for the detailed help, you're a legend Gamergull and no worries, my main approach at beamng modding is calm and steady. Edited since solution is above. Set "isAIControlled" to "0" for all AI vehicles in prefab. Old message: I've edited all the files and omg the ranking seems to be working but the AI is still triggering the scenario end. Also, removing the other vehicles field in the json will make the AI trigger each waypoint in the race, making the game spam " ding ding ding" sounds when 3 cars pass a wp. The active waypoint shown also cylces through everytime an AI car passes a waypoint. However, laps still get counted correctly for player and AI. Edit: Funnily when trying a few things, removing the original success/fail functions (I thought they might mess with the new ranking code and message) that were in the lua, the scenario doesn't stop at all. The cam freezes at the point the first AI car enters and the scenario continues to run. Changing cam isn't possible anymore. If you'd like to have a look and test it at some point, here's packed a zip (for easy editing it's the folder for unpacked mods) just a single scenario with 2 laps for quicker testing (it's still called 9 AI / 4 laps in scenario screen). Also, same filenames (might overwrite?) as my mod, in case you still have those files.
Nice. Haha, I really should've tested things; I seem to have made some misconceptions in my post. I was thinking that removing the "*" was it, because the Field Run scenario didn't have it in the .json (it actually doesn't have a vehicle table there at all, thus relying on a default player name and settings). Gonna make some corrections in a while, with a bit of testing .
It turns out, the key to having scenario waypoints untriggerable by AI is to set the "isAIControlled" value in the prefab to "0" for all AI vehicles. This means that onRaceWaypointReached(data) will only trigger for the player vehicle, so keep that in mind for restructuring the .lua. This is where onBeamNGTrigger(data) becomes useful; track the AI laps and wins with that instead (btw, don't forget M.onBeamNGTrigger = onBeamNGTrigger at the bottom of your .lua file). Short post because I'm short on time; I'll elaborate on this later.
I've put in the changes you've mentioned, asterisk back and OMG it's working. Thank you so much! This is probably helpful to anyone wanting to create realistic race scenarios with multiple AI. Being right there in the json, setting "isAIControlled" to 0 never crossed my mind. I've spent a few hours last weekend tinkering with the lua and already had the same idea that onRaceWaypointReached(data) would track the player, and onBeamNGTrigger the AI. I also had cool stuff already working like AI making a powerslide after crossing the finish line, and flashing messages for which AI car did so. But the early triggering of the scenario end was driving me a little crazy and it's funny now seeing how close it was to working as intended. Now I gotta think about if finishing 3rd for example is a fail or not
Yay, glad it worked! I tested it out so I wouldn't be wrong, haha. But yeah, for the race rankings, I don't think I've even seen this before in any race scenarios (unless I missed something). It opens up some fun possibilities.
Indeed, something new to experiment with in scenarios. Also the recent updates to rendering make races, especially with larger grids, more possible now. Btw a small issue I've found doing further tests, if the player and AI have a photo finish, the aifinishedcount will still count up until the end message is shown (there's a 3s delay between finish and message shown). So if 3 cars enter the finish right behind the player it'll still counts them. I think this will be easily fixed when adding another if to onBeamNGTrigger, to only count up aifinishedcount while player is not yet finished. I'll be on it this weekend, also adding back the flashing messages etc. Yeah something like this, 1st = gold, 2nd = silver, 3rd = bronze and everything below gets the player a wood medal.
Hmm... one solution is to set running to false when the player hits the last waypoint, and test for running for the AI trigger, so that it won't count after the player finishes. As for medals, they're usually score based for a lot of scenarios, but I think there's a way to make them reflect your ranking. Hmm.
I've added a variable called playerFinished that's sets to true if player crosses final WP: Code: if lapsCompleted[playerInstance] == noOfLaps then playerFinished = true if not aiFinished then playerWon = true end end Then is checked to only count up AI as long as player hasn't finished: Code: if lapsCompleted[data.subjectName] == noOfLaps and not playerFinished then aiFinished = true aiFinishCount = aiFinishCount + 1 end Medals could be cool, I've had a look in your Hectic Delivery scenario since it features a ranking score. This is my current experiment to see how things interact, the messages seem to show up correctly (always as a success). Maybe reaching the finish-line in this scenario should always be a success Code: local extraText = { -- lines of text to use for scenario fail or win screens second = "2nd", third = "3rd", noPodium = "No podium!", last = "Last, lol" } local function onRaceResult() local text = 'You finished the race in '..finishRanks[aiFinishCount + 1]..' place. ' if playerWon then text = text..'Congratulations!' scenario_scenarios.finish({msg = text}) elseif aiFinishCount == 1 then scenario_scenarios.finish({msg = extraText.second..' '..text}) elseif aiFinishCount == 2 then scenario_scenarios.finish({msg = extraText.third..' '..text}) elseif aiFinishCount == 9 then scenario_scenarios.finish({msg = extraText.last..' '..text}) elseif aiFinishCount ~= 1 and aiFinishCount ~= 2 and aiFinishCount ~= 9 then scenario_scenarios.finish({msg = extraText.noPodium..' '..text}) end end Here's a interesting detail, probably important for photo finishes. I've set the trigger to a sphere and scaled it just like the final waypoint. Now there are 3 modes. Center, contain and overlap. Testing contain currently and it seems to trigger tight finishes correctly.