local RailroadSchedule = require 'RailroadSchedule' local RailroadGate = require 'RailroadGate' local RailroadTrainFactory = require 'RailroadTrainFactory' local RailroadUtils = require 'RailroadUtils' ---@param line LineDescription ---@param from integer ---@param dir integer local function getKey(line, from, dir) return bit.bxor(line.index, from * 397, dir * 397397) end ---@param points vec3[] local function getChunkKey(points) return tonumber(bit.bxor(ac.checksumXXH(points[1]), ac.checksumXXH(points[#points]))) end ---@alias RailroadTweaks {from: number, to: number, speedMultiplier: number, colliding: boolean, lights: boolean} ---@alias RailroadChunk {index: integer, line: LineDescription, points: vec3[], length: number, aabbFrom: vec3, aabbTo: vec3, connected: {chunk: RailroadChunk, chunkIndex: integer, ownIndex: integer}, name: string} ---@param chunks RailroadChunk[] ---@param point vec3 ---@return RailroadChunk|nil local function findNearestChunk(chunks, point) local minDistance, minChunk = math.huge, nil for j = 1, #chunks do local chunk = chunks[j] if chunk.aabbFrom < point and chunk.aabbTo > point then for i = 2, #chunk.points do local d = point:distanceToLineSquared(chunk.points[i - 1], chunk.points[i]) if d < minDistance then minDistance, minChunk = d, chunk end end end end return minChunk end ---@param lines LineDescription[] ---@param stations vec3[] ---@return RailroadChunk[] local function splitLinesToChunks(lines, stations) local space = ac.HashSpace(8) local stationsSpace = ac.HashSpace(40) for i = 1, #lines do local line = lines[i] for j = 1, #line.points do space:addFixed(i * 16384 + j, line.points[j]) end end for i = 1, #stations do stationsSpace:addFixed(i, stations[i]) end local chunks = {} local processed = {} local queue = table.map(lines, function (line) return {line, 1, 1} end) local foundIntersections = {} while #queue > 0 do local item = table.remove(queue, #queue) local line, size, from, dir = item[1], #item[1].points, item[2], item[3] local uniqueKey = getKey(line, from, dir) if not processed[uniqueKey] then processed[uniqueKey] = true local chunk = {} local previousStation for j = from, dir > 0 and size or 1, dir do local p = line.points[j] table.insert(chunk, p) space:iterate(p, function (id) local l, i = lines[math.floor(id / 16384)], id % 16384 if l == line then return end local v = l.points[i] if v:closerToThan(p, 2) then if i == 1 then table.insert(foundIntersections, {l, i, 1}) elseif i == #l.points then table.insert(foundIntersections, {l, i, -1}) else table.insert(foundIntersections, {l, i, 1}) table.insert(foundIntersections, {l, i, -1}) end end end) local curStation = 0 stationsSpace:iterate(p, function (id) curStation = bit.bxor(curStation * 397, id) end) if #foundIntersections > 0 or previousStation and previousStation ~= curStation then table.insert(queue, {line, j, dir}) for i = 1, #foundIntersections do table.insert(queue, foundIntersections[i]) end table.clear(foundIntersections) if #chunk > 1 then chunks[getChunkKey(chunk)] = {line = line, points = chunk, connected = {}} end if j > from then local newKey = getKey(line, j, dir) if processed[newKey] then break end processed[newKey] = true end chunk = {p} end previousStation = curStation end if #chunk > 1 then chunks[getChunkKey(chunk)] = {line = line, points = chunk, connected = {}} end end end return table.map(chunks, function (item) return item end) end ---@param chunks RailroadChunk[] local function setChunkConnections(chunks) local vertices = {} local function registerChunk(vertex, chunk, connectionPoint) for i = 1, #vertices do if vertices[i].point:closerToThan(vertex, 2) then table.insert(vertices[i].chunks, {chunk, connectionPoint}) return end end table.insert(vertices, {point = vertex, chunks = {{chunk, connectionPoint}}}) end for i = 1, #chunks do local chunk = chunks[i] local aabbFrom, aabbTo = vec3(math.huge, math.huge, math.huge), vec3(-math.huge, -math.huge, -math.huge) local len = 0 for j = 1, #chunk.points do aabbFrom:min(chunk.points[j]) aabbTo:max(chunk.points[j]) if j > 1 then len = len + chunk.points[j]:distance(chunk.points[j - 1]) end end aabbFrom:add(-10) aabbTo:add(10) chunk.aabbFrom, chunk.aabbTo, chunk.length = aabbFrom, aabbTo, len chunk.name = string.format('<%.0f, %.0f>', (chunk.aabbFrom.x + chunk.aabbTo.x) / 2, (chunk.aabbFrom.z + chunk.aabbTo.z) / 2) chunk.index = i registerChunk(chunk.points[1], chunk, 1) registerChunk(chunk.points[#chunk.points], chunk, #chunk.points) end for i = 1, #vertices do table.forEach(vertices[i].chunks, function (chunk) local p1 = chunk[1].points[chunk[2]] local p2 = chunk[1].points[chunk[2] == 1 and 2 or chunk[2] - 1] table.forEach(vertices[i].chunks, function (other) if other == chunk then return end local o1 = other[1].points[other[2]] local o2 = other[1].points[other[2] == 1 and 2 or other[2] - 1] if (o2 - o1):dot(p2 - p1) < 0 then table.insert(chunk[1].connected, {chunk = other[1], chunkIndex = other[2], ownIndex = chunk[2]}) end end) end) end end ---@param lines LineDescription[] ---@param stations vec3[] ---@return RailroadChunk[] local function convertToGraph(lines, stations) local chunks = splitLinesToChunks(lines, stations) setChunkConnections(chunks) return chunks end ---@param chunkFrom RailroadChunk ---@param chunkTo RailroadChunk ---@param preceedingIndex integer|nil ---@return RailroadChunk[] local function findRoute(chunkFrom, chunkTo, preceedingIndex, followingIndex) local visited = {} local queue, n = {{chunkFrom, 0, {chunkFrom}, preceedingIndex}}, 1 local foundDistance, found = math.huge, nil while n > 0 do local i = queue[n] local c = i[1] ---@type RailroadChunk n = n - 1 local anyFound = false for j = 1, #c.connected do if c.connected[j].ownIndex ~= i[4] then local h = c.connected[j].chunk ---@type RailroadChunk local k = h.index * 10000 + c.connected[j].chunkIndex if h == chunkTo and c.connected[j].ownIndex ~= followingIndex then if i[2] < foundDistance then foundDistance, found = i[2], table.chain(i[3], {h}) end else if not visited[k] or visited[k] > i[2] then visited[k] = i[2] queue[n + 1], n = {h, i[2] + h.length * (h.line.priority or 1), table.chain(i[3], {h}), c.connected[j].chunkIndex}, n + 1 end anyFound = true end end end if not anyFound and not chunkTo then foundDistance, found = i[2], i[3] end end return found --or error('No route found') end ---@param chunk RailroadChunk ---@param previousChunk RailroadChunk ---@return integer? local function findContinuationIndex(chunk, previousChunk) for i = 1, #chunk.connected do if chunk.connected[i].chunk == previousChunk then return chunk.connected[i].ownIndex end end return nil end ---@param stations {[1]: StationDescription, [2]: RailroadChunk}[] ---@param looped boolean local function collectChunks(stations, looped) for i = 2, #stations do if stations[i - 1][2] == stations[i][2] then error(string.format('Stations %s and %s are within the same chunk, move them closer to vertices', stations[i - 1][1].name, stations[i][1].name)) end end local route = findRoute(stations[1][2], stations[2][2]) if not route then error(string.format('Can’t find route from %s (%s) to %s (%s)', stations[1][1].name, stations[1][2].name, stations[2][1].name, stations[2][2].name)) end for i = 3, looped and #stations or #stations + 1 do local nextPiece = findRoute(stations[i - 1][2], stations[i] and stations[i][2], findContinuationIndex(route[#route], route[#route - 1])) if not nextPiece and i <= #stations then error(string.format('Can’t find route from %s (%s) to %s (%s)', stations[i - 1][1].name, stations[i - 1][2].name, stations[i][1].name, stations[i][2].name)) end if nextPiece then if nextPiece[1] ~= route[#route] then error('Unexpected continuation') end route = table.chain(route, table.slice(nextPiece, 2)) end end if looped then local nextPiece = findRoute(stations[#stations][2], stations[1][2], findContinuationIndex(route[#route], route[#route - 1]), findContinuationIndex(route[2], route[1])) if not nextPiece then error(string.format('Can’t find route from %s (%s) to %s (%s)', stations[#stations][1].name, stations[#stations][2].name, stations[1][1].name, stations[1][2].name)) else route = table.chain(route, table.slice(nextPiece, 2, #nextPiece - 1)) end else local nextPiece = findRoute(stations[1][2], nil, findContinuationIndex(route[1], route[2])) if nextPiece ~= nil then route = table.chain(table.reverse(nextPiece), table.slice(route, 2)) end end return route end ---@param chunks RailroadChunk[] ---@param looped boolean ---@return vec3[] ---@return RailroadTweaks[] local function extractSplineFromChunks(chunks, looped) local s = findContinuationIndex(chunks[1], chunks[2]) == 1 and 0 or 1 local p, n = {chunks[1].points[s == 1 and 1 or #chunks[1].points]}, 2 local m, d = {}, 0 for i = 1, #chunks do local c = chunks[i] local x = i > 1 and findContinuationIndex(chunks[i], chunks[i - 1]) or s for j = x == 1 and 2 or #c.points - 1, x == 1 and #c.points or 1, x == 1 and 1 or -1 do p[n], n = c.points[j], n + 1 end local l = c.length table.insert(m, {from = d, to = d + l, speedMultiplier = c.line.speedMultiplier or 1, colliding = c.line.colliding or false, lights = c.line.lights or false}) d = d + l end for i = 1, #m do m[i].from, m[i].to = m[i].from / d, m[i].to / d end return p, m end ---@class Railroad ---@field schedules RailroadSchedule[] ---@field gates RailroadGate[] ---@field trainFactory RailroadTrainFactory Railroad = class 'Railroad' ---@param data RailroadData ---@return Railroad function Railroad.allocate(data) RailroadUtils.raiseSettingsUpdate(data.settings or {}) local trainFactory = RailroadTrainFactory(data.trains) ac.log('Creating graph') local stationPoints = table.map(data.stations, function (item) return item.position end) local chunks = convertToGraph(data.lines, stationPoints) ac.log('Mapping stations') local mappedStations = table.map(data.stations, function (item) return { item, findNearestChunk(chunks, item.position) or error('Station '..item.name..' is too far from any lanes') }, item.index end) ac.log('Mapping schedules') local mappedSchedules = table.map(data.schedules, function (item) ---@param item ScheduleDescription return RailroadUtils.tryCreate(item, function () local pathChunks = collectChunks(table.map(item.points, function (point) ---@param point SchedulePointDescription return mappedStations[point.station] or error('Station is missing: '..point.station) end), item.looped) local spline, tweaks = extractSplineFromChunks(pathChunks, item.looped) return RailroadSchedule(item, table.map(item.points, function (point) return mappedStations[point.station][1].position end), spline, tweaks, trainFactory) end) end) ac.log('Mapping gates') local mappedGates = table.map(data.gates, function (gate) ---@param gate GateDescription return RailroadUtils.tryCreate(gate, function () local hits = table.map(mappedSchedules, function (item) ---@param item RailroadSchedule local located = item:locate(gate.position) if located and gate.colliding then table.insert(item.collidingGatesLocations, located) end return located and { located, item } or nil end) if #hits == 0 then ac.warn('No lines found next to a gate: '..gate.name) return nil end return RailroadGate(gate, hits) end) end) for i = 1, #mappedSchedules do local s = mappedSchedules[i] ---@type RailroadSchedule table.sort(s.collidingGatesLocations, function (a, b) return a < b end) end return {schedules = mappedSchedules, gates = mappedGates, trainFactory = trainFactory} end function Railroad:initialize() ac.log('Graph created') end function Railroad:dispose() for i = 1, #self.schedules do self.schedules[i]:dispose() end for i = 1, #self.gates do self.gates[i]:dispose() end self.trainFactory:dispose() end function Railroad:update(dt) for i = 1, #self.schedules do self.schedules[i]:update(dt) end for i = 1, #self.gates do self.gates[i]:update(dt) end end return class.emmy(Railroad, Railroad.allocate)