This page walks through the two mods that ship with the hook under samples/mods. Each is a ModInfo.ini and one Scripts\main.lua. To run one, copy its folder to <game>\Mods\<name>, with lua51.dll next to dinput8.dll (see Installation), and start a race.

Magnet#

Magnet makes every prop within 80 m of the player's car slide towards it, twenty times a second, stopping five metres short so that the props gather in a ring instead of piling under the wheels. It demonstrates the prop class: a search by distance, a velocity write that wakes a prop, and timers that start and stop with the race.

Mods\Magnet\Scripts\main.lua
-- Magnet: the level's props slowly gravitate towards the player's car.
--
-- An example of the live prop objects (docs/lua-scripting.md, "Props"). Every
-- update the nearest props within RADIUS are given a velocity towards the
-- car on the horizontal plane (their vertical velocity is left alone, so
-- gravity still works), and the pull stops STOP_AT metres short so they
-- gather in a ring instead of piling under the wheels. The first velocity
-- write wakes a prop: a static one gets its physics body (the game's own
-- break-off), so bollards, bins and boxes start sliding in, and the AIs
-- plough through them.
--
-- Install: copy this folder to <game>\Mods\Magnet (Scripts\main.lua beside
-- ModInfo.ini); lua51.dll must be next to dinput8.dll.

local RADIUS = 80       -- metres: props this close feel the pull
local SPEED = 3         -- metres per second towards the car
local STOP_AT = 5       -- metres: the ring they gather in
local MAX_PROPS = 24    -- the nearest ones per update
local RATE = 0.05       -- seconds between updates (20 a second)

local pulling            -- the timer while a race runs

local function pull()
  local car = ss.vehicle
  if not car then return end
  local here = car.position:copy()      -- a plain copy: the car may move while we loop
  local moved = 0
  for _, prop in ipairs(ss.props.near(here, RADIUS)) do
    if moved >= MAX_PROPS then break end
    if prop.state ~= 'broken' then
      local at = prop.position
      local dx, dz = here.x - at.x, here.z - at.z
      local dist = math.sqrt(dx * dx + dz * dz)
      if dist > STOP_AT then
        local s = SPEED / dist
        -- the write wakes the prop (a static one gets its body first); the
        -- vertical component is kept so a falling prop keeps falling
        prop.velocity = ss.vec3(dx * s, prop.velocity.y, dz * s)
        moved = moved + 1
      end
    end
  end
end

ss.on("race_start", function()
  ss.log("magnet on: %d props in the level", ss.props.count)
  pulling = ss.every(RATE, pull)
end)

ss.on("race_end", function()
  if pulling then ss.off(pulling); pulling = nil end
  ss.log("magnet off")
end)

-- a line every five seconds on how the gathering goes
ss.every(5, function()
  local car = ss.vehicle
  if not car or not pulling then return end
  local near = ss.props.near(car.position, RADIUS)
  local ring, nearest, sample = 0, nil, nil
  for _, prop in ipairs(near) do
    local d = prop.position:distance(car.position)
    if d <= STOP_AT + 2 then ring = ring + 1 end
    if not nearest or d < nearest then nearest = d; sample = prop end
  end
  ss.log("magnet: %d props within %d m, %d in the ring, nearest %.1f m (%s, %s, moving %s at %.1f m/s), car at %s",
    #near, RADIUS, ring, nearest or -1, sample and sample.name or "-", sample and sample.state or "-",
    sample and tostring(sample.velocity) or "-", sample and sample.velocity:length() or 0, tostring(car.position))
end)

How it works#

pull runs from a repeating timer. It first takes car.position:copy(), a plain copy of the car's position, because a live vector reads the game on every access and the car moves while the loop runs. ss.props.near(point, radius) returns the props within the radius, nearest first. The distances are compared on the hook's side, so the loop only reads the props it is going to move.

The write to prop.velocity is what moves a prop. The horizontal components point at the car, scaled to SPEED; the vertical component is the prop's own, so a prop that is falling keeps falling. A prop that has never been touched is part of the level's static hull, and the first velocity write cuts it out and gives it a physics body, the same thing the game does when a car first hits it. From then on the AIs drive through moving bollards. Props the game has already broken apart (state == "broken") are skipped.

The timer starts on race_start and stops on race_end. By the time the race_end handler runs the props are gone, so the timer must not fire again. The five-second report shows the plain reads a prop offers: name, state, velocity and its :length(), and :distance() between two positions.

Trucks#

Trucks puts four of Survival's trucks on the circuit in every race, on any track and in any mode. It demonstrates the Nemesis actors: a request for them before the race is built, and the live objects that follow them once it runs.

Mods\Trucks\Scripts\main.lua
-- Trucks: every race gets four of Survival's trucks, on any track and in any mode.
--
-- An example of the Nemesis actors (docs/lua-scripting.md, "Nemesis actors").
-- ss.race.nemesis_slots names what the game builds at race start, by the
-- template files it ships: the trucks here, or 'DemolitionTemplate.params' for
-- the attack modes' cars. ss.nemesis[i] follows them once the race runs.
--
-- Install: copy this folder to <game>\Mods\Trucks (Scripts\main.lua beside
-- ModInfo.ini); lua51.dll must be next to dinput8.dll.

local TRUCKS = 4

local slots = {}
for i = 1, TRUCKS do slots[i] = 'TruckTemplate.params' end
ss.race.nemesis_slots = slots

ss.on("race_start", function()
  ss.log("%d trucks on the track", ss.nemesis.count)
end)

-- a line every ten seconds: where the nearest truck is
ss.every(10, function()
  local car = ss.vehicle
  if not car then return end
  local nearest, dist
  for truck in ss.nemesis.each() do
    if truck.kind == 'truck' then
      local d = truck.position:distance(car.position)
      if not dist or d < dist then nearest, dist = truck, d end
    end
  end
  if nearest then
    ss.log("nearest truck %d: %.0f m away at %.0f km/h", nearest.index, dist, nearest.speed * 3.6)
  end
end)

How it works#

ss.race.nemesis_slots is a list of the template files the game builds its actors from, one per slot. The game offers the templates in every mode, not only in Survival. The mod sets the list from main.lua, before any race, and the hook applies it when the next race builds its actors. Setting the list to nil restores whatever the mode file says.

Once the race runs, ss.nemesis holds the actors as live objects: count, each(), and on each actor kind (truck, car or helicopter), index, a live position and speed in metres per second. The ten-second report walks them and keeps the nearest truck. Nothing on an actor is writable yet.