navigator.lua
--- Utilities and data relating to wilderness map navigation, both on land and sea.
--@module Navigator
Navigator = {
_error = -1, -- The
_lineInMap = 0, -- Ship's last seen line in Navigator.maps.current
_easting = -1, -- User's current easting
_southing = -1, -- User's current southing
_autoCalibrateDisabled = false,
}
Navigator.sailplans = {}
Navigator.cachedSailplan = {}
Navigator.maps = {}
Navigator.maps.full = {}
Navigator.harbourNames = {}
Navigator.lastKnownBearings = {}
Navigator.waypoints = {}
table.load(cloudDir..[[tables\Navigator_fullMap.lua]], Navigator.maps.full)
table.load(cloudDir..[[tables\waypoints.lua]], Navigator.waypoints)
table.load(cloudDir..[[tables\Navigator_harbourNames.lua]], Navigator.harbourNames)
-----------------------------------------------
--- Basic navigation
-- @section basicnav
-----------------------------------------------
----------------------------------------
--- Finds bearing and distance to waypoint from current location.
-- @tparam string targetWaypoint Shortname of the waypoint to located.
-- @treturn float Bearing to waypoint in degrees.
-- @treturn int Chebyshev distance to waypoint.
function Navigator.findWaypoint(targetWaypoint)
targetWaypoint = string.lower(targetWaypoint)
if Navigator.waypoints[targetWaypoint] then
local toWPbearing = Navigator.getBearing( Navigator.easting, Navigator.southing, Navigator.waypoints[targetWaypoint].easting, Navigator.waypoints[targetWaypoint].southing )
local toWPdistance = Navigator.getChebyshevDistance( Navigator.easting, Navigator.southing, Navigator.waypoints[targetWaypoint].easting, Navigator.waypoints[targetWaypoint].southing )
return toWPbearing, toWPdistance
else
return false, false
end
end
----------------------------------------
--- Returns the bearing to target coordinates from origin coordinates.
-- @tparam int _originX Origin point X-coordinate.
-- @tparam int _originY Origin point Y-coordinate.
-- @tparam int _targetX Target point X-coordinate.
-- @tparam int _targetY Target point Y-coordinate.
-- @treturn float Bearing to target coordinate in degrees.
-- @usage local bearing = myNavigator.getPointBearings( 418,443 , 553,221 )
-- @todo Rebuild this whole thing. It's a wreck.
function Navigator.getPointBearings(_originX,_originY,_targetX,_targetY)
--Predeclarations
local _quadrant = 0 -- What grid quadrant we're in, for converting to
local _polarAngle = 0 --
--Inverting the Y coordinate. UTM to screen coordinates, effectively.
local _originY = math.abs(_originY) * -1
local _targetY = math.abs(_targetY) * -1
--Find the target's coordinate relative to us, and we can assume we're at 0,0 to make this easy.
local _relativeX = (_targetX - _originX)
local _relativeY = (_targetY - _originY)
if _relativeX == 0 and _relativeY == 0 then
return 0
else
--Get the arctangent of the relative coordinates.
local _arctanResult = math.deg(math.atan(_relativeY/_relativeX))
--Find what quadrant our target point is in, and adjust the conversion accordingly.
if _relativeX >= 0 then
if _relativeY >= 0 then
_quadrant = 1
_polarAngle = _arctanResult
elseif _relativeY < 0 then
_quadrant = 4
_polarAngle = _arctanResult + 360
end
elseif _relativeX < 0 then
if _relativeY >= 0 then
_quadrant = 2
_polarAngle = _arctanResult + 180
elseif _relativeY < 0 then
_quadrant = 3
_polarAngle = _arctanResult + 180
end
end
-- Derive compass heading from polarAngle
local _bearing = ((360 - _polarAngle) + 90)
if _bearing > 360 then --But if the polar angle is too small, we'll blow past 360!
_bearing = _bearing - 360 --We'll subtract 360 if it does, that brings it back to correct.
end
return _bearing
end
end
----------------------------------------
--- Returns the Chebyshev distance between two points.
-- Does not account for impassable seas or obstacles.
-- @tparam int originX Origin point X-coordinate.
-- @tparam int originY Origin point Y-coordinate.
-- @tparam int targetX Target point X-coordinate.
-- @tparam int targetY Target point Y-coordinate.
-- @treturn int Chebyshev distance to target point.
-- @usage local distance = myNavigator.getChebyshevDistance(374,804,447,878)
function Navigator.getChebyshevDistance(originX,originY,targetX,targetY)
local cDist = math.max( math.abs( targetX - originX ), math.abs( targetY - originY ) )
return cDist
end
----------------------------------------
--- Locates an arbitrary number of the nearest harbours.
-- Gathers the name, distance, and bearings of the nearest harbours, and organises them for further processing.
-- Limited in number of returns by **numReturns**.
-- @tparam int numReturns The number of nearest harbours to return results for, after sorting.
-- @treturn table Indexed table containing information for the **numReturns** nearest harbours, sorted by ascending distance..
-- @usage local nearestHarbour = myNavigator.getNearestHarbours(1)[1]
function Navigator.getNearestHarbours(numReturns)
numReturns = numReturns or 9999
local distanceSortedHarbours = {}
for k,v in pairs(Navigator.waypoints) do
if v.type == "H" then
table.insert(distanceSortedHarbours, {["name"] = v.shortName, ["distance"] = v.distance, ["bearing"] = v.bearing } )
end
table.sort(distanceSortedHarbours, function(a,b) return (a.distance < b.distance) end)
for k,v in ipairs(distanceSortedHarbours) do
if k > numReturns then
distanceSortedHarbours[k] = nil
end
end
end
return distanceSortedHarbours
end
----------------------------------------
--- Returns a copy of the full waypoint table.
-- @treturn table A full copy of the master **Navigator.waypoints** table.
-- @usage for k,v in pairs(myNavigator.getWaypointTable()) do echo(v.name.."\n") end
function Navigator.getWaypointTable()
return table.deepcopy(Navigator.waypoints)
end
----------------------------------------
--- Something to do with movement speed.
function Navigator.calculateMovementSpeed()
local lastMetronTime = 0 --stopStopWatch(tempShipStopwatch)
local roundedBearing = "error"
local roundedDistance = "error"
Navigator.updateINS()
if Navigator.WPheading then
roundedBearing = string.format("%.1f",Navigator.WPheading)
end
if Navigator.WPdistance then
roundedDistance = Navigator.getChebyshevDistance(Navigator.easting,Navigator.southing,Navigator.WPeasting,Navigator.WPsouthing)
end
if roundedBearing == "225.0" or roundedBearing == "45.0" or roundedBearing == "315.0" or roundedBearing == "135.0" then
--roundedDistance = round((roundedDistance/1.414))
end
--local timeToTarget = tonumber(round(roundedDistance*lastMetronTime))
local timeToTarget = (roundedDistance*lastMetronTime)
local timeToTargetHr,timeToTargetMn,timeToTargetSc = shms(timeToTarget,false)
--moveCursor(70,getLineNumber())
--cinsertText("<light_sky_blue>S/M: <white>"..lastMetronTime.."<light_sky_blue>s")
--moveCursor(85,getLineNumber())
--cinsertText("<light_sky_blue>TTT: <white>"..timeToTargetMn..":"..timeToTargetSc.."<light_sky_blue>s")
resetStopWatch(tempShipStopwatch)
startStopWatch(tempShipStopwatch)
end
--------------------------------------------------------------------------------
--- Getters/setters.
-- For creating and initialising Navigator object.
-- @section getterssetters
--------------------------------------------------------------------------------
----------------------------------------
--- Returns the player's current easting.
-- @treturn int Current Navigator easting.
function Navigator.getEasting()
return Navigator._easting
end
----------------------------------------
--- Returns the player's current southing.
-- @treturn int Current Navigator southing.
function Navigator.getSouthing()
return Navigator._southing
end
----------------------------------------
--- Returns the player's current CEP.
-- @treturn int Current Navigator CEP.
function Navigator.getError()
return Navigator._error
end
----------------------------------------
--- Sets the player's current easting.
-- @tparam int input New Navigator easting.
-- @usage myNavigator.setEasting(261)
function Navigator.setEasting(input)
input = tonumber(input)
Navigator._easting = input
end
----------------------------------------
--- Sets the player's current southing.
-- @tparam int input New Navigator southing.
-- @usage myNavigator.setSouthing(729)
function Navigator.setSouthing(input)
input = tonumber(input)
Navigator._southing = input
end
----------------------------------------
--- Sets the player's current CEP.
-- @tparam int input New Navigator CEP.
-- @usage myNavigator.setError(2)
function Navigator.setError(input)
input = tonumber(input)
Navigator._error = input
end
--------------------------------------------------------------------------------
--- Location status
-- @section location
--------------------------------------------------------------------------------
----------------------------------------
--- Returns true if user is aboard a ship.
-- Does its best to avoid false positives, such as ferries, using a table of failure states as its metric.
-- @treturn bool Whether user is aboard a ship.
-- @usage if myNavigator.aboardShip() then...
-- @see Navigator.onSeafloor
function Navigator.aboardShip()
local failStates = {
-- If gmcp.Room isn't yet populatoed.
(not gmcp.Room),
-- If gmcp.Room.Info isn't yet populatoed.
(not gmcp.Room.Info),
-- If we're not even aboard a Vessel-type room.
gmcp.Room.Info.environment ~= "Vessel",
-- If we're on a Vessel, but there's no exits, and we're not in the crow's nest or bell... must be a ferry.
(table.size(gmcp.Room.Info.exits) == 0) and (not string.find(string.lower(gmcp.Room.Info.name),"row's nest")) and (not string.find(string.lower(gmcp.Room.Info.name),"within a diving bell")),
-- ??????????? Why this?????
--string.find(gmcp.Room.Info.name,"crow's nest"),
-- Well, being aboard the Margam doesn't help us much.
string.find(gmcp.Room.Info.name,"Margam"),
}
return not table.contains(failStates,true)
end
----------------------------------------
--- Returns true if Navigator has a valid position set.
-- @treturn bool True, if valid position set.
-- @usage if myNavigator.havePosition() then...
function Navigator.havePosition()
return ((Navigator._easting >= 0) and (Navigator._southing >= 0))
end
----------------------------------------
--- Returns true if user is in a subdivision.
-- @treturn bool True, if in subdivision.
-- @usage if myNavigator.inSubdivision() then...
function Navigator.inSubdivision()
if not Navigator.inWildernessMap() then return false end
local atpGrid, _, _ = string.match(gmcp.Room.Info.num,"^(%d+)(%d%d%d)(%d%d%d)$")
return (tonumber(atpGrid) >= 17 and tonumber(atpGrid) <= 30)
end
----------------------------------------
--- Returns true if user is in the wilderness map.
-- Does not count being aboard a vessel as being in the wilderness map.
-- @treturn bool True, if in subdivision.
-- @usage if myNavigator.aboardShip() then...
-- @see Navigator.aboardShip
-- @see Navigator.onSeafloor
function Navigator.inWildernessMap()
return (utf8.len(gmcp.Room.Info.num) >= 7)
end
----------------------------------------
--- Returns true if user is in in a seafloor area.
-- Useful to keep us from zeroing out data when we don't actually want to.
-- @treturn bool True, if in seafloor area.
-- @usage if myNavigator.onSeafloor() then...
-- @see Navigator.aboardShip
-- @see Navigator.inWildernessMap
function Navigator.onSeafloor()
return (gmcp.Room.Info.area == "deep below the sea")
end
--------------------------------------------------------------------------------
--- Conversions.
-- @section conversions
--------------------------------------------------------------------------------
----------------------------------------
--- Converts a cardinal-direction string to heading in degrees.
-- @tparam string _cardinalDirection Direction (e.g. "north", "e", "south-southwest", "ene")
-- @treturn float Heading in degrees
function Navigator.cardinalToHeading(_cardinalDirection)
local _cardinalDirection = string.lower(string.trim(_cardinalDirection))
local _lookupTable = {
["north"] = 360, ["n"] = 360,
["north-northeast"] = 22.5, ["nne"] = 22.5,
["northeast"] = 45, ["ne"] = 45,
["east-northeast"] = 67.5, ["ene"] = 67.5,
["east"] = 90, ["e"] = 90,
["east-southeast"] = 112.5, ["ese"] = 112.5,
["southeast"] = 135, ["se"] = 135,
["south-southeast"] = 157.5, ["sse"] = 157.5,
["south"] = 180, ["s"] = 180,
["south-southwest"] = 202.5, ["ssw"] = 202.5,
["southwest"] = 225, ["sw"] = 225,
["west-southwest"] = 247.5, ["wsw"] = 247.5,
["west"] = 270, ["w"] = 270,
["west-northwest"] = 292.5, ["wnw"] = 292.5,
["northwest"] = 315, ["nw"] = 315,
["north-northwest"] = 337.5, ["nnw"] = 337.5,
}
return _lookupTable[_cardinalDirection] or -1
end
----------------------------------------
--- Converts standard three-parameter ATP coordinates to screen coordinates.
-- Used in setting our position when we have a wilderness room number.
-- @tparam int _roomNum The room number we wish to convert.
-- @treturn int Easting in screen coordinates.
-- @treturn int Southing in screen coordinates.
function Navigator.roomNumToCoords(_roomNum)
local _roomNum_square, _roomNum_easting, _roomNum_southing = string.match(tostring(_roomNum), "^(%d+)(%d%d%d)(%d%d%d)$")
_roomNum_square = tonumber(_roomNum_square)
_roomNum_easting = tonumber(_roomNum_easting)
_roomNum_southing = tonumber(_roomNum_southing)
local _adjustmentMatrix = {
[1] = { 0, 0 },
[2] = { 250, 0 },
[3] = { 500, 0 },
[4] = { 750, 0 },
[5] = { 0, 250 },
[6] = { 250, 250 },
[7] = { 500, 250 },
[8] = { 750, 250 },
[9] = { 0, 500 },
[10] = { 250, 500 },
[11] = { 500, 500 },
[12] = { 750, 500 },
[13] = { 0, 750 },
[14] = { 250, 750 },
[15] = { 500, 750 },
[16] = { 750, 750 },
[31] = { 0, 1000 },
[32] = { 250, 1000 },
[33] = { 500, 1000 },
[34] = { 750, 1000 },
[35] = { 0, 1250 },
[36] = { 250, 1250 },
[37] = { 500, 1250 },
[38] = { 750, 1250 },
[39] = { 0, 1500 },
[40] = { 250, 1500 },
[41] = { 500, 1500 },
[42] = { 750, 1500 },
}
local _return1 = (_roomNum_easting + _adjustmentMatrix[_roomNum_square][1]) or -1
local _return2 = (_roomNum_southing + _adjustmentMatrix[_roomNum_square][2]) or -1
return _return1, _return2
end
--------------------------------------------------------------------------------
--- Auto-calibration.
-- @section autocal
--------------------------------------------------------------------------------
----------------------------------------
--- Returns current coordinates adjusted for input direction.
-- Also invoked by Figurehead for wavecall.
-- @tparam string _dirMoved Short-form direction string.
-- @tparam int _inputEasting Current Navigator easting.
-- @tparam int _inputSouthing Current Navigator southing.
-- @treturn int Adjusted easting.
-- @treturn int Adjusted southing.
-- @usage myNavigator.adjustCoords("n",625,715)
function Navigator.adjustCoords(_dirMoved,_inputEasting,_inputSouthing)
local _errorString = "meep"
local _cleanDirMoved = "uwu"
_inputEasting = tonumber(_inputEasting)
_inputSouthing = tonumber(_inputSouthing)
local _adjustmentMatrix = {
["n"] = { 0, -1 },
["ne"] = { 1, -1 },
["e"] = { 1, 0 },
["se"] = { 1, 1 },
["s"] = { 0, 1 },
["sw"] = { -1, 1 },
["w"] = { -1, 0 },
["nw"] = { -1, -1 }
}
---------------
if type(_dirMoved) == "string" then
_cleanDirMoved = string.lower( string.trim(_dirMoved) )
_errorString = "\"".._cleanDirMoved.."\""
else
_cleanDirMoved = _dirMoved
_errorString = tostring(_dirMoved)
end
assert(_adjustmentMatrix[_cleanDirMoved], "Method Navigator.adjustCoords() - _adjustmentMatrix["..tostring(_errorString).."] does not exist!")
---------------
echo("Old coords: "..tostring(_inputEasting)..", "..tostring(_inputSouthing).."\n")
echo("New coords: " .. tostring(_inputEasting + _adjustmentMatrix[_cleanDirMoved][1]) .. ", " .. tostring(_inputSouthing + _adjustmentMatrix[_cleanDirMoved][2]) .. "\n\n" )
---------------
return (_inputEasting + _adjustmentMatrix[_cleanDirMoved][1]), (_inputSouthing + _adjustmentMatrix[_cleanDirMoved][2])
end
----------------------------------------
--- Doesn't actually autocalibrate, just takes in stuff to pass to self variables.
-- @todo Probably just throw it out tbh
-- @tparam int inputEasting New easting to set.
-- @tparam int inputSouthing New southing to set.
function Navigator.autoCal(inputEasting, inputSouthing)
if not Navigator.autoCalibrateDisabled then
if Navigator.easting ~= inputEasting or Navigator.southing ~= inputSouthing then
oldEasting = Navigator.easting
oldSouthing = Navigator.southing
Navigator.easting = inputEasting
Navigator.southing = inputSouthing
Navigator.error = 0
Navigator.movedNonCardinal = false
--svo.prompttrigger("new calibration set", function() cecho("<green>INS auto-calibrated: <grey>"..oldEasting..","..oldSouthing.." > "..Navigator.easting..","..Navigator.southing.."") end )
raiseEvent("ship position updated")
elseif Navigator.easting == inputEasting and Navigator.southing == inputSouthing then
Navigator.error = 0
raiseEvent("ship position updated")
end
end
end
----------------------------------------
--- Populates searchTable with the relevant rows of text, before we begin finding candidates.
-- @tparam bool doFullSearch True, to search the entire map for candidates. False if building a limited array to search through.
function Navigator.buildSearchTable(doFullSearch)
if doFullSearch then
Navigator.searchTable = Navigator.fullMap
Navigator.searchRow = 1
Navigator.searchY = Navigator.searchRow - 1
else -- Not implemented yet, full search is okay for now
--[[local mapHeight = #Navigator.currentMap
local numberOfRowsToSearch = (mapHeight + (Navigator.error * 10) + 10)
for i=1,searchHeight do table.insert(Navigator.searchRows,Navigator.fullMap[i+])
end
(#Navigator.currentMap-1)/2
Navigator.searchRow = ]]--
end
end
----------------------------------------
--- It checks candidates?
function Navigator.checkCandidates(locationCandidates,shipColumn)
local inLoop = true
local reverseSearch = false
local searchWidthPullback = 0
while inLoop do
inLoop = false
-- Check if tile above is ocean before continuing!
-- Run a comparative check on each entry
if table.size(locationCandidates) >= 2 then
if reverseSearch then
local tempTable = {}
for k,v in pairs(locationCandidates) do
table.insert(tempTable,{["x_pos"] = v.x_pos, ["y_pos"] = v.y_pos})
end
locationCandidates = table.deepcopy(tempTable)
end
for k,v in ipairs(locationCandidates) do
local confidence = 0
for i=1,Navigator.lineInMap-1+searchWidthPullback do
local delta = i
if reverseSearch then delta = i*-1 end
local searchTerm = string.sub( Navigator.fullMap[v.y_pos+1+delta], v.x_pos-#string.gsub(westString,"%%","")+1, v.x_pos+#string.gsub(eastString,"%%","")+1)
if string.find(searchTerm, Navigator.currentMap[Navigator.lineInMap+delta]) then
confidence = confidence + 1
else
locationCandidates[k] = nil
break
end
if confidence >= Navigator.lineInMap-1-searchWidthPullback-2 then
break
end
end
end
end
-- Stop if we have no entries in locationCandidates
if table.size(locationCandidates) == 0 then
navUpdateResult = 0
inLoop = nil
return
end
-- Stop if we have only one entry in locationCandidates
if table.size(locationCandidates) == 1 then
local final_x = -1
local final_y = -1
for k,v in pairs(locationCandidates) do
final_x = v.x_pos final_y = v.y_pos
end
navUpdateResult = 1
Navigator.autoCal(final_x, final_y)
inLoop = nil
return
end
if table.size(locationCandidates) >= 2 and not reverseSearch then
inLoop = true
reverseSearch = true
--echo("DOING REVERSE SEARCH\n")
else
inLoop = nil
end
--if reverseSearch then
-- echo("DOING FULL SEARCH\n")
-- --inLoop = true
-- reverseSearch = true
-- searchWidthPullback = -1
--end
end
--echo("a")
end
----------------------------------------
--- Old autocal
function Navigator.doShipAutocal()
-- CASES --
-- 1 - In open sea, use INS
-- 2 - Open sea line (ship is wildcard), delta located
-- 3 - Landmarked line, unique match
-- 4 - Landmarked line, multiple matches
--echo("debugWindow","\n")
-- Get the line we want to use as the basis of our search, and the delta of said line.
-- Remember that we have to assume anything could be beneath =, so we have to treat " = " as open sea!
local searchLine, searchLineDelta = Navigator.findFirstNonBlankLine()
Navigator.lineDelta = searchLineDelta or 0
-- If we're in blank everything, we're in open sea and have no ability to calibrate.
-- However, we should ignore this if we're docked. We can only be docked in so many spots!
if Navigator.isInOpenSeas() and not Navigator.docked then
navUpdateResult = "Open sea, in inertial mode"
mfd.writeNavPage()
blinkShipMovedIndicator("yellow")
return
end
-- Get the strings west and east of us, to help size things later
westString = string.sub(searchLine,1,(#searchLine-1)/2)
eastString = string.sub(searchLine,(#searchLine-1)/2+2)
-- Any sort of landmark? Cool, go get our locationCandidates table built
locationCandidates = Navigator.gatherCandidates(mapLineToPattern(searchLine),Navigator.fullMap,Navigator.lineDelta)
-------
-- If we only found one location candidate...
if #locationCandidates > 1 then
-- Delete entries that're more than a certain distance away.
local entriesDeleted = 0
local distanceLimit = 50
-----------
for k,v in pairs(locationCandidates) do
-- If it's reasonably close, delete it from the table. Don't do this if
local candidateDistance = Navigator.getChebyshevDistance(Navigator.easting,Navigator.southing,v.x_pos,v.y_pos)
if candidateDistance > distanceLimit and Navigator.easting >= 0 and Navigator.southing >= 0 and Navigator.error < 5 then
locationCandidates[k] = nil
entriesDeleted = entriesDeleted + 1
end
end
-----------
-- Go through the remaining candidates and evaluate them.
for k,v in pairs(locationCandidates) do
-- We have a location pair candidate
--echo("\nLocation candidate "..k.." ("..v.x_pos..","..v.y_pos.."):\n")
local totalMatch = false
local candidateString = string.sub( Navigator.fullMap[v.y_pos+1], v.x_pos-#westString+1, v.x_pos+#eastString+1 )
-- Search down from the northmost row (most negative delta)
for i=(Navigator.lineInMap-1)*-1,Navigator.lineInMap-1 do
local candidateString = string.sub( Navigator.fullMap[v.y_pos+1+i], v.x_pos-#westString+1, v.x_pos+#eastString+1 )
local searchPatternString = Navigator.currentMapPatterns[Navigator.lineInMap+i]
--echo("\""..Navigator.currentMap[Navigator.lineInMap+i].."\"")
if string.find(candidateString,searchPatternString) then
--echo("Match: Δ"..i.."\n")
else
--echo("Break: Δ"..i.."\n")
locationCandidates[k] = nil
break
end
end
end
-----------
if table.size(locationCandidates) == 1 then
for k,v in pairs(locationCandidates) do
Navigator.easting = v.x_pos
Navigator.southing = v.y_pos
Navigator.error = 0
navUpdateResult = "One candidate left ("..Navigator.easting..","..Navigator.southing..")"
end
end
--checkCandidates(locationCandidates, inputColumn)
elseif table.size(locationCandidates) == 1 then
for k,v in pairs(locationCandidates) do
Navigator.easting = v.x_pos
Navigator.southing = v.y_pos-Navigator.lineDelta
Navigator.error = 0
navUpdateResult = "Single candidate found ("..Navigator.easting..","..Navigator.southing.." Δ"..Navigator.lineDelta..")"
end
else
navUpdateResult = "Uhhhh"
end
-------------------------------
end
----------------------------------------
--- Finds nearest Δ candidate in Navigator.currentMap
.
-- @treturn string Contents of the line containing the located reference. Used in later steps to verify location candidates, before heading into confidence calculations.
-- @treturn int Latitudinal distance from the central calibration point, Δ. Negative values are north.
-- @usage local locatorString, lineDelta = myNavigator.findLineDelta()
function Navigator.findLineDelta()
-- If we're on an open ocean line
if string.find(string.gsub(Navigator.currentMap[Navigator.lineInMap],"="," "), "^ +$") then
local landFound = false
Navigator.lineDelta = false
-- Search southwards
for i=1,Navigator.lineInMap-1 do
local lineIsOcean = false
if string.find(string.gsub(Navigator.currentMap[Navigator.lineInMap+i],"="," "), "^ +$") then
lineIsOcean = true
else
lineIsOcean = false
end
if not lineIsOcean then
landFound = Navigator.currentMap[Navigator.lineInMap+i]
Navigator.lineDelta = i
break
end
end
-- Search northwards
for i=-1,(Navigator.lineInMap-1)*-1,-1 do
local lineIsOcean = false
if string.find(string.gsub(Navigator.currentMap[Navigator.lineInMap+i],"="," "), "^ +$") then
lineIsOcean = true
else
lineIsOcean = false
end
if not lineIsOcean then
landFound = Navigator.currentMap[Navigator.lineInMap+i]
Navigator.lineDelta = i
break
end
end
return landFound, Navigator.lineDelta
else
-- If not, just send back the line with delta 0.
return Navigator.currentMap[Navigator.lineInMap], 0
end
end
----------------------------------------
--- Gathers the candidates for our line.
-- @todo Add optional latitude/longitude limiting, for performance.
-- @tparam string searchString Map line to gather candidates for.
function Navigator.gatherCandidates(searchString)
local candidateTable = {}
--if Navigator.isInOpenSeas() then return {} end
for k,v in ipairs(Navigator.fullMap) do
local searchResult = string.find(Navigator.fullMap[k],searchString)
if searchResult then
table.insert(candidateTable, {
["x_pos"] = tonumber(searchResult + #westString - 1),
["y_pos"] = tonumber(k-1),
})
end
end
return candidateTable
end
----------------------------------------
-- Gets search height.
function Navigator.getSearchHeight()
return #Navigator.currentMap
end
----------------------------------------
--- Feed our last movement direction to the INS routine. Adjusts current Navigator coords acordingly.
function Navigator.doINSmove()
-- Feed our last movement direction to the INS routine. Adjusts current Navigator coords acordingly.
local a = 1
end
----------------------------------------
--- See if all the lines of our map are blank spaces, to see if we even have any sort of landmarks.
function Navigator.isInOpenSeas()
for k,v in ipairs(Navigator.currentMap) do
if not string.find(string.gsub(v,"="," "),"^ +$") then
return false
end
end
return true
end
----------------------------------------
--- This finds things.
-- allegedly, not great though
-- @param inputTable what.
-- @param distanceLimit what.
function Navigator.trimLocationCandidates(inputTable, distanceLimit)
local entriesDeleted = 0
for k,v in pairs(locationCandidates) do
-- If it's reasonably close, delete it from the table. Don't do this if
local candidateDistance = Navigator.getChebyshevDistance(Navigator.easting,Navigator.southing,v.x_pos,v.y_pos)
if candidateDistance > distanceLimit and Navigator.easting >= 0 and Navigator.southing >= 0 and Navigator.error < 5 then
locationCandidates[k] = nil
cecho("<medium_spring_green:black>Deleted candidate: <white:black>"..v.x_pos.."<medium_spring_green:black>,<white:black>"..v.y_pos.." <medium_spring_green:black>(<white:black>"..candidateDistance.."<medium_spring_green:black> Chebyshev distance)\n")
entriesDeleted = entriesDeleted + 1
end
end
cecho("<cyan:black>Deleted <white:black>"..entriesDeleted.." <cyan:black>candidates. Distance limit <white:black>"..distanceLimit.."<cyan:black>. CEP <white:black>"..Navigator.error..".\n")
end
----------------------------------------
--- Old INS stuff.
function Navigator.updateINS()
if insDebug then
if insStopWatch then
stopStopWatch(insStopWatch)
deleteStopWatch(insStopWatch)
end
insStopWatch = createStopWatch()
startStopWatch(insStopWatch)
end
---------------------
--If no sailplan exists
if not Navigator.sailplan[1] then
Navigator.WPeasting = -1 -- Easting of current target waypoint
Navigator.WPsouthing = -1 -- Southing of current target waypoint
Navigator.WPheading = -1 -- Heading from current position to current waypoint
Navigator.WPdistance = -1 -- Distance from current poisition to current waypoint
Navigator.nextWPeasting = -1 -- Easting of waypoint after current
Navigator.nextWPsouthing = -1 -- Southing of waypoint after current
Navigator.nextWPheading = -1 -- Heading from current position to next waypoint
Navigator.nextWPdistance = -1 -- Distance from current poisition to next waypoint
Navigator.nextHeading = -1 -- Heading between current and next waypoints
Navigator.nextDistance = -1 -- Distance between current and next waypoints
--If a sailplan exists
elseif Navigator.sailplan[1] and Navigator.sailplan[(Navigator.sailplanIndex+1)] then
local wp1 = Navigator.sailplan[Navigator.sailplanIndex]
local wp2 = Navigator.sailplan[(Navigator.sailplanIndex+1)]
local toWPheading = Navigator.getBearing( Navigator.easting, Navigator.southing, wp1.easting, wp1.southing )
local toNextWPheading = Navigator.getBearing( Navigator.easting, Navigator.southing, wp2.easting, wp2.southing )
local currentToNextWPheading = Navigator.getBearing( wp1.easting, wp1.southing, wp2.easting, wp2.southing )
local toWPdistance = Navigator.getChebyshevDistance(Navigator.easting, Navigator.southing, wp1.easting, wp1.southing)
local toNextWPdistance = Navigator.getChebyshevDistance(Navigator.easting, Navigator.southing, wp2.easting, wp2.southing)
local currentToNextWPdistance = Navigator.getChebyshevDistance(wp1.easting, wp1.southing, wp2.easting, wp2.southing)
local turnToWaypoint = false
-- Assign to globals --
Navigator.WPeasting = wp1.easting -- Easting of current target waypoint
Navigator.WPsouthing = wp1.southing -- Southing of current target waypoint
Navigator.WPheading = toWPheading -- Heading from current position to current waypoint
Navigator.WPdistance = toWPdistance -- Distance from current poisition to current waypoint
Navigator.nextWPeasting = wp2.easting -- Easting of waypoint after current
Navigator.nextWPsouthing = wp2.southing -- Southing of waypoint after current
Navigator.nextWPheading = toNextWPheading -- Heading from current position to next waypoint
Navigator.nextWPdistance = toNextWPdistance -- Distance from current poisition to next waypoint
Navigator.nextHeading = currentToNextWPheading -- Heading between current and next waypoints
Navigator.nextDistance = currentToNextWPdistance -- Distance between current and next waypoints
if Navigator.isAtDestination() then
cecho("\n<green>At destination\n\n")
playSoundFile(cloudDir..[[Sounds\arriveDestination.wav]])
turnToWaypoint = true
elseif Navigator.nextWPheading == Navigator.nextHeading and gmcp.Room.Info.environment == "Vessel" and Navigator.heading ~= Navigator.nextHeading and Navigator.easting ~= -1 and Navigator.WPeasting ~= -1 then
cecho("\n<green>Intercepted course\n\n")
playSoundFile(cloudDir..[[Sounds\arriveDestination.wav]])
turnToWaypoint = true
end
if turnToWaypoint then
Navigator.ap.waypointTurn()
end
elseif Navigator.sailplan[1] and not Navigator.sailplan[2] then
local wp1 = Navigator.sailplan[Navigator.sailplanIndex]
local toWPheading, toWPdistance = Navigator.getBearing( Navigator.easting, Navigator.southing, wp1.easting, wp1.southing )
Navigator.WPeasting = wp1.easting -- Easting of current target waypoint
Navigator.WPsouthing = wp1.southing -- Southing of current target waypoint
Navigator.WPheading = toWPheading -- Heading from current position to current waypoint
--Navigator.WPdistance = toWPdistance -- Distance from current poisition to current waypoint
Navigator.WPdistance = math.abs(Navigator.easting - wp1.easting) + math.abs(Navigator.southing - wp1.southing)
Navigator.nextWPeasting = -1 -- Easting of waypoint after current
Navigator.nextWPsouthing = -1 -- Southing of waypoint after current
Navigator.nextWPheading = -1 -- Heading from current position to next waypoint
Navigator.nextWPdistance = -1 -- Distance from current poisition to next waypoint
Navigator.nextHeading = -1 -- Heading between current and next waypoints
Navigator.nextDistance = -1 -- Distance between current and next waypoints
end
if insStopWatch then
local insElapsedTime = stopStopWatch(insStopWatch)
display("INS routine time: "..insElapsedTime.."s")
deleteStopWatch(insStopWatch)
insStopWatch = nil
end
if Navigator.autopilotEnabled and Navigator.WPdistance < 10 and Navigator.speed > 0 and not Navigator.nextWpFlag then
Navigator.nextWpFlag = true
playSoundFile(cloudDir..[[Sounds\selfChord.wav]])
elseif Navigator.autopilotEnabled and Navigator.WPdistance < 10 and Navigator.speed > 0 then
Navigator.nextWpFlag = true
else
Navigator.nextWpFlag = nil
end
end
-----------------------------------------------
--- Sailplan stuff
-- @section sailplan
----------------------------------------
--- Returns true if current position matches destination position.
-- @treturn bool True if at destination position.
function Navigator.isAtDestination()
--if Navigator.easting ~= Navigator.WPeasting then
-- return false
--elseif Navigator.southing ~= Navigator.WPsouthing then
-- return false
--elseif gmcp.Room.Info.environment ~= "Vessel" then
-- return false
--elseif Navigator.easting == -1 then
-- return false
--else
-- return true
--end
end
----------------------------------------
--- Next steerpoint.
function Navigator.steerpointNext()
if Navigator.sailplan[(Navigator.sailplanIndex+1)] then
Navigator.sailplanIndex = Navigator.sailplanIndex + 1 -- Increment the index from 5 to 6, as WP6 (CITTN) is the next steerpoint
Navigator.WPeasting = Navigator.sailplan[Navigator.sailplanIndex].easting -- Get the easting of CITTN
Navigator.WPsouthing = Navigator.sailplan[Navigator.sailplanIndex].southing -- Get the southing of CITTN
end
end
----------------------------------------
--- Previous steerpoint.
function Navigator.steerpointPrevious()
if Navigator.sailplan[(Navigator.sailplanIndex-1)] then
Navigator.sailplanIndex = Navigator.sailplanIndex - 1
Navigator.WPeasting = Navigator.sailplan[Navigator.sailplanIndex].easting
Navigator.WPsouthing = Navigator.sailplan[Navigator.sailplanIndex].southing
end
end
-----------------------------------------------
--- Autopilot
-- @section autopilot
------------------------------------------------
--- Container for autopilot functionality
Navigator.autopilot = {
_autopilotEnabled = "", -- If autopilot is enabled
_stoppedToTurn = "", -- If autopilot is stopped for purposes of turning
}
----------------------------------------
--- These do things, I guess.
-- Yep. I guess.
function Navigator.checkAlignment() --
--local WPheading, _ = Navigator.getBearing( Navigator.easting, Navigator.southing, Navigator.WPeasting, Navigator.WPsouthing )
--if WPheading % 22.5 == 0 then
-- return true
--else
-- return false
--end
end
----------------------------------------
--- These do things, I guess.
-- Yep. I guess.
function Navigator.waypointTurn()
if not Navigator.sailplan[(Navigator.sailplanIndex+1)] and not Navigator.state == 0 and gmcp.Room.Info.environment == "Vessel" then
cecho("<green>\nDestination reached - autopilot disabled\n\n")
Navigator.autopilotEnabled = false
elseif Navigator.sailplan[Navigator.sailplanIndex] then
Navigator.sailplanIndex = Navigator.sailplanIndex + 1 -- Increment the index from 5 to 6, as WP6 (CITTN) is the next steerpoint
Navigator.WPeasting = Navigator.sailplan[Navigator.sailplanIndex].easting -- Get the easting of CITTN
Navigator.WPsouthing = Navigator.sailplan[Navigator.sailplanIndex].southing -- Get the southing of CITTN
if Navigator.autopilotEnabled then
local postTurnHeading, postTurnDistance = Navigator.getBearing(Navigator.easting,Navigator.southing,Navigator.WPeasting,Navigator.WPsouthing)
if Navigator.type ~= 1 then
send("ship all stop")
Navigator.stoppedToTurn = true
end
if postTurnHeading % 45 == 0 then
send("queue add ship ship turn "..Navigator.longToNumeric())
else
cecho("<orange>\nSteerpoint error! - Navigator.updateINS()\n\n")
playSoundFile(soundDir.."masterCaution.wav")
end
end
end
end
-----------------------------------------------
--- Maps and map readers
-- @section mapobject
------------------------------------------------
--- Container for maps
Navigator.maps = {}
----------------------------------------
--- Full world map as a table. Table indices are 1 higher than their actual coordinate value!
Navigator.maps.full = {}
----------------------------------------
--- The last map sent to us by the game.
-- @usage
-- {
-- "nnnnnnnnnn....wwwwww ",
-- "nnnnnnnnn.....wwwwwww ",
-- "nnnnnnnn.......wwwwww ",
-- "++++++++++++++.w.www ",
-- "nnnnnn.........wwwww ",
-- "nnnnnn..........ww ",
-- "nnnnn..........ww ",
-- "nnnnn.........ww ",
-- "nnnnn.......= ",
-- "nnnnn.........ww ",
-- "nnnnn..........www ",
-- "nnnnn.......wwwwww ",
-- "nnnnn......wwwwww ",
-- "nnnnnn.....www;www ",
-- "nnnnnnn...wwwwwwwww ",
-- "nnnnnnnn..wwwwwwwww ",
-- "nnnnnnnn..wwwwwwww "
-- }
Navigator.maps.current = {}
----------------------------------------
--- True if = under the cursor is our ship, as determined by text colour.
function Navigator.isOurShip()
if string.find(matches[1],"C/S") then return false end
local fg_r,fg_g,fg_b = getFgColor()
local bg_r,bg_g,bg_b = getBgColor()
return (fg_r..","..fg_g..","..fg_b == "255,255,255") and (bg_r..","..bg_g..","..bg_b == "0,0,0")
end
----------------------------------------
--- True if M under cursor is a seamonster, as determined by the text colour.
function Navigator.isSeamonster()
if string.find(matches[1],"C/S") then return false end
local fg_r,fg_g,fg_b = getFgColor()
local bg_r,bg_g,bg_b = getBgColor()
return (fg_r..","..fg_g..","..fg_b == "255,22,255") and (bg_r..","..bg_g..","..bg_b == "0,0,0")
end
----------------------------------------
--- True if = under the cursor is our ship, as determined by text colour.
function Navigator.isTargetShip()
if string.find(matches[1],"C/S") then return false end
local fg_r,fg_g,fg_b = getFgColor()
local bg_r,bg_g,bg_b = getBgColor()
return (fg_r..","..fg_g..","..fg_b == "255,22,255") and (bg_r..","..bg_g..","..bg_b == "0,0,0")
end