1. For unfinished units/building, upon subsequent scout/spotting, updates the marker with the current building progress. Adds game time at which the info was acquired (e.g., "Antinuke (10% at 08:17)", then on the next scout replaces with "Antinuke (20% at 12:15)"), or, if finished, with simply "Antinuke").
2. For moving units, if spotted in a new location, removes the old marker, and places a new marker in the new location.
3. If a unit/building destroyed, removes its marker.
Upon installing, one needs to uncheck the original unit marker widget, and to select the desired units/buildings in "Settings->Interface->Tracking Unit Marker"
Widget code:
[Spoiler]function widget:GetInfo() return {
name = "Tracking Unit Marker",
desc = "[v1.0.0] Modified Sprung's unit_marker.lua. Marks spotted buildings and units of interest, updates location and building progress.",
author = "rollmops",
date = "2022-08-29",
license = "GNU GPL v2",
layer = -1,
enabled = true,
} end
-- the main modification is to remember the text and the position of the last marker for each unit (by unit I mean buildings also), and, if the same unitID is spotted in another location, or the text needs to be changed (e.g., building progress increased), to remove the previous marker and to make the new one.
-- in the original code, a spotted unit was remembered (in knownUnits) and no new markers were being issued for it. I removed this functionality.
-- non-commented parts are mostly the original Sprung's code.
local unitList
local activeDefID = {}
-- associative arrays, where keys are 'unitID's, to save the position and the text of the last marker of a given unit.
local lastMarkerText = {}
local lastPos = {}
-- since issuing spMarkerErasePosition (to remove the old marker) and then spMarkerAddPoint (to make a new marker) doesn't seem to work in cases where location is not changed (the new marker disappears after less than 1 second, I think it is somehow related to both commands being processed in the same drawing frame?), I made an ugly hack, namely, after spMarkerErasePosition, I defer the creation of the new marker: the text and the position of the new marker are stored in markersToMake, (keys are again 'unitID's), then the script counts 'frames_defer' game frames, and only then spMarkerAddPoint is issued.
local markersToMake = {}
local frames_defer = 15
local markingActive = false
local sp = Spring
local spGetAIInfo = sp.GetAIInfo
local spGetPlayerInfo = sp.GetPlayerInfo
local spGetSpectatingState = sp.GetSpectatingState
local spGetTeamInfo = sp.GetTeamInfo
local spGetUnitDefID = sp.GetUnitDefID
local spGetUnitHealth = sp.GetUnitHealth
local spGetUnitPosition = sp.GetUnitPosition
local spIsUnitAllied = sp.IsUnitAllied
local spMarkerAddPoint = sp.MarkerAddPoint
local spMarkerErasePosition = sp.MarkerErasePosition
local sputGetHumanName = sp.Utilities.GetHumanName
-- for debug
local Echo = sp.Echo
-- for additional feature: for markers of building in progress, add the game time at which the specified building progress was spotted
local spGetGameSeconds = sp.GetGameSeconds
if VFS.FileExists("LuaUI/Configs/unit_marker_local.lua", nil, VFS.RAW) then
unitList = VFS.Include("LuaUI/Configs/unit_marker_local.lua", nil, VFS.RAW)
else
unitList = VFS.Include("LuaUI/Configs/unit_marker.lua")
end
-- modified options_path, so there will be a separate settings menu for the modified unit maker
options_path = 'Settings/Interface/Trackign Unit Marker'
options_order = { 'enableAll', 'disableAll', 'unitslabel'}
options = {
enableAll = {
type='button',
name= "Enable All",
desc = "Marks all listed units.",
path = options_path .. "/Presets",
OnChange = function ()
for i = 1, #options_order do
local opt = options_order[i]
local find = string.find(opt, "_mark")
local name = find and string.sub(opt,0,find-1)
local ud = name and UnitDefNames[name]
if ud then
options[opt].value = true
end
end
for unitDefID in pairs(unitList) do
activeDefID[unitDefID] = true
end
if not markingActive then
widgetHandler:UpdateCallIn('UnitEnteredLos')
markingActive = true
end
end,
noHotkey = true,
},
disableAll = {
type='button',
name= "Disable All",
desc = "Mark nothing.",
path = options_path .. "/Presets",
OnChange = function ()
for i = 1, #options_order do
local opt = options_order[i]
local find = string.find(opt, "_mark")
local name = find and string.sub(opt,0,find-1)
local ud = name and UnitDefNames[name]
if ud then
options[opt].value = false
end
end
for unitDefID,_ in pairs(unitList) do
activeDefID[unitDefID] = false
end
if markingActive then
widgetHandler:RemoveCallIn('UnitEnteredLos')
markingActive = false
end
end,
noHotkey = true,
},
unitslabel = {name = "unitslabel", type = 'label', value = "Individual Toggles", path = options_path},
}
for unitDefID in pairs(unitList) do
local ud = UnitDefs[unitDefID]
options[ud.name .. "_mark"] = {
name = " " .. sputGetHumanName(ud) or "",
type = 'bool',
value = false,
OnChange = function (self)
activeDefID[unitDefID] = self.value
if self.value and not markingActive then
widgetHandler:UpdateCallIn('UnitEnteredLos')
markingActive = true
end
end,
noHotkey = true,
}
options_order[#options_order+1] = ud.name .. "_mark"
end
local function refreshCallin()
if not markingActive then
widgetHandler:RemoveCallIn("UnitEnteredLos")
end
if spGetSpectatingState() then
widgetHandler:RemoveCallIn("UnitEnteredLos")
elseif markingActive then
widgetHandler:UpdateCallIn('UnitEnteredLos')
end
end
widget.PlayerChanged = refreshCallin
widget.Initialize = refreshCallin
widget.TeamDied = refreshCallin
function widget:UnitEnteredLos (unitID, teamID)
if spIsUnitAllied(unitID) or spGetSpectatingState() then
return
end
local unitDefID = spGetUnitDefID (unitID)
if not unitDefID or not activeDefID[unitDefID] then
return
end
local data = unitList[unitDefID]
if not data then
return
end
local markerText = data.markerText or sputGetHumanName(UnitDefs[unitDefID])
if data.show_owner then
local _,playerID,_,isAI = spGetTeamInfo(teamID, false)
local owner_name
if isAI then
local _,botName,_,botType = spGetAIInfo(teamID)
owner_name = (botType or "AI") .." - " .. (botName or "unnamed")
else
owner_name = spGetPlayerInfo(playerID, false) or "nobody"
end
markerText = markerText .. " (" .. owner_name .. ")"
end
local _, _, _, _, buildProgress = spGetUnitHealth(unitID)
if buildProgress < 1 then
markerText = markerText .. " (" .. math.floor(100 * buildProgress) .. "% at " .. os.date( "%M:%S", spGetGameSeconds()) .. ")"
end
local x, y, z = spGetUnitPosition(unitID)
-- if there were no markers issued for the given unitID, make a marker immediately
if not lastMarkerText[unitID] then
spMarkerAddPoint (x, y, z, markerText, true)
-- if there was a marker, but the text of it or the location of the unit has changed, remove the existing marker and save the details of the new marker, which will be actually made after 'frames_defer' game frames, see widget:GameFrame below).
elseif markerText ~= lastMarkerText[unitID] or x ~= lastPos[unitID][1] or y ~= lastPos[unitID][2] or z ~= lastPos[unitID][3] then
spMarkerErasePosition(lastPos[unitID][1], lastPos[unitID][2], lastPos[unitID][3])
markersToMake[unitID] = { x, y, z, markerText, frames_defer }
end
-- save the text and position of the marker as last known.
lastPos[unitID] = {x, y, z}
lastMarkerText[unitID] = markerText
end
-- each game frame, loop over all the deferred markers and decrease their deferment counters. For a maker that its counter reached zero, issue marker command and remove its details from markersToMake
function widget:GameFrame()
for u, m in pairs(markersToMake) do
if m[5] > 0 then
markersToMake[u][5] = markersToMake[u][5] - 1
else
spMarkerAddPoint ( m[1], m[2], m[3], m[4], true)
markersToMake[u] = nil
end
end
end
-- if a unit destroyed, remove both actual and deferred markers and all the related info.
function widget:UnitDestroyed(unitID, unitDefID, unitTeam)
markersToMake[unitID] = nil
lastMarkerText[unitID] = nil
if lastPos[unitID] then spMarkerErasePosition(lastPos[unitID][1], lastPos[unitID][2], lastPos[unitID][3]) end
lastPos[unitID] = nil
end
The following part is for people having some experience with widgets:
Since it is my first widget, and actually first anything Spring-related and first Lua code, I suspect it can be improved. Particularly, I had the following issue: to update marker of a building in progress, I tried to issue
spMarkerErasePosition(position)
and then
spMarkerAddPoint(same postion, new text)
What happened is the old marker gone, but the new marker appeared just for half a second or so, then disappeared. Seems to me both commands were issued in the same drawing frame or something like that. To solve this I made an (ugly) hack: to defer the creation of the new marker for 15 game frames. Seems to work, but I think there should be some more elegant (native) solution. I commented the code, so if one wants to see how it works, it should be relatively easy.
More generally, I'm confused with the relationships between the following concepts: "game frame", "drawing frame", "sim frame", FPS.
For example, as mentioned in
https://springrts.com/wiki/Lua:Callins:addon.Update(dt)
Called for every draw frame (including when the game is paused) and at least once per sim frame except when catching up. The parameter is the time since the last update.
addon.GameFrame(frame)
Called for every game simulation frame (30 per second). Starts at frame 0 in 101.0+ and 1 in previous versions.
I'll be glad if someone could point to the explanation of these terms and how they are related to the issue I had.