diff --git a/cmd_better_build_pathing.lua b/cmd_better_build_pathing.lua index 10bced9..c42b0e0 100644 --- a/cmd_better_build_pathing.lua +++ b/cmd_better_build_pathing.lua @@ -1,10 +1,11 @@ function widget:GetInfo() return { name = "Better Build Pathing", - desc = "Optimizes builder's pathing when the builder needs to move out of the way of the building location. The builder(s) will always move towards the shortest distance vector outward from the center of the building.", + desc = + "Optimizes builder's pathing when the builder needs to move out of the way of the building location. The builder(s) will always move towards the shortest distance vector outward from the center of the building.", author = "Errrrrrr", date = "June, 2023", - version = "1.0", + version = "1.1", license = "GNU GPL, v2 or later", layer = 10, enabled = true, @@ -25,6 +26,11 @@ local spGetFeaturePosition = Spring.GetFeaturePosition local spGetUnitDefID = Spring.GetUnitDefID local spGiveOrderToUnit = Spring.GiveOrderToUnit +-- debug drawing globals for build target and builder +local buildTargetVertexes = { 0, 0, 0, 0 } +local builderVertexes = { 0, 0, 0, 0 } +local drawBoxes = false + function widget:Initialize() selectedUnits = Spring.GetSelectedUnits() end @@ -61,49 +67,143 @@ end -- What commands can be issued at a position or unit/feature ID (Only used by GetUnitPosition) local CMD_SETTARGET = 34923 local positionCmds = { - [CMD.MOVE]=true, [CMD.ATTACK]=true, [CMD.RECLAIM]=true, [CMD.RESTORE]=true, [CMD.RESURRECT]=true, - [CMD.PATROL]=true, [CMD.CAPTURE]=true, [CMD.FIGHT]=true, [CMD.MANUALFIRE]=true, - [CMD.UNLOAD_UNIT]=true, [CMD.UNLOAD_UNITS]=true,[CMD.LOAD_UNITS]=true, [CMD.GUARD]=true, [CMD.AREA_ATTACK] = true, + [CMD.MOVE] = true, + [CMD.ATTACK] = true, + [CMD.RECLAIM] = true, + [CMD.RESTORE] = true, + [CMD.RESURRECT] = true, + [CMD.PATROL] = true, + [CMD.CAPTURE] = true, + [CMD.FIGHT] = true, + [CMD.MANUALFIRE] = true, + [CMD.UNLOAD_UNIT] = true, + [CMD.UNLOAD_UNITS] = true, + [CMD.LOAD_UNITS] = true, + [CMD.GUARD] = true, + [CMD.AREA_ATTACK] = true, [CMD_SETTARGET] = true -- set target } -local function GetUnitFinalPosition(uID) +-- this requires modification if we want to only use CMD.MOVE +local function GetUnitFinalPosition(uID) + --Spring.Echo("GetUnitFinalPosition") local ux, uy, uz = spGetUnitPosition(uID) - local cmds = spGetCommandQueue(uID,5000) - if cmds then - for i = #cmds, 1, -1 do - - local cmd = cmds[i] - if (cmd.id < 0) or positionCmds[cmd.id] then - - local params = cmd.params - if #params >= 3 then - return params[1], params[2], params[3] - else - if #params == 1 then - - local pID = params[1] - local px, py, pz - - if pID > 32000 then - px, py, pz = spGetFeaturePosition(pID - 32000) - else - px, py, pz = spGetUnitPosition(pID) - end - - if px then - return px, py, pz - end - end - end - end - end - end + local cmds = spGetCommandQueue(uID, 5000) + if cmds then + for i = #cmds, 1, -1 do + local cmd = cmds[i] + if (cmd.id < 0) or positionCmds[cmd.id] then + local params = cmd.params + if #params >= 3 then + return params[1], params[2], params[3] + else + if #params == 1 then + local pID = params[1] + local px, py, pz + + if pID > 32000 then + px, py, pz = spGetFeaturePosition(pID - 32000) + else + px, py, pz = spGetUnitPosition(pID) + end + + if px then + return px, py, pz + end + end + end + end + end + end return ux, uy, uz end +-- this function determines whether the building location is obstructed by the supplied unitID +local function isObstructed(builderID, buildtargetID, builderX, builderZ, buildX, buildZ, buildFacing) + -- use the size of builderID and buildtargetID and the build location as well as buildFacing to determine if the build location is obstructed by the builder + local builderDef = UnitDefs[spGetUnitDefID(builderID)] + local buildtargetDef = UnitDefs[buildtargetID] + + local builderSizeX, builderSizeZ = builderDef.xsize, builderDef.zsize + local buildtargetSizeX, buildtargetSizeZ = buildtargetDef.xsize, buildtargetDef.zsize + + -- if buildFacing is odd, flip the buildTargetSizeX and buildTargetSizeZ + if buildFacing % 2 == 1 then + buildtargetSizeX, buildtargetSizeZ = buildtargetSizeZ, buildtargetSizeX + end + + -- determine if the area occupied by the builder is overlapping with the area occupied by the building + local builderMinX, builderMinZ = builderX - builderSizeX / 2, builderZ - builderSizeZ / 2 + local builderMaxX, builderMaxZ = builderX + builderSizeX / 2, builderZ + builderSizeZ / 2 + + local buildtargetMinX, buildtargetMinZ = buildX - buildtargetSizeX / 2, buildZ - buildtargetSizeZ / 2 + local buildtargetMaxX, buildtargetMaxZ = buildX + buildtargetSizeX / 2, buildZ + buildtargetSizeZ / 2 + + -- we want to set some tolerance for the overlap, so we add tolerance to every direction of the checking + local tolerance = 1 + if builderMinX > buildtargetMaxX + tolerance or builderMaxX < buildtargetMinX - tolerance then return false end + if builderMinZ > buildtargetMaxZ + tolerance or builderMaxZ < buildtargetMinZ - tolerance then return false end + + --Spring.Echo("isObstructed: true") + return true +end + +-- This function returns the vector flag that the builder should move to for minimal movement +local function CalculateMovement(builderPos, buildTargetPos, builderID, buildID, buildFacing, tolerance) + -- get unit definitions + local buildDef = UnitDefs[buildID] -- remember buildID is actually the unitDefID of the build target!!! + local builderDef = UnitDefs[Spring.GetUnitDefID(builderID)] + + if not buildDef or not builderDef then return end + + local builderXSize, builderZSize = builderDef.xsize, builderDef.zsize + local buildXSize, buildZSize = buildDef.xsize, buildDef.zsize + + if buildFacing % 2 == 1 then + buildXSize, buildZSize = buildZSize, buildXSize + end + + -- Get xsize and zsize for both units + local builderSizeX = builderXSize * 0.5 -- Spring uses full footprint sizes, while we want radius-like sizes. + local builderSizeZ = builderZSize * 0.5 + local buildTargetSizeX = buildXSize * 0.5 + local buildTargetSizeZ = buildZSize * 0.5 + + -- calculate the difference in positions + local dx = buildTargetPos[1] - builderPos[1] + local dz = buildTargetPos[3] - builderPos[3] + + -- calculate the minimal non-overlapping distances in both axes + local minDistX = buildTargetSizeX + builderSizeX + local minDistZ = buildTargetSizeZ + builderSizeZ + + -- Now, we have to decide in which direction we want to move our builder unit + -- We choose the direction where we have to move the least. + + -- calculate the current overlaps (if any) + local overlapX = minDistX - math.abs(dx) + local overlapZ = minDistZ - math.abs(dz) + -- print debug info + --Spring.Echo("minDistX: " .. minDistX .. " minDistZ: " .. minDistZ) + + -- Initialize the movement vector + local moveVector = { 0, 0, 0 } + + -- Now decide where to move. + if math.abs(overlapX) > math.abs(overlapZ) then + -- Move along x + moveVector[1] = 1 + else + -- Move along z + moveVector[3] = 1 + end + --Spring.Echo("Line vector for unitID: ".. builderID..", : " .. moveVector[1] .. ", " .. moveVector[3]) + + return moveVector +end + function widget:CommandNotify(cmdID, cmdParams, cmdOpts) if #selectedUnits > max_builders_affected then return end if #cmdParams < 3 then return end @@ -113,10 +213,17 @@ function widget:CommandNotify(cmdID, cmdParams, cmdOpts) local buildID = -cmdID local buildUnitDef = UnitDefs[buildID] - local obstructed = Spring.TestBuildOrder(buildID, cmdParams[1], cmdParams[2], cmdParams[3], 0) - obstructed = obstructed + Spring.TestBuildOrder(buildID, cmdParams[1], cmdParams[2], cmdParams[3], 1) + local buildFacing = Spring.GetBuildFacing() - if obstructed == 4 then return end -- not obstructed by anything (safely) + -- the following gives us 0 if obstructed by a unit, 1 if obstructed by a feature, and 2 if not obstructed + local obstructed2 = Spring.TestBuildOrder(buildID, cmdParams[1], cmdParams[2], cmdParams[3], buildFacing) + + -- for now we only double check the first builder's final position in queue, maybe in future we'll include all builders + local builder1 = selectedUnits[1] + local builder1X, _, builder1Z = GetUnitFinalPosition(builder1) + local obstructed = isObstructed(builder1, buildID, builder1X, builder1Z, cmdParams[1], cmdParams[3], buildFacing) + + if not obstructed and obstructed == 2 then return end -- not obstructed by anything -- add to a list of all the builders that can build this unit -- add to a list of all the assistants that can assist this unit @@ -142,38 +249,50 @@ function widget:CommandNotify(cmdID, cmdParams, cmdOpts) --local mainBuilder = builders[1] --table.remove(assistants, mainBuilder) + local builderPos = { 0, 0, 0 } for _, unitID in ipairs(assistants) do local x, y, z if cmdOpts.shift then - x, y, z = GetUnitFinalPosition(unitID) + builderPos[1], builderPos[2], builderPos[3] = GetUnitFinalPosition(unitID) else - x, y, z = spGetUnitPosition(unitID) + builderPos[1], builderPos[2], builderPos[3] = spGetUnitPosition(unitID) end - local builderPos = { x, y, z } - -- check if the current builder is obstructing the build target location if buildID == nil then return end + local builderID = unitID + local buildTargetPos = { cmdParams[1], cmdParams[2], cmdParams[3] } - local buildTarget = cmdParams - local buildTargetPos = { buildTarget[1], buildTarget[2], buildTarget[3] } - -- calculate the line vector from builder to build target, then move the builder back along this line until it no longer obstructs the build target - local lineVector = { buildTarget[1] - builderPos[1], buildTarget[2] - builderPos[2], - buildTarget[3] - builderPos[3] } + local lineVector = { buildTargetPos[1] - builderPos[1], 0, + buildTargetPos[3] - builderPos[3] } local lineVectorLength = math.sqrt(lineVector[1] ^ 2 + lineVector[2] ^ 2 + lineVector[3] ^ 2) - local lineVectorUnit = { + local lineVectorUnit = { lineVector[1] / lineVectorLength, lineVector[2] / lineVectorLength, lineVector[3] / lineVectorLength } - local offset = isABuilder[unitID] and 10 or 14 -- being safe is never a bad idea - local buildTargetPos = { - buildTargetPos[1] - lineVectorUnit[1] * buildUnitDef.xsize * offset, + -- calculate the line vector of the movement of the builder, we want the builder to move only along the x and z axis (because buildings are always rectangular) + local lineVector2 = CalculateMovement(builderPos, buildTargetPos, builderID, buildID, buildFacing) + + local offset = isABuilder[unitID] and 11 or 14 -- being safe is never a bad idea + --[[ local canTurnInPlace = UnitDefs[spGetUnitDefID(unitID)].turnInPlace + if canTurnInPlace then offset = offset - 1 end ]] + + -- account for buildFacing, if it's odd number, we switch x and z + local xsize = buildUnitDef.xsize + local zsize = buildUnitDef.zsize + if buildFacing % 2 == 1 then + xsize, zsize = zsize, xsize + end + local buildTargetPos = { -- we are removing y component, as well one of the other components + buildTargetPos[1] - lineVectorUnit[1] * xsize * offset * lineVector2[1], buildTargetPos[2], - buildTargetPos[3] - lineVectorUnit[3] * buildUnitDef.zsize * offset + buildTargetPos[3] - lineVectorUnit[3] * zsize * offset * lineVector2[3] } + Spring.MarkerAddPoint(buildTargetPos[1], buildTargetPos[2], buildTargetPos[3], "!") + -- detect if command is inserted or not if not cmdOpts.meta then -- issue command move @@ -196,3 +315,44 @@ function widget:CommandNotify(cmdID, cmdParams, cmdOpts) end return true end + +-- helper +function tableToString(t) + local result = "" + + if type(t) ~= "table" then + result = tostring(t) + elseif t == nil then + result = "nil" + else + for k, v in pairs(t) do + result = result .. "[" .. tostring(k) .. "] = " + + if type(v) == "table" then + result = result .. "{" + + for k2, v2 in pairs(v) do + result = result .. "[" .. tostring(k2) .. "] = " + + if type(v2) == "table" then + result = result .. "{" + + for k3, v3 in pairs(v2) do + result = result .. "[" .. tostring(k3) .. "] = " .. tostring(v3) .. ", " + end + + result = result .. "}, " + else + result = result .. tostring(v2) .. ", " + end + end + + result = result .. "}, " + else + result = result .. tostring(v) .. ", " + end + end + end + + return "{" .. result:sub(1, -3) .. "}" +end