From 4cee69e3e11c421bc28dda9daa668184add828b3 Mon Sep 17 00:00:00 2001 From: David Schwietering Date: Wed, 26 Mar 2025 19:59:22 +0100 Subject: [PATCH 01/73] WIP --- Courseplay.lua | 29 +- config/GraphEditorCategories.xml | 20 + config/MasterTranslations.xml | 163 ++++++ modDesc.xml | 21 +- scripts/dev/ConsoleCommands.lua | 2 + scripts/editor/EditorCourseWrapper.lua | 2 +- scripts/editor/EditorGraphWrapper.lua | 479 ++++++++++++++++++ scripts/editor/GraphEditor.lua | 62 +++ scripts/editor/brushes/BaseBrush.lua | 23 +- scripts/editor/brushes/graph/GraphBrush.lua | 62 +++ .../brushes/graph/misc/ConnectWaypoints.lua | 138 +++++ .../brushes/graph/misc/CurveLineWaypoints.lua | 105 ++++ .../graph/misc/MoveAdvancedWaypoint.lua | 72 +++ .../graph/misc/StraightLineWaypoints.lua | 118 +++++ .../brushes/graph/points/DeletePointBrush.lua | 44 ++ .../brushes/graph/points/InsertPointBrush.lua | 88 ++++ .../brushes/graph/points/MovePointBrush.lua | 41 ++ .../graph/points/StraightLinePointBrush.lua | 174 +++++++ .../graph/segments/ChangeSegmentTypBrush.lua | 21 + .../graph/segments/CreateSegmentBrush.lua | 24 + .../graph/segments/DeleteSegmentBrush.lua | 43 ++ .../graph/segments/MergeSplitSegmentBrush.lua | 52 ++ .../graph/targets/CreateTargetBrush.lua | 28 + .../graph/targets/DeleteTargetBrush.lua | 29 ++ .../graph/targets/RenameTargetBrush.lua | 30 ++ scripts/graph/Graph.lua | 108 ++++ scripts/graph/GraphNode.lua | 274 ++++++++++ scripts/graph/GraphPoint.lua | 206 ++++++++ scripts/graph/GraphSegment.lua | 136 +++++ scripts/graph/GraphTarget.lua | 29 ++ scripts/gui/CpInGameMenu.lua | 3 +- 31 files changed, 2608 insertions(+), 18 deletions(-) create mode 100644 config/GraphEditorCategories.xml create mode 100644 scripts/editor/EditorGraphWrapper.lua create mode 100644 scripts/editor/GraphEditor.lua create mode 100644 scripts/editor/brushes/graph/GraphBrush.lua create mode 100644 scripts/editor/brushes/graph/misc/ConnectWaypoints.lua create mode 100644 scripts/editor/brushes/graph/misc/CurveLineWaypoints.lua create mode 100644 scripts/editor/brushes/graph/misc/MoveAdvancedWaypoint.lua create mode 100644 scripts/editor/brushes/graph/misc/StraightLineWaypoints.lua create mode 100644 scripts/editor/brushes/graph/points/DeletePointBrush.lua create mode 100644 scripts/editor/brushes/graph/points/InsertPointBrush.lua create mode 100644 scripts/editor/brushes/graph/points/MovePointBrush.lua create mode 100644 scripts/editor/brushes/graph/points/StraightLinePointBrush.lua create mode 100644 scripts/editor/brushes/graph/segments/ChangeSegmentTypBrush.lua create mode 100644 scripts/editor/brushes/graph/segments/CreateSegmentBrush.lua create mode 100644 scripts/editor/brushes/graph/segments/DeleteSegmentBrush.lua create mode 100644 scripts/editor/brushes/graph/segments/MergeSplitSegmentBrush.lua create mode 100644 scripts/editor/brushes/graph/targets/CreateTargetBrush.lua create mode 100644 scripts/editor/brushes/graph/targets/DeleteTargetBrush.lua create mode 100644 scripts/editor/brushes/graph/targets/RenameTargetBrush.lua create mode 100644 scripts/graph/Graph.lua create mode 100644 scripts/graph/GraphNode.lua create mode 100644 scripts/graph/GraphPoint.lua create mode 100644 scripts/graph/GraphSegment.lua create mode 100644 scripts/graph/GraphTarget.lua diff --git a/Courseplay.lua b/Courseplay.lua index a3e239124..12c063a93 100644 --- a/Courseplay.lua +++ b/Courseplay.lua @@ -32,6 +32,7 @@ function Courseplay:registerXmlSchema() CpBaseHud.registerXmlSchema(self.xmlSchema, self.xmlKey) CpHudInfoTexts.registerXmlSchema(self.xmlSchema, self.xmlKey) CpInGameMenu.registerXmlSchema(self.xmlSchema, self.xmlKey) + Graph.registerXmlSchema(self.xmlSchema, self.xmlKey) end --- Loads data not tied to a savegame. @@ -115,6 +116,7 @@ function Courseplay:loadMap(filename) self.xmlFile = XMLFile.load("cpXml", filePath , self.xmlSchema) if self.xmlFile == nil then return end self.globalSettings:loadFromXMLFile(self.xmlFile, g_Courseplay.xmlKey) + g_graph:loadFromXMLFile(self.xmlFile, g_Courseplay.xmlKey) self.xmlFile:delete() end @@ -128,6 +130,7 @@ function Courseplay:deleteMap() self:saveUserSettings() end g_courseEditor:delete() + g_graphEditor:delete() BufferedCourseDisplay.deleteBuffer() g_signPrototypes:delete() g_consoleCommands:delete() @@ -166,16 +169,19 @@ end --- Saves all global data, for example global settings. function Courseplay.saveToXMLFile(missionInfo) if missionInfo.isValid then - local saveGamePath = missionInfo.savegameDirectory .."/" - local xmlFile = XMLFile.create("cpXml", saveGamePath.. "Courseplay.xml", - "Courseplay", g_Courseplay.xmlSchema) - if xmlFile then - g_Courseplay.globalSettings:saveToXMLFile(xmlFile, g_Courseplay.xmlKey) - xmlFile:save() - xmlFile:delete() - end - g_Courseplay:saveUserSettings() - g_assignedCoursesManager:saveAssignedCourses(saveGamePath) + CpUtil.try(function () + local saveGamePath = missionInfo.savegameDirectory .."/" + local xmlFile = XMLFile.create("cpXml", saveGamePath.. "Courseplay.xml", + "Courseplay", g_Courseplay.xmlSchema) + if xmlFile then + g_Courseplay.globalSettings:saveToXMLFile(xmlFile, g_Courseplay.xmlKey) + g_graph:saveToXMLFile(xmlFile, g_Courseplay.xmlKey) + xmlFile:save() + xmlFile:delete() + end + g_Courseplay:saveUserSettings() + g_assignedCoursesManager:saveAssignedCourses(saveGamePath) + end) end end FSCareerMissionInfo.saveToXMLFile = Utils.prependedFunction(FSCareerMissionInfo.saveToXMLFile, Courseplay.saveToXMLFile) @@ -186,6 +192,8 @@ function Courseplay:update(dt) g_triggerManager:update(dt) g_baleToCollectManager:update(dt) g_courseEditor:update(dt) + g_graphEditor:update(dt) + g_graph:update(dt) if not self.postInit then -- Doubles the map zoom for 4x Maps. Mainly to make it easier to set targets for unload triggers. self.postInit = true @@ -206,6 +214,7 @@ function Courseplay:draw() g_triggerManager:draw() g_baleToCollectManager:draw() end + g_graph:draw() g_devHelper:draw() CpDebug:draw() if not g_gui:getIsGuiVisible() and not g_noHudModeEnabled then diff --git a/config/GraphEditorCategories.xml b/config/GraphEditorCategories.xml new file mode 100644 index 000000000..1deb5945b --- /dev/null +++ b/config/GraphEditorCategories.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/config/MasterTranslations.xml b/config/MasterTranslations.xml index 6c3ce8357..3d361e9d9 100644 --- a/config/MasterTranslations.xml +++ b/config/MasterTranslations.xml @@ -1694,6 +1694,169 @@ The course is saved automatically on closing of the editor and overrides the sel --> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modDesc.xml b/modDesc.xml index 85c147bdf..49fa8f0a9 100644 --- a/modDesc.xml +++ b/modDesc.xml @@ -141,6 +141,12 @@ Changelog 8.0.0.0: + + + + + + @@ -307,9 +313,22 @@ Changelog 8.0.0.0: + + + + + + + + + + + + - + + diff --git a/scripts/dev/ConsoleCommands.lua b/scripts/dev/ConsoleCommands.lua index f61451450..50c66734d 100644 --- a/scripts/dev/ConsoleCommands.lua +++ b/scripts/dev/ConsoleCommands.lua @@ -1,4 +1,5 @@ --- All the cp console commands are here. +---@class CpConsoleCommands CpConsoleCommands = CpObject() CpConsoleCommands.commands = { @@ -276,4 +277,5 @@ if g_consoleCommands then g_consoleCommands:delete() end +---@type CpConsoleCommands g_consoleCommands = CpConsoleCommands() \ No newline at end of file diff --git a/scripts/editor/EditorCourseWrapper.lua b/scripts/editor/EditorCourseWrapper.lua index bc7be7ced..0596b2827 100644 --- a/scripts/editor/EditorCourseWrapper.lua +++ b/scripts/editor/EditorCourseWrapper.lua @@ -16,7 +16,7 @@ function EditorCourseWrapper:getCourse() return self.course end -function EditorCourseWrapper:getWaypoints() +function EditorCourseWrapper:getVisiblePoints() return self.course.waypoints end diff --git a/scripts/editor/EditorGraphWrapper.lua b/scripts/editor/EditorGraphWrapper.lua new file mode 100644 index 000000000..95c823f8c --- /dev/null +++ b/scripts/editor/EditorGraphWrapper.lua @@ -0,0 +1,479 @@ +---@class EditorGraphWrapper +EditorGraphWrapper = CpObject() +EditorGraphWrapper.MIN_DISTANCE = 1 +function EditorGraphWrapper:init(graph) + ---@type Graph + self.graph = graph + + self.selectedNodeIds = {} + self.hoveredNodeId = nil + self.disabledBuffer = {} + + self.visiblePoints = {} + self.lastPosition = {0, 0, 0} + self.isDirty = false + + ---@type GraphSegment + self.temporarySegment = GraphSegment() +end + +function EditorGraphWrapper:draw(position) + self.graph:draw(self.hoveredNodeId, self.selectedNodeIds) + self.temporarySegment:draw(nil, nil, true, + self:getPointByIndex(self:getFirstSelectedNodeID())) + if not position then + return + end + local x, _, z = unpack(position) + if x == nil or z == nil then + return + end +end + +--- TODO only shown points/segments in range? +function EditorGraphWrapper:getVisiblePoints() + return self.graph:getAllPoints() +end + +--- +---@param id string|nil +---@return GraphPoint|nil +---@return string|nil +function EditorGraphWrapper:getPointByIndex(id) + local point = self.graph:getPointByIndex(id) + if point == nil then + return nil, "err_point_not_found" + end + return point +end + +---@param id string|nil +---@return GraphSegment|nil +---@return string|nil +function EditorGraphWrapper:getSegmentByIndex(id) + local point, err = self:getPointByIndex(id) + if point == nil then + return nil, err + end + ---@type GraphSegment + local segment = point:getParentNode() + if segment == nil then + return nil, "err_segment_not_found" + end + return segment +end + +---@return number|nil +---@return number|nil +---@return number|nil +function EditorGraphWrapper:getPositionByIndex(id) + local point = self:getPointByIndex(id) + if point == nil then + return + end + local x, y, z = point:getPosition() + return x, y, z +end + +---@param x number +---@param y number +---@param z number +---@return string|nil +function EditorGraphWrapper:createSegmentWithPoint(x, y, z) + if x == nil or z == nil then + return + end + local segment = self.graph:createSegmentWithPoint(x, y, z) + return segment:getChildNodeByIndex(1):getRelativeID() +end + +---@param id string|nil +---@return boolean +---@return string|nil +function EditorGraphWrapper:removePointByIndex(id) + local point, err = self:getPointByIndex(id) + if point == nil then + return false, err + end + point:unlink(function(p, segment) + if not segment:hasChildNodes() then + segment:unlink() + end + end) + return true +end + +---@param id string|nil +---@return boolean +---@return string|nil +function EditorGraphWrapper:removeSegmentByPointIndex(id) + local segment, err = self:getSegmentByIndex(id) + if segment == nil then + return false, err + end + segment:clearChildNodes() + segment:unlink() + return true +end + +---@param id string +---@param newPoint GraphPoint +---@return boolean +---@return string|nil +---@return string|nil +function EditorGraphWrapper:insertPointBehindIndex(id, newPoint) + local point, err = self:getPointByIndex(id) + if point == nil then + return false, err + end + local segment, err = self:getSegmentByIndex(id) + if segment == nil then + return false, err + end + if point:getDistance2DToPoint(newPoint) <= EditorGraphWrapper.MIN_DISTANCE then + return false, "err_min_distance_to_small" + end + local ix = segment:getChildNodeIndex(point) + return true, nil, segment:insertChildNodeAtIndex(newPoint, ix + 1) +end + +---@param id string +---@param newPoint GraphPoint +---@return boolean +---@return string|nil +---@return string|nil +function EditorGraphWrapper:insertPointAheadOfIndex(id, newPoint) + local point, err = self:getPointByIndex(id) + if point == nil then + return false, err + end + local segment, err = self:getSegmentByIndex(id) + if segment == nil then + return false, err + end + if point:getDistance2DToPoint(newPoint) <= EditorGraphWrapper.MIN_DISTANCE then + return false, "err_min_distance_to_small" + end + local ix = segment:getChildNodeIndex(point) + return true, nil, segment:insertChildNodeAtIndex(newPoint, ix) +end + +---@param id string|nil +---@param dx number +---@param dy number +---@param dz number +---@return boolean +---@return string|nil +function EditorGraphWrapper:movePointByIndex(id, dx, dy, dz) + local point, err = self:getPointByIndex(id) + if point == nil then + return false, err + end + point:moveTo(dx, dy, dz) + return true +end + +---@param id string|nil +---@return boolean +---@return string|nil +function EditorGraphWrapper:changeSegmentDirection(id) + local segment, err = self:getSegmentByIndex(id) + if segment == nil then + return false, err + end + segment:changeDirection() + return true +end + +---@param id string|nil +---@return boolean +---@return string|nil +function EditorGraphWrapper:isFirstSegmentPoint(id) + local point, err = self:getPointByIndex(id) + if point == nil then + return false, err + end + return point:isFirstNode() +end + +---@param id string|nil +---@return boolean +---@return string|nil +function EditorGraphWrapper:isLastSegmentPoint(id) + local point, err = self:getPointByIndex(id) + if point == nil then + return false, err + end + return point:isLastNode() +end + +---@param id string|nil +---@return boolean +---@return string|nil +function EditorGraphWrapper:isNotFirstOrLastSegmentPoint(id) + local point, err = self:getPointByIndex(id) + if point == nil then + return false, err + end + local isFirst = self:isFirstSegmentPoint(id) + if isFirst then + return false, "err_node_is_first" + end + local isLast = self:isLastSegmentPoint(id) + if isLast then + return false, "err_node_is_last" + end + return true +end + +---@param id string|nil +---@return boolean +---@return string|nil +function EditorGraphWrapper:isFirstOrLastSegmentPoint(id) + local point, err = self:getPointByIndex(id) + if point == nil then + return false, err + end + local isFirst = self:isFirstSegmentPoint(id) + if isFirst then + return true + end + local isLast = self:isLastSegmentPoint(id) + if isLast then + return true + end + return false, "err_node_not_first_or_last" +end + +---@param idA string|nil +---@param idB string|nil +---@return boolean +---@return string|nil +function EditorGraphWrapper:isSegmentIDEqual(idA, idB) + local segmentA, errA = self:getSegmentByIndex(idA) + local segmentB, errB = self:getSegmentByIndex(idB) + if segmentA == nil or segmentB == nil then + return false, errA or errB + end + return segmentA:getID() == segmentB:getID() +end + +---@param idA string|nil +---@param idB string|nil +---@return boolean +---@return string|nil +function EditorGraphWrapper:mergeSegments(idA, idB) + local segmentA, errA = self:getSegmentByIndex(idA) + local segmentB, errB = self:getSegmentByIndex(idB) + if segmentA == nil then + return false, errA + end + if segmentB == nil then + return false, errB + end + if segmentA:getID() == segmentB:getID() then + return false, "err_same_segment" + end + local success, err = self:isFirstOrLastSegmentPoint(idA) + if not success then + return false, err + end + local success, err = self:isFirstOrLastSegmentPoint(idB) + if not success then + return false, err + end + if self:isFirstSegmentPoint(idA) then + if self:isFirstSegmentPoint(idB) then + segmentA:prepandByChildren(segmentB, false) + else + segmentA:prepandByChildren(segmentB, true) + end + else + if self:isFirstSegmentPoint(idB) then + segmentA:extendByChildren(segmentB, false) + else + segmentA:extendByChildren(segmentB, true) + end + end + segmentB:clearChildNodes() + segmentB:unlink() + return true +end + +---@param id string|nil +---@return boolean +---@return string|nil +function EditorGraphWrapper:splitSegment(id) + local node, err = self:getPointByIndex(id) + if node == nil then + return false, err + end + local segment, err = self:getSegmentByIndex(id) + if segment == nil then + return false, err + end + local success, err = self:isNotFirstOrLastSegmentPoint(id) + if not success then + return false, err + end + local ix = segment:getChildNodeIndex(node) + local postNodes = segment:cloneChildNodesBetweenIndex(ix + 1, segment:getNumChildNodes()) + segment:removeChildNodesBetweenIndex(ix + 1, segment:getNumChildNodes()) + ---@type GraphSegment + local newSegment = GraphSegment() + newSegment:extendByChildNodes(postNodes, false) + self.graph:appendChildNode(segment) + return true +end + +-------------------------- +--- Selected nodes +-------------------------- + +function EditorGraphWrapper:setSelected(ix) + if ix ~=nil then + self.selectedNodeIds[ix] = true + end +end + +function EditorGraphWrapper:getSelectedNodeIDs() + return self.selectedNodeIds +end + +function EditorGraphWrapper:getFirstSelectedNodeID() + return next(self.selectedNodeIds) +end + +function EditorGraphWrapper:isSelected(ix) + return ix ~= nil and self.selectedNodeIds[ix] +end + +function EditorGraphWrapper:resetSelected() + self.selectedNodeIds = {} +end + +function EditorGraphWrapper:hasSelectedNode() + return next(self.selectedNodeIds) ~= nil +end + +-------------------------- +--- Hovered node nodes +-------------------------- + +function EditorGraphWrapper:setHovered(ix) + self.hoveredNodeId = ix +end + +function EditorGraphWrapper:isHovered(ix) + return ix ~= nil and self.hoveredNodeId == ix +end + +function EditorGraphWrapper:resetHovered() + self.hoveredNodeId = nil +end + +---------------------------- +--- Temporary Waypoints +---------------------------- + +---@param x any +---@param y any +---@param z any +---@return GraphPoint|nil +function EditorGraphWrapper:addTemporaryPoint(x, y, z) + if x == nil or y == nil or z == nil then + return + end + local point = GraphPoint() + point:setPosition(x, y, z) + self.temporarySegment:appendChildNode(point) + return point +end + +---@return boolean +function EditorGraphWrapper:hasTemporaryPoints() + return self.temporarySegment:hasChildNodes() +end + +---@return GraphPoint[] +function EditorGraphWrapper:getTemporaryPoints() + return self.temporarySegment:getAllChildNodes() +end + +---@return GraphPoint|nil +function EditorGraphWrapper:getFirstTemporaryPoint() + return self.temporarySegment:getChildNodeByIndex(1) +end + +---@return GraphPoint[] +function EditorGraphWrapper:cloneTemporarySegment() + return self.temporarySegment:clone() +end + +function EditorGraphWrapper:clearTemporaryPoints() + self.temporarySegment:clearChildNodes() +end + +function EditorGraphWrapper:resetTemporaryPoints() + self:clearTemporaryPoints() +end + +---------------------------- +--- Destinations +---------------------------- + +---@param id string|nil +---@return boolean +---@return string|nil +function EditorGraphWrapper:hasTargetByIndex(id) + local point, err = self:getPointByIndex(id) + if not point then + return false, err + end + if not point:hasTarget() then + return false, "err_target_not_found" + end + return true +end + +---@param id string|nil +---@return boolean +---@return string|nil +function EditorGraphWrapper:createTargetForIndex(id, name) + local point, err = self:getPointByIndex(id) + if not point then + return false, err + end + if not point:createTarget(name) then + return false, "err_already_has_target" + end + return point:createTarget(name) +end + +---@param id string|nil +---@return boolean|nil +---@return string|nil +function EditorGraphWrapper:removeTargetForIndex(id) + local point, err = self:getPointByIndex(id) + if not point then + return false, err + end + if not point:removeTarget() then + return false, "err_target_not_found" + end + return true +end + +---@param id string|nil +---@return GraphTarget|nil +---@return string|nil +function EditorGraphWrapper:getTargetForIndex(id) + local point, err = self:getPointByIndex(id) + if not point then + return nil, err + end + if not point:hasTarget() then + return nil, "err_target_not_found" + end + return point:getTarget() +end \ No newline at end of file diff --git a/scripts/editor/GraphEditor.lua b/scripts/editor/GraphEditor.lua new file mode 100644 index 000000000..ec89a56de --- /dev/null +++ b/scripts/editor/GraphEditor.lua @@ -0,0 +1,62 @@ + +---@class GraphEditor : CourseEditor +GraphEditor = CpObject(CourseEditor) +GraphEditor.TRANSLATION_PREFIX = "CP_editor_graph_" + +function GraphEditor:init() + CourseEditor.init(self) + self.graphWrapper = EditorGraphWrapper(g_graph) + self.title = string.format("TODO: CP GraphEditor") + + g_consoleCommands:registerConsoleCommand("cpOpenGraphEditor", "Opens the CP Graph editor", "open", self) +end + +function GraphEditor:load() + self.brushCategory = self:loadCategory(Utils.getFilename("config/GraphEditorCategories.xml", g_Courseplay.BASE_DIRECTORY)) +end + +function GraphEditor:draw(x, y, z) + self.graphWrapper:draw({x, y, z}) +end + +function GraphEditor:open() + self.isActive = true + g_messageCenter:publish(MessageType.GUI_CP_INGAME_OPEN_CONSTRUCTION_MENU, self) +end + +function GraphEditor:getStartPosition() + return +end + +function GraphEditor:getCourseWrapper() + return self.graphWrapper +end + +function GraphEditor:onClickExit(callbackFunc) + self.isActive = false + callbackFunc() + -- YesNoDialog.show( + -- function (self, clickOk) + -- self:deactivate(clickOk) + -- callbackFunc() + -- end, + -- self, string.format(g_i18n:getText("CP_GraphEditor_save_changes"), self.file:getName())) +end + + +--- Updates the course display, when a waypoint change happened. +function GraphEditor:updateChanges(ix) + -- self.courseDisplay:updateChanges(ix) +end + +--- Updates the course display, when a single waypoint change happened. +function GraphEditor:updateChangeSingle(ix) + -- self.courseDisplay:updateWaypoint(ix) +end + +--- Updates the course display, between to waypoints. +function GraphEditor:updateChangesBetween(firstIx, lastIx) + -- self.courseDisplay:updateChangesBetween(firstIx, lastIx) +end + +g_graphEditor = GraphEditor() \ No newline at end of file diff --git a/scripts/editor/brushes/BaseBrush.lua b/scripts/editor/brushes/BaseBrush.lua index 405250645..db39c2c7a 100644 --- a/scripts/editor/brushes/BaseBrush.lua +++ b/scripts/editor/brushes/BaseBrush.lua @@ -2,6 +2,7 @@ Basic brush, that manipulates waypoints. ]] ---@class CpBrush +---@field setInputTextDirty function CpBrush = CpObject(ConstructionBrush) CpBrush.TRANSLATION_PREFIX = "CP_editor_" CpBrush.radius = 2 @@ -12,7 +13,7 @@ CpBrush.secondaryAxisText = "secondary_axis_text" CpBrush.tertiaryButtonText = "tertiary_text" CpBrush.inputTitle = "input_title" CpBrush.yesNoTitle = "yesNo_title" -CpBrush.errMessage = "err" +CpBrush.defaultErrorMessage = "err" CpBrush.ERR_MESSAGE_DURATION = 15 * 1000 -- 15 sec function CpBrush:init(cursor, camera, editor) self.isActive = false @@ -34,6 +35,7 @@ function CpBrush:init(cursor, camera, editor) self.cursor:setShape(GuiTopDownCursor.SHAPES.CIRCLE) self.lastHoveredIx = nil self.errorMsgTimer = CpTemporaryObject(false) + self.errorMsgText = nil self.editor = editor self.courseWrapper = editor:getCourseWrapper() end @@ -49,7 +51,7 @@ function CpBrush:getHoveredWaypointIx() return end -- try to get a waypoint in mouse range - for ix, point in ipairs(self.courseWrapper:getWaypoints()) do + for ix, point in ipairs(self.courseWrapper:getVisiblePoints()) do if self:isAtPos(point, x, y, z) then return ix end @@ -68,6 +70,8 @@ function CpBrush:update(dt) end if self.errorMsgTimer:get() then self.cursor:setErrorMessage(self:getErrorMessage()) + else + self.errorMsgText = nil end end @@ -90,17 +94,28 @@ function CpBrush:getTranslation(translation, ...) end function CpBrush:getErrorMessage() - return self:getTranslation(self.errMessage) + if self.errorMsgText then + return self.editor.TRANSLATION_PREFIX .. self.errorMsgText + end + return self:getTranslation(self.defaultErrorMessage) end -function CpBrush:setError() +---@param errMsg string|nil +function CpBrush:setError(errMsg) self.errorMsgTimer:set(true, self.ERR_MESSAGE_DURATION) + self.errorMsgText = errMsg + self:debug("setting error: %s", tostring(errMsg)) end function CpBrush:resetError() self.errorMsgTimer:reset() + self.errorMsgText = nil end function CpBrush:getTitle() return self:getTranslation("title") +end + +function CpBrush:debug(...) + CpUtil.info(...) end \ No newline at end of file diff --git a/scripts/editor/brushes/graph/GraphBrush.lua b/scripts/editor/brushes/graph/GraphBrush.lua new file mode 100644 index 000000000..239e4f6bb --- /dev/null +++ b/scripts/editor/brushes/graph/GraphBrush.lua @@ -0,0 +1,62 @@ +--[[ + Brushes that can be used for waypoint selection/manipulation. +]] +---@class GraphBrush : CpBrush +GraphBrush = CpObject(CpBrush) +GraphBrush.radius = 0.5 +GraphBrush.sizeModifierMax = 10 +-- -- GraphBrush.translationPrefix = "gui_ad_editor_" +-- GraphBrush.primaryButtonText = "primary_text" +-- GraphBrush.primaryAxisText = "primary_axis_text" +-- GraphBrush.secondaryButtonText = "secondary_text" +-- GraphBrush.secondaryAxisText = "secondary_axis_text" +-- GraphBrush.tertiaryButtonText = "tertiary_text" +-- GraphBrush.inputTitle = "input_title" +-- GraphBrush.yesNoTitle = "yesNo_title" + +function GraphBrush:init(...) + CpBrush.init(self, ...) + self.sizeModifier = 1 + ---@type EditorGraphWrapper + self.graphWrapper = self.courseWrapper +end + +function GraphBrush:changeSizeModifier(modifier) + self.sizeModifier = modifier + self.cursor:setShapeSize(self.radius * modifier * (1+self.camera.zoomFactor)) +end + +---@param point GraphPoint +---@param x number +---@param y number +---@param z number +---@return boolean +function GraphBrush:isAtPos(point, x, y, z) + local dx, dy, dz = point:getPosition() + if MathUtil.getPointPointDistance(dx, dz, x, z) < self.radius * self.sizeModifier * (1 + self.camera.zoomFactor) then + return math.abs(dy - y) < 3 + end +end + +---@param excludeLambda any +---@return string|nil +function GraphBrush:getHoveredNodeId(excludeLambda) + local x, y, z = self.cursor:getPosition() + -- try to get a waypoint in mouse range + for _, point in pairs(self.graphWrapper:getVisiblePoints()) do + if self:isAtPos(point, x, y, z) then + if excludeLambda == nil or not excludeLambda(point:getRelativeID()) then + return point:getRelativeID() + end + end + end +end + +function GraphBrush:update(dt) + self.graphWrapper:setHovered(self:getHoveredNodeId()) + --- Updates the cursor size depending on the zoom. + self.cursor:setShapeSize(self.radius * self.sizeModifier * (1 + self.camera.zoomFactor)) + if self.errorMsgTimer:get() then + self.cursor:setErrorMessage(self:getErrorMessage()) + end +end \ No newline at end of file diff --git a/scripts/editor/brushes/graph/misc/ConnectWaypoints.lua b/scripts/editor/brushes/graph/misc/ConnectWaypoints.lua new file mode 100644 index 000000000..b98a2ec02 --- /dev/null +++ b/scripts/editor/brushes/graph/misc/ConnectWaypoints.lua @@ -0,0 +1,138 @@ + +--- Connects two waypoints. +---@class BrushConnect : GraphBrush +BrushConnect = CpObject(GraphBrush) + +BrushConnect.TYPE_NORMAL = 1 +BrushConnect.TYPE_LOW_PRIO = 2 +BrushConnect.TYPE_CROSSING = 3 +BrushConnect.TYPE_CROSSING_LOW_PRIO = 4 +BrushConnect.TYPE_REVERSE_NORMAL = 5 +BrushConnect.TYPE_DISCONNECT = 6 +BrushConnect.TYPE_MIN = 1 +BrushConnect.TYPE_MAX = 6 + +BrushConnect.typeTexts = { + [BrushConnect.TYPE_NORMAL] = "type_normal", + [BrushConnect.TYPE_LOW_PRIO] = "type_sub_route", + [BrushConnect.TYPE_REVERSE_NORMAL] = "type_reverse_route", + [BrushConnect.TYPE_CROSSING] = "type_crossing_route", + [BrushConnect.TYPE_CROSSING_LOW_PRIO] = "type_sub_crossing_route", + [BrushConnect.TYPE_DISCONNECT] = "type_disconnect_route", +} + +function BrushConnect:init(...) + GraphBrush.init(self, ...) + self.supportsPrimaryButton = true + self.supportsPrimaryDragging = true + self.supportsSecondaryButton = true + self.supportsTertiaryButton = true + + self.changedWaypoints = {} + + self.mode = self.TYPE_NORMAL +end + +function BrushConnect:onButtonPrimary(isDown, isDrag, isUp) + if self.selectedNodeId == nil and isDown then + self.selectedNodeId = self:getHoveredNodeId() + self.graphWrapper:setSelected(self.selectedNodeId) + return + end + local nodeId = self:getHoveredNodeId() + if nodeId ~= nil then + if isDrag then + if nodeId ~= self.selectedNodeId and not self.changedWaypoints[nodeId] then + self:connectWaypoints(self.selectedNodeId, nodeId) + self.selectedNodeId = nodeId + self.changedWaypoints[nodeId] = true + end + end + end + if isUp then + self.selectedNodeId = nil + self.graphWrapper:resetSelected() + self.changedWaypoints = {} + end +end + +function BrushConnect:onButtonSecondary() + local d = self.sizeModifier + 1 + if d > self.sizeModifierMax then + self:changeSizeModifier(1) + elseif d <= 0 then + self:changeSizeModifier(self.sizeModifierMax) + else + self:changeSizeModifier(d) + end + self:setInputTextDirty() +end + +function BrushConnect:connectWaypoints(nodeId, targetNodeId, sendEvent) + self.graphWrapper:setConnectionAndSubPriority(nodeId, targetNodeId, self:getCurrentConnectionType(), self:getIsSubPrio(), sendEvent) +end + +function BrushConnect:getCurrentConnectionType() + local dir = 1 + if self.mode == self.TYPE_CROSSING or self.mode == self.TYPE_CROSSING_LOW_PRIO then + dir = 3 + elseif self.mode == self.TYPE_REVERSE_NORMAL then + dir = 4 + elseif self.mode == self.TYPE_DISCONNECT then + dir = 0 + end + return dir +end + +function BrushConnect:getIsSubPrio() + return self.mode == self.TYPE_LOW_PRIO or self.mode == self.TYPE_CROSSING_LOW_PRIO +end + +function BrushConnect:getIsReverse() + return self.mode == self.TYPE_REVERSE_NORMAL +end + +function BrushConnect:getIsCrossing() + return self.mode == self.TYPE_CROSSING or self.mode == self.TYPE_CROSSING_LOW_PRIO +end + +function BrushConnect:onButtonTertiary() + self.mode = self.mode + 1 + if self.mode > self.TYPE_MAX then + self.mode = self.TYPE_MIN + end + self:setInputTextDirty() +end + +--- Not working, as the brush classes need to be the same. +function BrushConnect:copyState(from) + if from.mode ~= nil then + self.mode = from.mode + self.sizeModifier = from.sizeModifier or 1 + self:setInputTextDirty() + end +end + +function BrushConnect:activate() + self.selectedNodeId = nil + self.changedWaypoints = {} + self.graphWrapper:resetSelected() +end + +function BrushConnect:deactivate() + self.selectedNodeId = nil + self.changedWaypoints = {} + self.graphWrapper:resetSelected() +end + +function BrushConnect:getButtonPrimaryText() + return self:getTranslation(self.primaryButtonText) +end + +function BrushConnect:getButtonSecondaryText() + return self:getTranslation(self.secondaryButtonText, self.sizeModifier) +end + +function BrushConnect:getButtonTertiaryText() + return self:getTranslation(self.tertiaryButtonText, self:getTranslation(self.typeTexts[self.mode])) +end diff --git a/scripts/editor/brushes/graph/misc/CurveLineWaypoints.lua b/scripts/editor/brushes/graph/misc/CurveLineWaypoints.lua new file mode 100644 index 000000000..12e9546ef --- /dev/null +++ b/scripts/editor/brushes/graph/misc/CurveLineWaypoints.lua @@ -0,0 +1,105 @@ + +--- Connects two waypoints. +---@class BrushCurve : BrushStraightLine +BrushCurve = CpObject(BrushStraightLine) +BrushCurve.MIN_OFFSET = -1 +BrushCurve.MAX_OFFSET = 1 +BrushCurve.MIN_CENTER = 0 +BrushCurve.MAX_CENTER = 1 +BrushCurve.START_CENTER = 0.5 +BrushCurve.START_OFFSET = 0 + +function BrushCurve:init(...) + BrushStraightLine.init(self, ...) + self.supportsSecondaryAxis = true + self.secondaryAxisIsContinuous = true + self.primaryAxisIsContinuous = true + self.offset = 0 + self.center = self.START_CENTER +end + +--- De-Casteljau algorithm +function BrushCurve:getNextPoint(t,points) + local q0_x, q0_y = (1-t) * points[1][1] + t * points[2][1], + (1-t) * points[1][2] + t * points[2][2] + local q1_x, q1_y = (1-t) * points[2][1] + t * points[3][1], + (1-t) * points[2][2] + t * points[3][2] + + return (1-t)*q0_x + t*q1_x, (1-t)*q0_y + t*q1_y +end + +function BrushCurve:moveWaypoints() + local x, y, z = self.cursor:getPosition() + if x == nil then + return + end + local waypoints = self.graphWrapper:cloneTemporaryPoints() + self.graphWrapper:clearTemporaryPoints() + local tx, ty, tz = 0, 0, 0 + if self.startAnchorWaypointId ~= nil then + tx, ty, tz = self.graphWrapper:getPosition(self.startAnchorWaypointId) + else + tx, ty, tz = waypoints[1].x, waypoints[1].y, waypoints[1].z + self.graphWrapper:addTemporaryPoint(tx, ty, tz) + end + local dist = MathUtil.vector2Length(x-tx,z-tz) + local spacing = 2 + local nx, nz = MathUtil.vector2Normalize(x-tx, z-tz) + if nx == nil or nz == nil then + nx = 0 + nz = 1 + end + local distCenter = dist*self.center + local ax, az = tx + nx * distCenter, tz + nz * distCenter + --- Rotation + local ncx = nx * math.cos(math.pi/2) - nz * math.sin(math.pi/2) + local ncz = nx * math.sin(math.pi/2) + nz * math.cos(math.pi/2) + --- Translation + local cx, cz = ax + ncx * self.offset * dist, az + ncz * self.offset * dist + local halfDist = MathUtil.vector2Length(cx - tx, cz - tz) + local dt = 2/(1.5*halfDist) + local n = math.ceil(halfDist/spacing) + spacing = halfDist/n + local points = { + { + tx, + tz + }, + { + cx, + cz + }, + { + x, + z + } + } + + local dx, dz + for t=dt , 1, dt do + dx, dz = BrushCurve:getNextPoint(t,points) + self.graphWrapper:addTemporaryPoint(dx, y, dz) + end +end + +function BrushCurve:onAxisPrimary(inputValue) + self.offset = math.clamp(self.offset+inputValue/50,self.MIN_OFFSET,self.MAX_OFFSET) + self:setInputTextDirty() +end + +function BrushCurve:onAxisSecondary(inputValue) + self.center = math.clamp(self.center+inputValue/50,self.MIN_CENTER,self.MAX_CENTER) + self:setInputTextDirty() +end + +function BrushCurve:getButtonPrimaryText() + return self:getTranslation(self.primaryButtonText) +end + +function BrushCurve:getAxisPrimaryText() + return self:getTranslation(self.primaryAxisText, self.offset) +end + +function BrushCurve:getAxisSecondaryText() + return self:getTranslation(self.secondaryAxisText, self.center) +end diff --git a/scripts/editor/brushes/graph/misc/MoveAdvancedWaypoint.lua b/scripts/editor/brushes/graph/misc/MoveAdvancedWaypoint.lua new file mode 100644 index 000000000..9ab929eda --- /dev/null +++ b/scripts/editor/brushes/graph/misc/MoveAdvancedWaypoint.lua @@ -0,0 +1,72 @@ + +--- Moves a waypoint relative to the mouse position. +---@class BrushMoveAdvanced : GraphBrush +BrushMoveAdvanced = CpObject(GraphBrush) + +function BrushMoveAdvanced:init(...) + GraphBrush.init(self, ...) + self.supportsPrimaryButton = true + self.supportsPrimaryDragging = true + self.supportsSecondaryButton = true + self.supportsSecondaryDragging = true + self.supportsTertiaryButton = true + self.selectedNodes = {} + self.lastPosition = {} +end + +function BrushMoveAdvanced:onButtonPrimary(isDown, isDrag, isUp) + local x, y, z = self.cursor:getPosition() + if isDown then + self.lastPosition = {x, y, z} + end + if isDrag then + if next(self.lastPosition) ~= nil then + local dx, dy, dz = unpack(self.lastPosition) + local nx, ny, nz = x - dx, y - dy, z - dz + for nodeId, _ in pairs(self.selectedNodes) do + self.graphWrapper:translateTo(nodeId, nx, ny, nz) + end + end + self.lastPosition = {x, y, z} + end +end + +function BrushMoveAdvanced:onButtonSecondary(isDown, isDrag, isUp) + if isDown or isDrag then + local selectedNodeId = self:getHoveredNodeId() + if selectedNodeId then + self.graphWrapper:setSelected(selectedNodeId) + self.selectedNodes[selectedNodeId] = true + end + end +end + +function BrushMoveAdvanced:onButtonTertiary() + self.selectedNodes = {} + self.lastPosition = {} + self.graphWrapper:resetSelected() +end + +function BrushMoveAdvanced:activate() + self.selectedNodes = {} + self.lastPosition = {} + self.graphWrapper:resetSelected() +end + +function BrushMoveAdvanced:deactivate() + self.selectedNodes = {} + self.lastPosition = {} + self.graphWrapper:resetSelected() +end + +function BrushMoveAdvanced:getButtonPrimaryText() + return self:getTranslation(self.primaryButtonText) +end + +function BrushMoveAdvanced:getButtonSecondaryText() + return self:getTranslation(self.secondaryButtonText) +end + +function BrushMoveAdvanced:getButtonTertiaryText() + return self:getTranslation(self.tertiaryButtonText) +end diff --git a/scripts/editor/brushes/graph/misc/StraightLineWaypoints.lua b/scripts/editor/brushes/graph/misc/StraightLineWaypoints.lua new file mode 100644 index 000000000..03ae2f8f9 --- /dev/null +++ b/scripts/editor/brushes/graph/misc/StraightLineWaypoints.lua @@ -0,0 +1,118 @@ + +--- Connects two waypoints. +---@class BrushStraightLine : BrushConnect +BrushStraightLine = CpObject(BrushConnect) +BrushStraightLine.DELAY = 1 --- The mouse event oscillates.., so we have to wait one update tick before release is allowed. +BrushStraightLine.MIN_DIST = 2 +BrushStraightLine.MAX_DIST = 20 +BrushStraightLine.START_DIST = 6 + +function BrushStraightLine:init(...) + BrushConnect.init(self, ...) + self.supportsPrimaryAxis = true + + self.spacing = self.START_DIST + self.delay = g_updateLoopIndex + self.startAnchorWaypointId = nil +end + +function BrushStraightLine:onButtonPrimary(isDown, isDrag, isUp) + if isDown then + if not self.graphWrapper:hasTemporaryPoints() and self.startAnchorWaypointId == nil then + local nodeId = self:getHoveredNodeId() + if nodeId then + self.startAnchorWaypointId = nodeId + self.graphWrapper:setSelected(self.startAnchorWaypointId) + self:debug("Start with node: %d", nodeId) + else + local x, y, z = self.cursor:getPosition() + self.graphWrapper:addTemporaryPoint(x, y, z) + self:debug("Start with a new temp node") + end + self.delay = g_updateLoopIndex + self.DELAY + end + end + if isDrag and (self.graphWrapper:hasTemporaryPoints() or self.startAnchorWaypointId ~= nil) then + self:moveWaypoints() + end + if isUp then + if g_updateLoopIndex > self.delay and self.graphWrapper:hasTemporaryPoints() then + local tempWaypoints = self.graphWrapper:getTemporaryPoints() + self:debug("Finished drawing of %d waypoints.", #tempWaypoints) + self.graphWrapper:createSplineFromTemporyPoints(self.startAnchorWaypointId, self:getHoveredNodeId(), + self:getIsReverse(), self:getIsSubPrio(), self:getIsCrossing()) + end + self.graphWrapper:resetTemporaryPoints() + self.graphWrapper:resetSelected() + self.startAnchorWaypointId = nil + end +end + +function BrushStraightLine:moveWaypoints() + local x, y, z = self.cursor:getPosition() + if x == nil then + return + end + local waypoints = self.graphWrapper:cloneTemporaryPoints() + self.graphWrapper:clearTemporaryPoints() + local tx, ty, tz = 0, 0, 0 + if self.startAnchorWaypointId ~= nil then + tx, ty, tz = self.graphWrapper:getPosition(self.startAnchorWaypointId) + else + tx, ty, tz = waypoints[1].x, waypoints[1].y, waypoints[1].z + self.graphWrapper:addTemporaryPoint(tx, ty, tz) + end + local dist = MathUtil.vector2Length(x-tx,z-tz) + if dist <= 1 then + return + end + local spacing = self.spacing + local nx, nz = MathUtil.vector2Normalize(x-tx, z-tz) + if nx == nil or nz == nil then + nx = 0 + nz = 1 + end + local n = math.max(math.ceil(dist/spacing), 2) + spacing = dist/n + for i = 1, n + 1 do + local dx, dy, dz = tx + nx * i * spacing, y, tz + nz * i * spacing + self.graphWrapper:addTemporaryPoint(dx, dy, dz) + end +end + +function BrushStraightLine:update(dt) + BrushConnect.update(self, dt) + self.graphWrapper:updateTemporaryPoints( + self:getIsReverse(), self:getIsSubPrio(), self:getIsCrossing()) +end + +function BrushStraightLine:onAxisPrimary(inputValue) + self:setSpacing(inputValue) + self:setInputTextDirty() +end + +function BrushStraightLine:setSpacing(inputValue) + self.spacing = math.clamp(self.spacing + inputValue, self.MIN_DIST, self.MAX_DIST) +end + +function BrushStraightLine:activate() + self.graphWrapper:resetTemporaryPoints() + self.startAnchorWaypointId = nil + BrushConnect.activate(self) +end + +function BrushStraightLine:deactivate() + self.graphWrapper:resetTemporaryPoints() + self.graphWrapper:resetSelected() + self.startAnchorWaypointId = nil + BrushConnect.deactivate(self) +end + +function BrushStraightLine:getButtonPrimaryText() + return self:getTranslation(self.primaryButtonText) +end + +function BrushStraightLine:getAxisPrimaryText() + return self:getTranslation(self.primaryAxisText, self.spacing) +end + diff --git a/scripts/editor/brushes/graph/points/DeletePointBrush.lua b/scripts/editor/brushes/graph/points/DeletePointBrush.lua new file mode 100644 index 000000000..eb5612a3e --- /dev/null +++ b/scripts/editor/brushes/graph/points/DeletePointBrush.lua @@ -0,0 +1,44 @@ + +--- Creates a new waypoint at the mouse position. +---@class DeletePointBrush : GraphBrush +DeletePointBrush = CpObject(GraphBrush) + +function DeletePointBrush:init(...) + GraphBrush.init(self, ...) + self.supportsPrimaryButton = true + self.supportsPrimaryDragging = true + self.supportsPrimaryAxis = true + return self +end + +function DeletePointBrush:onButtonPrimary(isDown, isDrag, isUp) + local nodeId = self:getHoveredNodeId() + if nodeId ~= nil then + if isDown or isDrag then + local succes, err = self.graphWrapper:removePointByIndex(nodeId) + if not succes then + self:setError(err) + end + end + end +end + +function DeletePointBrush:onAxisPrimary(delta) + local d = self.sizeModifier + delta + if d > self.sizeModifierMax then + self:changeSizeModifier(1) + elseif d <= 0 then + self:changeSizeModifier(self.sizeModifierMax) + else + self:changeSizeModifier(d) + end + self:setInputTextDirty() +end + +function DeletePointBrush:getButtonPrimaryText() + return self:getTranslation(self.primaryButtonText) +end + +function DeletePointBrush:getAxisPrimaryText() + return self:getTranslation(self.primaryAxisText, self.sizeModifier) +end \ No newline at end of file diff --git a/scripts/editor/brushes/graph/points/InsertPointBrush.lua b/scripts/editor/brushes/graph/points/InsertPointBrush.lua new file mode 100644 index 000000000..12a95f8d5 --- /dev/null +++ b/scripts/editor/brushes/graph/points/InsertPointBrush.lua @@ -0,0 +1,88 @@ + +--- Inserts a new waypoint at the mouse position. +---@class InsertPointBrush : GraphBrush +InsertPointBrush = CpObject(GraphBrush) +function InsertPointBrush:init(...) + GraphBrush.init(self, ...) + self.supportsPrimaryButton = true + self.supportsPrimaryDragging = true + self.supportsSecondaryButton = true + self.supportsSecondaryDragging = true +end + +function InsertPointBrush:onButtonPrimary(isDown, isDrag, isUp) + self:handleButtonEvent(isDown, isDrag, isUp, function (selectedId, point) + local success, err = self.graphWrapper:insertPointAheadOfIndex(selectedId, point:clone()) + if not success then + self:setError(err) + return + end + self:debug("Successfully inserted Point: %s ahead of index: %s", + point:getRelativeID(), selectedId) + end) +end + +function InsertPointBrush:onButtonSecondary(isDown, isDrag, isUp) + self:handleButtonEvent(isDown, isDrag, isUp, function (selectedId, point) + local success, err = self.graphWrapper:insertPointBehindIndex(selectedId, point:clone()) + if not success then + self:setError(err) + return + end + self:debug("Successfully inserted Point: %s behind index: %s", + point:getRelativeID(), selectedId) + end) +end + +function InsertPointBrush:handleButtonEvent(isDown, isDrag, isUp, insertLambda) + if isDown then + local ix = self:getHoveredNodeId() + local x, y, z = self.cursor:getPosition() + if ix then + self.graphWrapper:setSelected(ix) + self.graphWrapper:addTemporaryPoint(x, y, z) + else + ix = self.graphWrapper:createSegmentWithPoint(x, y, z) + if ix then + --- TODO Update editor + self.graphWrapper:setSelected(ix) + self.graphWrapper:addTemporaryPoint(x, y, z) + end + end + end + if isDrag then + local point = self.graphWrapper:getFirstTemporaryPoint() + if point then + local x, y, z = self.cursor:getPosition() + point:moveTo(x, y, z) + end + end + if isUp then + local point = self.graphWrapper:getFirstTemporaryPoint() + local selectedId = self.graphWrapper:getFirstSelectedNodeID() + if selectedId ~= nil and point then + insertLambda(selectedId, point) + self.graphWrapper:resetSelected() + self.graphWrapper:resetTemporaryPoints() + end + end +end + +function InsertPointBrush:activate() + self.graphWrapper:resetSelected() + self.graphWrapper:resetTemporaryPoints() +end + +function InsertPointBrush:deactivate() + self.graphWrapper:resetSelected() + self.graphWrapper:resetTemporaryPoints() +end + + +function InsertPointBrush:getButtonPrimaryText() + return self:getTranslation(self.primaryButtonText) +end + +function InsertPointBrush:getButtonSecondaryText() + return self:getTranslation(self.secondaryButtonText) +end diff --git a/scripts/editor/brushes/graph/points/MovePointBrush.lua b/scripts/editor/brushes/graph/points/MovePointBrush.lua new file mode 100644 index 000000000..7ef2bd8c5 --- /dev/null +++ b/scripts/editor/brushes/graph/points/MovePointBrush.lua @@ -0,0 +1,41 @@ + +--- Moves a waypoint relative to the mouse position. +---@class MovePointBrush : GraphBrush +MovePointBrush = CpObject(GraphBrush) + +function MovePointBrush:init(...) + GraphBrush.init(self, ...) + self.supportsPrimaryButton = true + self.supportsPrimaryDragging = true + + return self +end + +function MovePointBrush:onButtonPrimary(isDown, isDrag, isUp) + if isDown then + local id = self:getHoveredNodeId() + if id then + self.graphWrapper:setSelected(id) + end + end + if isDrag and self.graphWrapper:hasSelectedNode() then + local id = self.graphWrapper:getFirstSelectedNodeID() + local x, y, z = self.cursor:getPosition() + self.graphWrapper:movePointByIndex(id, x, y, z) + end + if isUp then + self.graphWrapper:resetSelected() + end +end + +function MovePointBrush:activate() + self.graphWrapper:resetSelected() +end + +function MovePointBrush:deactivate() + self.graphWrapper:resetSelected() +end + +function MovePointBrush:getButtonPrimaryText() + return self:getTranslation(self.primaryButtonText) +end \ No newline at end of file diff --git a/scripts/editor/brushes/graph/points/StraightLinePointBrush.lua b/scripts/editor/brushes/graph/points/StraightLinePointBrush.lua new file mode 100644 index 000000000..8b5918d48 --- /dev/null +++ b/scripts/editor/brushes/graph/points/StraightLinePointBrush.lua @@ -0,0 +1,174 @@ + +--- Inserts a new waypoint at the mouse position. +---@class StraightLinePointBrush : GraphBrush +StraightLinePointBrush = CpObject(GraphBrush) +StraightLinePointBrush.MIN_DIST = 2 +StraightLinePointBrush.MAX_DIST = 20 +StraightLinePointBrush.START_DIST = 6 +StraightLinePointBrush.DELAY = 1 --- The mouse event oscillates.., so we have to wait one update tick before release is allowed. +function StraightLinePointBrush:init(...) + GraphBrush.init(self, ...) + self.supportsPrimaryButton = true + self.supportsPrimaryDragging = true + self.supportsSecondaryButton = true + self.supportsSecondaryDragging = true + self.supportsPrimaryAxis = true + self.spacing = self.START_DIST + + self.delay = g_updateLoopIndex +end + +function StraightLinePointBrush:onButtonPrimary(isDown, isDrag, isUp) + self:handleButtonEvent(isDown, isDrag, isUp) +end + +function StraightLinePointBrush:onButtonSecondary(isDown, isDrag, isUp) + +end + +function StraightLinePointBrush:movePoints() + local x, y, z = self.cursor:getPosition() + if x == nil or z == nil then + return + end + local tx, ty, tz = self.graphWrapper:getPositionByIndex( + self.graphWrapper:getFirstSelectedNodeID()) + if tx == nil or tz == nil then + return + end + self.graphWrapper:clearTemporaryPoints() + local tx, ty, tz = self.graphWrapper:getPositionByIndex( + self.graphWrapper:getFirstSelectedNodeID()) + local dist = MathUtil.vector2Length(x-tx, z-tz) + if dist <= 1 then + return + end + local spacing = self.spacing + local nx, nz = MathUtil.vector2Normalize(x-tx, z-tz) + if nx == nil or nz == nil then + nx = 0 + nz = 1 + end + local n = math.max(math.ceil(dist/spacing), 2) + spacing = dist / n + if self.graphWrapper:isLastSegmentPoint( + self.graphWrapper:getFirstSelectedNodeID()) then + --- Forwards + for i = 1, n + 1 do + local dx = tx + nx * i * spacing + local dz = tz + nz * i * spacing + local dy = getTerrainHeightAtWorldPos( + g_currentMission.terrainRootNode, dx, y, dz) + if dy > y - 2 and dy < y + 2 then + y = dy + end + self.graphWrapper:addTemporaryPoint(dx, y, dz) + end + else + --- Backwards + for i = 1, n + 1 do + local dx = tx + nx * i * spacing + local dz = tz + nz * i * spacing + local dy = getTerrainHeightAtWorldPos( + g_currentMission.terrainRootNode, dx, y, dz) + if dy > y - 2 and dy < y + 2 then + y = dy + end + self.graphWrapper:addTemporaryPoint(dx, y, dz) + end + end +end + +function StraightLinePointBrush:onAxisPrimary(inputValue) + self.spacing = math.clamp(self.spacing + inputValue, + self.MIN_DIST, self.MAX_DIST) +end + +function StraightLinePointBrush:handleButtonEvent(isDown, isDrag, isUp, insertLambda) + if isDown and not self.graphWrapper:hasSelectedNode() then + local ix = self:getHoveredNodeId() + local x, y, z = self.cursor:getPosition() + if ix then + local isNotFirsOrLast, err = self.graphWrapper:isNotFirstOrLastSegmentPoint(ix) + if isNotFirsOrLast then + self:setError(err) + return + end + if self.graphWrapper:isFirstSegmentPoint(ix) then + self.graphWrapper:setSelected(ix) + self.graphWrapper:addTemporaryPoint(x, y, z) + elseif self.graphWrapper:isLastSegmentPoint(ix) then + self.graphWrapper:setSelected(ix) + self.graphWrapper:addTemporaryPoint(x, y, z) + end + else + ix = self.graphWrapper:createSegmentWithPoint(x, y, z) + if ix then + --- TODO Update editor + self.graphWrapper:setSelected(ix) + self.graphWrapper:addTemporaryPoint(x, y, z) + end + end + self.delay = g_updateLoopIndex + self.DELAY + end + if isDrag and self.graphWrapper:hasSelectedNode() then + self:movePoints() + end + if isUp and self.graphWrapper:hasSelectedNode() then + local points = self.graphWrapper:getTemporaryPoints() + local selectedId = self.graphWrapper:getFirstSelectedNodeID() + local ix, success, err = selectedId, false, nil + if self.graphWrapper:isLastSegmentPoint(selectedId) then + for _, p in ipairs(points) do + success, err, ix = self.graphWrapper:insertPointBehindIndex(ix, p:clone()) + if not success then + self:setError(err) + break + end + self:debug("Successfully inserted Point: %s behind index: %s", + p:getRelativeID(), ix) + end + else + for _, p in ipairs(points) do + success, err, ix = self.graphWrapper:insertPointAheadOfIndex(ix, p:clone()) + if not success then + self:setError(err) + break + end + self:debug("Successfully inserted Point: %s ahead of index: %s", + p:getRelativeID(), ix) + + end + end + self.graphWrapper:resetTemporaryPoints() + self.graphWrapper:resetSelected() + end +end + +function StraightLinePointBrush:update(dt) + GraphBrush.update(self, dt) + +end + +function StraightLinePointBrush:activate() + self.graphWrapper:resetSelected() + self.graphWrapper:resetTemporaryPoints() +end + +function StraightLinePointBrush:deactivate() + self.graphWrapper:resetSelected() + self.graphWrapper:resetTemporaryPoints() +end + + +function StraightLinePointBrush:getButtonPrimaryText() + return self:getTranslation(self.primaryButtonText) +end + +function StraightLinePointBrush:getButtonSecondaryText() + return self:getTranslation(self.secondaryButtonText) +end + +function StraightLinePointBrush:getAxisPrimaryText() + return self:getTranslation(self.primaryAxisText, self.spacing) +end \ No newline at end of file diff --git a/scripts/editor/brushes/graph/segments/ChangeSegmentTypBrush.lua b/scripts/editor/brushes/graph/segments/ChangeSegmentTypBrush.lua new file mode 100644 index 000000000..12c64b64e --- /dev/null +++ b/scripts/editor/brushes/graph/segments/ChangeSegmentTypBrush.lua @@ -0,0 +1,21 @@ + +--- Creates a new segement with a point at the mouse position. +---@class ChangeSegmentTypBrush : GraphBrush +ChangeSegmentTypBrush = CpObject(GraphBrush) + +function ChangeSegmentTypBrush:init(...) + GraphBrush.init(self, ...) + self.supportsPrimaryButton = true +end + +function ChangeSegmentTypBrush:onButtonPrimary() + local nodeId = self:getHoveredNodeId() + local success, err = self.graphWrapper:changeSegmentDirection(nodeId) + if not success then + self:setError(err) + end +end + +function ChangeSegmentTypBrush:getButtonPrimaryText() + return self:getTranslation(self.primaryButtonText) +end diff --git a/scripts/editor/brushes/graph/segments/CreateSegmentBrush.lua b/scripts/editor/brushes/graph/segments/CreateSegmentBrush.lua new file mode 100644 index 000000000..a51b94542 --- /dev/null +++ b/scripts/editor/brushes/graph/segments/CreateSegmentBrush.lua @@ -0,0 +1,24 @@ + +--- Creates a new segement with a point at the mouse position. +---@class CreateSegmentBrush : GraphBrush +CreateSegmentBrush = CpObject(GraphBrush) + +function CreateSegmentBrush:init(...) + GraphBrush.init(self, ...) + self.supportsPrimaryButton = true +end + +function CreateSegmentBrush:onButtonPrimary() + if self:getHoveredNodeId() then + self:setError() + return + end + local x, y, z = self.cursor:getPosition() + if not self.graphWrapper:createSegmentWithPoint(x, y, z) then + self:setError() + end +end + +function CreateSegmentBrush:getButtonPrimaryText() + return self:getTranslation(self.primaryButtonText) +end diff --git a/scripts/editor/brushes/graph/segments/DeleteSegmentBrush.lua b/scripts/editor/brushes/graph/segments/DeleteSegmentBrush.lua new file mode 100644 index 000000000..f22aa1192 --- /dev/null +++ b/scripts/editor/brushes/graph/segments/DeleteSegmentBrush.lua @@ -0,0 +1,43 @@ + +--- Creates a new waypoint at the mouse position. +---@class DeleteSegmentBrush : GraphBrush +DeleteSegmentBrush = CpObject(GraphBrush) + +function DeleteSegmentBrush:init(...) + GraphBrush.init(self, ...) + self.supportsPrimaryButton = true + self.supportsPrimaryDragging = true + self.supportsPrimaryAxis = true + return self +end + +function DeleteSegmentBrush:onButtonPrimary(isDown, isDrag, isUp) + local nodeId = self:getHoveredNodeId() + if nodeId ~= nil then + if isDown or isDrag then + if not self.graphWrapper:removeSegmentByPointIndex(nodeId) then + self:setError() + end + end + end +end + +function DeleteSegmentBrush:onAxisPrimary(delta) + local d = self.sizeModifier + delta + if d > self.sizeModifierMax then + self:changeSizeModifier(1) + elseif d <= 0 then + self:changeSizeModifier(self.sizeModifierMax) + else + self:changeSizeModifier(d) + end + self:setInputTextDirty() +end + +function DeleteSegmentBrush:getButtonPrimaryText() + return self:getTranslation(self.primaryButtonText) +end + +function DeleteSegmentBrush:getAxisPrimaryText() + return self:getTranslation(self.primaryAxisText, self.sizeModifier) +end \ No newline at end of file diff --git a/scripts/editor/brushes/graph/segments/MergeSplitSegmentBrush.lua b/scripts/editor/brushes/graph/segments/MergeSplitSegmentBrush.lua new file mode 100644 index 000000000..9465c9270 --- /dev/null +++ b/scripts/editor/brushes/graph/segments/MergeSplitSegmentBrush.lua @@ -0,0 +1,52 @@ +--- Creates a new waypoint at the mouse position. +---@class MergeSplitSegmentBrush : GraphBrush +MergeSplitSegmentBrush = CpObject(GraphBrush) + +function MergeSplitSegmentBrush:init(...) + GraphBrush.init(self, ...) + self.supportsPrimaryButton = true + self.supportsPrimaryDragging = true + self.supportsSecondaryButton = true + return self +end + +function MergeSplitSegmentBrush:onButtonPrimary(isDown, isDrag, isUp) + local nodeId = self:getHoveredNodeId() + if isDown and not self.graphWrapper:hasSelectedNode() then + if nodeId ~= nil then + local isNotFirstOrLast, err = self.graphWrapper:isNotFirstOrLastSegmentPoint(nodeId) + if isNotFirstOrLast then + self:setError(err) + return + end + self.graphWrapper:setSelected(nodeId) + end + end + if isUp and self.graphWrapper:hasSelectedNode() then + local selectedId = self.graphWrapper:getFirstSelectedNodeID() + local success, err = self.graphWrapper:mergeSegments(nodeId, selectedId) + if not success then + self:setError(err) + end + self.graphWrapper:resetSelected() + end +end + +function MergeSplitSegmentBrush:onButtonSecondary() + local nodeId = self:getHoveredNodeId() + if nodeId ~= nil then + local success, err = self.graphWrapper:splitSegment(nodeId) + if not success then + self:setError(err) + return + end + end +end + +function MergeSplitSegmentBrush:getButtonPrimaryText() + return self:getTranslation(self.primaryButtonText) +end + +function MergeSplitSegmentBrush:getButtonSecondaryText() + return self:getTranslation(self.secondaryButtonText) +end \ No newline at end of file diff --git a/scripts/editor/brushes/graph/targets/CreateTargetBrush.lua b/scripts/editor/brushes/graph/targets/CreateTargetBrush.lua new file mode 100644 index 000000000..c540309c6 --- /dev/null +++ b/scripts/editor/brushes/graph/targets/CreateTargetBrush.lua @@ -0,0 +1,28 @@ + +--- Creates a new waypoint at the mouse position. +---@class CreateTargetBrush : GraphBrush +CreateTargetBrush = CpObject(GraphBrush) +function CreateTargetBrush:init(...) + GraphBrush.init(self, ...) + self.supportsPrimaryButton = true + --self.supportsPrimaryDragging = true +end + +function CreateTargetBrush:onButtonPrimary() + local nodeId = self:getHoveredNodeId() + if nodeId ~= nil then + local found, err = self.graphWrapper:hasTargetByIndex(nodeId) + if not found then + self:setError(err) + return + end + self:openTextInput(function(self, text, clickOk, nodeId) + if clickOk then + self.graphWrapper:createTargetForIndex(nodeId, text) + end + end, self:getTranslation(self.inputTitle), nodeId) + end +end +function CreateTargetBrush:getButtonPrimaryText() + return self:getTranslation(self.primaryButtonText) +end diff --git a/scripts/editor/brushes/graph/targets/DeleteTargetBrush.lua b/scripts/editor/brushes/graph/targets/DeleteTargetBrush.lua new file mode 100644 index 000000000..f4a46d95a --- /dev/null +++ b/scripts/editor/brushes/graph/targets/DeleteTargetBrush.lua @@ -0,0 +1,29 @@ + + +--- Creates a new waypoint at the mouse position. +---@class DeleteTargetBrush : GraphBrush +DeleteTargetBrush = CpObject(GraphBrush) + +function DeleteTargetBrush:init(...) + GraphBrush.init(self, ...) + self.supportsPrimaryButton = true + --self.supportsPrimaryDragging = true +end + +function DeleteTargetBrush:onButtonPrimary() + local nodeId = self:getHoveredNodeId() + if nodeId ~= nil then + local target, err = self.graphWrapper:getTargetForIndex(nodeId) + if not target then + self:setError(err) + return + end + self:showYesNoDialog(function() + self.graphWrapper:removeTargetForIndex(nodeId) + end, self:getTranslation(self.yesNoTitle, target:getName()), nodeId) + end +end + +function DeleteTargetBrush:getButtonPrimaryText() + return self:getTranslation(self.primaryButtonText) +end \ No newline at end of file diff --git a/scripts/editor/brushes/graph/targets/RenameTargetBrush.lua b/scripts/editor/brushes/graph/targets/RenameTargetBrush.lua new file mode 100644 index 000000000..32229eb2b --- /dev/null +++ b/scripts/editor/brushes/graph/targets/RenameTargetBrush.lua @@ -0,0 +1,30 @@ + +--- Creates a new waypoint at the mouse position. +---@class RenameTargetBrush : GraphBrush +RenameTargetBrush = CpObject(GraphBrush) + +function RenameTargetBrush:init(...) + GraphBrush.init(self, ...) + self.supportsPrimaryButton = true + --self.supportsPrimaryDragging = true +end + +function RenameTargetBrush:onButtonPrimary() + local nodeId = self:getHoveredNodeId() + if nodeId ~= nil then + local target, err = self.graphWrapper:getTargetForIndex(nodeId) + if not target then + self:setError(err) + return + end + self:openTextInput(function(self, text, clickOk, target) + if clickOk then + target:setName(text) + end + end, self:getTranslation(self.inputTitle, target:getName()), target) + end +end + +function RenameTargetBrush:getButtonPrimaryText() + return self:getTranslation(self.primaryButtonText) +end \ No newline at end of file diff --git a/scripts/graph/Graph.lua b/scripts/graph/Graph.lua new file mode 100644 index 000000000..8e9771b0b --- /dev/null +++ b/scripts/graph/Graph.lua @@ -0,0 +1,108 @@ +---@class Graph : GraphNode +---@field _childNodes GraphSegment[] +Graph = CpObject(GraphNode) +Graph.XML_KEY = "Graph" +function Graph:init() + GraphNode.init(self) + +end + +function Graph.registerXmlSchema(xmlSchema, baseKey) + GraphSegment.registerXmlSchema(xmlSchema, + baseKey .. Graph.XML_KEY .. ".") +end + +function Graph:loadFromXMLFile(xmlFile, baseKey) + xmlFile:iterate(baseKey .. self.XML_KEY .. "." .. GraphSegment.XML_KEY, function (ix, key) + local segment = GraphSegment() + segment:loadFromXMLFile(xmlFile, key) + self:appendChildNode(segment) + end) +end + +function Graph:saveToXMLFile(xmlFile, baseKey) + for i, segment in ipairs(self._childNodes) do + segment:saveToXMLFile(xmlFile, string.format("%s.%s(%i)", + baseKey .. self.XML_KEY, GraphSegment.XML_KEY, i - 1)) + end +end + +---@param node GraphNode +function Graph:onAddedChildNode(node) + GraphNode.onAddedChildNode(self, node) + +end + +---@param node GraphNode +function Graph:onRemovedChildNode(node) + GraphNode.onRemovedChildNode(self, node) + +end + +---@param hoveredNodeID string|nil +---@param selectedNodeIDs table|nil +function Graph:draw(hoveredNodeID, selectedNodeIDs) + for i, segment in ipairs(self._childNodes) do + segment:draw(hoveredNodeID, selectedNodeIDs) + end +end + +function Graph:update(dt) + +end + +---@return GraphSegment[] +function Graph:getSegments() + return self._childNodes +end + +---@return GraphPoint[] +function Graph:getAllPoints() + local points = {} + for _, segment in ipairs(self._childNodes) do + for _, point in ipairs(segment:getPoints()) do + table.insert(points, point) + end + end + return points +end + +---@param index string|nil +---@return GraphPoint|nil +function Graph:getPointByIndex(index) + if index == nil then + return + end + local _, _, ix, jx = string.find(index, "(%d+).(%d+)") + if ix ~= nil and jx ~= nil then + local segment = self:getChildNodeByIndex(tonumber(ix)) + if segment then + local point = segment:getChildNodeByIndex(tonumber(jx)) + if point then + return point + else + CpUtil.info("Failed to get Graph segement(%d) point: %d", ix, jx) + end + else + CpUtil.info("Failed to get Graph segment for: %d", ix) + end + else + CpUtil.info("Failed to get Graph index: %s", index) + end +end + +---@param x number +---@param y number +---@param z number +---@return GraphSegment +function Graph:createSegmentWithPoint(x, y, z) + local segment = GraphSegment() + local point = GraphPoint() + point:setPosition(x, y, z) + segment:appendChildNode(point) + self:appendChildNode(segment) + return segment +end + +---@type Graph +g_graph = Graph() \ No newline at end of file diff --git a/scripts/graph/GraphNode.lua b/scripts/graph/GraphNode.lua new file mode 100644 index 000000000..cf3f3aca4 --- /dev/null +++ b/scripts/graph/GraphNode.lua @@ -0,0 +1,274 @@ +---@class GraphNode +GraphNode = CpObject() +function GraphNode:init() + self._id = -1 + ---@type GraphNode[] + self._childNodes = {} + ---@type GraphNode|nil + self._parentNode = nil +end + +---@param id number +function GraphNode:setID(id) + self._id = id +end + +---@return number +function GraphNode:getID() + return self._id +end + +---@return string +function GraphNode:getRelativeID() + if self._parentNode then + return string.format("%s.%s", self._parentNode:getID(), self._id) + end + return "?." .. tostring(self._id) +end + +function GraphNode:decrementID() + self._id = self._id - 1 +end + +function GraphNode:incrementID() + self._id = self._id + 1 +end + +function GraphNode:repairIDs() + for i, node in ipairs(self._childNodes) do + node:setID(i) + end +end + +---@return boolean +function GraphNode:isValid() + return self._id >= 0 +end + +function GraphNode:setInvalid() + self._id = -1 +end + +---@return GraphNode +function GraphNode:getParentNode() + return self._parentNode +end + +---@param node GraphNode|nil +function GraphNode:setParent(node) + self._parentNode = node +end + +---@return boolean +function GraphNode:isFirstNode() + if not self._parentNode then + return true + end + local index = self._parentNode:getChildNodeIndex(self) + return index == 1 +end + +---@return boolean +function GraphNode:isLastNode() + if not self._parentNode then + return false + end + local index = self._parentNode:getChildNodeIndex(self) + return index == self._parentNode:getNumChildNodes() +end + +function GraphNode:hasChildNodes() + return #self._childNodes > 0 +end + +---@param childNode GraphNode|nil +---@return number|nil +function GraphNode:getChildNodeIndex(childNode) + for ix, node in ipairs(self._childNodes) do + if node == childNode then + return ix + end + end +end + +---@param index number|nil +---@return GraphNode|nil +function GraphNode:getChildNodeByIndex(index) + if index ~= nil then + return self._childNodes[index] + end +end + +---@param sx number +---@param ex number +---@return GraphNode[] +function GraphNode:cloneChildNodesBetweenIndex(sx, ex) + local nodes = {} + for ix, node in ipairs(self._childNodes) do + if ix >= sx and ix <= ex then + table.insert(nodes, node:clone(true)) + end + end + return nodes +end + +---@return number +function GraphNode:getNumChildNodes() + return #self._childNodes +end + +---@return GraphNode[] +function GraphNode:getAllChildNodes() + return self._childNodes +end + +--- Unlink the node from the parent and remove it from it's children +---@param successCallback function|nil +---@return boolean +function GraphNode:unlink(successCallback) + local success = false + if self._parentNode then + local parentNode = self._parentNode + if self._parentNode:removeChildNode(self) then + success = true + if successCallback then + successCallback(self, parentNode) + end + end + end + return success +end + +---@param newNode GraphNode +function GraphNode:appendChildNode(newNode) + table.insert(self._childNodes, newNode) + newNode:setID(#self._childNodes) + self:onAddedChildNode(newNode) +end + +---@param newNode GraphNode +---@param reverse boolean +function GraphNode:extendByChildren(newNode, reverse) + self:extendByChildNodes(newNode:getAllChildNodes(), reverse) +end + +---@param newNodes GraphNode[] +---@param reverse boolean +function GraphNode:extendByChildNodes(newNodes, reverse) + local ix, l, dx = 1, #newNodes, 1 + if reverse then + ix, l, dx = #newNodes, 1, -1 + end + for i=ix, l, dx do + self:appendChildNode(newNodes[i]:clone(true)) + end +end + +---@param newNode GraphNode +---@param reverse boolean +function GraphNode:prepandByChildren(newNode, reverse) + local ix, l, dx = 1, newNode:getNumChildNodes(), 1 + if reverse then + ix, l, dx = newNode:getNumChildNodes(), 1, -1 + end + for i=ix, l, dx do + self:insertChildNodeAtIndex( + newNode:getChildNodeByIndex(i):clone(true), 1) + end +end + +---@param newNode GraphNode +---@param index number|nil [1, #self._childNodes + 1] +function GraphNode:insertChildNodeAtIndex(newNode, index) + if index == nil then + return + end + table.insert(self._childNodes, index, newNode) + newNode:setID(index) + for ix=index + 1, #self._childNodes do + self._childNodes[ix]:incrementID() + end + self:onAddedChildNode(newNode) + return newNode:getRelativeID() +end + +---@param node GraphNode +function GraphNode:onAddedChildNode(node) + node:setParent(self) +end + +---@param sx number +---@param ex number +function GraphNode:removeChildNodesBetweenIndex(sx, ex) + for ix = math.min(ex, #self._childNodes), sx, -1 do + local n = table.remove(self._childNodes, ix) + n:setInvalid() + self:onRemovedChildNode(n) + end + self:repairIDs() +end + +---@param oldNode GraphNode +function GraphNode:removeChildNode(oldNode) + local found = false + for ix, point in ipairs(self._childNodes) do + if point == oldNode then + self:removeChildNodeAtIndex(ix) + found = true + break + end + end + return found +end + +---@param index number +---@return boolean, GraphNode|nil +function GraphNode:removeChildNodeAtIndex(index) + local n = table.remove(self._childNodes, index) + if n ~= nil then + for ix = index, #self._childNodes do + self._childNodes[ix]:decrementID() + end + n:setInvalid() + self:onRemovedChildNode(n) + end + return n ~= nil, n +end + +---@param node GraphNode +function GraphNode:onRemovedChildNode(node) + node:setParent(nil) +end + +function GraphNode:clearChildNodes() + for i = #self._childNodes, 1, -1 do + self._childNodes[i]:unlink() + end +end + +---@param newNode GraphNode +---@param unlink boolean|nil +function GraphNode:copyTo(newNode, unlink) + if not unlink then + newNode._parentNode = self._parentNode + end + for _, node in ipairs(self._childNodes) do + newNode:appendChildNode(node) + end +end + +---@param unlink boolean|nil +function GraphNode:clone(unlink) + --- Override +end + +---@return string[] +function GraphNode:getDebugInfos() + local data = {string.format("ID: %s", self:getRelativeID())} + if self._parentNode then + for ix, l in ipairs(self._parentNode:getDebugInfos()) do + data[ix + 1] = l + end + end + return data +end \ No newline at end of file diff --git a/scripts/graph/GraphPoint.lua b/scripts/graph/GraphPoint.lua new file mode 100644 index 000000000..af7504379 --- /dev/null +++ b/scripts/graph/GraphPoint.lua @@ -0,0 +1,206 @@ +---@class GraphPoint : GraphNode +---@field _parentNode GraphSegment +GraphPoint = CpObject(GraphNode) +GraphPoint.XML_KEY = "Point" +function GraphPoint:init() + GraphNode.init(self) + self._x = 0 + self._y = 0 + self._z = 0 + ---@type GraphTarget|nil + self._target = nil +end + +function GraphPoint.registerXmlSchema(xmlSchema, baseKey) + local key = baseKey .. GraphPoint.XML_KEY .. "(?)" + GraphTarget.registerXmlSchema(xmlSchema, key) + xmlSchema:register(XMLValueType.VECTOR_3, + key .. "#pos", + "Position", {0,0,0}) + xmlSchema:register(XMLValueType.BOOL, + key .. "#hasTarget", + "Has associated Target?", false) +end + +function GraphPoint:loadFromXMLFile(xmlFile, key) + local pos = xmlFile:getValue(key .. "#pos", + self._x ,self._y, self._z) + if pos then + self._x, self._y, self._z = unpack(pos) + end + if xmlFile:getValue(key .. "#hasTarget", false) then + self._target = GraphTarget(self) + self._target:loadFromXMLFile(xmlFile, key) + end +end + +function GraphPoint:saveToXMLFile(xmlFile, key) + xmlFile:setValue(key .. "#pos", self._x, self._y, self._z) + if self._target then + xmlFile:setValue(key .. "#hasTarget", true) + self._target:saveToXMLFile(xmlFile, key) + end +end + +---@param newNode GraphPoint +---@param unlink boolean|nil +function GraphPoint:copyTo(newNode, unlink) + GraphNode.copyTo(self, newNode, unlink) + newNode._target = self._target + newNode._x = self._x + newNode._y = self._y + newNode._z = self._z +end + +---@return GraphPoint +function GraphPoint:clone(unlink) + local newPoint = GraphPoint() + self:copyTo(newPoint, unlink) + return newPoint +end + +---@param hoveredNodeID string|nil +---@param selectedNodeIDs table|nil +---@param isTemporary boolean|nil +function GraphPoint:draw(hoveredNodeID, selectedNodeIDs, isTemporary) + local color = Color.new(0, 0, 1) + if hoveredNodeID == self:getRelativeID() then + color = Color.new(1, 1, 1) + elseif selectedNodeIDs ~= nil and selectedNodeIDs[self:getRelativeID()] ~= nil then + color = Color.new(1, 1, 0) + elseif isTemporary then + color = Color.new(0, 1, 0) + end + DebugUtil.drawDebugSphere(self._x, self._y, self._z, + 1, 3, 3, color, false, false) + + local data = self:getDebugInfos() + local yOffset = 0 + for _, line in ipairs(data) do + Utils.renderTextAtWorldPosition(self._x, self._y, self._z, + line, getCorrectTextSize(0.012), yOffset) + yOffset = yOffset + getCorrectTextSize(0.012) + end +end + +function GraphPoint:getDebugInfos() + local data = GraphNode.getDebugInfos(self) + if self._target then + data[#data + 1] = string.format("Target: %s", self._target:getName()) + end + return data +end + +---@param x number +---@param y number +---@param z number +function GraphPoint:setPosition(x, y, z) + self._x = x + self._y = y + self._z = z +end + +---@param x number +---@param z number +function GraphPoint:setPosition2D(x, z) + self._x = x + if getTerrainHeightAtWorldPos then + self._y = getTerrainHeightAtWorldPos(g_currentMission.terrainRootNode, x, 0, z) + else + self._y = 0 + end + self._z = z +end + +---@return number, number, number +function GraphPoint:getPosition() + return self._x, self._y, self._z +end + +---@return number, number +function GraphPoint:getPosition2D() + return self._x, self._z +end + +---@param dx number +---@param dy number +---@param dz number +function GraphPoint:move(dx, dy, dz) + if dx == nil or dy == nil or dz == nil then + return + end + self._x = self._x + dx + self._y = self._y + dy + self._z = self._z + dz +end + +---@param dx number +---@param dz number +function GraphPoint:move2D(dx, dz) + if dx == nil or dz == nil then + return + end + self._x = self._x + dx + self._z = self._z + dz +end + +---@param dx number +---@param dy number +---@param dz number +function GraphPoint:moveTo(dx, dy, dz) + if dx == nil or dy == nil or dz == nil then + return + end + self._x = dx + self._y = dy + self._z = dz +end + +---@param dx number +---@param dz number +function GraphPoint:moveTo2D(dx, dz) + if dx == nil or dz == nil then + return + end + self._x = dx + self._z = dz +end + +---@param other GraphPoint +function GraphPoint:getDistance2DToPoint(other) + local dx, dz = other:getPosition2D() + return MathUtil.vector2Length(self._x - dx, self._z - dz) +end + +----------------------------- +--- Target +----------------------------- + +---@return boolean +function GraphPoint:hasTarget() + return self._target ~= nil +end + +---@return GraphTarget|nil +function GraphPoint:getTarget() + return self._target +end + +---@param name string +---@return boolean +function GraphPoint:createTarget(name) + if self:hasTarget() then + return false + end + self._target = GraphTarget(self, name) + return true +end + +---@return boolean +function GraphPoint:removeTarget() + if not self:hasTarget() then + return false + end + self._target = nil + return true +end \ No newline at end of file diff --git a/scripts/graph/GraphSegment.lua b/scripts/graph/GraphSegment.lua new file mode 100644 index 000000000..8e7369696 --- /dev/null +++ b/scripts/graph/GraphSegment.lua @@ -0,0 +1,136 @@ +---@class GraphSegmentDirection +GraphSegmentDirection = {} +GraphSegmentDirection.FORWARD = 1 +GraphSegmentDirection.REVERSE = 2 +GraphSegmentDirection.DUAL = 3 +GraphSegmentDirection.MAX_KEY = GraphSegmentDirection.DUAL +GraphSegmentDirection.DEBUG_TEXTS = { + [GraphSegmentDirection.FORWARD] = "Forward", + [GraphSegmentDirection.REVERSE] = "Reverse", + [GraphSegmentDirection.DUAL] = "Dual", +} + +---@class GraphSegment : GraphNode +---@field _childNodes GraphPoint[] +GraphSegment = CpObject(GraphNode) +GraphSegment.XML_KEY = "Segment" +function GraphSegment:init() + GraphNode.init(self) + self._direction = GraphSegmentDirection.FORWARD +end + +function GraphSegment.registerXmlSchema(xmlSchema, baseKey) + local key = baseKey .. GraphSegment.XML_KEY + xmlSchema:register(XMLValueType.INT, + key .. "(?)#direction", + "Current direction", GraphSegmentDirection.FORWARD) + GraphPoint.registerXmlSchema(xmlSchema, key .. "(?).") +end + +function GraphSegment:loadFromXMLFile(xmlFile, baseKey) + self._direction = xmlFile:getValue(baseKey .. "#direction", GraphSegmentDirection.FORWARD) + xmlFile:iterate(baseKey .. "." .. GraphPoint.XML_KEY, function (ix, key) + local point = GraphPoint() + point:loadFromXMLFile(xmlFile, key) + self:appendChildNode(point) + end) +end + +function GraphSegment:saveToXMLFile(xmlFile, baseKey) + xmlFile:setValue(baseKey .. "#direction", self._direction) + for i, point in ipairs(self._childNodes) do + local key = string.format("%s.%s(%d)", baseKey, GraphPoint.XML_KEY, i - 1) + point:saveToXMLFile(xmlFile, key) + end +end + +---@param newNode GraphSegment +---@param unlink boolean|nil +function GraphSegment:copyTo(newNode, unlink) + GraphNode.copyTo(self, newNode, unlink) + newNode._direction = self._direction +end + +---@return GraphSegment +---@param unlink boolean|nil +function GraphSegment:clone(unlink) + local newSegment = GraphSegment() + self:copyTo(newSegment, unlink) + return newSegment +end + +---@param hoveredNodeID string|nil +---@param selectedNodeIDs table|nil +---@param isTemporary boolean|nil +---@param temporaryPrevPoint GraphNode|nil +function GraphSegment:draw(hoveredNodeID, selectedNodeIDs, isTemporary, temporaryPrevPoint) + local prevPoint = temporaryPrevPoint + for _, point in ipairs(self._childNodes) do + point:draw(hoveredNodeID, selectedNodeIDs, isTemporary) + if prevPoint then + local color = {0, 0.5, 1} + local x, y, z = point:getPosition() + local dx, dy, dz = prevPoint:getPosition() + DebugUtil.drawDebugLine(x, y + 2, z, + dx, dy + 2, dz, unpack(color), 2) + local dist = MathUtil.vector3Length(x - dx, y - dy, z - dz) + if dist > 1 then + local nx, _, nz = MathUtil.vector3Normalize(x - dx, y - dy, z - dz) + local delta = 2 + local numArrows = dist / delta + 1 + local spacing = dist / (numArrows + 1) + if self._direction == GraphSegmentDirection.REVERSE then + nz = -1 * nz + nx = -1 * nx + end + for i = spacing/2, dist, spacing do + if self._direction == GraphSegmentDirection.FORWARD or + self._direction == GraphSegmentDirection.REVERSE then + + local tx, tz = dx + nx * i, dz + nz * i + if self._direction == GraphSegmentDirection.REVERSE then + tx, tz = x + nx * i, z + nz * i + end + local ncx = nx * math.cos(math.pi/4) - nz * math.sin(math.pi/4) + local ncz = nx * math.sin(math.pi/4) + nz * math.cos(math.pi/4) + DebugUtil.drawDebugLine(tx, y, tz, + tx - ncx * 2, y, tz - ncz * 2, unpack(color)) + ncx = nx * math.cos(-math.pi/4) - nz * math.sin(-math.pi/4) + ncz = nx * math.sin(-math.pi/4) + nz * math.cos(-math.pi/4) + DebugUtil.drawDebugLine(tx, y, tz, + tx - ncx * 2, y, tz - ncz * 2, unpack(color)) + elseif self._direction == GraphSegmentDirection.DUAL then + -- x, y, z, radius, steps, color, alignToTerrain, filled + DebugUtil.drawDebugCircle(dx + nx * i, y, dz + nz * i, + 1, 10, color) + end + end + end + end + prevPoint = point + end +end + +function GraphSegment:getDebugInfos() + return {string.format("Direction: %s", self:getDirectionString())} +end + +---@return GraphPoint[] +function GraphSegment:getPoints() + return self._childNodes +end + +---@param newDirection number|nil +function GraphSegment:changeDirection(newDirection) + if newDirection == nil then + newDirection = self._direction + 1 + if newDirection > GraphSegmentDirection.MAX_KEY then + newDirection = 1 + end + end + self._direction = newDirection +end + +function GraphSegment:getDirectionString() + return GraphSegmentDirection.DEBUG_TEXTS[self._direction] or "???" +end \ No newline at end of file diff --git a/scripts/graph/GraphTarget.lua b/scripts/graph/GraphTarget.lua new file mode 100644 index 000000000..1dc99cd12 --- /dev/null +++ b/scripts/graph/GraphTarget.lua @@ -0,0 +1,29 @@ +---@class GraphTarget +GraphTarget = CpObject() +function GraphTarget:init(point, name) + ---@type GraphPoint + self._point = point + self._name = name or "???" +end + +function GraphTarget.registerXmlSchema(xmlSchema, baseKey) + xmlSchema:register(XMLValueType.STRING, baseKey .. "#name", "Target name") +end + +function GraphTarget:loadFromXMLFile(xmlFile, baseKey) + self._name = xmlFile:getValue(baseKey .. "#name", "") +end + +function GraphTarget:saveToXMLFile(xmlFile, baseKey) + xmlFile:setValue(baseKey .. "#name", self._name) +end + +---@return string +function GraphTarget:getName() + return self._name +end + +---@param name string +function GraphTarget:setName(name) + self._name = name +end \ No newline at end of file diff --git a/scripts/gui/CpInGameMenu.lua b/scripts/gui/CpInGameMenu.lua index e971d9c84..777492241 100644 --- a/scripts/gui/CpInGameMenu.lua +++ b/scripts/gui/CpInGameMenu.lua @@ -219,10 +219,9 @@ function CpInGameMenu:setupMenuPages() end function CpInGameMenu:isContructionPageAvailiable() - return g_courseEditor:getIsActive() + return g_courseEditor:getIsActive() or g_graphEditor:getIsActive() end - function CpInGameMenu:setupMenuButtonInfo() CpInGameMenu:superClass().setupMenuButtonInfo(self) local onButtonBackFunction = self.clickBackCallback From 5b8b393e2f972120b145dea668b17743a4403016 Mon Sep 17 00:00:00 2001 From: schwiti6190 Date: Wed, 26 Mar 2025 19:03:01 +0000 Subject: [PATCH 02/73] Updated translations --- translations/translation_br.xml | 45 +++++++++++++++++++++++++++++++++ translations/translation_cs.xml | 45 +++++++++++++++++++++++++++++++++ translations/translation_ct.xml | 45 +++++++++++++++++++++++++++++++++ translations/translation_cz.xml | 45 +++++++++++++++++++++++++++++++++ translations/translation_da.xml | 45 +++++++++++++++++++++++++++++++++ translations/translation_de.xml | 45 +++++++++++++++++++++++++++++++++ translations/translation_ea.xml | 45 +++++++++++++++++++++++++++++++++ translations/translation_en.xml | 45 +++++++++++++++++++++++++++++++++ translations/translation_es.xml | 45 +++++++++++++++++++++++++++++++++ translations/translation_fc.xml | 45 +++++++++++++++++++++++++++++++++ translations/translation_fi.xml | 45 +++++++++++++++++++++++++++++++++ translations/translation_fr.xml | 45 +++++++++++++++++++++++++++++++++ translations/translation_hu.xml | 45 +++++++++++++++++++++++++++++++++ translations/translation_id.xml | 45 +++++++++++++++++++++++++++++++++ translations/translation_it.xml | 45 +++++++++++++++++++++++++++++++++ translations/translation_jp.xml | 45 +++++++++++++++++++++++++++++++++ translations/translation_kr.xml | 45 +++++++++++++++++++++++++++++++++ translations/translation_nl.xml | 45 +++++++++++++++++++++++++++++++++ translations/translation_no.xml | 45 +++++++++++++++++++++++++++++++++ translations/translation_pl.xml | 45 +++++++++++++++++++++++++++++++++ translations/translation_pt.xml | 45 +++++++++++++++++++++++++++++++++ translations/translation_ro.xml | 45 +++++++++++++++++++++++++++++++++ translations/translation_ru.xml | 45 +++++++++++++++++++++++++++++++++ translations/translation_sv.xml | 45 +++++++++++++++++++++++++++++++++ translations/translation_tr.xml | 45 +++++++++++++++++++++++++++++++++ translations/translation_uk.xml | 45 +++++++++++++++++++++++++++++++++ translations/translation_vi.xml | 45 +++++++++++++++++++++++++++++++++ 27 files changed, 1215 insertions(+) diff --git a/translations/translation_br.xml b/translations/translation_br.xml index b5b5de0fb..acebaef12 100644 --- a/translations/translation_br.xml +++ b/translations/translation_br.xml @@ -479,6 +479,51 @@ A rota é salva automaticamente ao fechar o editor e substituir a rota seleciona + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_cs.xml b/translations/translation_cs.xml index 4965fa0be..fe87900cf 100644 --- a/translations/translation_cs.xml +++ b/translations/translation_cs.xml @@ -479,6 +479,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_ct.xml b/translations/translation_ct.xml index a27e6975c..c4310ed84 100644 --- a/translations/translation_ct.xml +++ b/translations/translation_ct.xml @@ -479,6 +479,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_cz.xml b/translations/translation_cz.xml index 306425d4c..b23bf1eab 100644 --- a/translations/translation_cz.xml +++ b/translations/translation_cz.xml @@ -479,6 +479,51 @@ Trasa se automaticky uloží při zavření editoru a přepíše vybranou trasu. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_da.xml b/translations/translation_da.xml index 122225470..9caf441e2 100644 --- a/translations/translation_da.xml +++ b/translations/translation_da.xml @@ -479,6 +479,51 @@ Ruten gemmes automatisk ved lukning af editoren og tilsidesætter den valgte rut + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_de.xml b/translations/translation_de.xml index bf24954fe..4f2dc58c0 100644 --- a/translations/translation_de.xml +++ b/translations/translation_de.xml @@ -479,6 +479,51 @@ Der Kurs wird beim Schließen automatisch gespeichert und überschrieben. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_ea.xml b/translations/translation_ea.xml index f6e17c282..5c85e73a3 100644 --- a/translations/translation_ea.xml +++ b/translations/translation_ea.xml @@ -479,6 +479,51 @@ El curso se guarda automáticamente al cerrar el editor y anula el curso selecci + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_en.xml b/translations/translation_en.xml index adea7a1e9..72c55e6d7 100644 --- a/translations/translation_en.xml +++ b/translations/translation_en.xml @@ -479,6 +479,51 @@ The course is saved automatically on closing of the editor and overrides the sel + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_es.xml b/translations/translation_es.xml index 1f38ab7ee..99b77ea86 100644 --- a/translations/translation_es.xml +++ b/translations/translation_es.xml @@ -479,6 +479,51 @@ El curso se guarda automáticamente al cerrar el editor y anula el curso selecci + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_fc.xml b/translations/translation_fc.xml index 2b8f4c758..b007a1e5b 100644 --- a/translations/translation_fc.xml +++ b/translations/translation_fc.xml @@ -479,6 +479,51 @@ The course is saved automatically on closing of the editor and overrides the sel + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_fi.xml b/translations/translation_fi.xml index 8c8756b9d..b980f7532 100644 --- a/translations/translation_fi.xml +++ b/translations/translation_fi.xml @@ -479,6 +479,51 @@ The course is saved automatically on closing of the editor and overrides the sel + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_fr.xml b/translations/translation_fr.xml index 9d5f6a8bf..ff554ce03 100644 --- a/translations/translation_fr.xml +++ b/translations/translation_fr.xml @@ -479,6 +479,51 @@ La course est automatiquement sauvegardée à la fermeture de l'éditeur et remp + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_hu.xml b/translations/translation_hu.xml index f9c817ac7..728463c46 100644 --- a/translations/translation_hu.xml +++ b/translations/translation_hu.xml @@ -479,6 +479,51 @@ Az útvonal automatikusan mentésre kerül a szerkesztő bezárásakor és felü + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_id.xml b/translations/translation_id.xml index c11e7e36c..fdcbf8e0d 100644 --- a/translations/translation_id.xml +++ b/translations/translation_id.xml @@ -479,6 +479,51 @@ The course is saved automatically on closing of the editor and overrides the sel + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_it.xml b/translations/translation_it.xml index 4f1d0120e..d5cb0a44c 100644 --- a/translations/translation_it.xml +++ b/translations/translation_it.xml @@ -479,6 +479,51 @@ Il percorso viene salvato automaticamente alla chiusura dell'editor e sovrascriv + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_jp.xml b/translations/translation_jp.xml index 8b840e112..7e0bb1c4a 100644 --- a/translations/translation_jp.xml +++ b/translations/translation_jp.xml @@ -479,6 +479,51 @@ The course is saved automatically on closing of the editor and overrides the sel + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_kr.xml b/translations/translation_kr.xml index 499eb19f5..d1ea14c7a 100644 --- a/translations/translation_kr.xml +++ b/translations/translation_kr.xml @@ -478,6 +478,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_nl.xml b/translations/translation_nl.xml index fc76ba9ab..48c345a4d 100644 --- a/translations/translation_nl.xml +++ b/translations/translation_nl.xml @@ -479,6 +479,51 @@ The course is saved automatically on closing of the editor and overrides the sel + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_no.xml b/translations/translation_no.xml index 421043be9..ce9a3cedf 100644 --- a/translations/translation_no.xml +++ b/translations/translation_no.xml @@ -479,6 +479,51 @@ The course is saved automatically on closing of the editor and overrides the sel + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_pl.xml b/translations/translation_pl.xml index 4b9de7574..8b424d0e5 100644 --- a/translations/translation_pl.xml +++ b/translations/translation_pl.xml @@ -479,6 +479,51 @@ Kurs jest automatycznie zapisywany po zamknięciu edytora i nadpisuje wybrany ku + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_pt.xml b/translations/translation_pt.xml index 4694c61a6..c45962480 100644 --- a/translations/translation_pt.xml +++ b/translations/translation_pt.xml @@ -479,6 +479,51 @@ A rota é guardada automaticamente ao fechar o editor e sobrepõe-se a qualquer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_ro.xml b/translations/translation_ro.xml index a9cc0f718..01b8c8c6f 100644 --- a/translations/translation_ro.xml +++ b/translations/translation_ro.xml @@ -479,6 +479,51 @@ The course is saved automatically on closing of the editor and overrides the sel + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_ru.xml b/translations/translation_ru.xml index 8f037d698..eaa358699 100644 --- a/translations/translation_ru.xml +++ b/translations/translation_ru.xml @@ -479,6 +479,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_sv.xml b/translations/translation_sv.xml index 758a0e45a..46333aff2 100644 --- a/translations/translation_sv.xml +++ b/translations/translation_sv.xml @@ -479,6 +479,51 @@ Banan sparas automatiskt vid stängning av editorn och åsidosätter den valda b + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_tr.xml b/translations/translation_tr.xml index 712c69545..2e08cb60e 100644 --- a/translations/translation_tr.xml +++ b/translations/translation_tr.xml @@ -479,6 +479,51 @@ Düzenleyici kapatıldığında rota otomatik olarak kaydedilir ve önceki rotay + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_uk.xml b/translations/translation_uk.xml index afae59a1b..23d1e7dd1 100644 --- a/translations/translation_uk.xml +++ b/translations/translation_uk.xml @@ -479,6 +479,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_vi.xml b/translations/translation_vi.xml index 6cfade582..57dec80a5 100644 --- a/translations/translation_vi.xml +++ b/translations/translation_vi.xml @@ -479,6 +479,51 @@ The course is saved automatically on closing of the editor and overrides the sel + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From c93e9578666db8aabb711c8b464884c2d672e66b Mon Sep 17 00:00:00 2001 From: David Schwietering Date: Wed, 26 Mar 2025 20:23:04 +0100 Subject: [PATCH 03/73] =?UTF-8?q?Kleine=20Anpassung=20und=20Keybind=20hinz?= =?UTF-8?q?ugef=C3=BCgt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Courseplay.lua | 5 +++++ config/MasterTranslations.xml | 12 ++++++++---- modDesc.xml | 6 +++++- scripts/editor/brushes/BaseBrush.lua | 2 +- scripts/editor/brushes/graph/GraphBrush.lua | 9 --------- .../editor/brushes/graph/points/DeletePointBrush.lua | 2 +- .../brushes/graph/segments/DeleteSegmentBrush.lua | 2 +- .../graph/segments/MergeSplitSegmentBrush.lua | 4 ++-- 8 files changed, 23 insertions(+), 19 deletions(-) diff --git a/Courseplay.lua b/Courseplay.lua index 12c063a93..d7020a3b3 100644 --- a/Courseplay.lua +++ b/Courseplay.lua @@ -147,6 +147,11 @@ function Courseplay:setupGui() g_messageCenter:publishDelayed(MessageType.GUI_CP_INGAME_OPEN) end, false, true, false, true) g_inputBinding:setActionEventTextVisibility(id, false) + + local _, id = g_inputBinding:registerActionEvent(InputAction.CP_OPEN_GRAPH_EDITOR, self, function () + g_graphEditor:open() + end, false, true, false, true) + g_inputBinding:setActionEventTextVisibility(id, false) -- CpDebug.registerEvents() end PlayerInputComponent.registerGlobalPlayerActionEvents = Utils.overwrittenFunction( diff --git a/config/MasterTranslations.xml b/config/MasterTranslations.xml index 3d361e9d9..09efcc0cf 100644 --- a/config/MasterTranslations.xml +++ b/config/MasterTranslations.xml @@ -1773,11 +1773,11 @@ The course is saved automatically on closing of the editor and overrides the sel - + - + @@ -1786,11 +1786,11 @@ The course is saved automatically on closing of the editor and overrides the sel - + - + @@ -3363,6 +3363,10 @@ TPS extension + + + + @@ -1116,5 +1116,6 @@ Agora sua seleção deve ser semelhante à imagem. + diff --git a/translations/translation_cs.xml b/translations/translation_cs.xml index fe87900cf..f9bc4db5a 100644 --- a/translations/translation_cs.xml +++ b/translations/translation_cs.xml @@ -502,8 +502,8 @@ - - + + @@ -1080,5 +1080,6 @@ hud还显示助手工作时堆或思洛存储器的剩余填充水平。 + diff --git a/translations/translation_ct.xml b/translations/translation_ct.xml index c4310ed84..53c64e648 100644 --- a/translations/translation_ct.xml +++ b/translations/translation_ct.xml @@ -502,8 +502,8 @@ - - + + @@ -1080,5 +1080,6 @@ Now your selection should look similar to the image. + diff --git a/translations/translation_cz.xml b/translations/translation_cz.xml index b23bf1eab..92a277220 100644 --- a/translations/translation_cz.xml +++ b/translations/translation_cz.xml @@ -502,8 +502,8 @@ Trasa se automaticky uloží při zavření editoru a přepíše vybranou trasu. - - + + @@ -1078,5 +1078,6 @@ Nyní by váš výběr měl vypadat podobně jako na obrázku. + diff --git a/translations/translation_da.xml b/translations/translation_da.xml index 9caf441e2..5c2a84a03 100644 --- a/translations/translation_da.xml +++ b/translations/translation_da.xml @@ -502,8 +502,8 @@ Ruten gemmes automatisk ved lukning af editoren og tilsidesætter den valgte rut - - + + @@ -1091,5 +1091,6 @@ Now your selection should look similar to the image. + diff --git a/translations/translation_de.xml b/translations/translation_de.xml index 4f2dc58c0..e18c3fcc6 100644 --- a/translations/translation_de.xml +++ b/translations/translation_de.xml @@ -502,8 +502,8 @@ Der Kurs wird beim Schließen automatisch gespeichert und überschrieben. - - + + @@ -1112,5 +1112,6 @@ Das Kreuz sollte jetzt, wie im Bild dargestellt, gelb sein. + diff --git a/translations/translation_ea.xml b/translations/translation_ea.xml index 5c85e73a3..158a762a8 100644 --- a/translations/translation_ea.xml +++ b/translations/translation_ea.xml @@ -502,8 +502,8 @@ El curso se guarda automáticamente al cerrar el editor y anula el curso selecci - - + + @@ -1122,5 +1122,6 @@ Ahora su selección debería verse similar a la imagen. + diff --git a/translations/translation_en.xml b/translations/translation_en.xml index 72c55e6d7..e76cd34ae 100644 --- a/translations/translation_en.xml +++ b/translations/translation_en.xml @@ -502,8 +502,8 @@ The course is saved automatically on closing of the editor and overrides the sel - - + + @@ -1129,5 +1129,6 @@ Now your selection should look similar to the image. + diff --git a/translations/translation_es.xml b/translations/translation_es.xml index 99b77ea86..59b60a578 100644 --- a/translations/translation_es.xml +++ b/translations/translation_es.xml @@ -502,8 +502,8 @@ El curso se guarda automáticamente al cerrar el editor y anula el curso selecci - - + + @@ -1122,5 +1122,6 @@ Ahora su selección debería verse similar a la imagen. + diff --git a/translations/translation_fc.xml b/translations/translation_fc.xml index b007a1e5b..936b85e55 100644 --- a/translations/translation_fc.xml +++ b/translations/translation_fc.xml @@ -502,8 +502,8 @@ The course is saved automatically on closing of the editor and overrides the sel - - + + @@ -1088,5 +1088,6 @@ Now your selection should look similar to the image. + diff --git a/translations/translation_fi.xml b/translations/translation_fi.xml index b980f7532..aeeac19de 100644 --- a/translations/translation_fi.xml +++ b/translations/translation_fi.xml @@ -502,8 +502,8 @@ The course is saved automatically on closing of the editor and overrides the sel - - + + @@ -1088,5 +1088,6 @@ Now your selection should look similar to the image. + diff --git a/translations/translation_fr.xml b/translations/translation_fr.xml index ff554ce03..773dd9a1f 100644 --- a/translations/translation_fr.xml +++ b/translations/translation_fr.xml @@ -502,8 +502,8 @@ La course est automatiquement sauvegardée à la fermeture de l'éditeur et remp - - + + @@ -1073,5 +1073,6 @@ Votre sélection devrait ressembler à l'illustration ci-contre. + diff --git a/translations/translation_hu.xml b/translations/translation_hu.xml index 728463c46..d4355e03c 100644 --- a/translations/translation_hu.xml +++ b/translations/translation_hu.xml @@ -502,8 +502,8 @@ Az útvonal automatikusan mentésre kerül a szerkesztő bezárásakor és felü - - + + @@ -1097,5 +1097,6 @@ A kijelölésnek hasonlónak kell lennie, mint a képen. + diff --git a/translations/translation_id.xml b/translations/translation_id.xml index fdcbf8e0d..c1eee20ae 100644 --- a/translations/translation_id.xml +++ b/translations/translation_id.xml @@ -502,8 +502,8 @@ The course is saved automatically on closing of the editor and overrides the sel - - + + @@ -1129,5 +1129,6 @@ Now your selection should look similar to the image. + diff --git a/translations/translation_it.xml b/translations/translation_it.xml index d5cb0a44c..60713a3a8 100644 --- a/translations/translation_it.xml +++ b/translations/translation_it.xml @@ -502,8 +502,8 @@ Il percorso viene salvato automaticamente alla chiusura dell'editor e sovrascriv - - + + @@ -1129,5 +1129,6 @@ Ora la tua selezione dovrebbe essere simile all'immagine. + diff --git a/translations/translation_jp.xml b/translations/translation_jp.xml index 7e0bb1c4a..79430b6a8 100644 --- a/translations/translation_jp.xml +++ b/translations/translation_jp.xml @@ -502,8 +502,8 @@ The course is saved automatically on closing of the editor and overrides the sel - - + + @@ -1087,5 +1087,6 @@ Now your selection should look similar to the image. + diff --git a/translations/translation_kr.xml b/translations/translation_kr.xml index d1ea14c7a..b4ff42c2c 100644 --- a/translations/translation_kr.xml +++ b/translations/translation_kr.xml @@ -501,8 +501,8 @@ - - + + @@ -1187,5 +1187,6 @@ HUD의 '목표 아이콘'을 사용하여 AI 지도에서 적재 및 하역 위 + diff --git a/translations/translation_nl.xml b/translations/translation_nl.xml index 48c345a4d..b5230832e 100644 --- a/translations/translation_nl.xml +++ b/translations/translation_nl.xml @@ -502,8 +502,8 @@ The course is saved automatically on closing of the editor and overrides the sel - - + + @@ -1087,5 +1087,6 @@ Now your selection should look similar to the image. + diff --git a/translations/translation_no.xml b/translations/translation_no.xml index ce9a3cedf..4608e8977 100644 --- a/translations/translation_no.xml +++ b/translations/translation_no.xml @@ -502,8 +502,8 @@ The course is saved automatically on closing of the editor and overrides the sel - - + + @@ -1088,5 +1088,6 @@ Now your selection should look similar to the image. + diff --git a/translations/translation_pl.xml b/translations/translation_pl.xml index 8b424d0e5..a26c3d0b1 100644 --- a/translations/translation_pl.xml +++ b/translations/translation_pl.xml @@ -502,8 +502,8 @@ Kurs jest automatycznie zapisywany po zamknięciu edytora i nadpisuje wybrany ku - - + + @@ -1057,5 +1057,6 @@ Twój wybór powinien wyglądać podobnie do tego zdjęcia. + diff --git a/translations/translation_pt.xml b/translations/translation_pt.xml index c45962480..da5a0a58e 100644 --- a/translations/translation_pt.xml +++ b/translations/translation_pt.xml @@ -502,8 +502,8 @@ A rota é guardada automaticamente ao fechar o editor e sobrepõe-se a qualquer - - + + @@ -1082,5 +1082,6 @@ Now your selection should look similar to the image. + diff --git a/translations/translation_ro.xml b/translations/translation_ro.xml index 01b8c8c6f..40340d047 100644 --- a/translations/translation_ro.xml +++ b/translations/translation_ro.xml @@ -502,8 +502,8 @@ The course is saved automatically on closing of the editor and overrides the sel - - + + @@ -1088,5 +1088,6 @@ Now your selection should look similar to the image. + diff --git a/translations/translation_ru.xml b/translations/translation_ru.xml index eaa358699..4bb5d87b5 100644 --- a/translations/translation_ru.xml +++ b/translations/translation_ru.xml @@ -502,8 +502,8 @@ - - + + @@ -1099,5 +1099,6 @@ HUD также показывает оставшийся уровень запо + diff --git a/translations/translation_sv.xml b/translations/translation_sv.xml index 46333aff2..1b86298c8 100644 --- a/translations/translation_sv.xml +++ b/translations/translation_sv.xml @@ -502,8 +502,8 @@ Banan sparas automatiskt vid stängning av editorn och åsidosätter den valda b - - + + @@ -1085,5 +1085,6 @@ Now your selection should look similar to the image. + diff --git a/translations/translation_tr.xml b/translations/translation_tr.xml index 2e08cb60e..16d6c6666 100644 --- a/translations/translation_tr.xml +++ b/translations/translation_tr.xml @@ -502,8 +502,8 @@ Düzenleyici kapatıldığında rota otomatik olarak kaydedilir ve önceki rotay - - + + @@ -1129,5 +1129,6 @@ Seçimler tamamlandığında örnek görsele benzer bir görüntü olması gerek + diff --git a/translations/translation_uk.xml b/translations/translation_uk.xml index 23d1e7dd1..b65e0e3f9 100644 --- a/translations/translation_uk.xml +++ b/translations/translation_uk.xml @@ -502,8 +502,8 @@ - - + + @@ -1101,5 +1101,6 @@ CoursePlay здатен розподіляти та пресувати січк + diff --git a/translations/translation_vi.xml b/translations/translation_vi.xml index 57dec80a5..db5ba9f89 100644 --- a/translations/translation_vi.xml +++ b/translations/translation_vi.xml @@ -502,8 +502,8 @@ The course is saved automatically on closing of the editor and overrides the sel - - + + @@ -1129,5 +1129,6 @@ Now your selection should look similar to the image. + From f71d4b2a84494bc0deec0fa983b221b22b16f00e Mon Sep 17 00:00:00 2001 From: David Schwietering Date: Thu, 27 Mar 2025 21:03:23 +0100 Subject: [PATCH 05/73] Line Brush improvements --- config/GraphEditorCategories.xml | 2 +- config/MasterTranslations.xml | 23 +- modDesc.xml | 2 +- scripts/editor/EditorGraphWrapper.lua | 13 +- .../graph/misc/StraightLineWaypoints.lua | 118 ---------- .../brushes/graph/points/InsertPointBrush.lua | 9 +- .../brushes/graph/points/LinePointBrush.lua | 208 ++++++++++++++++++ .../graph/points/StraightLinePointBrush.lua | 174 --------------- scripts/graph/GraphNode.lua | 6 + scripts/graph/GraphSegment.lua | 11 + 10 files changed, 252 insertions(+), 314 deletions(-) delete mode 100644 scripts/editor/brushes/graph/misc/StraightLineWaypoints.lua create mode 100644 scripts/editor/brushes/graph/points/LinePointBrush.lua delete mode 100644 scripts/editor/brushes/graph/points/StraightLinePointBrush.lua diff --git a/config/GraphEditorCategories.xml b/config/GraphEditorCategories.xml index 1deb5945b..bdcdb8c16 100644 --- a/config/GraphEditorCategories.xml +++ b/config/GraphEditorCategories.xml @@ -4,7 +4,7 @@ - + diff --git a/config/MasterTranslations.xml b/config/MasterTranslations.xml index 09efcc0cf..3a5d0cbf5 100644 --- a/config/MasterTranslations.xml +++ b/config/MasterTranslations.xml @@ -1769,30 +1769,25 @@ The course is saved automatically on closing of the editor and overrides the sel - + - + - + - - - - - - - - + + + - - - + + + diff --git a/modDesc.xml b/modDesc.xml index f394233a9..2f904b57d 100644 --- a/modDesc.xml +++ b/modDesc.xml @@ -317,7 +317,7 @@ Changelog 8.0.0.0: - + diff --git a/scripts/editor/EditorGraphWrapper.lua b/scripts/editor/EditorGraphWrapper.lua index 95c823f8c..fe135bb77 100644 --- a/scripts/editor/EditorGraphWrapper.lua +++ b/scripts/editor/EditorGraphWrapper.lua @@ -330,28 +330,34 @@ end --- Selected nodes -------------------------- +---@param ix string|nil function EditorGraphWrapper:setSelected(ix) if ix ~=nil then self.selectedNodeIds[ix] = true end end +---@return table function EditorGraphWrapper:getSelectedNodeIDs() return self.selectedNodeIds end +---@return string|nil function EditorGraphWrapper:getFirstSelectedNodeID() return next(self.selectedNodeIds) end +---@param ix string|nil +---@return boolean|nil function EditorGraphWrapper:isSelected(ix) - return ix ~= nil and self.selectedNodeIds[ix] + return self.selectedNodeIds[ix] end function EditorGraphWrapper:resetSelected() self.selectedNodeIds = {} end +---@return boolean|nil function EditorGraphWrapper:hasSelectedNode() return next(self.selectedNodeIds) ~= nil end @@ -400,6 +406,11 @@ function EditorGraphWrapper:getTemporaryPoints() return self.temporarySegment:getAllChildNodes() end +---@return GraphSegment +function EditorGraphWrapper:getTemporarySegment() + return self.temporarySegment +end + ---@return GraphPoint|nil function EditorGraphWrapper:getFirstTemporaryPoint() return self.temporarySegment:getChildNodeByIndex(1) diff --git a/scripts/editor/brushes/graph/misc/StraightLineWaypoints.lua b/scripts/editor/brushes/graph/misc/StraightLineWaypoints.lua deleted file mode 100644 index 03ae2f8f9..000000000 --- a/scripts/editor/brushes/graph/misc/StraightLineWaypoints.lua +++ /dev/null @@ -1,118 +0,0 @@ - ---- Connects two waypoints. ----@class BrushStraightLine : BrushConnect -BrushStraightLine = CpObject(BrushConnect) -BrushStraightLine.DELAY = 1 --- The mouse event oscillates.., so we have to wait one update tick before release is allowed. -BrushStraightLine.MIN_DIST = 2 -BrushStraightLine.MAX_DIST = 20 -BrushStraightLine.START_DIST = 6 - -function BrushStraightLine:init(...) - BrushConnect.init(self, ...) - self.supportsPrimaryAxis = true - - self.spacing = self.START_DIST - self.delay = g_updateLoopIndex - self.startAnchorWaypointId = nil -end - -function BrushStraightLine:onButtonPrimary(isDown, isDrag, isUp) - if isDown then - if not self.graphWrapper:hasTemporaryPoints() and self.startAnchorWaypointId == nil then - local nodeId = self:getHoveredNodeId() - if nodeId then - self.startAnchorWaypointId = nodeId - self.graphWrapper:setSelected(self.startAnchorWaypointId) - self:debug("Start with node: %d", nodeId) - else - local x, y, z = self.cursor:getPosition() - self.graphWrapper:addTemporaryPoint(x, y, z) - self:debug("Start with a new temp node") - end - self.delay = g_updateLoopIndex + self.DELAY - end - end - if isDrag and (self.graphWrapper:hasTemporaryPoints() or self.startAnchorWaypointId ~= nil) then - self:moveWaypoints() - end - if isUp then - if g_updateLoopIndex > self.delay and self.graphWrapper:hasTemporaryPoints() then - local tempWaypoints = self.graphWrapper:getTemporaryPoints() - self:debug("Finished drawing of %d waypoints.", #tempWaypoints) - self.graphWrapper:createSplineFromTemporyPoints(self.startAnchorWaypointId, self:getHoveredNodeId(), - self:getIsReverse(), self:getIsSubPrio(), self:getIsCrossing()) - end - self.graphWrapper:resetTemporaryPoints() - self.graphWrapper:resetSelected() - self.startAnchorWaypointId = nil - end -end - -function BrushStraightLine:moveWaypoints() - local x, y, z = self.cursor:getPosition() - if x == nil then - return - end - local waypoints = self.graphWrapper:cloneTemporaryPoints() - self.graphWrapper:clearTemporaryPoints() - local tx, ty, tz = 0, 0, 0 - if self.startAnchorWaypointId ~= nil then - tx, ty, tz = self.graphWrapper:getPosition(self.startAnchorWaypointId) - else - tx, ty, tz = waypoints[1].x, waypoints[1].y, waypoints[1].z - self.graphWrapper:addTemporaryPoint(tx, ty, tz) - end - local dist = MathUtil.vector2Length(x-tx,z-tz) - if dist <= 1 then - return - end - local spacing = self.spacing - local nx, nz = MathUtil.vector2Normalize(x-tx, z-tz) - if nx == nil or nz == nil then - nx = 0 - nz = 1 - end - local n = math.max(math.ceil(dist/spacing), 2) - spacing = dist/n - for i = 1, n + 1 do - local dx, dy, dz = tx + nx * i * spacing, y, tz + nz * i * spacing - self.graphWrapper:addTemporaryPoint(dx, dy, dz) - end -end - -function BrushStraightLine:update(dt) - BrushConnect.update(self, dt) - self.graphWrapper:updateTemporaryPoints( - self:getIsReverse(), self:getIsSubPrio(), self:getIsCrossing()) -end - -function BrushStraightLine:onAxisPrimary(inputValue) - self:setSpacing(inputValue) - self:setInputTextDirty() -end - -function BrushStraightLine:setSpacing(inputValue) - self.spacing = math.clamp(self.spacing + inputValue, self.MIN_DIST, self.MAX_DIST) -end - -function BrushStraightLine:activate() - self.graphWrapper:resetTemporaryPoints() - self.startAnchorWaypointId = nil - BrushConnect.activate(self) -end - -function BrushStraightLine:deactivate() - self.graphWrapper:resetTemporaryPoints() - self.graphWrapper:resetSelected() - self.startAnchorWaypointId = nil - BrushConnect.deactivate(self) -end - -function BrushStraightLine:getButtonPrimaryText() - return self:getTranslation(self.primaryButtonText) -end - -function BrushStraightLine:getAxisPrimaryText() - return self:getTranslation(self.primaryAxisText, self.spacing) -end - diff --git a/scripts/editor/brushes/graph/points/InsertPointBrush.lua b/scripts/editor/brushes/graph/points/InsertPointBrush.lua index 12a95f8d5..b43401a5a 100644 --- a/scripts/editor/brushes/graph/points/InsertPointBrush.lua +++ b/scripts/editor/brushes/graph/points/InsertPointBrush.lua @@ -12,24 +12,24 @@ end function InsertPointBrush:onButtonPrimary(isDown, isDrag, isUp) self:handleButtonEvent(isDown, isDrag, isUp, function (selectedId, point) - local success, err = self.graphWrapper:insertPointAheadOfIndex(selectedId, point:clone()) + local success, err = self.graphWrapper:insertPointBehindIndex(selectedId, point:clone()) if not success then self:setError(err) return end - self:debug("Successfully inserted Point: %s ahead of index: %s", + self:debug("Successfully inserted Point: %s behind index: %s", point:getRelativeID(), selectedId) end) end function InsertPointBrush:onButtonSecondary(isDown, isDrag, isUp) self:handleButtonEvent(isDown, isDrag, isUp, function (selectedId, point) - local success, err = self.graphWrapper:insertPointBehindIndex(selectedId, point:clone()) + local success, err = self.graphWrapper:insertPointAheadOfIndex(selectedId, point:clone()) if not success then self:setError(err) return end - self:debug("Successfully inserted Point: %s behind index: %s", + self:debug("Successfully inserted Point: %s ahead of index: %s", point:getRelativeID(), selectedId) end) end @@ -44,7 +44,6 @@ function InsertPointBrush:handleButtonEvent(isDown, isDrag, isUp, insertLambda) else ix = self.graphWrapper:createSegmentWithPoint(x, y, z) if ix then - --- TODO Update editor self.graphWrapper:setSelected(ix) self.graphWrapper:addTemporaryPoint(x, y, z) end diff --git a/scripts/editor/brushes/graph/points/LinePointBrush.lua b/scripts/editor/brushes/graph/points/LinePointBrush.lua new file mode 100644 index 000000000..b851017bf --- /dev/null +++ b/scripts/editor/brushes/graph/points/LinePointBrush.lua @@ -0,0 +1,208 @@ + +--- Inserts a new waypoint at the mouse position. +---@class LinePointBrush : GraphBrush +LinePointBrush = CpObject(GraphBrush) +LinePointBrush.MIN_DIST = 2 +LinePointBrush.MAX_DIST = 20 +LinePointBrush.START_DIST = 6 +LinePointBrush.MIN_OFFSET = -1 +LinePointBrush.MAX_OFFSET = 1 +LinePointBrush.MIN_CENTER = 0 +LinePointBrush.MAX_CENTER = 1 +LinePointBrush.START_CENTER = 0.5 +LinePointBrush.START_OFFSET = 0 +function LinePointBrush:init(...) + GraphBrush.init(self, ...) + self.supportsPrimaryButton = true + self.supportsSecondaryButton = true + self.supportsPrimaryAxis = true + self.supportsSecondaryAxis = true + + self.offset = 0 + self.center = 0.5 +end + +function LinePointBrush:onButtonPrimary() + local ix = self:getHoveredNodeId() + local x, y, z = self.cursor:getPosition() + if self.graphWrapper:hasSelectedNode() then + local tempSegment = self.graphWrapper:getTemporarySegment() + local selectedNodeId = self.graphWrapper:getFirstSelectedNodeID() + if tempSegment:getLength() < EditorGraphWrapper.MIN_DISTANCE then + self:setError("err_min_distance_to_small") + return + end + local segment, err = self.graphWrapper:getSegmentByIndex(selectedNodeId) + if not segment then + self:setError(err) + return + end + if ix then + local success, err = self.graphWrapper:isFirstSegmentPoint(ix) + if not success then + self:setError(err) + return + end + segment:extendByChildren(tempSegment, false) + local success, err = self.graphWrapper:mergeSegments( + segment:getLastNodeID(), ix) + if not success then + self:setError(err) + return + end + else + segment:extendByChildren(tempSegment, false) + end + self.graphWrapper:clearTemporaryPoints() + self.graphWrapper:resetSelected() + self.graphWrapper:setSelected(segment:getLastNodeID()) + + else + if ix then + local isNotFirsOrLast, err = self.graphWrapper:isNotFirstOrLastSegmentPoint(ix) + if isNotFirsOrLast then + self:setError(err) + return + end + if self.graphWrapper:isFirstSegmentPoint(ix) then + self.graphWrapper:setSelected(ix) + self.graphWrapper:addTemporaryPoint(x, y, z) + elseif self.graphWrapper:isLastSegmentPoint(ix) then + self.graphWrapper:setSelected(ix) + self.graphWrapper:addTemporaryPoint(x, y, z) + end + else + ix = self.graphWrapper:createSegmentWithPoint(x, y, z) + if ix then + --- TODO Update editor + self.graphWrapper:setSelected(ix) + self.graphWrapper:addTemporaryPoint(x, y, z) + end + end + end +end + +function LinePointBrush:onButtonSecondary() + self.graphWrapper:resetTemporaryPoints() + self.graphWrapper:resetSelected() +end + +function LinePointBrush:update(dt) + GraphBrush.update(self, dt) + if self.graphWrapper:hasSelectedNode() then + self:movePoints() + end +end + +function LinePointBrush:movePoints() + local x, y, z = self.cursor:getPosition() + if x == nil or z == nil then + return + end + local tx, ty, tz = self.graphWrapper:getPositionByIndex( + self.graphWrapper:getFirstSelectedNodeID()) + if tx == nil or tz == nil then + return + end + self.graphWrapper:clearTemporaryPoints() + local tx, ty, tz = self.graphWrapper:getPositionByIndex( + self.graphWrapper:getFirstSelectedNodeID()) + local dist = MathUtil.vector2Length(x-tx, z-tz) + if dist <= 1 then + return + end + local spacing = 3 + local nx, nz = MathUtil.vector2Normalize(x-tx, z-tz) + if nx == nil or nz == nil then + nx = 0 + nz = 1 + end + -- local n = math.max(math.ceil(dist/spacing), 2) + -- spacing = dist / n + -- if self.graphWrapper:isLastSegmentPoint( + -- self.graphWrapper:getFirstSelectedNodeID()) then + -- --- Forwards + -- for i = 1, n + 1 do + -- local dx = tx + nx * i * spacing + -- local dz = tz + nz * i * spacing + -- local dy = getTerrainHeightAtWorldPos( + -- g_currentMission.terrainRootNode, dx, y, dz) + -- if dy > y - 2 and dy < y + 2 then + -- y = dy + -- end + -- self.graphWrapper:addTemporaryPoint(dx, y, dz) + -- end + -- else + -- --- Backwards + -- for i = 1, n + 1 do + -- local dx = tx + nx * i * spacing + -- local dz = tz + nz * i * spacing + -- local dy = getTerrainHeightAtWorldPos( + -- g_currentMission.terrainRootNode, dx, y, dz) + -- if dy > y - 2 and dy < y + 2 then + -- y = dy + -- end + -- self.graphWrapper:addTemporaryPoint(dx, y, dz) + -- end + -- end + local distCenter = dist*self.center + local ax, az = tx + nx * distCenter, tz + nz * distCenter + --- Rotation + local ncx = nx * math.cos(math.pi/2) - nz * math.sin(math.pi/2) + local ncz = nx * math.sin(math.pi/2) + nz * math.cos(math.pi/2) + --- Translation + local cx, cz = ax + ncx * self.offset * dist, az + ncz * self.offset * dist + local halfDist = MathUtil.vector2Length(cx - tx, cz - tz) + local dt = 2/(1.5*halfDist) + local n = math.ceil(halfDist/spacing) + spacing = halfDist/n + local points = { + { tx, tz }, + { cx, cz }, + { x, z}} + local dx, dz + for t=dt , 1, dt do + dx, dz = CpMathUtil.de_casteljau(t, points) + local dy = getTerrainHeightAtWorldPos( + g_currentMission.terrainRootNode, dx, y, dz) + if dy > y - 2 and dy < y + 2 then + y = dy + end + self.graphWrapper:addTemporaryPoint(dx, y, dz) + end +end + +function LinePointBrush:onAxisPrimary(inputValue) + self.offset = math.clamp(self.offset+inputValue/50, self.MIN_OFFSET, self.MAX_OFFSET) + self:setInputTextDirty() +end + +function LinePointBrush:onAxisSecondary(inputValue) + self.center = math.clamp(self.center+inputValue/50, self.MIN_CENTER, self.MAX_CENTER) + self:setInputTextDirty() +end + +function LinePointBrush:activate() + self.graphWrapper:resetSelected() + self.graphWrapper:resetTemporaryPoints() +end + +function LinePointBrush:deactivate() + self.graphWrapper:resetSelected() + self.graphWrapper:resetTemporaryPoints() +end +function LinePointBrush:getButtonPrimaryText() + return self:getTranslation(self.primaryButtonText) +end + +function LinePointBrush:getButtonSecondaryText() + return self:getTranslation(self.secondaryButtonText) +end + +function LinePointBrush:getAxisPrimaryText() + return self:getTranslation(self.primaryAxisText, self.offset) +end + +function LinePointBrush:getAxisSecondaryText() + return self:getTranslation(self.secondaryAxisText, self.center) +end diff --git a/scripts/editor/brushes/graph/points/StraightLinePointBrush.lua b/scripts/editor/brushes/graph/points/StraightLinePointBrush.lua deleted file mode 100644 index 8b5918d48..000000000 --- a/scripts/editor/brushes/graph/points/StraightLinePointBrush.lua +++ /dev/null @@ -1,174 +0,0 @@ - ---- Inserts a new waypoint at the mouse position. ----@class StraightLinePointBrush : GraphBrush -StraightLinePointBrush = CpObject(GraphBrush) -StraightLinePointBrush.MIN_DIST = 2 -StraightLinePointBrush.MAX_DIST = 20 -StraightLinePointBrush.START_DIST = 6 -StraightLinePointBrush.DELAY = 1 --- The mouse event oscillates.., so we have to wait one update tick before release is allowed. -function StraightLinePointBrush:init(...) - GraphBrush.init(self, ...) - self.supportsPrimaryButton = true - self.supportsPrimaryDragging = true - self.supportsSecondaryButton = true - self.supportsSecondaryDragging = true - self.supportsPrimaryAxis = true - self.spacing = self.START_DIST - - self.delay = g_updateLoopIndex -end - -function StraightLinePointBrush:onButtonPrimary(isDown, isDrag, isUp) - self:handleButtonEvent(isDown, isDrag, isUp) -end - -function StraightLinePointBrush:onButtonSecondary(isDown, isDrag, isUp) - -end - -function StraightLinePointBrush:movePoints() - local x, y, z = self.cursor:getPosition() - if x == nil or z == nil then - return - end - local tx, ty, tz = self.graphWrapper:getPositionByIndex( - self.graphWrapper:getFirstSelectedNodeID()) - if tx == nil or tz == nil then - return - end - self.graphWrapper:clearTemporaryPoints() - local tx, ty, tz = self.graphWrapper:getPositionByIndex( - self.graphWrapper:getFirstSelectedNodeID()) - local dist = MathUtil.vector2Length(x-tx, z-tz) - if dist <= 1 then - return - end - local spacing = self.spacing - local nx, nz = MathUtil.vector2Normalize(x-tx, z-tz) - if nx == nil or nz == nil then - nx = 0 - nz = 1 - end - local n = math.max(math.ceil(dist/spacing), 2) - spacing = dist / n - if self.graphWrapper:isLastSegmentPoint( - self.graphWrapper:getFirstSelectedNodeID()) then - --- Forwards - for i = 1, n + 1 do - local dx = tx + nx * i * spacing - local dz = tz + nz * i * spacing - local dy = getTerrainHeightAtWorldPos( - g_currentMission.terrainRootNode, dx, y, dz) - if dy > y - 2 and dy < y + 2 then - y = dy - end - self.graphWrapper:addTemporaryPoint(dx, y, dz) - end - else - --- Backwards - for i = 1, n + 1 do - local dx = tx + nx * i * spacing - local dz = tz + nz * i * spacing - local dy = getTerrainHeightAtWorldPos( - g_currentMission.terrainRootNode, dx, y, dz) - if dy > y - 2 and dy < y + 2 then - y = dy - end - self.graphWrapper:addTemporaryPoint(dx, y, dz) - end - end -end - -function StraightLinePointBrush:onAxisPrimary(inputValue) - self.spacing = math.clamp(self.spacing + inputValue, - self.MIN_DIST, self.MAX_DIST) -end - -function StraightLinePointBrush:handleButtonEvent(isDown, isDrag, isUp, insertLambda) - if isDown and not self.graphWrapper:hasSelectedNode() then - local ix = self:getHoveredNodeId() - local x, y, z = self.cursor:getPosition() - if ix then - local isNotFirsOrLast, err = self.graphWrapper:isNotFirstOrLastSegmentPoint(ix) - if isNotFirsOrLast then - self:setError(err) - return - end - if self.graphWrapper:isFirstSegmentPoint(ix) then - self.graphWrapper:setSelected(ix) - self.graphWrapper:addTemporaryPoint(x, y, z) - elseif self.graphWrapper:isLastSegmentPoint(ix) then - self.graphWrapper:setSelected(ix) - self.graphWrapper:addTemporaryPoint(x, y, z) - end - else - ix = self.graphWrapper:createSegmentWithPoint(x, y, z) - if ix then - --- TODO Update editor - self.graphWrapper:setSelected(ix) - self.graphWrapper:addTemporaryPoint(x, y, z) - end - end - self.delay = g_updateLoopIndex + self.DELAY - end - if isDrag and self.graphWrapper:hasSelectedNode() then - self:movePoints() - end - if isUp and self.graphWrapper:hasSelectedNode() then - local points = self.graphWrapper:getTemporaryPoints() - local selectedId = self.graphWrapper:getFirstSelectedNodeID() - local ix, success, err = selectedId, false, nil - if self.graphWrapper:isLastSegmentPoint(selectedId) then - for _, p in ipairs(points) do - success, err, ix = self.graphWrapper:insertPointBehindIndex(ix, p:clone()) - if not success then - self:setError(err) - break - end - self:debug("Successfully inserted Point: %s behind index: %s", - p:getRelativeID(), ix) - end - else - for _, p in ipairs(points) do - success, err, ix = self.graphWrapper:insertPointAheadOfIndex(ix, p:clone()) - if not success then - self:setError(err) - break - end - self:debug("Successfully inserted Point: %s ahead of index: %s", - p:getRelativeID(), ix) - - end - end - self.graphWrapper:resetTemporaryPoints() - self.graphWrapper:resetSelected() - end -end - -function StraightLinePointBrush:update(dt) - GraphBrush.update(self, dt) - -end - -function StraightLinePointBrush:activate() - self.graphWrapper:resetSelected() - self.graphWrapper:resetTemporaryPoints() -end - -function StraightLinePointBrush:deactivate() - self.graphWrapper:resetSelected() - self.graphWrapper:resetTemporaryPoints() -end - - -function StraightLinePointBrush:getButtonPrimaryText() - return self:getTranslation(self.primaryButtonText) -end - -function StraightLinePointBrush:getButtonSecondaryText() - return self:getTranslation(self.secondaryButtonText) -end - -function StraightLinePointBrush:getAxisPrimaryText() - return self:getTranslation(self.primaryAxisText, self.spacing) -end \ No newline at end of file diff --git a/scripts/graph/GraphNode.lua b/scripts/graph/GraphNode.lua index cf3f3aca4..5d7a677f8 100644 --- a/scripts/graph/GraphNode.lua +++ b/scripts/graph/GraphNode.lua @@ -122,6 +122,12 @@ function GraphNode:getAllChildNodes() return self._childNodes end +---@return string|nil +function GraphNode:getLastNodeID() + local node = self._childNodes[#self._childNodes] + return node and node:getRelativeID() +end + --- Unlink the node from the parent and remove it from it's children ---@param successCallback function|nil ---@return boolean diff --git a/scripts/graph/GraphSegment.lua b/scripts/graph/GraphSegment.lua index 8e7369696..8b820f006 100644 --- a/scripts/graph/GraphSegment.lua +++ b/scripts/graph/GraphSegment.lua @@ -133,4 +133,15 @@ end function GraphSegment:getDirectionString() return GraphSegmentDirection.DEBUG_TEXTS[self._direction] or "???" +end + +---@return number +function GraphSegment:getLength() + local length = 0 + for ix, node in ipairs(self._childNodes) do + if ix > 1 then + length = length + node:getDistance2DToPoint(self._childNodes[ix-1]) + end + end + return length end \ No newline at end of file From e0d1015cdb96b1f0ed28ccbb49d9f67863df80c2 Mon Sep 17 00:00:00 2001 From: schwiti6190 Date: Thu, 27 Mar 2025 20:03:52 +0000 Subject: [PATCH 06/73] Updated translations --- translations/translation_br.xml | 8 +++++--- translations/translation_cs.xml | 8 +++++--- translations/translation_ct.xml | 8 +++++--- translations/translation_cz.xml | 8 +++++--- translations/translation_da.xml | 8 +++++--- translations/translation_de.xml | 8 +++++--- translations/translation_ea.xml | 8 +++++--- translations/translation_en.xml | 8 +++++--- translations/translation_es.xml | 8 +++++--- translations/translation_fc.xml | 8 +++++--- translations/translation_fi.xml | 8 +++++--- translations/translation_fr.xml | 8 +++++--- translations/translation_hu.xml | 8 +++++--- translations/translation_id.xml | 8 +++++--- translations/translation_it.xml | 8 +++++--- translations/translation_jp.xml | 8 +++++--- translations/translation_kr.xml | 8 +++++--- translations/translation_nl.xml | 8 +++++--- translations/translation_no.xml | 8 +++++--- translations/translation_pl.xml | 8 +++++--- translations/translation_pt.xml | 8 +++++--- translations/translation_ro.xml | 8 +++++--- translations/translation_ru.xml | 8 +++++--- translations/translation_sv.xml | 8 +++++--- translations/translation_tr.xml | 8 +++++--- translations/translation_uk.xml | 8 +++++--- translations/translation_vi.xml | 8 +++++--- 27 files changed, 135 insertions(+), 81 deletions(-) diff --git a/translations/translation_br.xml b/translations/translation_br.xml index fb13afa61..f2f745460 100644 --- a/translations/translation_br.xml +++ b/translations/translation_br.xml @@ -501,9 +501,11 @@ A rota é salva automaticamente ao fechar o editor e substituir a rota seleciona - - - + + + + + diff --git a/translations/translation_cs.xml b/translations/translation_cs.xml index f9bc4db5a..1a1469d84 100644 --- a/translations/translation_cs.xml +++ b/translations/translation_cs.xml @@ -501,9 +501,11 @@ - - - + + + + + diff --git a/translations/translation_ct.xml b/translations/translation_ct.xml index 53c64e648..fb69adc38 100644 --- a/translations/translation_ct.xml +++ b/translations/translation_ct.xml @@ -501,9 +501,11 @@ - - - + + + + + diff --git a/translations/translation_cz.xml b/translations/translation_cz.xml index 92a277220..4111ad082 100644 --- a/translations/translation_cz.xml +++ b/translations/translation_cz.xml @@ -501,9 +501,11 @@ Trasa se automaticky uloží při zavření editoru a přepíše vybranou trasu. - - - + + + + + diff --git a/translations/translation_da.xml b/translations/translation_da.xml index 5c2a84a03..60af86265 100644 --- a/translations/translation_da.xml +++ b/translations/translation_da.xml @@ -501,9 +501,11 @@ Ruten gemmes automatisk ved lukning af editoren og tilsidesætter den valgte rut - - - + + + + + diff --git a/translations/translation_de.xml b/translations/translation_de.xml index e18c3fcc6..2af2abfa1 100644 --- a/translations/translation_de.xml +++ b/translations/translation_de.xml @@ -501,9 +501,11 @@ Der Kurs wird beim Schließen automatisch gespeichert und überschrieben. - - - + + + + + diff --git a/translations/translation_ea.xml b/translations/translation_ea.xml index 158a762a8..2ff412ef0 100644 --- a/translations/translation_ea.xml +++ b/translations/translation_ea.xml @@ -501,9 +501,11 @@ El curso se guarda automáticamente al cerrar el editor y anula el curso selecci - - - + + + + + diff --git a/translations/translation_en.xml b/translations/translation_en.xml index e76cd34ae..d2de02e05 100644 --- a/translations/translation_en.xml +++ b/translations/translation_en.xml @@ -501,9 +501,11 @@ The course is saved automatically on closing of the editor and overrides the sel - - - + + + + + diff --git a/translations/translation_es.xml b/translations/translation_es.xml index 59b60a578..26c58a3ee 100644 --- a/translations/translation_es.xml +++ b/translations/translation_es.xml @@ -501,9 +501,11 @@ El curso se guarda automáticamente al cerrar el editor y anula el curso selecci - - - + + + + + diff --git a/translations/translation_fc.xml b/translations/translation_fc.xml index 936b85e55..420a9c083 100644 --- a/translations/translation_fc.xml +++ b/translations/translation_fc.xml @@ -501,9 +501,11 @@ The course is saved automatically on closing of the editor and overrides the sel - - - + + + + + diff --git a/translations/translation_fi.xml b/translations/translation_fi.xml index aeeac19de..158b0323a 100644 --- a/translations/translation_fi.xml +++ b/translations/translation_fi.xml @@ -501,9 +501,11 @@ The course is saved automatically on closing of the editor and overrides the sel - - - + + + + + diff --git a/translations/translation_fr.xml b/translations/translation_fr.xml index 773dd9a1f..3a61c089d 100644 --- a/translations/translation_fr.xml +++ b/translations/translation_fr.xml @@ -501,9 +501,11 @@ La course est automatiquement sauvegardée à la fermeture de l'éditeur et remp - - - + + + + + diff --git a/translations/translation_hu.xml b/translations/translation_hu.xml index d4355e03c..7e90b8d22 100644 --- a/translations/translation_hu.xml +++ b/translations/translation_hu.xml @@ -501,9 +501,11 @@ Az útvonal automatikusan mentésre kerül a szerkesztő bezárásakor és felü - - - + + + + + diff --git a/translations/translation_id.xml b/translations/translation_id.xml index c1eee20ae..6c39524ee 100644 --- a/translations/translation_id.xml +++ b/translations/translation_id.xml @@ -501,9 +501,11 @@ The course is saved automatically on closing of the editor and overrides the sel - - - + + + + + diff --git a/translations/translation_it.xml b/translations/translation_it.xml index 60713a3a8..1ac49a56f 100644 --- a/translations/translation_it.xml +++ b/translations/translation_it.xml @@ -501,9 +501,11 @@ Il percorso viene salvato automaticamente alla chiusura dell'editor e sovrascriv - - - + + + + + diff --git a/translations/translation_jp.xml b/translations/translation_jp.xml index 79430b6a8..f5c24904e 100644 --- a/translations/translation_jp.xml +++ b/translations/translation_jp.xml @@ -501,9 +501,11 @@ The course is saved automatically on closing of the editor and overrides the sel - - - + + + + + diff --git a/translations/translation_kr.xml b/translations/translation_kr.xml index b4ff42c2c..dae90c44b 100644 --- a/translations/translation_kr.xml +++ b/translations/translation_kr.xml @@ -500,9 +500,11 @@ - - - + + + + + diff --git a/translations/translation_nl.xml b/translations/translation_nl.xml index b5230832e..9fd204bb7 100644 --- a/translations/translation_nl.xml +++ b/translations/translation_nl.xml @@ -501,9 +501,11 @@ The course is saved automatically on closing of the editor and overrides the sel - - - + + + + + diff --git a/translations/translation_no.xml b/translations/translation_no.xml index 4608e8977..3d1881b9a 100644 --- a/translations/translation_no.xml +++ b/translations/translation_no.xml @@ -501,9 +501,11 @@ The course is saved automatically on closing of the editor and overrides the sel - - - + + + + + diff --git a/translations/translation_pl.xml b/translations/translation_pl.xml index a26c3d0b1..3c63105c0 100644 --- a/translations/translation_pl.xml +++ b/translations/translation_pl.xml @@ -501,9 +501,11 @@ Kurs jest automatycznie zapisywany po zamknięciu edytora i nadpisuje wybrany ku - - - + + + + + diff --git a/translations/translation_pt.xml b/translations/translation_pt.xml index da5a0a58e..84c68b6f4 100644 --- a/translations/translation_pt.xml +++ b/translations/translation_pt.xml @@ -501,9 +501,11 @@ A rota é guardada automaticamente ao fechar o editor e sobrepõe-se a qualquer - - - + + + + + diff --git a/translations/translation_ro.xml b/translations/translation_ro.xml index 40340d047..618f695ad 100644 --- a/translations/translation_ro.xml +++ b/translations/translation_ro.xml @@ -501,9 +501,11 @@ The course is saved automatically on closing of the editor and overrides the sel - - - + + + + + diff --git a/translations/translation_ru.xml b/translations/translation_ru.xml index 4bb5d87b5..c555a2889 100644 --- a/translations/translation_ru.xml +++ b/translations/translation_ru.xml @@ -501,9 +501,11 @@ - - - + + + + + diff --git a/translations/translation_sv.xml b/translations/translation_sv.xml index 1b86298c8..bc8af5d27 100644 --- a/translations/translation_sv.xml +++ b/translations/translation_sv.xml @@ -501,9 +501,11 @@ Banan sparas automatiskt vid stängning av editorn och åsidosätter den valda b - - - + + + + + diff --git a/translations/translation_tr.xml b/translations/translation_tr.xml index 16d6c6666..08d0eb02e 100644 --- a/translations/translation_tr.xml +++ b/translations/translation_tr.xml @@ -501,9 +501,11 @@ Düzenleyici kapatıldığında rota otomatik olarak kaydedilir ve önceki rotay - - - + + + + + diff --git a/translations/translation_uk.xml b/translations/translation_uk.xml index b65e0e3f9..72f171ec0 100644 --- a/translations/translation_uk.xml +++ b/translations/translation_uk.xml @@ -501,9 +501,11 @@ - - - + + + + + diff --git a/translations/translation_vi.xml b/translations/translation_vi.xml index db5ba9f89..8984a540a 100644 --- a/translations/translation_vi.xml +++ b/translations/translation_vi.xml @@ -501,9 +501,11 @@ The course is saved automatically on closing of the editor and overrides the sel - - - + + + + + From 261e17558f0c362a33c2e96ae46e31cfd838f290 Mon Sep 17 00:00:00 2001 From: Peter Vaiko Date: Fri, 28 Feb 2025 10:04:43 -0500 Subject: [PATCH 07/73] feat: Graph pathfinder --- scripts/pathfinder/GraphPathfinder.lua | 113 +++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 scripts/pathfinder/GraphPathfinder.lua diff --git a/scripts/pathfinder/GraphPathfinder.lua b/scripts/pathfinder/GraphPathfinder.lua new file mode 100644 index 000000000..c0e0938ec --- /dev/null +++ b/scripts/pathfinder/GraphPathfinder.lua @@ -0,0 +1,113 @@ +--- A pathfinder based on A* to find the shortest path in a directed graph. +--- The graph is defined by its edges only, there are no nodes needed. +--- +--- Edges are represented by polylines, and they can unidirectional or bidirectional. +--- Unidirectional edges can only be entered at one end (at the first vertex of the +--- polyline) and exited at the other (last vertex of the polyline) +--- +--- Bidirectional edges can be entered at either end, and exited at the other. +--- +--- Edges don't have to be connected, as long as the entry of another edge is close +--- enough to the exit of another, the entry is a valid successor node (of the exit, +--- which is the predecessor) + +---@class GraphPathfinder : HybridAStar +GraphPathfinder = CpObject(HybridAStar) + +--- An edge of a directed graph +---@class GraphPathfinder.GraphEdge : Polyline +GraphPathfinder.GraphEdge = CpObject(Polyline) + +GraphPathfinder.GraphEdge.UNIDIRECTIONAL = {} +GraphPathfinder.GraphEdge.BIDIRECTIONAL = {} + +---@param direction table GraphEdge.UNIDIRECTIONAL or GraphEdge.BIDIRECTIONAL +---@param vertices table[] array of tables with x, y (Vector, Vertex, State3D or just plain {x, y} +function GraphPathfinder.GraphEdge:init(direction, vertices) + Polyline.init(self, vertices) + self.direction = direction +end + +---@return boolean is this a bidirectional edge? +function GraphPathfinder.GraphEdge:isBidirectional() + return self.direction == GraphPathfinder.GraphEdge.BIDIRECTIONAL +end + +---@return Vertex[] array of vertices that can be used to enter this edge (one for +--- unidirectional, two for bidirectional edges) +function GraphPathfinder.GraphEdge:getEntries() + if self:isBidirectional() then + return { self[1], self[#self] } + else + return { self[1] } + end +end + +---@param entry Vector +---@return Vector the exit when entered through the given entry +function GraphPathfinder.GraphEdge:getExit(entry) + if entry == self[1] then + return self[#self] + else + return self[1] + end +end + +---@param yieldAfter number coroutine yield after so many iterations (number of iterations in one update loop) +---@param maxIterations number maximum iterations before failing +---@param range number when an edge's exit is closer than range to another edge's entry, the +--- two edges are considered as connected (and thus can traverse from one to the other) +---@param graph Vector[] the graph as described in the file header +function GraphPathfinder:init(yieldAfter, maxIterations, range, graph) + HybridAStar.init(self, { }, yieldAfter, maxIterations) + self.range = range + self.graph = graph + self.deltaPosGoal = self.range + self.deltaThetaDeg = 181 + self.deltaThetaGoal = math.rad(self.deltaThetaDeg) + self.maxDeltaTheta = math.pi + self.originalDeltaThetaGoal = self.deltaThetaGoal + self.analyticSolverEnabled = false + self.ignoreValidityAtStart = false +end + +function GraphPathfinder:getMotionPrimitives(turnRadius, allowReverse) + return GraphPathfinder.GraphMotionPrimitives(self.range, self.graph) +end + +--- Motion primitives to use with the graph pathfinder, providing the entries +--- to the next edges. +---@class GraphPathfinder.GraphMotionPrimitives : HybridAStar.MotionPrimitives +GraphPathfinder.GraphMotionPrimitives = CpObject(HybridAStar.MotionPrimitives) + +---@param range number when an edge's exit is closer than range to another edge's entry, the +--- two edges are considered as connected (and thus can traverse from one to the other) +---@param graph Vector[] the graph as described in the file header +function GraphPathfinder.GraphMotionPrimitives:init(range, graph) + self.range = range + self.graph = graph +end + +---@return table [{x, y, d}] array of the next possible entries, their coordinates and +--- the distance to the entry + the length of the edge +function GraphPathfinder.GraphMotionPrimitives:getPrimitives(node) + local primitives = {} + for _, edge in ipairs(self.graph) do + local entries = edge:getEntries() + for _, entry in ipairs(entries) do + local distanceToEntry = (node - entry):length() + if distanceToEntry <= self.range then + local exit = edge:getExit(entry) + table.insert(primitives, {x = exit.x, y = exit.y, d = edge:getLength() + distanceToEntry} ) + end + end + end + return primitives +end + +---@return State3D successor for the given primitive +function GraphPathfinder.GraphMotionPrimitives:createSuccessor(node, primitive, hitchLength) + return State3D(primitive.x, primitive.y, 0, node.g, node, node.gear, node.steer, + 0, node.d + primitive.d) +end + From 83083e1ab792bdec708aad2d77837085d819dd6c Mon Sep 17 00:00:00 2001 From: Peter Vaiko Date: Fri, 28 Mar 2025 08:30:41 -0400 Subject: [PATCH 08/73] fix: goal node heading threshold --- scripts/pathfinder/GraphPathfinder.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/pathfinder/GraphPathfinder.lua b/scripts/pathfinder/GraphPathfinder.lua index c0e0938ec..7c47b43cf 100644 --- a/scripts/pathfinder/GraphPathfinder.lua +++ b/scripts/pathfinder/GraphPathfinder.lua @@ -57,7 +57,7 @@ end ---@param maxIterations number maximum iterations before failing ---@param range number when an edge's exit is closer than range to another edge's entry, the --- two edges are considered as connected (and thus can traverse from one to the other) ----@param graph Vector[] the graph as described in the file header +---@param graph GraphPathfinder.GraphEdge[] Array of edges, the graph as described in the file header function GraphPathfinder:init(yieldAfter, maxIterations, range, graph) HybridAStar.init(self, { }, yieldAfter, maxIterations) self.range = range @@ -65,7 +65,7 @@ function GraphPathfinder:init(yieldAfter, maxIterations, range, graph) self.deltaPosGoal = self.range self.deltaThetaDeg = 181 self.deltaThetaGoal = math.rad(self.deltaThetaDeg) - self.maxDeltaTheta = math.pi + self.maxDeltaTheta = self.deltaThetaGoal self.originalDeltaThetaGoal = self.deltaThetaGoal self.analyticSolverEnabled = false self.ignoreValidityAtStart = false From 9e42bb3e9f27d03e2e5cd44135d531eea0416dcf Mon Sep 17 00:00:00 2001 From: David Schwietering Date: Fri, 28 Mar 2025 21:18:59 +0100 Subject: [PATCH 09/73] Added simple drawing on the ingame map --- Courseplay.lua | 1 + modDesc.xml | 4 ++- scripts/graph/Graph.lua | 8 ++++++ scripts/graph/GraphSegment.lua | 26 ++++++++++++++++++++ scripts/gui/pages/CpCourseGeneratorFrame.lua | 1 + scripts/gui/plots/CoursePlot.lua | 9 +++++-- scripts/gui/plots/GraphPlot.lua | 24 ++++++++++++++++++ 7 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 scripts/gui/plots/GraphPlot.lua diff --git a/Courseplay.lua b/Courseplay.lua index d7020a3b3..244650c97 100644 --- a/Courseplay.lua +++ b/Courseplay.lua @@ -275,6 +275,7 @@ function Courseplay:load() --- Register additional AI messages. CpAIMessages.register() g_vineScanner:setup() + g_graph:setup() end --- Registers all cp specializations. diff --git a/modDesc.xml b/modDesc.xml index 2f904b57d..e935a81da 100644 --- a/modDesc.xml +++ b/modDesc.xml @@ -83,6 +83,7 @@ Changelog 8.0.0.0: + @@ -275,12 +276,13 @@ Changelog 8.0.0.0: - + + diff --git a/scripts/graph/Graph.lua b/scripts/graph/Graph.lua index 8e9771b0b..b21d5ea19 100644 --- a/scripts/graph/Graph.lua +++ b/scripts/graph/Graph.lua @@ -4,7 +4,11 @@ Graph = CpObject(GraphNode) Graph.XML_KEY = "Graph" function Graph:init() GraphNode.init(self) +end +function Graph:setup() + ---@type GraphPlot + self._ingameMapPlot = GraphPlot(self) end function Graph.registerXmlSchema(xmlSchema, baseKey) @@ -47,6 +51,10 @@ function Graph:draw(hoveredNodeID, selectedNodeIDs) end end +function Graph:drawMap(map) + self._ingameMapPlot:draw(map) +end + function Graph:update(dt) end diff --git a/scripts/graph/GraphSegment.lua b/scripts/graph/GraphSegment.lua index 8b820f006..65e152e13 100644 --- a/scripts/graph/GraphSegment.lua +++ b/scripts/graph/GraphSegment.lua @@ -4,12 +4,18 @@ GraphSegmentDirection.FORWARD = 1 GraphSegmentDirection.REVERSE = 2 GraphSegmentDirection.DUAL = 3 GraphSegmentDirection.MAX_KEY = GraphSegmentDirection.DUAL +GraphSegmentDirection.COLORS = { + [GraphSegmentDirection.FORWARD] = {0.0742, 0.4341, 0.6939, 1}, + [GraphSegmentDirection.REVERSE] = {0.0284, 0.0284, 0.0284, 1}, + [GraphSegmentDirection.DUAL] = {0.8, 0.4, 0, 1}, +} GraphSegmentDirection.DEBUG_TEXTS = { [GraphSegmentDirection.FORWARD] = "Forward", [GraphSegmentDirection.REVERSE] = "Reverse", [GraphSegmentDirection.DUAL] = "Dual", } + ---@class GraphSegment : GraphNode ---@field _childNodes GraphPoint[] GraphSegment = CpObject(GraphNode) @@ -111,6 +117,7 @@ function GraphSegment:draw(hoveredNodeID, selectedNodeIDs, isTemporary, temporar end end +---@return table function GraphSegment:getDebugInfos() return {string.format("Direction: %s", self:getDirectionString())} end @@ -131,10 +138,19 @@ function GraphSegment:changeDirection(newDirection) self._direction = newDirection end +---@return string function GraphSegment:getDirectionString() return GraphSegmentDirection.DEBUG_TEXTS[self._direction] or "???" end +---@return number +---@return number +---@return number +---@return number +function GraphSegment:getDirectionColor() + return GraphSegmentDirection.COLORS[self._direction] or 0,0,0,1 +end + ---@return number function GraphSegment:getLength() local length = 0 @@ -144,4 +160,14 @@ function GraphSegment:getLength() end end return length +end + +---@return boolean +function GraphSegment:isReverse() + return self._direction == GraphSegmentDirection.REVERSE +end + +---@return boolean +function GraphSegment:isDual() + return self._direction == GraphSegmentDirection.DUAL end \ No newline at end of file diff --git a/scripts/gui/pages/CpCourseGeneratorFrame.lua b/scripts/gui/pages/CpCourseGeneratorFrame.lua index c76814fad..d33b6e1e1 100644 --- a/scripts/gui/pages/CpCourseGeneratorFrame.lua +++ b/scripts/gui/pages/CpCourseGeneratorFrame.lua @@ -662,6 +662,7 @@ function CpCourseGeneratorFrame:onDrawPostIngameMap(element, ingameMap) if self.currentJob and self.currentJob.draw then self.currentJob:draw(ingameMap, self.mode == self.MODE_OVERVIEW) end + g_graph:drawMap(ingameMap) --- Draws the current progress, while creating a custom field. -- if pageAI.mode == CpInGameMenuAIFrameExtended.MODE_DRAW_FIELD_BORDER and next(CpInGameMenuAIFrameExtended.curDrawPositions) then diff --git a/scripts/gui/plots/CoursePlot.lua b/scripts/gui/plots/CoursePlot.lua index e111fae50..6cdff40db 100644 --- a/scripts/gui/plots/CoursePlot.lua +++ b/scripts/gui/plots/CoursePlot.lua @@ -39,6 +39,7 @@ function CoursePlot:init() self.stopPosition = {} self.drawArrows = true self.isVisible = false + self.invertArrowDirection = false end function CoursePlot:delete() @@ -60,8 +61,9 @@ function CoursePlot:setVisible( isVisible ) self.isVisible = isVisible end -function CoursePlot:setDrawingArrows(draw) +function CoursePlot:setDrawingArrows(draw, invert) self.drawArrows = draw + self.invertArrowDirection = invert end function CoursePlot:setWaypoints( waypoints ) @@ -182,6 +184,9 @@ function CoursePlot:drawArrow(map, x, z, rotation, r, g, b, a, isHudMap) local arrowHeight = self.arrowThickness * map.uiScale * zoom * g_screenAspectRatio local ax, ay, _ = CpGuiUtil.worldToScreen(map, x, z, isHudMap) setOverlayColor( self.arrowOverlayId, r, g, b, a or 0.8) + if self.invertArrowDirection then + rotation = rotation + math.pi + end setOverlayRotation(self.arrowOverlayId, rotation, arrowWidth/2, arrowHeight/2 ) renderOverlay( self.arrowOverlayId, ax - arrowWidth/2, @@ -211,7 +216,7 @@ function CoursePlot:drawPoints(map, points, isHudMap) end if wp and np then r, g, b = MathUtil.vector3ArrayLerp(self.lightColor, self.darkColor, wp.progress or 1) - self:drawLineBetween(map, wp.x, wp.z, np.x, np.z, + self:drawLineBetween(map, wp.x or wp._x, wp.z or wp._z, np.x or np._x, np.z or np._z, isHudMap, lineThickness, r, g, b, 0.8, wp.attributes and wp.attributes.rowStart, np.attributes and np.attributes.rowEnd) diff --git a/scripts/gui/plots/GraphPlot.lua b/scripts/gui/plots/GraphPlot.lua new file mode 100644 index 000000000..a28f0b464 --- /dev/null +++ b/scripts/gui/plots/GraphPlot.lua @@ -0,0 +1,24 @@ +---@class GraphPlot : CoursePlot +GraphPlot = CpObject(CoursePlot) +function GraphPlot:init(graph) + CoursePlot.init(self) + self:setDrawingArrows(true) + -- use a thicker line + self.isVisible = true + ---@type Graph + self.graph = graph +end + +--- Draws custom fields. +---@param map table +function GraphPlot:draw(map) + if not self.isVisible then return end + local segments = self.graph:getSegments() + for _, segment in pairs(segments) do + local points = segment:getAllChildNodes() + self.darkColor = segment:getDirectionColor() + self:setDrawingArrows(not segment:isDual(), segment:isReverse()) + self:drawPoints(map, points, false) + end +end + From 4fdd28caca4e6961959c5b08d0f16f25be83c303 Mon Sep 17 00:00:00 2001 From: David Schwietering Date: Sat, 29 Mar 2025 00:20:08 +0100 Subject: [PATCH 10/73] WIP add mirror brush function --- config/MasterTranslations.xml | 8 ++- scripts/editor/EditorGraphWrapper.lua | 58 +++++++++++++++++++ .../brushes/graph/points/LinePointBrush.lua | 54 ++++++++++++----- scripts/graph/GraphNode.lua | 2 +- 4 files changed, 105 insertions(+), 17 deletions(-) diff --git a/config/MasterTranslations.xml b/config/MasterTranslations.xml index 3a5d0cbf5..f45c735a7 100644 --- a/config/MasterTranslations.xml +++ b/config/MasterTranslations.xml @@ -1781,13 +1781,17 @@ The course is saved automatically on closing of the editor and overrides the sel + + + + - - + + diff --git a/scripts/editor/EditorGraphWrapper.lua b/scripts/editor/EditorGraphWrapper.lua index fe135bb77..dac70eb62 100644 --- a/scripts/editor/EditorGraphWrapper.lua +++ b/scripts/editor/EditorGraphWrapper.lua @@ -15,12 +15,18 @@ function EditorGraphWrapper:init(graph) ---@type GraphSegment self.temporarySegment = GraphSegment() + ---@type GraphSegment + self.mirrorTemporarySegment = GraphSegment() + self.isMirrorTemporaryActive = false end function EditorGraphWrapper:draw(position) self.graph:draw(self.hoveredNodeId, self.selectedNodeIds) self.temporarySegment:draw(nil, nil, true, self:getPointByIndex(self:getFirstSelectedNodeID())) + if self.isMirrorTemporaryActive then + self.mirrorTemporarySegment:draw(nil, nil, true) + end if not position then return end @@ -87,6 +93,11 @@ function EditorGraphWrapper:createSegmentWithPoint(x, y, z) return segment:getChildNodeByIndex(1):getRelativeID() end +---@param segment GraphSegment +function EditorGraphWrapper:addSegment(segment) + self.graph:appendChildNode(segment) +end + ---@param id string|nil ---@return boolean ---@return string|nil @@ -245,6 +256,17 @@ function EditorGraphWrapper:isFirstOrLastSegmentPoint(id) return false, "err_node_not_first_or_last" end +---@param id string|nil +---@return boolean +---@return string|nil +function EditorGraphWrapper:isOnlyNodeLeftInSegment(id) + local segment, err = self:getSegmentByIndex(id) + if segment == nil then + return false, err + end + return segment:getNumChildNodes() <= 1 +end + ---@param idA string|nil ---@param idB string|nil ---@return boolean @@ -396,6 +418,22 @@ function EditorGraphWrapper:addTemporaryPoint(x, y, z) return point end + +---@param x any +---@param y any +---@param z any +---@return GraphPoint|nil +function EditorGraphWrapper:addMirrorTemporaryPoint(x, y, z) + if x == nil or y == nil or z == nil then + return + end + local point = GraphPoint() + point:setPosition(x, y, z) + self.mirrorTemporarySegment:insertChildNodeAtIndex(point, 1) + return point +end + + ---@return boolean function EditorGraphWrapper:hasTemporaryPoints() return self.temporarySegment:hasChildNodes() @@ -423,12 +461,32 @@ end function EditorGraphWrapper:clearTemporaryPoints() self.temporarySegment:clearChildNodes() + self.mirrorTemporarySegment:clearChildNodes() end function EditorGraphWrapper:resetTemporaryPoints() self:clearTemporaryPoints() end +---@param active boolean +function EditorGraphWrapper:setMirrorSegmentActive(active) + self.isMirrorTemporaryActive = active +end + +---@return boolean +function EditorGraphWrapper:isMirrorSegmentActive() + return self.isMirrorTemporaryActive +end + +function EditorGraphWrapper:toggleMirrorSegmentActive() + self.isMirrorTemporaryActive = not self.isMirrorTemporaryActive +end + +---@return GraphSegment +function EditorGraphWrapper:getMirrorSegment() + return self.mirrorTemporarySegment +end + ---------------------------- --- Destinations ---------------------------- diff --git a/scripts/editor/brushes/graph/points/LinePointBrush.lua b/scripts/editor/brushes/graph/points/LinePointBrush.lua index b851017bf..840b88bf9 100644 --- a/scripts/editor/brushes/graph/points/LinePointBrush.lua +++ b/scripts/editor/brushes/graph/points/LinePointBrush.lua @@ -7,19 +7,21 @@ LinePointBrush.MAX_DIST = 20 LinePointBrush.START_DIST = 6 LinePointBrush.MIN_OFFSET = -1 LinePointBrush.MAX_OFFSET = 1 -LinePointBrush.MIN_CENTER = 0 -LinePointBrush.MAX_CENTER = 1 -LinePointBrush.START_CENTER = 0.5 +LinePointBrush.MIN_CENTER = 1 +LinePointBrush.MAX_CENTER = 5 +LinePointBrush.START_CENTER = 0 LinePointBrush.START_OFFSET = 0 function LinePointBrush:init(...) GraphBrush.init(self, ...) self.supportsPrimaryButton = true self.supportsSecondaryButton = true + self.supportsTertiaryButton = true self.supportsPrimaryAxis = true + self.primaryAxisIsContinuous = true self.supportsSecondaryAxis = true - + self.secondaryAxisIsContinuous = true self.offset = 0 - self.center = 0.5 + self.center = 1 end function LinePointBrush:onButtonPrimary() @@ -53,6 +55,10 @@ function LinePointBrush:onButtonPrimary() else segment:extendByChildren(tempSegment, false) end + if self.graphWrapper:isMirrorSegmentActive() then + local mirrorSegment = self.graphWrapper:getMirrorSegment() + self.graphWrapper:addSegment(mirrorSegment:clone(true)) + end self.graphWrapper:clearTemporaryPoints() self.graphWrapper:resetSelected() self.graphWrapper:setSelected(segment:getLastNodeID()) @@ -74,7 +80,6 @@ function LinePointBrush:onButtonPrimary() else ix = self.graphWrapper:createSegmentWithPoint(x, y, z) if ix then - --- TODO Update editor self.graphWrapper:setSelected(ix) self.graphWrapper:addTemporaryPoint(x, y, z) end @@ -83,10 +88,21 @@ function LinePointBrush:onButtonPrimary() end function LinePointBrush:onButtonSecondary() + if self.graphWrapper:hasSelectedNode() then + local ix = self.graphWrapper:getFirstSelectedNodeID() + if self.graphWrapper:isOnlyNodeLeftInSegment(ix) then + self.graphWrapper:removeSegmentByPointIndex(ix) + end + end self.graphWrapper:resetTemporaryPoints() self.graphWrapper:resetSelected() end +function LinePointBrush:onButtonTertiary() + self.graphWrapper:toggleMirrorSegmentActive() + self:setInputTextDirty() +end + function LinePointBrush:update(dt) GraphBrush.update(self, dt) if self.graphWrapper:hasSelectedNode() then @@ -105,8 +121,6 @@ function LinePointBrush:movePoints() return end self.graphWrapper:clearTemporaryPoints() - local tx, ty, tz = self.graphWrapper:getPositionByIndex( - self.graphWrapper:getFirstSelectedNodeID()) local dist = MathUtil.vector2Length(x-tx, z-tz) if dist <= 1 then return @@ -145,7 +159,7 @@ function LinePointBrush:movePoints() -- self.graphWrapper:addTemporaryPoint(dx, y, dz) -- end -- end - local distCenter = dist*self.center + local distCenter = dist * 0.5 --self.center local ax, az = tx + nx * distCenter, tz + nz * distCenter --- Rotation local ncx = nx * math.cos(math.pi/2) - nz * math.sin(math.pi/2) @@ -161,24 +175,36 @@ function LinePointBrush:movePoints() { cx, cz }, { x, z}} local dx, dz + self.graphWrapper:addMirrorTemporaryPoint( + tx + ncx * self.center, ty, tz + ncz * self.center) + for t=dt , 1, dt do dx, dz = CpMathUtil.de_casteljau(t, points) local dy = getTerrainHeightAtWorldPos( - g_currentMission.terrainRootNode, dx, y, dz) + g_currentMission.terrainRootNode, dx, y + 2, dz) if dy > y - 2 and dy < y + 2 then y = dy end self.graphWrapper:addTemporaryPoint(dx, y, dz) + local mx, mz = dx + ncx * self.center, dz + ncz * self.center + self.graphWrapper:addMirrorTemporaryPoint(mx, y, mz) end end function LinePointBrush:onAxisPrimary(inputValue) - self.offset = math.clamp(self.offset+inputValue/50, self.MIN_OFFSET, self.MAX_OFFSET) + self.offset = math.clamp(self.offset+inputValue/100, self.MIN_OFFSET, self.MAX_OFFSET) self:setInputTextDirty() end function LinePointBrush:onAxisSecondary(inputValue) - self.center = math.clamp(self.center+inputValue/50, self.MIN_CENTER, self.MAX_CENTER) + local newCenter = self.center - inputValue + if self.center < 0 and newCenter > -1 then + self.center = 1 + elseif self.center > 0 and newCenter < 1 then + self.center = -1 + else + self.center = math.clamp(newCenter, -self.MAX_CENTER, self.MAX_CENTER) + end self:setInputTextDirty() end @@ -188,8 +214,8 @@ function LinePointBrush:activate() end function LinePointBrush:deactivate() - self.graphWrapper:resetSelected() - self.graphWrapper:resetTemporaryPoints() + self:onButtonSecondary() + self.graphWrapper:setMirrorSegmentActive(false) end function LinePointBrush:getButtonPrimaryText() return self:getTranslation(self.primaryButtonText) diff --git a/scripts/graph/GraphNode.lua b/scripts/graph/GraphNode.lua index 5d7a677f8..643dfe0a1 100644 --- a/scripts/graph/GraphNode.lua +++ b/scripts/graph/GraphNode.lua @@ -259,7 +259,7 @@ function GraphNode:copyTo(newNode, unlink) newNode._parentNode = self._parentNode end for _, node in ipairs(self._childNodes) do - newNode:appendChildNode(node) + newNode:appendChildNode(node:clone(unlink)) end end From 8fb6afde5adb5bc2f13aebd11bdc3ac5b46bbb45 Mon Sep 17 00:00:00 2001 From: schwiti6190 Date: Fri, 28 Mar 2025 23:20:31 +0000 Subject: [PATCH 11/73] Updated translations --- translations/translation_br.xml | 1 + translations/translation_cs.xml | 1 + translations/translation_ct.xml | 1 + translations/translation_cz.xml | 1 + translations/translation_da.xml | 1 + translations/translation_de.xml | 3 ++- translations/translation_ea.xml | 1 + translations/translation_en.xml | 3 ++- translations/translation_es.xml | 1 + translations/translation_fc.xml | 1 + translations/translation_fi.xml | 1 + translations/translation_fr.xml | 1 + translations/translation_hu.xml | 1 + translations/translation_id.xml | 1 + translations/translation_it.xml | 1 + translations/translation_jp.xml | 1 + translations/translation_kr.xml | 1 + translations/translation_nl.xml | 1 + translations/translation_no.xml | 1 + translations/translation_pl.xml | 1 + translations/translation_pt.xml | 1 + translations/translation_ro.xml | 1 + translations/translation_ru.xml | 1 + translations/translation_sv.xml | 1 + translations/translation_tr.xml | 1 + translations/translation_uk.xml | 1 + translations/translation_vi.xml | 1 + 27 files changed, 29 insertions(+), 2 deletions(-) diff --git a/translations/translation_br.xml b/translations/translation_br.xml index f2f745460..11469144a 100644 --- a/translations/translation_br.xml +++ b/translations/translation_br.xml @@ -504,6 +504,7 @@ A rota é salva automaticamente ao fechar o editor e substituir a rota seleciona + diff --git a/translations/translation_cs.xml b/translations/translation_cs.xml index 1a1469d84..37381aca7 100644 --- a/translations/translation_cs.xml +++ b/translations/translation_cs.xml @@ -504,6 +504,7 @@ + diff --git a/translations/translation_ct.xml b/translations/translation_ct.xml index fb69adc38..7a8eb33a1 100644 --- a/translations/translation_ct.xml +++ b/translations/translation_ct.xml @@ -504,6 +504,7 @@ + diff --git a/translations/translation_cz.xml b/translations/translation_cz.xml index 4111ad082..37155de1f 100644 --- a/translations/translation_cz.xml +++ b/translations/translation_cz.xml @@ -504,6 +504,7 @@ Trasa se automaticky uloží při zavření editoru a přepíše vybranou trasu. + diff --git a/translations/translation_da.xml b/translations/translation_da.xml index 60af86265..a345a863f 100644 --- a/translations/translation_da.xml +++ b/translations/translation_da.xml @@ -504,6 +504,7 @@ Ruten gemmes automatisk ved lukning af editoren og tilsidesætter den valgte rut + diff --git a/translations/translation_de.xml b/translations/translation_de.xml index 2af2abfa1..0a37f20cf 100644 --- a/translations/translation_de.xml +++ b/translations/translation_de.xml @@ -504,8 +504,9 @@ Der Kurs wird beim Schließen automatisch gespeichert und überschrieben. + - + diff --git a/translations/translation_ea.xml b/translations/translation_ea.xml index 2ff412ef0..6244db62c 100644 --- a/translations/translation_ea.xml +++ b/translations/translation_ea.xml @@ -504,6 +504,7 @@ El curso se guarda automáticamente al cerrar el editor y anula el curso selecci + diff --git a/translations/translation_en.xml b/translations/translation_en.xml index d2de02e05..4f70af5df 100644 --- a/translations/translation_en.xml +++ b/translations/translation_en.xml @@ -504,8 +504,9 @@ The course is saved automatically on closing of the editor and overrides the sel + - + diff --git a/translations/translation_es.xml b/translations/translation_es.xml index 26c58a3ee..dee9bf572 100644 --- a/translations/translation_es.xml +++ b/translations/translation_es.xml @@ -504,6 +504,7 @@ El curso se guarda automáticamente al cerrar el editor y anula el curso selecci + diff --git a/translations/translation_fc.xml b/translations/translation_fc.xml index 420a9c083..52cb3fc87 100644 --- a/translations/translation_fc.xml +++ b/translations/translation_fc.xml @@ -504,6 +504,7 @@ The course is saved automatically on closing of the editor and overrides the sel + diff --git a/translations/translation_fi.xml b/translations/translation_fi.xml index 158b0323a..775366737 100644 --- a/translations/translation_fi.xml +++ b/translations/translation_fi.xml @@ -504,6 +504,7 @@ The course is saved automatically on closing of the editor and overrides the sel + diff --git a/translations/translation_fr.xml b/translations/translation_fr.xml index 3a61c089d..849a205cc 100644 --- a/translations/translation_fr.xml +++ b/translations/translation_fr.xml @@ -504,6 +504,7 @@ La course est automatiquement sauvegardée à la fermeture de l'éditeur et remp + diff --git a/translations/translation_hu.xml b/translations/translation_hu.xml index 7e90b8d22..57dcc85e1 100644 --- a/translations/translation_hu.xml +++ b/translations/translation_hu.xml @@ -504,6 +504,7 @@ Az útvonal automatikusan mentésre kerül a szerkesztő bezárásakor és felü + diff --git a/translations/translation_id.xml b/translations/translation_id.xml index 6c39524ee..3829feede 100644 --- a/translations/translation_id.xml +++ b/translations/translation_id.xml @@ -504,6 +504,7 @@ The course is saved automatically on closing of the editor and overrides the sel + diff --git a/translations/translation_it.xml b/translations/translation_it.xml index 1ac49a56f..8e394c90d 100644 --- a/translations/translation_it.xml +++ b/translations/translation_it.xml @@ -504,6 +504,7 @@ Il percorso viene salvato automaticamente alla chiusura dell'editor e sovrascriv + diff --git a/translations/translation_jp.xml b/translations/translation_jp.xml index f5c24904e..62356219e 100644 --- a/translations/translation_jp.xml +++ b/translations/translation_jp.xml @@ -504,6 +504,7 @@ The course is saved automatically on closing of the editor and overrides the sel + diff --git a/translations/translation_kr.xml b/translations/translation_kr.xml index dae90c44b..26aba7615 100644 --- a/translations/translation_kr.xml +++ b/translations/translation_kr.xml @@ -503,6 +503,7 @@ + diff --git a/translations/translation_nl.xml b/translations/translation_nl.xml index 9fd204bb7..61832d7e9 100644 --- a/translations/translation_nl.xml +++ b/translations/translation_nl.xml @@ -504,6 +504,7 @@ The course is saved automatically on closing of the editor and overrides the sel + diff --git a/translations/translation_no.xml b/translations/translation_no.xml index 3d1881b9a..bc2b7c42f 100644 --- a/translations/translation_no.xml +++ b/translations/translation_no.xml @@ -504,6 +504,7 @@ The course is saved automatically on closing of the editor and overrides the sel + diff --git a/translations/translation_pl.xml b/translations/translation_pl.xml index 3c63105c0..2748ead8b 100644 --- a/translations/translation_pl.xml +++ b/translations/translation_pl.xml @@ -504,6 +504,7 @@ Kurs jest automatycznie zapisywany po zamknięciu edytora i nadpisuje wybrany ku + diff --git a/translations/translation_pt.xml b/translations/translation_pt.xml index 84c68b6f4..5c6aed858 100644 --- a/translations/translation_pt.xml +++ b/translations/translation_pt.xml @@ -504,6 +504,7 @@ A rota é guardada automaticamente ao fechar o editor e sobrepõe-se a qualquer + diff --git a/translations/translation_ro.xml b/translations/translation_ro.xml index 618f695ad..8948fe5e1 100644 --- a/translations/translation_ro.xml +++ b/translations/translation_ro.xml @@ -504,6 +504,7 @@ The course is saved automatically on closing of the editor and overrides the sel + diff --git a/translations/translation_ru.xml b/translations/translation_ru.xml index c555a2889..2978bfcb1 100644 --- a/translations/translation_ru.xml +++ b/translations/translation_ru.xml @@ -504,6 +504,7 @@ + diff --git a/translations/translation_sv.xml b/translations/translation_sv.xml index bc8af5d27..d7bdb7cc1 100644 --- a/translations/translation_sv.xml +++ b/translations/translation_sv.xml @@ -504,6 +504,7 @@ Banan sparas automatiskt vid stängning av editorn och åsidosätter den valda b + diff --git a/translations/translation_tr.xml b/translations/translation_tr.xml index 08d0eb02e..034f047ba 100644 --- a/translations/translation_tr.xml +++ b/translations/translation_tr.xml @@ -504,6 +504,7 @@ Düzenleyici kapatıldığında rota otomatik olarak kaydedilir ve önceki rotay + diff --git a/translations/translation_uk.xml b/translations/translation_uk.xml index 72f171ec0..a15996189 100644 --- a/translations/translation_uk.xml +++ b/translations/translation_uk.xml @@ -504,6 +504,7 @@ + diff --git a/translations/translation_vi.xml b/translations/translation_vi.xml index 8984a540a..6414f44fb 100644 --- a/translations/translation_vi.xml +++ b/translations/translation_vi.xml @@ -504,6 +504,7 @@ The course is saved automatically on closing of the editor and overrides the sel + From af9a90f7b8d83f7717d32fdcdb1df32b9722433c Mon Sep 17 00:00:00 2001 From: David Schwietering Date: Sat, 29 Mar 2025 00:27:32 +0100 Subject: [PATCH 12/73] Translation fixes --- config/MasterTranslations.xml | 18 +++++++++++++++--- scripts/editor/CourseEditor.lua | 5 +++-- scripts/gui/pages/CpConstructionFrame.lua | 2 +- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/config/MasterTranslations.xml b/config/MasterTranslations.xml index f45c735a7..8aecdc4a4 100644 --- a/config/MasterTranslations.xml +++ b/config/MasterTranslations.xml @@ -1700,6 +1700,18 @@ The course is saved automatically on closing of the editor and overrides the sel + + + + + + + + + + + + @@ -1833,7 +1845,7 @@ The course is saved automatically on closing of the editor and overrides the sel - + @@ -1842,7 +1854,7 @@ The course is saved automatically on closing of the editor and overrides the sel - + @@ -1851,7 +1863,7 @@ The course is saved automatically on closing of the editor and overrides the sel - + diff --git a/scripts/editor/CourseEditor.lua b/scripts/editor/CourseEditor.lua index 24b7877cd..a65e7dbbf 100644 --- a/scripts/editor/CourseEditor.lua +++ b/scripts/editor/CourseEditor.lua @@ -40,8 +40,9 @@ function CourseEditor:loadCategory(path) local category = {} local xmlFile = XMLFile.load("cpConstructionCategories", path, self.categorySchema) xmlFile:iterate("Category.Tab", function (_, tabKey) + local tabName = xmlFile:getValue(tabKey .. "#name") local tab = { - name = xmlFile:getValue(tabKey .. "#name"), + name = self.TRANSLATION_PREFIX .. tabName .. "_title", iconSliceId = xmlFile:getValue(tabKey .. "#iconSliceId"), brushes = {} } @@ -53,7 +54,7 @@ function CourseEditor:loadCategory(path) iconSliceId = xmlFile:getValue(brushKey .. "#iconSliceId"), isCourseOnly = xmlFile:getValue(brushKey .. "#isCourseOnly"), brushParameters = { - self.TRANSLATION_PREFIX .. tab.name .. "_" .. name + self.TRANSLATION_PREFIX .. tabName .. "_" .. name } } table.insert(tab.brushes, brush) diff --git a/scripts/gui/pages/CpConstructionFrame.lua b/scripts/gui/pages/CpConstructionFrame.lua index 56e9a246d..c34bf4937 100644 --- a/scripts/gui/pages/CpConstructionFrame.lua +++ b/scripts/gui/pages/CpConstructionFrame.lua @@ -108,7 +108,7 @@ function CpConstructionFrame:onFrameOpen() local texts = {} for _, tab in pairs(self.brushCategory) do - table.insert(texts, tab.name) + table.insert(texts, g_i18n:convertText(tab.name)) end self.subCategorySelector:setTexts(texts) for ix, clone in ipairs(self.subCategoryDotBox.elements) do From 198522fd2ae453d80ffa73ed420e73ab21c034fa Mon Sep 17 00:00:00 2001 From: schwiti6190 Date: Fri, 28 Mar 2025 23:28:07 +0000 Subject: [PATCH 13/73] Updated translations --- translations/translation_br.xml | 9 ++++++--- translations/translation_cs.xml | 9 ++++++--- translations/translation_ct.xml | 9 ++++++--- translations/translation_cz.xml | 9 ++++++--- translations/translation_da.xml | 9 ++++++--- translations/translation_de.xml | 9 ++++++--- translations/translation_ea.xml | 9 ++++++--- translations/translation_en.xml | 9 ++++++--- translations/translation_es.xml | 9 ++++++--- translations/translation_fc.xml | 9 ++++++--- translations/translation_fi.xml | 9 ++++++--- translations/translation_fr.xml | 9 ++++++--- translations/translation_hu.xml | 9 ++++++--- translations/translation_id.xml | 9 ++++++--- translations/translation_it.xml | 9 ++++++--- translations/translation_jp.xml | 9 ++++++--- translations/translation_kr.xml | 9 ++++++--- translations/translation_nl.xml | 9 ++++++--- translations/translation_no.xml | 9 ++++++--- translations/translation_pl.xml | 9 ++++++--- translations/translation_pt.xml | 9 ++++++--- translations/translation_ro.xml | 9 ++++++--- translations/translation_ru.xml | 9 ++++++--- translations/translation_sv.xml | 9 ++++++--- translations/translation_tr.xml | 9 ++++++--- translations/translation_uk.xml | 9 ++++++--- translations/translation_vi.xml | 9 ++++++--- 27 files changed, 162 insertions(+), 81 deletions(-) diff --git a/translations/translation_br.xml b/translations/translation_br.xml index 11469144a..be661fded 100644 --- a/translations/translation_br.xml +++ b/translations/translation_br.xml @@ -482,6 +482,9 @@ A rota é salva automaticamente ao fechar o editor e substituir a rota seleciona + + + @@ -521,11 +524,11 @@ A rota é salva automaticamente ao fechar o editor e substituir a rota seleciona - + - + - + diff --git a/translations/translation_cs.xml b/translations/translation_cs.xml index 37381aca7..c9139e4a5 100644 --- a/translations/translation_cs.xml +++ b/translations/translation_cs.xml @@ -482,6 +482,9 @@ + + + @@ -521,11 +524,11 @@ - + - + - + diff --git a/translations/translation_ct.xml b/translations/translation_ct.xml index 7a8eb33a1..b134e8f1f 100644 --- a/translations/translation_ct.xml +++ b/translations/translation_ct.xml @@ -482,6 +482,9 @@ + + + @@ -521,11 +524,11 @@ - + - + - + diff --git a/translations/translation_cz.xml b/translations/translation_cz.xml index 37155de1f..bf97cb633 100644 --- a/translations/translation_cz.xml +++ b/translations/translation_cz.xml @@ -482,6 +482,9 @@ Trasa se automaticky uloží při zavření editoru a přepíše vybranou trasu. + + + @@ -521,11 +524,11 @@ Trasa se automaticky uloží při zavření editoru a přepíše vybranou trasu. - + - + - + diff --git a/translations/translation_da.xml b/translations/translation_da.xml index a345a863f..151e55a58 100644 --- a/translations/translation_da.xml +++ b/translations/translation_da.xml @@ -482,6 +482,9 @@ Ruten gemmes automatisk ved lukning af editoren og tilsidesætter den valgte rut + + + @@ -521,11 +524,11 @@ Ruten gemmes automatisk ved lukning af editoren og tilsidesætter den valgte rut - + - + - + diff --git a/translations/translation_de.xml b/translations/translation_de.xml index 0a37f20cf..66cbbb029 100644 --- a/translations/translation_de.xml +++ b/translations/translation_de.xml @@ -482,6 +482,9 @@ Der Kurs wird beim Schließen automatisch gespeichert und überschrieben. + + + @@ -521,11 +524,11 @@ Der Kurs wird beim Schließen automatisch gespeichert und überschrieben. - + - + - + diff --git a/translations/translation_ea.xml b/translations/translation_ea.xml index 6244db62c..8c9bc89df 100644 --- a/translations/translation_ea.xml +++ b/translations/translation_ea.xml @@ -482,6 +482,9 @@ El curso se guarda automáticamente al cerrar el editor y anula el curso selecci + + + @@ -521,11 +524,11 @@ El curso se guarda automáticamente al cerrar el editor y anula el curso selecci - + - + - + diff --git a/translations/translation_en.xml b/translations/translation_en.xml index 4f70af5df..2cabdfb0b 100644 --- a/translations/translation_en.xml +++ b/translations/translation_en.xml @@ -482,6 +482,9 @@ The course is saved automatically on closing of the editor and overrides the sel + + + @@ -521,11 +524,11 @@ The course is saved automatically on closing of the editor and overrides the sel - + - + - + diff --git a/translations/translation_es.xml b/translations/translation_es.xml index dee9bf572..a3fd35b7b 100644 --- a/translations/translation_es.xml +++ b/translations/translation_es.xml @@ -482,6 +482,9 @@ El curso se guarda automáticamente al cerrar el editor y anula el curso selecci + + + @@ -521,11 +524,11 @@ El curso se guarda automáticamente al cerrar el editor y anula el curso selecci - + - + - + diff --git a/translations/translation_fc.xml b/translations/translation_fc.xml index 52cb3fc87..aff8d07de 100644 --- a/translations/translation_fc.xml +++ b/translations/translation_fc.xml @@ -482,6 +482,9 @@ The course is saved automatically on closing of the editor and overrides the sel + + + @@ -521,11 +524,11 @@ The course is saved automatically on closing of the editor and overrides the sel - + - + - + diff --git a/translations/translation_fi.xml b/translations/translation_fi.xml index 775366737..e1538048e 100644 --- a/translations/translation_fi.xml +++ b/translations/translation_fi.xml @@ -482,6 +482,9 @@ The course is saved automatically on closing of the editor and overrides the sel + + + @@ -521,11 +524,11 @@ The course is saved automatically on closing of the editor and overrides the sel - + - + - + diff --git a/translations/translation_fr.xml b/translations/translation_fr.xml index 849a205cc..c218f7d5b 100644 --- a/translations/translation_fr.xml +++ b/translations/translation_fr.xml @@ -482,6 +482,9 @@ La course est automatiquement sauvegardée à la fermeture de l'éditeur et remp + + + @@ -521,11 +524,11 @@ La course est automatiquement sauvegardée à la fermeture de l'éditeur et remp - + - + - + diff --git a/translations/translation_hu.xml b/translations/translation_hu.xml index 57dcc85e1..d8994683d 100644 --- a/translations/translation_hu.xml +++ b/translations/translation_hu.xml @@ -482,6 +482,9 @@ Az útvonal automatikusan mentésre kerül a szerkesztő bezárásakor és felü + + + @@ -521,11 +524,11 @@ Az útvonal automatikusan mentésre kerül a szerkesztő bezárásakor és felü - + - + - + diff --git a/translations/translation_id.xml b/translations/translation_id.xml index 3829feede..cd79bf827 100644 --- a/translations/translation_id.xml +++ b/translations/translation_id.xml @@ -482,6 +482,9 @@ The course is saved automatically on closing of the editor and overrides the sel + + + @@ -521,11 +524,11 @@ The course is saved automatically on closing of the editor and overrides the sel - + - + - + diff --git a/translations/translation_it.xml b/translations/translation_it.xml index 8e394c90d..ea1170da4 100644 --- a/translations/translation_it.xml +++ b/translations/translation_it.xml @@ -482,6 +482,9 @@ Il percorso viene salvato automaticamente alla chiusura dell'editor e sovrascriv + + + @@ -521,11 +524,11 @@ Il percorso viene salvato automaticamente alla chiusura dell'editor e sovrascriv - + - + - + diff --git a/translations/translation_jp.xml b/translations/translation_jp.xml index 62356219e..da456a5df 100644 --- a/translations/translation_jp.xml +++ b/translations/translation_jp.xml @@ -482,6 +482,9 @@ The course is saved automatically on closing of the editor and overrides the sel + + + @@ -521,11 +524,11 @@ The course is saved automatically on closing of the editor and overrides the sel - + - + - + diff --git a/translations/translation_kr.xml b/translations/translation_kr.xml index 26aba7615..e3ba71a3c 100644 --- a/translations/translation_kr.xml +++ b/translations/translation_kr.xml @@ -481,6 +481,9 @@ + + + @@ -520,11 +523,11 @@ - + - + - + diff --git a/translations/translation_nl.xml b/translations/translation_nl.xml index 61832d7e9..3652e903f 100644 --- a/translations/translation_nl.xml +++ b/translations/translation_nl.xml @@ -482,6 +482,9 @@ The course is saved automatically on closing of the editor and overrides the sel + + + @@ -521,11 +524,11 @@ The course is saved automatically on closing of the editor and overrides the sel - + - + - + diff --git a/translations/translation_no.xml b/translations/translation_no.xml index bc2b7c42f..229aa963c 100644 --- a/translations/translation_no.xml +++ b/translations/translation_no.xml @@ -482,6 +482,9 @@ The course is saved automatically on closing of the editor and overrides the sel + + + @@ -521,11 +524,11 @@ The course is saved automatically on closing of the editor and overrides the sel - + - + - + diff --git a/translations/translation_pl.xml b/translations/translation_pl.xml index 2748ead8b..64ffa3156 100644 --- a/translations/translation_pl.xml +++ b/translations/translation_pl.xml @@ -482,6 +482,9 @@ Kurs jest automatycznie zapisywany po zamknięciu edytora i nadpisuje wybrany ku + + + @@ -521,11 +524,11 @@ Kurs jest automatycznie zapisywany po zamknięciu edytora i nadpisuje wybrany ku - + - + - + diff --git a/translations/translation_pt.xml b/translations/translation_pt.xml index 5c6aed858..10a75f36d 100644 --- a/translations/translation_pt.xml +++ b/translations/translation_pt.xml @@ -482,6 +482,9 @@ A rota é guardada automaticamente ao fechar o editor e sobrepõe-se a qualquer + + + @@ -521,11 +524,11 @@ A rota é guardada automaticamente ao fechar o editor e sobrepõe-se a qualquer - + - + - + diff --git a/translations/translation_ro.xml b/translations/translation_ro.xml index 8948fe5e1..97fd60c4d 100644 --- a/translations/translation_ro.xml +++ b/translations/translation_ro.xml @@ -482,6 +482,9 @@ The course is saved automatically on closing of the editor and overrides the sel + + + @@ -521,11 +524,11 @@ The course is saved automatically on closing of the editor and overrides the sel - + - + - + diff --git a/translations/translation_ru.xml b/translations/translation_ru.xml index 2978bfcb1..581d882d3 100644 --- a/translations/translation_ru.xml +++ b/translations/translation_ru.xml @@ -482,6 +482,9 @@ + + + @@ -521,11 +524,11 @@ - + - + - + diff --git a/translations/translation_sv.xml b/translations/translation_sv.xml index d7bdb7cc1..720d89a4a 100644 --- a/translations/translation_sv.xml +++ b/translations/translation_sv.xml @@ -482,6 +482,9 @@ Banan sparas automatiskt vid stängning av editorn och åsidosätter den valda b + + + @@ -521,11 +524,11 @@ Banan sparas automatiskt vid stängning av editorn och åsidosätter den valda b - + - + - + diff --git a/translations/translation_tr.xml b/translations/translation_tr.xml index 034f047ba..9c63c597e 100644 --- a/translations/translation_tr.xml +++ b/translations/translation_tr.xml @@ -482,6 +482,9 @@ Düzenleyici kapatıldığında rota otomatik olarak kaydedilir ve önceki rotay + + + @@ -521,11 +524,11 @@ Düzenleyici kapatıldığında rota otomatik olarak kaydedilir ve önceki rotay - + - + - + diff --git a/translations/translation_uk.xml b/translations/translation_uk.xml index a15996189..3cf995449 100644 --- a/translations/translation_uk.xml +++ b/translations/translation_uk.xml @@ -482,6 +482,9 @@ + + + @@ -521,11 +524,11 @@ - + - + - + diff --git a/translations/translation_vi.xml b/translations/translation_vi.xml index 6414f44fb..e9b2e70dc 100644 --- a/translations/translation_vi.xml +++ b/translations/translation_vi.xml @@ -482,6 +482,9 @@ The course is saved automatically on closing of the editor and overrides the sel + + + @@ -521,11 +524,11 @@ The course is saved automatically on closing of the editor and overrides the sel - + - + - + From 56fd9ccc184cec94605865f0e3041b7bd39b1d35 Mon Sep 17 00:00:00 2001 From: Peter Vaiko Date: Sat, 29 Mar 2025 07:44:26 -0400 Subject: [PATCH 14/73] feat: graph pathfinder Add all points of the edges to the path. Add unit tests. --- scripts/pathfinder/GraphPathfinder.lua | 61 +++++- .../pathfinder/test/GraphPathfinderTest.lua | 181 ++++++++++++++++++ 2 files changed, 239 insertions(+), 3 deletions(-) create mode 100644 scripts/pathfinder/test/GraphPathfinderTest.lua diff --git a/scripts/pathfinder/GraphPathfinder.lua b/scripts/pathfinder/GraphPathfinder.lua index 7c47b43cf..01d3e63d1 100644 --- a/scripts/pathfinder/GraphPathfinder.lua +++ b/scripts/pathfinder/GraphPathfinder.lua @@ -53,6 +53,39 @@ function GraphPathfinder.GraphEdge:getExit(entry) end end +function GraphPathfinder.GraphEdge:rollUpIterator(entry) + local from, to, step + if entry == self[1] then + -- unidirectional, or bidirectional, travelling from the start to end, roll up backwards + from, to, step = #self + 1, 1, -1 + else + from, to, step = 0, #self, 1 + end + local i = from + return function() + i = i + step + if i == to + step then + return nil, nil + else + return i, self[i] + end + end +end + +--- A pathfinder node, specialized for the GraphPathfinder +---@class GraphPathfinder.Node : State3D +GraphPathfinder.Node = CpObject(State3D) + +---@param edge GraphPathfinder.GraphEdge the edge leading to this node: when rolling up the path, we need to add all +--- vertices of the edge +---@param entry Vector the entry point to this edge (when bidirectional, we may be travelling the edge from the end to +--- the start. +function GraphPathfinder.Node:init(x, y, g, pred, d, edge, entry) + State3D.init(self, x, y, 0, g, pred, Gear.Forward, Steer.Straight, 0, d) + self.edge = edge + self.entry = entry +end + ---@param yieldAfter number coroutine yield after so many iterations (number of iterations in one update loop) ---@param maxIterations number maximum iterations before failing ---@param range number when an edge's exit is closer than range to another edge's entry, the @@ -75,6 +108,28 @@ function GraphPathfinder:getMotionPrimitives(turnRadius, allowReverse) return GraphPathfinder.GraphMotionPrimitives(self.range, self.graph) end +--- Override path roll up since here, the path also includes all edges of the graph, not just the pathfinder nodes +---@param lastNode GraphPathfinder.Node +function GraphPathfinder:rollUpPath(lastNode, goal, path) + path = path or {} + local currentNode = lastNode + self:debug('Goal node at %.2f/%.2f, cost %.1f (%.1f - %.1f)', goal.x, goal.y, lastNode.cost, + self.nodes.lowestCost, self.nodes.highestCost) + while currentNode.pred and currentNode ~= currentNode.pred do + if currentNode.edge then + -- add the edge leading to the node + for _, node in currentNode.edge:rollUpIterator(currentNode.entry) do + table.insert(path, 1, node) + end + end + currentNode = currentNode.pred + end + table.insert(path, 1, currentNode) + self:debug('Nodes %d, iterations %d, yields %d, deltaTheta %.1f', #path, self.iterations, self.yields, + math.deg(self.deltaThetaGoal)) + return path +end + --- Motion primitives to use with the graph pathfinder, providing the entries --- to the next edges. ---@class GraphPathfinder.GraphMotionPrimitives : HybridAStar.MotionPrimitives @@ -98,7 +153,8 @@ function GraphPathfinder.GraphMotionPrimitives:getPrimitives(node) local distanceToEntry = (node - entry):length() if distanceToEntry <= self.range then local exit = edge:getExit(entry) - table.insert(primitives, {x = exit.x, y = exit.y, d = edge:getLength() + distanceToEntry} ) + table.insert(primitives, { x = exit.x, y = exit.y, d = edge:getLength() + distanceToEntry, + edge = edge, entry = entry }) end end end @@ -107,7 +163,6 @@ end ---@return State3D successor for the given primitive function GraphPathfinder.GraphMotionPrimitives:createSuccessor(node, primitive, hitchLength) - return State3D(primitive.x, primitive.y, 0, node.g, node, node.gear, node.steer, - 0, node.d + primitive.d) + return GraphPathfinder.Node(primitive.x, primitive.y, node.g, node, node.d + primitive.d, primitive.edge, primitive.entry) end diff --git a/scripts/pathfinder/test/GraphPathfinderTest.lua b/scripts/pathfinder/test/GraphPathfinderTest.lua new file mode 100644 index 000000000..1b11db763 --- /dev/null +++ b/scripts/pathfinder/test/GraphPathfinderTest.lua @@ -0,0 +1,181 @@ +package.path = package.path .. ";../../test/?.lua;../../geometry/?.lua;../../courseGenerator/geometry/?.lua;../../courseGenerator/?.lua;../?.lua;../../?.lua;../../util/?.lua" +lu = require("luaunit") +require('mock-GiantsEngine') +require('mock-Courseplay') +require('CpObject') +require('CpUtil') +require('Logger') +require('BinaryHeap') +require('CpMathUtil') +require('Dubins') +require('ReedsShepp') +require('ReedsSheppSolver') +require('AnalyticSolution') +require('CourseGenerator') +require('WaypointAttributes') +require('Vector') +require('LineSegment') +require('State3D') +require('Vertex') +require('Polyline') +require('Polygon') +require('PathfinderUtil') +require('HybridAStar') +require('GraphPathfinder') + +local GraphEdge = GraphPathfinder.GraphEdge +local TestConstraints = CpObject(PathfinderConstraintInterface) +local pathfinder, start, goal, done, path, goalNodeInvalid +local function printPath() + for _, p in ipairs(path) do + print(p) + end +end + +function testDirection() + local graph = { + GraphEdge(GraphEdge.UNIDIRECTIONAL, + { + Vertex(100, 100), + Vertex(110, 100), + Vertex(120, 100) + }), + GraphEdge( + GraphEdge.UNIDIRECTIONAL, + { + Vertex(120, 105), + Vertex(110, 105), + Vertex(100, 105), + }), + } + pathfinder = GraphPathfinder(math.huge, 500, 20, graph) + start = State3D(90, 105, 0, 0) + goal = State3D(130, 105, 0, 0) + done, path, goalNodeInvalid = pathfinder:run(start, goal, 1, false, TestConstraints(), 0) + lu.assertIsTrue(done) + lu.assertEquals(#path, 4) + -- path contains the start node and all points of the edge it goes through + lu.assertEquals(start, path[1]) + path[2]:assertAlmostEquals(Vector(100, 100)) + path[3]:assertAlmostEquals(Vector(110, 100)) + path[#path]:assertAlmostEquals(Vector(120, 100)) + start, goal = goal, start + done, path, goalNodeInvalid = pathfinder:run(start, goal, 1, false, TestConstraints(), 0) + lu.assertIsTrue(done) + lu.assertEquals(#path, 4) + lu.assertEquals(start, path[1]) + path[2]:assertAlmostEquals(Vector(120, 105)) + path[3]:assertAlmostEquals(Vector(110, 105)) + path[#path]:assertAlmostEquals(Vector(100, 105)) +end + +function testBidirectional() + local graph = { + GraphEdge(GraphEdge.BIDIRECTIONAL, + { + Vertex(120, 100), + Vertex(110, 100), + Vertex(100, 100), + }), + GraphEdge( + GraphEdge.UNIDIRECTIONAL, + { + Vertex(120, 105), + Vertex(110, 105), + Vertex(100, 105), + }), + } + pathfinder = GraphPathfinder(math.huge, 500, 20, graph) + start = State3D(90, 105, 0, 0) + goal = State3D(130, 105, 0, 0) + done, path, goalNodeInvalid = pathfinder:run(start, goal, 1, false, TestConstraints(), 0) + lu.assertIsTrue(done) + lu.assertEquals(#path, 4) + -- path contains the start node and all points of the edge it goes through + lu.assertEquals(start, path[1]) + path[2]:assertAlmostEquals(Vector(100, 100)) + path[3]:assertAlmostEquals(Vector(110, 100)) + path[#path]:assertAlmostEquals(Vector(120, 100)) + start, goal = goal, start + done, path, goalNodeInvalid = pathfinder:run(start, goal, 1, false, TestConstraints(), 0) + lu.assertIsTrue(done) + lu.assertEquals(#path, 4) + lu.assertEquals(start, path[1]) + -- TODO: here, it should have taken the other path, over y = 105, as it is slightly shorter since both start and + -- goal are on y = 105, but since we reach the goal in a single step, + -- it just goes with the first one it finds. This isn't the hill we want to die on, so for now, + -- we will just accept this behavior. + path[2]:assertAlmostEquals(Vector(120, 100)) + path[3]:assertAlmostEquals(Vector(110, 100)) + path[#path]:assertAlmostEquals(Vector(100, 100)) +end + +function testShorterPath() + local graph = { + GraphEdge(GraphEdge.UNIDIRECTIONAL, + { + Vertex(100, 100), + Vertex(110, 100), + Vertex(120, 100) + }), + GraphEdge( + GraphEdge.UNIDIRECTIONAL, + { + Vertex(100, 105), + Vertex(110, 200), + Vertex(120, 105), + }), + } + pathfinder = GraphPathfinder(math.huge, 500, 20, graph) + start = State3D(90, 105, 0, 0) + goal = State3D(130, 105, 0, 0) + done, path, goalNodeInvalid = pathfinder:run(start, goal, 1, false, TestConstraints(), 0) + lu.assertIsTrue(done) + lu.assertEquals(#path, 4) + -- path contains the start node and all points of the edge it goes through + lu.assertEquals(start, path[1]) + path[2]:assertAlmostEquals(Vector(100, 100)) + path[3]:assertAlmostEquals(Vector(110, 100)) + path[#path]:assertAlmostEquals(Vector(120, 100)) +end + +function testRange() + local graph = { + GraphEdge(GraphEdge.UNIDIRECTIONAL, + { + Vertex(100, 100), + Vertex(110, 100), + Vertex(120, 100) + }), + GraphEdge( + GraphEdge.UNIDIRECTIONAL, + { + Vertex(120, 105), + Vertex(110, 105), + Vertex(100, 105), + }), + GraphEdge(GraphEdge.UNIDIRECTIONAL, + { + Vertex(130, 100), + Vertex(140, 100), + Vertex(150, 100) + }), + } + pathfinder = GraphPathfinder(math.huge, 500, 20, graph) + start = State3D(90, 105, 0, 0) + goal = State3D(150, 105, 0, 0) + done, path, goalNodeInvalid = pathfinder:run(start, goal, 1, false, TestConstraints(), 0) + lu.assertIsTrue(done) + lu.assertEquals(#path, 7) + -- path contains the start node and all points of the edge it goes through + lu.assertEquals(start, path[1]) + path[2]:assertAlmostEquals(Vector(100, 100)) + path[3]:assertAlmostEquals(Vector(110, 100)) + path[#path]:assertAlmostEquals(Vector(150, 100)) + pathfinder = GraphPathfinder(math.huge, 500, 10, graph) + done, path, goalNodeInvalid = pathfinder:run(start, goal, 1, false, TestConstraints(), 0) + lu.assertIsNil(path) +end + + +os.exit(lu.LuaUnit.run()) From 35fdceb6d9293c563459f233b55767eccbd659db Mon Sep 17 00:00:00 2001 From: Peter Vaiko Date: Sat, 29 Mar 2025 07:46:13 -0400 Subject: [PATCH 15/73] test: pipeline and mocks --- .github/workflows/unit-test.yml | 2 ++ scripts/test/mock-Courseplay.lua | 21 ++++++++++++++++++++- scripts/test/mock-GiantsEngine.lua | 7 +++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 012636f3d..b02f9bdef 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -53,3 +53,5 @@ jobs: lua TransformTest.lua lua VertexTest.lua lua WrapAroundIndexTest.lua + cd scripts/pathfinder/test + lua GraphPathfinderTest.lua \ No newline at end of file diff --git a/scripts/test/mock-Courseplay.lua b/scripts/test/mock-Courseplay.lua index bc499742e..36c910d92 100644 --- a/scripts/test/mock-Courseplay.lua +++ b/scripts/test/mock-Courseplay.lua @@ -23,4 +23,23 @@ CpDebug.getText = function () return '' end g_vehicleConfigurations = {} function g_vehicleConfigurations:get() return false -end \ No newline at end of file +end + +g_Courseplay = { + globalSettings = { + getSettings = function() + return { + deltaAngleRelaxFactorDeg = { + getValue = function() + return 10 + end + }, + maxDeltaAngleAtGoalDeg = { + getValue = function() + return 45 + end + }, + } + end + } +} \ No newline at end of file diff --git a/scripts/test/mock-GiantsEngine.lua b/scripts/test/mock-GiantsEngine.lua index f64745188..88f0cc870 100644 --- a/scripts/test/mock-GiantsEngine.lua +++ b/scripts/test/mock-GiantsEngine.lua @@ -110,3 +110,10 @@ end function printCallstack() end + +CollisionFlag = {} +setmetatable(CollisionFlag, {__index = function() return 0 end}) + +function openIntervalTimer() end +function closeIntervalTimer() end +function readIntervalTimerMs() return 0 end \ No newline at end of file From 4160f755bdd390d6972f90db62b17f04346eaccb Mon Sep 17 00:00:00 2001 From: Peter Vaiko Date: Sat, 29 Mar 2025 07:52:51 -0400 Subject: [PATCH 16/73] chore: pipeline fix --- .github/workflows/unit-test.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index b02f9bdef..767048df2 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -35,7 +35,7 @@ jobs: lua LoggerTest.lua - name: Run course generator unit tests run: | - cd scripts/courseGenerator/test + pushd scripts/courseGenerator/test lua BlockSequencerTest.lua lua CacheMapTest.lua lua CenterTest.lua @@ -53,5 +53,6 @@ jobs: lua TransformTest.lua lua VertexTest.lua lua WrapAroundIndexTest.lua - cd scripts/pathfinder/test + popd + pushd scripts/pathfinder/test lua GraphPathfinderTest.lua \ No newline at end of file From 7b8efd22d69efadc5fd07e0e1022beb8ed12b144 Mon Sep 17 00:00:00 2001 From: Peter Vaiko Date: Sat, 29 Mar 2025 10:29:17 -0400 Subject: [PATCH 17/73] feat: entry/exit graph mid-edge --- scripts/pathfinder/GraphPathfinder.lua | 51 +++++++++++++++++++ scripts/pathfinder/HybridAStar.lua | 1 + .../pathfinder/test/GraphPathfinderTest.lua | 51 +++++++++++++++++++ 3 files changed, 103 insertions(+) diff --git a/scripts/pathfinder/GraphPathfinder.lua b/scripts/pathfinder/GraphPathfinder.lua index 01d3e63d1..638003ec1 100644 --- a/scripts/pathfinder/GraphPathfinder.lua +++ b/scripts/pathfinder/GraphPathfinder.lua @@ -28,6 +28,10 @@ function GraphPathfinder.GraphEdge:init(direction, vertices) self.direction = direction end +function GraphPathfinder.GraphEdge:getDirection() + return self.direction +end + ---@return boolean is this a bidirectional edge? function GraphPathfinder.GraphEdge:isBidirectional() return self.direction == GraphPathfinder.GraphEdge.BIDIRECTIONAL @@ -92,6 +96,7 @@ end --- two edges are considered as connected (and thus can traverse from one to the other) ---@param graph GraphPathfinder.GraphEdge[] Array of edges, the graph as described in the file header function GraphPathfinder:init(yieldAfter, maxIterations, range, graph) + self.logger = Logger('GraphPathfinder', Logger.level.debug, CpDebug.DBG_PATHFINDER) HybridAStar.init(self, { }, yieldAfter, maxIterations) self.range = range self.graph = graph @@ -104,6 +109,11 @@ function GraphPathfinder:init(yieldAfter, maxIterations, range, graph) self.ignoreValidityAtStart = false end +--- for backwards compatibility with the old pathfinder +function GraphPathfinder:debug(...) + self.logger:debug(...) +end + function GraphPathfinder:getMotionPrimitives(turnRadius, allowReverse) return GraphPathfinder.GraphMotionPrimitives(self.range, self.graph) end @@ -130,6 +140,43 @@ function GraphPathfinder:rollUpPath(lastNode, goal, path) return path end +function GraphPathfinder:initRun(start, goal, ...) + self:createGraphEntryAndExit(start, goal) + return HybridAStar.initRun(self, start, goal, ...) +end + +--- The start location may not be close to the start or end of an edge. Therefore, +--- we need to look for entries among all the vertices of all edges in the graph. When we find that vertex, and +--- it isn't the first or last point of the edge, we simply split that edge at that vertex so the parts can +--- be used as entries. +--- We do the same for the goal node to be able to exit the graph at the middle of an edge. +function GraphPathfinder:createGraphEntryAndExit(start, goal) + local function splitClosestEdge(node) + local closestEdge, closestVertex + local closestDistance = math.huge + for _, edge in ipairs(self.graph) do + local v, d = edge:findClosestVertexToPoint(node) + if d and d < closestDistance then + closestDistance = d + closestEdge = edge + closestVertex = v + end + end + if closestVertex.ix ~= 1 and closestVertex.ix ~= #closestEdge then + self.logger:trace('Graph entry found and split at vertex %d, %.1f %.1f', closestVertex.ix, closestVertex.x, closestVertex.y) + local newEdge = GraphPathfinder.GraphEdge(closestEdge:getDirection()) + for i = closestVertex.ix, #closestEdge do + newEdge:append(closestEdge[i]) + end + newEdge:calculateProperties() + table.insert(self.graph, newEdge) + closestEdge:cutEndAtIx(closestVertex.ix) + end + end + splitClosestEdge(start) + splitClosestEdge(goal) +end + --- Motion primitives to use with the graph pathfinder, providing the entries --- to the next edges. ---@class GraphPathfinder.GraphMotionPrimitives : HybridAStar.MotionPrimitives @@ -139,6 +186,7 @@ GraphPathfinder.GraphMotionPrimitives = CpObject(HybridAStar.MotionPrimitives) --- two edges are considered as connected (and thus can traverse from one to the other) ---@param graph Vector[] the graph as described in the file header function GraphPathfinder.GraphMotionPrimitives:init(range, graph) + self.logger = Logger('GraphMotionPrimitives', Logger.level.debug, CpDebug.DBG_PATHFINDER) self.range = range self.graph = graph end @@ -155,6 +203,7 @@ function GraphPathfinder.GraphMotionPrimitives:getPrimitives(node) local exit = edge:getExit(entry) table.insert(primitives, { x = exit.x, y = exit.y, d = edge:getLength() + distanceToEntry, edge = edge, entry = entry }) + self.logger:trace('\t primitives: %.1f %.1f', exit.x, exit.y) end end end @@ -163,6 +212,8 @@ end ---@return State3D successor for the given primitive function GraphPathfinder.GraphMotionPrimitives:createSuccessor(node, primitive, hitchLength) + self.logger:trace('\t\tsuccessor: %.1f %.1f (d=%.1f) from node: %.1f %.1f (g=%.1f, d=%.1f)', + primitive.x, primitive.y, primitive.d, node.x, node.y, node.g, node.d) return GraphPathfinder.Node(primitive.x, primitive.y, node.g, node, node.d + primitive.d, primitive.edge, primitive.entry) end diff --git a/scripts/pathfinder/HybridAStar.lua b/scripts/pathfinder/HybridAStar.lua index cb4651cdb..e1228be6e 100644 --- a/scripts/pathfinder/HybridAStar.lua +++ b/scripts/pathfinder/HybridAStar.lua @@ -624,6 +624,7 @@ function HybridAStar:run(start, goal, turnRadius, allowReverse, constraints, hit if succ:equals(self.goal, self.deltaPosGoal, self.deltaThetaGoal) then succ.pred = succ.pred self:debug('Successor at the goal (%d).', self.iterations) + self:debug('%s', succ) return self:finishRun(true, self:rollUpPath(succ, self.goal)) end diff --git a/scripts/pathfinder/test/GraphPathfinderTest.lua b/scripts/pathfinder/test/GraphPathfinderTest.lua index 1b11db763..114cb1bcd 100644 --- a/scripts/pathfinder/test/GraphPathfinderTest.lua +++ b/scripts/pathfinder/test/GraphPathfinderTest.lua @@ -177,5 +177,56 @@ function testRange() lu.assertIsNil(path) end +function testStartInTheMiddle() + local graph = { + GraphEdge(GraphEdge.BIDIRECTIONAL, + { + Vertex(200, 100), + Vertex(150, 100), + Vertex(100, 100), + }), + GraphEdge( + GraphEdge.UNIDIRECTIONAL, + { + Vertex(200, 105), + Vertex(150, 105), + Vertex(100, 105), + }), + } + pathfinder = GraphPathfinder(math.huge, 500, 20, graph) + start = State3D(150, 95, 0, 0) + goal = State3D(95, 95, 0, 0) + done, path, goalNodeInvalid = pathfinder:run(start, goal, 1, false, TestConstraints(), 0) + lu.assertIsTrue(done) + lu.assertEquals(#path, 3) + -- path contains the start node and all points of the edge it goes through + lu.assertEquals(start, path[1]) + path[2]:assertAlmostEquals(Vector(150, 100)) + path[3]:assertAlmostEquals(Vector(100, 100)) + graph = { + GraphEdge(GraphEdge.BIDIRECTIONAL, + { + Vertex(200, 100), + Vertex(150, 100), + Vertex(100, 100), + }), + GraphEdge( + GraphEdge.UNIDIRECTIONAL, + { + Vertex(200, 105), + Vertex(150, 105), + Vertex(100, 105), + }), + } + pathfinder = GraphPathfinder(math.huge, 500, 10, graph) + start, goal = goal, start + done, path, goalNodeInvalid = pathfinder:run(start, goal, 1, false, TestConstraints(), 0) + printPath() + lu.assertIsTrue(done) + lu.assertEquals(#path, 3) + lu.assertEquals(start, path[1]) + path[2]:assertAlmostEquals(Vector(100, 100)) + path[3]:assertAlmostEquals(Vector(150, 100)) +end os.exit(lu.LuaUnit.run()) From 46bd11905fcf87134ad4f0ec10660fbc725b4297 Mon Sep 17 00:00:00 2001 From: David Schwietering Date: Sat, 29 Mar 2025 18:37:45 +0100 Subject: [PATCH 18/73] WIP Graphpathfinder find path to target console command --- modDesc.xml | 1 + scripts/graph/Graph.lua | 43 ++++++++++++++++++++++++++++++++++ scripts/graph/GraphSegment.lua | 18 ++++++++++++++ 3 files changed, 62 insertions(+) diff --git a/modDesc.xml b/modDesc.xml index e935a81da..6542621be 100644 --- a/modDesc.xml +++ b/modDesc.xml @@ -175,6 +175,7 @@ Changelog 8.0.0.0: + diff --git a/scripts/graph/Graph.lua b/scripts/graph/Graph.lua index b21d5ea19..240fca13c 100644 --- a/scripts/graph/Graph.lua +++ b/scripts/graph/Graph.lua @@ -4,6 +4,49 @@ Graph = CpObject(GraphNode) Graph.XML_KEY = "Graph" function Graph:init() GraphNode.init(self) + g_consoleCommands:registerConsoleCommand("cpGraphFindPathTo", "Tries to find a path to: ", "consoleCommandFindPathTo", self) +end + +function Graph:consoleCommandFindPathTo(name) + if not name then + return "No target given!" + end + local cmd = function () + local edges = {} + local targetPos + for _, seg in ipairs(self._childNodes) do + for _, node in ipairs(seg:getAllChildNodes()) do + local target = node:getTarget() + if target and target:getName() == name then + local x, z = node:getPosition2D() + targetPos = CpMathUtil.pointFromGame({x = x, z = z}) + end + end + table.insert(edges, seg:toGraphEdge()) + end + if targetPos == nil or targetPos.x == nil or targetPos.y == nil then + return "Failed to find target!" + end + local vehicle = CpUtil.getCurrentVehicle() + if vehicle == nil then + return "Must be in a vehicle!" + end + local pathfinder = GraphPathfinder(math.huge, 500, 20, edges) + local start = PathfinderUtil.getVehiclePositionAsState3D(vehicle) + local goal = State3D(targetPos.x, targetPos.y, 0, 0) + CpUtil.info("Goal: %s", tostring(goal)) + local TestConstraints = CpObject(PathfinderConstraintInterface) + local done, path, goalNodeInvalid = pathfinder:run(start, goal, 1, false, TestConstraints(), 0) + if not done or path == nil or #path < 2 then + return "Pathfinder failed!" + end + local course = Course.createFromAnalyticPath(vehicle, path, true) + vehicle:setFieldWorkCourse(course) + end + local success, ret = CpUtil.try(cmd) + if not success then + CpUtil.info(ret) + end end function Graph:setup() diff --git a/scripts/graph/GraphSegment.lua b/scripts/graph/GraphSegment.lua index 65e152e13..a0b7b4a3b 100644 --- a/scripts/graph/GraphSegment.lua +++ b/scripts/graph/GraphSegment.lua @@ -170,4 +170,22 @@ end ---@return boolean function GraphSegment:isDual() return self._direction == GraphSegmentDirection.DUAL +end + +function GraphSegment:toGraphEdge() + local firstNode = self._childNodes[1] + local lastNode = self._childNodes[#self._childNodes] + if self:isDual() then + return GraphPathfinder.GraphEdge( + GraphPathfinder.GraphEdge.BIDIRECTIONAL, + {Vector(firstNode:getPosition2D()), Vector(lastNode:getPosition2D())}) + elseif self:isReverse() then + return GraphPathfinder.GraphEdge( + GraphPathfinder.GraphEdge.UNIDIRECTIONAL, + {Vector(lastNode:getPosition2D()), Vector(firstNode:getPosition2D())}) + else + return GraphPathfinder.GraphEdge( + GraphPathfinder.GraphEdge.UNIDIRECTIONAL, + {Vector(firstNode:getPosition2D()), Vector(lastNode:getPosition2D())}) + end end \ No newline at end of file From cb55f60b4e12efd8ed46bc1bfcf88ad78cbe89b7 Mon Sep 17 00:00:00 2001 From: David Schwietering Date: Sat, 29 Mar 2025 19:05:56 +0100 Subject: [PATCH 19/73] The complete Segment is translated into a graph edge --- scripts/graph/Graph.lua | 8 +++++--- scripts/graph/GraphPoint.lua | 4 ++++ scripts/graph/GraphSegment.lua | 20 ++++++++++---------- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/scripts/graph/Graph.lua b/scripts/graph/Graph.lua index 240fca13c..566ec1e08 100644 --- a/scripts/graph/Graph.lua +++ b/scripts/graph/Graph.lua @@ -19,10 +19,12 @@ function Graph:consoleCommandFindPathTo(name) local target = node:getTarget() if target and target:getName() == name then local x, z = node:getPosition2D() - targetPos = CpMathUtil.pointFromGame({x = x, z = z}) + targetPos = Vector(x, -z) end end - table.insert(edges, seg:toGraphEdge()) + local edge = seg:toGraphEdge() + print(tostring(edge)) + table.insert(edges, edge) end if targetPos == nil or targetPos.x == nil or targetPos.y == nil then return "Failed to find target!" @@ -44,7 +46,7 @@ function Graph:consoleCommandFindPathTo(name) vehicle:setFieldWorkCourse(course) end local success, ret = CpUtil.try(cmd) - if not success then + if not success or ret then CpUtil.info(ret) end end diff --git a/scripts/graph/GraphPoint.lua b/scripts/graph/GraphPoint.lua index af7504379..0a4a9f73e 100644 --- a/scripts/graph/GraphPoint.lua +++ b/scripts/graph/GraphPoint.lua @@ -172,6 +172,10 @@ function GraphPoint:getDistance2DToPoint(other) return MathUtil.vector2Length(self._x - dx, self._z - dz) end +function GraphPoint:toVector() + return Vector(self._x, -self._z) +end + ----------------------------- --- Target ----------------------------- diff --git a/scripts/graph/GraphSegment.lua b/scripts/graph/GraphSegment.lua index a0b7b4a3b..d7d72f8a2 100644 --- a/scripts/graph/GraphSegment.lua +++ b/scripts/graph/GraphSegment.lua @@ -173,19 +173,19 @@ function GraphSegment:isDual() end function GraphSegment:toGraphEdge() - local firstNode = self._childNodes[1] - local lastNode = self._childNodes[#self._childNodes] + local points = {} + local sx, ex, inc = 1, #self._childNodes, 1 + if self:isReverse() then + sx, ex, inc = #self._childNodes, 1, -1 + end + for i = sx, ex, inc do + table.insert(points, self._childNodes[i]:toVector()) + end if self:isDual() then return GraphPathfinder.GraphEdge( - GraphPathfinder.GraphEdge.BIDIRECTIONAL, - {Vector(firstNode:getPosition2D()), Vector(lastNode:getPosition2D())}) - elseif self:isReverse() then - return GraphPathfinder.GraphEdge( - GraphPathfinder.GraphEdge.UNIDIRECTIONAL, - {Vector(lastNode:getPosition2D()), Vector(firstNode:getPosition2D())}) + GraphPathfinder.GraphEdge.BIDIRECTIONAL, points) else return GraphPathfinder.GraphEdge( - GraphPathfinder.GraphEdge.UNIDIRECTIONAL, - {Vector(firstNode:getPosition2D()), Vector(lastNode:getPosition2D())}) + GraphPathfinder.GraphEdge.UNIDIRECTIONAL, points) end end \ No newline at end of file From 98d5f5c01d482deb207159d66d851bf49b370ac2 Mon Sep 17 00:00:00 2001 From: David Schwietering Date: Sat, 29 Mar 2025 19:14:37 +0100 Subject: [PATCH 20/73] Added nil checkt --- scripts/Course.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/Course.lua b/scripts/Course.lua index 0e5a178ea..110efe787 100644 --- a/scripts/Course.lua +++ b/scripts/Course.lua @@ -1671,7 +1671,7 @@ function Course.createFromAnalyticPath(vehicle, path, isTemporary) local course = Course(vehicle, CpMathUtil.pointsToGameInPlace(path), isTemporary) -- enrichWaypointData rotated the last waypoint in the direction of the second to last, -- correct that according to the analytic path's last waypoint - local yRot = CpMathUtil.angleToGame(path[#path].t) + local yRot = CpMathUtil.angleToGame(path[#path].t or 0) course.waypoints[#course.waypoints].yRot = yRot course.waypoints[#course.waypoints].angle = math.deg(yRot) course.waypoints[#course.waypoints].dx, course.waypoints[#course.waypoints].dz = MathUtil.getDirectionFromYRotation(yRot) From 7e8e4229e90234f1c1386697935c303e720bdf17 Mon Sep 17 00:00:00 2001 From: David Schwietering Date: Sat, 29 Mar 2025 19:18:45 +0100 Subject: [PATCH 21/73] Target creation bug fix --- scripts/editor/brushes/graph/targets/CreateTargetBrush.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/editor/brushes/graph/targets/CreateTargetBrush.lua b/scripts/editor/brushes/graph/targets/CreateTargetBrush.lua index c540309c6..ec89dfbf8 100644 --- a/scripts/editor/brushes/graph/targets/CreateTargetBrush.lua +++ b/scripts/editor/brushes/graph/targets/CreateTargetBrush.lua @@ -11,9 +11,9 @@ end function CreateTargetBrush:onButtonPrimary() local nodeId = self:getHoveredNodeId() if nodeId ~= nil then - local found, err = self.graphWrapper:hasTargetByIndex(nodeId) - if not found then - self:setError(err) + local found = self.graphWrapper:hasTargetByIndex(nodeId) + if found then + self:setError("err_already_has_target") return end self:openTextInput(function(self, text, clickOk, nodeId) From 3782ec86bbab9d841ba52b88220e6f787cbfbe4c Mon Sep 17 00:00:00 2001 From: Peter Vaiko Date: Sat, 29 Mar 2025 15:50:32 -0400 Subject: [PATCH 22/73] test: edges with two points only --- .../pathfinder/test/GraphPathfinderTest.lua | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/scripts/pathfinder/test/GraphPathfinderTest.lua b/scripts/pathfinder/test/GraphPathfinderTest.lua index 114cb1bcd..441e5340d 100644 --- a/scripts/pathfinder/test/GraphPathfinderTest.lua +++ b/scripts/pathfinder/test/GraphPathfinderTest.lua @@ -229,4 +229,38 @@ function testStartInTheMiddle() path[3]:assertAlmostEquals(Vector(150, 100)) end +function testTwoPointSegments() + local graph = { + GraphEdge(GraphEdge.UNIDIRECTIONAL, + { + Vertex(100, 100), + Vertex(120, 100) + }), + GraphEdge( + GraphEdge.UNIDIRECTIONAL, + { + Vertex(120, 105), + Vertex(100, 105), + }), + } + pathfinder = GraphPathfinder(math.huge, 500, 20, graph) + start = State3D(90, 105, 0, 0) + goal = State3D(130, 105, 0, 0) + done, path, goalNodeInvalid = pathfinder:run(start, goal, 1, false, TestConstraints(), 0) + lu.assertIsTrue(done) + lu.assertEquals(#path, 3) + -- path contains the start node and all points of the edge it goes through + lu.assertEquals(start, path[1]) + path[2]:assertAlmostEquals(Vector(100, 100)) + path[#path]:assertAlmostEquals(Vector(120, 100)) + start, goal = goal, start + done, path, goalNodeInvalid = pathfinder:run(start, goal, 1, false, TestConstraints(), 0) + lu.assertIsTrue(done) + lu.assertEquals(#path, 3) + lu.assertEquals(start, path[1]) + path[2]:assertAlmostEquals(Vector(120, 105)) + path[#path]:assertAlmostEquals(Vector(100, 105)) +end + + os.exit(lu.LuaUnit.run()) From 8a04c79d007c704a26d8f6c078c0be18e20546ae Mon Sep 17 00:00:00 2001 From: David Schwietering Date: Sat, 29 Mar 2025 21:53:17 +0100 Subject: [PATCH 23/73] Target adjustments and line brush improvements --- .../brushes/graph/points/LinePointBrush.lua | 62 +++++-------------- scripts/graph/Graph.lua | 20 ++++++ scripts/graph/GraphPoint.lua | 19 ++++-- scripts/graph/GraphSegment.lua | 14 ++--- scripts/graph/GraphTarget.lua | 16 +++++ 5 files changed, 73 insertions(+), 58 deletions(-) diff --git a/scripts/editor/brushes/graph/points/LinePointBrush.lua b/scripts/editor/brushes/graph/points/LinePointBrush.lua index 840b88bf9..bbbaa85c4 100644 --- a/scripts/editor/brushes/graph/points/LinePointBrush.lua +++ b/scripts/editor/brushes/graph/points/LinePointBrush.lua @@ -2,15 +2,10 @@ --- Inserts a new waypoint at the mouse position. ---@class LinePointBrush : GraphBrush LinePointBrush = CpObject(GraphBrush) -LinePointBrush.MIN_DIST = 2 -LinePointBrush.MAX_DIST = 20 -LinePointBrush.START_DIST = 6 LinePointBrush.MIN_OFFSET = -1 LinePointBrush.MAX_OFFSET = 1 LinePointBrush.MIN_CENTER = 1 LinePointBrush.MAX_CENTER = 5 -LinePointBrush.START_CENTER = 0 -LinePointBrush.START_OFFSET = 0 function LinePointBrush:init(...) GraphBrush.init(self, ...) self.supportsPrimaryButton = true @@ -21,7 +16,7 @@ function LinePointBrush:init(...) self.supportsSecondaryAxis = true self.secondaryAxisIsContinuous = true self.offset = 0 - self.center = 1 + self.center = -3.5 end function LinePointBrush:onButtonPrimary() @@ -62,7 +57,7 @@ function LinePointBrush:onButtonPrimary() self.graphWrapper:clearTemporaryPoints() self.graphWrapper:resetSelected() self.graphWrapper:setSelected(segment:getLastNodeID()) - + self.offset = 0 else if ix then local isNotFirsOrLast, err = self.graphWrapper:isNotFirstOrLastSegmentPoint(ix) @@ -94,6 +89,7 @@ function LinePointBrush:onButtonSecondary() self.graphWrapper:removeSegmentByPointIndex(ix) end end + self.offset = 0 self.graphWrapper:resetTemporaryPoints() self.graphWrapper:resetSelected() end @@ -131,35 +127,7 @@ function LinePointBrush:movePoints() nx = 0 nz = 1 end - -- local n = math.max(math.ceil(dist/spacing), 2) - -- spacing = dist / n - -- if self.graphWrapper:isLastSegmentPoint( - -- self.graphWrapper:getFirstSelectedNodeID()) then - -- --- Forwards - -- for i = 1, n + 1 do - -- local dx = tx + nx * i * spacing - -- local dz = tz + nz * i * spacing - -- local dy = getTerrainHeightAtWorldPos( - -- g_currentMission.terrainRootNode, dx, y, dz) - -- if dy > y - 2 and dy < y + 2 then - -- y = dy - -- end - -- self.graphWrapper:addTemporaryPoint(dx, y, dz) - -- end - -- else - -- --- Backwards - -- for i = 1, n + 1 do - -- local dx = tx + nx * i * spacing - -- local dz = tz + nz * i * spacing - -- local dy = getTerrainHeightAtWorldPos( - -- g_currentMission.terrainRootNode, dx, y, dz) - -- if dy > y - 2 and dy < y + 2 then - -- y = dy - -- end - -- self.graphWrapper:addTemporaryPoint(dx, y, dz) - -- end - -- end - local distCenter = dist * 0.5 --self.center + local distCenter = dist * 0.5 local ax, az = tx + nx * distCenter, tz + nz * distCenter --- Rotation local ncx = nx * math.cos(math.pi/2) - nz * math.sin(math.pi/2) @@ -167,7 +135,7 @@ function LinePointBrush:movePoints() --- Translation local cx, cz = ax + ncx * self.offset * dist, az + ncz * self.offset * dist local halfDist = MathUtil.vector2Length(cx - tx, cz - tz) - local dt = 2/(1.5*halfDist) + local dt = 3/halfDist local n = math.ceil(halfDist/spacing) spacing = halfDist/n local points = { @@ -177,27 +145,25 @@ function LinePointBrush:movePoints() local dx, dz self.graphWrapper:addMirrorTemporaryPoint( tx + ncx * self.center, ty, tz + ncz * self.center) - + local lastY = ty for t=dt , 1, dt do dx, dz = CpMathUtil.de_casteljau(t, points) - local dy = getTerrainHeightAtWorldPos( - g_currentMission.terrainRootNode, dx, y + 2, dz) - if dy > y - 2 and dy < y + 2 then - y = dy - end - self.graphWrapper:addTemporaryPoint(dx, y, dz) + local _, _, dy = RaycastUtil.raycastClosest(dx, lastY + 3, dz, 0, -1, 0, 5, + CollisionFlag.STATIC_OBJECT + CollisionFlag.ROAD + CollisionFlag.AI_DRIVABLE + CollisionFlag.TERRAIN) + lastY = dy + self.graphWrapper:addTemporaryPoint(dx, y, dz) local mx, mz = dx + ncx * self.center, dz + ncz * self.center self.graphWrapper:addMirrorTemporaryPoint(mx, y, mz) end end function LinePointBrush:onAxisPrimary(inputValue) - self.offset = math.clamp(self.offset+inputValue/100, self.MIN_OFFSET, self.MAX_OFFSET) + self.offset = math.clamp(self.offset+inputValue/125, self.MIN_OFFSET, self.MAX_OFFSET) self:setInputTextDirty() end function LinePointBrush:onAxisSecondary(inputValue) - local newCenter = self.center - inputValue + local newCenter = self.center - inputValue/20 if self.center < 0 and newCenter > -1 then self.center = 1 elseif self.center > 0 and newCenter < 1 then @@ -225,6 +191,10 @@ function LinePointBrush:getButtonSecondaryText() return self:getTranslation(self.secondaryButtonText) end +function LinePointBrush:getButtonTertiaryText() + return self:getTranslation(self.tertiaryButtonText) +end + function LinePointBrush:getAxisPrimaryText() return self:getTranslation(self.primaryAxisText, self.offset) end diff --git a/scripts/graph/Graph.lua b/scripts/graph/Graph.lua index 566ec1e08..d98bd957d 100644 --- a/scripts/graph/Graph.lua +++ b/scripts/graph/Graph.lua @@ -5,6 +5,9 @@ Graph.XML_KEY = "Graph" function Graph:init() GraphNode.init(self) g_consoleCommands:registerConsoleCommand("cpGraphFindPathTo", "Tries to find a path to: ", "consoleCommandFindPathTo", self) + + ---@type GraphTarget[] + self._targets = {} end function Graph:consoleCommandFindPathTo(name) @@ -157,5 +160,22 @@ function Graph:createSegmentWithPoint(x, y, z) return segment end +---@param target GraphTarget +function Graph:onTargetCreated(target) + table.insert(self._targets, target) +end + +---@param target GraphTarget +function Graph:onTargetDeleted(target) + local ixToRemove + for i=#self._targets, 1, -1 do + if self._targets[i] == target then + ixToRemove = i + break + end + end + table.remove(self._targets, ixToRemove) +end + ---@type Graph g_graph = Graph() \ No newline at end of file diff --git a/scripts/graph/GraphPoint.lua b/scripts/graph/GraphPoint.lua index 0a4a9f73e..f36a65954 100644 --- a/scripts/graph/GraphPoint.lua +++ b/scripts/graph/GraphPoint.lua @@ -46,7 +46,14 @@ end ---@param unlink boolean|nil function GraphPoint:copyTo(newNode, unlink) GraphNode.copyTo(self, newNode, unlink) - newNode._target = self._target + if self._target then + if unlink then + newNode._target = GraphTarget() + self._target:copyTo(newNode._target) + else + newNode._target = self._target + end + end newNode._x = self._x newNode._y = self._y newNode._z = self._z @@ -71,13 +78,14 @@ function GraphPoint:draw(hoveredNodeID, selectedNodeIDs, isTemporary) elseif isTemporary then color = Color.new(0, 1, 0) end - DebugUtil.drawDebugSphere(self._x, self._y, self._z, - 1, 3, 3, color, false, false) - + -- drawDebugPoint(self._x, self._y + 1, self._z, + -- color.r, color.g, color.b, color.a, true) + DebugUtil.drawDebugSphere(self._x, self._y + 2, self._z, + 1, 6, 6, color, false, false) local data = self:getDebugInfos() local yOffset = 0 for _, line in ipairs(data) do - Utils.renderTextAtWorldPosition(self._x, self._y, self._z, + Utils.renderTextAtWorldPosition(self._x, self._y + 1, self._z, line, getCorrectTextSize(0.012), yOffset) yOffset = yOffset + getCorrectTextSize(0.012) end @@ -205,6 +213,7 @@ function GraphPoint:removeTarget() if not self:hasTarget() then return false end + self._target:delete() self._target = nil return true end \ No newline at end of file diff --git a/scripts/graph/GraphSegment.lua b/scripts/graph/GraphSegment.lua index d7d72f8a2..a4cc419ab 100644 --- a/scripts/graph/GraphSegment.lua +++ b/scripts/graph/GraphSegment.lua @@ -82,7 +82,7 @@ function GraphSegment:draw(hoveredNodeID, selectedNodeIDs, isTemporary, temporar local dist = MathUtil.vector3Length(x - dx, y - dy, z - dz) if dist > 1 then local nx, _, nz = MathUtil.vector3Normalize(x - dx, y - dy, z - dz) - local delta = 2 + local delta = 6 local numArrows = dist / delta + 1 local spacing = dist / (numArrows + 1) if self._direction == GraphSegmentDirection.REVERSE then @@ -99,16 +99,16 @@ function GraphSegment:draw(hoveredNodeID, selectedNodeIDs, isTemporary, temporar end local ncx = nx * math.cos(math.pi/4) - nz * math.sin(math.pi/4) local ncz = nx * math.sin(math.pi/4) + nz * math.cos(math.pi/4) - DebugUtil.drawDebugLine(tx, y, tz, - tx - ncx * 2, y, tz - ncz * 2, unpack(color)) + DebugUtil.drawDebugLine(tx, y + 2, tz, + tx - ncx * 2, y + 2, tz - ncz * 2, unpack(color)) ncx = nx * math.cos(-math.pi/4) - nz * math.sin(-math.pi/4) ncz = nx * math.sin(-math.pi/4) + nz * math.cos(-math.pi/4) - DebugUtil.drawDebugLine(tx, y, tz, - tx - ncx * 2, y, tz - ncz * 2, unpack(color)) + DebugUtil.drawDebugLine(tx, y + 2, tz, + tx - ncx * 2, y + 2, tz - ncz * 2, unpack(color)) elseif self._direction == GraphSegmentDirection.DUAL then -- x, y, z, radius, steps, color, alignToTerrain, filled - DebugUtil.drawDebugCircle(dx + nx * i, y, dz + nz * i, - 1, 10, color) + DebugUtil.drawDebugCircle(dx + nx * i, y + 2, dz + nz * i, + 1, 8, color) end end end diff --git a/scripts/graph/GraphTarget.lua b/scripts/graph/GraphTarget.lua index 1dc99cd12..2c5dda86f 100644 --- a/scripts/graph/GraphTarget.lua +++ b/scripts/graph/GraphTarget.lua @@ -4,6 +4,11 @@ function GraphTarget:init(point, name) ---@type GraphPoint self._point = point self._name = name or "???" + g_graph:onTargetCreated(self) +end + +function GraphTarget:delete() + g_graph:onTargetDeleted(self) end function GraphTarget.registerXmlSchema(xmlSchema, baseKey) @@ -18,6 +23,11 @@ function GraphTarget:saveToXMLFile(xmlFile, baseKey) xmlFile:setValue(baseKey .. "#name", self._name) end +---@param otherTarget GraphTarget +function GraphTarget:copyTo(otherTarget) + otherTarget._name = self._name +end + ---@return string function GraphTarget:getName() return self._name @@ -26,4 +36,10 @@ end ---@param name string function GraphTarget:setName(name) self._name = name +end + +---@return Vector +function GraphTarget:toVector() + local x, z = self._point:getPosition2D() + return Vector(x, -z) end \ No newline at end of file From b94680c604d224fb090636e3196a25ec47efb9fc Mon Sep 17 00:00:00 2001 From: Peter Vaiko Date: Sat, 29 Mar 2025 19:15:07 -0400 Subject: [PATCH 24/73] feat: more debugs --- scripts/pathfinder/GraphPathfinder.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/pathfinder/GraphPathfinder.lua b/scripts/pathfinder/GraphPathfinder.lua index 638003ec1..9f4d3ba5a 100644 --- a/scripts/pathfinder/GraphPathfinder.lua +++ b/scripts/pathfinder/GraphPathfinder.lua @@ -163,7 +163,8 @@ function GraphPathfinder:createGraphEntryAndExit(start, goal) end end if closestVertex.ix ~= 1 and closestVertex.ix ~= #closestEdge then - self.logger:trace('Graph entry found and split at vertex %d, %.1f %.1f', closestVertex.ix, closestVertex.x, closestVertex.y) + self.logger:debug('Graph entry/exit found and split at vertex %d, d: %.1f, x: %.1f y: %.1f', + closestVertex.ix, closestDistance, closestVertex.x, closestVertex.y) local newEdge = GraphPathfinder.GraphEdge(closestEdge:getDirection()) for i = closestVertex.ix, #closestEdge do newEdge:append(closestEdge[i]) From fa32112108138acb71bc96187d587dfbd6d224d5 Mon Sep 17 00:00:00 2001 From: David Schwietering Date: Sun, 30 Mar 2025 13:12:30 +0200 Subject: [PATCH 25/73] Changed insert waypoint brush --- .../brushes/graph/points/InsertPointBrush.lua | 96 ++++++++++++------- scripts/graph/GraphPoint.lua | 2 - scripts/graph/GraphSegment.lua | 89 +++++++++-------- 3 files changed, 109 insertions(+), 78 deletions(-) diff --git a/scripts/editor/brushes/graph/points/InsertPointBrush.lua b/scripts/editor/brushes/graph/points/InsertPointBrush.lua index b43401a5a..827e70c52 100644 --- a/scripts/editor/brushes/graph/points/InsertPointBrush.lua +++ b/scripts/editor/brushes/graph/points/InsertPointBrush.lua @@ -5,13 +5,11 @@ InsertPointBrush = CpObject(GraphBrush) function InsertPointBrush:init(...) GraphBrush.init(self, ...) self.supportsPrimaryButton = true - self.supportsPrimaryDragging = true self.supportsSecondaryButton = true - self.supportsSecondaryDragging = true end -function InsertPointBrush:onButtonPrimary(isDown, isDrag, isUp) - self:handleButtonEvent(isDown, isDrag, isUp, function (selectedId, point) +function InsertPointBrush:onButtonPrimary() + self:handleButtonEvent(function (selectedId, point) local success, err = self.graphWrapper:insertPointBehindIndex(selectedId, point:clone()) if not success then self:setError(err) @@ -22,49 +20,76 @@ function InsertPointBrush:onButtonPrimary(isDown, isDrag, isUp) end) end -function InsertPointBrush:onButtonSecondary(isDown, isDrag, isUp) - self:handleButtonEvent(isDown, isDrag, isUp, function (selectedId, point) - local success, err = self.graphWrapper:insertPointAheadOfIndex(selectedId, point:clone()) - if not success then - self:setError(err) - return - end - self:debug("Successfully inserted Point: %s ahead of index: %s", - point:getRelativeID(), selectedId) - end) +function InsertPointBrush:onButtonSecondary() + -- self:handleButtonEvent(function (selectedId, point) + -- local success, err = self.graphWrapper:insertPointAheadOfIndex(selectedId, point:clone()) + -- if not success then + -- self:setError(err) + -- return + -- end + -- self:debug("Successfully inserted Point: %s ahead of index: %s", + -- point:getRelativeID(), selectedId) + -- end) + if self.graphWrapper:hasSelectedNode() then + local ix = self.graphWrapper:getFirstSelectedNodeID() + if self.graphWrapper:isOnlyNodeLeftInSegment(ix) then + self.graphWrapper:removeSegmentByPointIndex(ix) + end + end + self.graphWrapper:resetTemporaryPoints() + self.graphWrapper:resetSelected() end -function InsertPointBrush:handleButtonEvent(isDown, isDrag, isUp, insertLambda) - if isDown then - local ix = self:getHoveredNodeId() - local x, y, z = self.cursor:getPosition() +function InsertPointBrush:handleButtonEvent(insertLambda) + local ix = self:getHoveredNodeId() + local x, y, z = self.cursor:getPosition() + if self.graphWrapper:hasSelectedNode() then + local point = self.graphWrapper:getFirstTemporaryPoint() + local selectedId = self.graphWrapper:getFirstSelectedNodeID() + local segment, err = self.graphWrapper:getSegmentByIndex(selectedId) + if not segment then + self:setError(err) + return + end + if ix then + self:setError("err_min_distance_to_small") + return + end + if selectedId and point then + insertLambda(selectedId, point) + end + self.graphWrapper:resetTemporaryPoints() + self.graphWrapper:resetSelected() + self.graphWrapper:setSelected(segment:getLastNodeID()) + else if ix then self.graphWrapper:setSelected(ix) - self.graphWrapper:addTemporaryPoint(x, y, z) else ix = self.graphWrapper:createSegmentWithPoint(x, y, z) if ix then self.graphWrapper:setSelected(ix) - self.graphWrapper:addTemporaryPoint(x, y, z) end end end - if isDrag then - local point = self.graphWrapper:getFirstTemporaryPoint() - if point then - local x, y, z = self.cursor:getPosition() - point:moveTo(x, y, z) - end +end + +function InsertPointBrush:update(dt) + GraphBrush.update(self, dt) + local x, y, z = self.cursor:getPosition() + if x == nil or z == nil then + return end - if isUp then - local point = self.graphWrapper:getFirstTemporaryPoint() - local selectedId = self.graphWrapper:getFirstSelectedNodeID() - if selectedId ~= nil and point then - insertLambda(selectedId, point) - self.graphWrapper:resetSelected() - self.graphWrapper:resetTemporaryPoints() - end + local tx, ty, tz = self.graphWrapper:getPositionByIndex( + self.graphWrapper:getFirstSelectedNodeID()) + if tx == nil or tz == nil then + return + end + self.graphWrapper:clearTemporaryPoints() + local dist = MathUtil.vector2Length(x-tx, z-tz) + if dist <= 1 then + return end + self.graphWrapper:addTemporaryPoint(x, y, z) end function InsertPointBrush:activate() @@ -73,8 +98,7 @@ function InsertPointBrush:activate() end function InsertPointBrush:deactivate() - self.graphWrapper:resetSelected() - self.graphWrapper:resetTemporaryPoints() + self:onButtonSecondary() end diff --git a/scripts/graph/GraphPoint.lua b/scripts/graph/GraphPoint.lua index f36a65954..bbb6d627b 100644 --- a/scripts/graph/GraphPoint.lua +++ b/scripts/graph/GraphPoint.lua @@ -78,8 +78,6 @@ function GraphPoint:draw(hoveredNodeID, selectedNodeIDs, isTemporary) elseif isTemporary then color = Color.new(0, 1, 0) end - -- drawDebugPoint(self._x, self._y + 1, self._z, - -- color.r, color.g, color.b, color.a, true) DebugUtil.drawDebugSphere(self._x, self._y + 2, self._z, 1, 6, 6, color, false, false) local data = self:getDebugInfos() diff --git a/scripts/graph/GraphSegment.lua b/scripts/graph/GraphSegment.lua index a4cc419ab..f083fa133 100644 --- a/scripts/graph/GraphSegment.lua +++ b/scripts/graph/GraphSegment.lua @@ -15,11 +15,11 @@ GraphSegmentDirection.DEBUG_TEXTS = { [GraphSegmentDirection.DUAL] = "Dual", } - ---@class GraphSegment : GraphNode ---@field _childNodes GraphPoint[] GraphSegment = CpObject(GraphNode) GraphSegment.XML_KEY = "Segment" +GraphSegment.DRAW_CAMERA_RANGE = 200 function GraphSegment:init() GraphNode.init(self) self._direction = GraphSegmentDirection.FORWARD @@ -68,52 +68,61 @@ end ---@param hoveredNodeID string|nil ---@param selectedNodeIDs table|nil ---@param isTemporary boolean|nil ----@param temporaryPrevPoint GraphNode|nil +---@param temporaryPrevPoint GraphPoint|nil function GraphSegment:draw(hoveredNodeID, selectedNodeIDs, isTemporary, temporaryPrevPoint) local prevPoint = temporaryPrevPoint for _, point in ipairs(self._childNodes) do - point:draw(hoveredNodeID, selectedNodeIDs, isTemporary) - if prevPoint then - local color = {0, 0.5, 1} - local x, y, z = point:getPosition() - local dx, dy, dz = prevPoint:getPosition() - DebugUtil.drawDebugLine(x, y + 2, z, - dx, dy + 2, dz, unpack(color), 2) - local dist = MathUtil.vector3Length(x - dx, y - dy, z - dz) - if dist > 1 then - local nx, _, nz = MathUtil.vector3Normalize(x - dx, y - dy, z - dz) - local delta = 6 - local numArrows = dist / delta + 1 - local spacing = dist / (numArrows + 1) - if self._direction == GraphSegmentDirection.REVERSE then - nz = -1 * nz - nx = -1 * nx - end - for i = spacing/2, dist, spacing do - if self._direction == GraphSegmentDirection.FORWARD or - self._direction == GraphSegmentDirection.REVERSE then - - local tx, tz = dx + nx * i, dz + nz * i - if self._direction == GraphSegmentDirection.REVERSE then - tx, tz = x + nx * i, z + nz * i - end - local ncx = nx * math.cos(math.pi/4) - nz * math.sin(math.pi/4) - local ncz = nx * math.sin(math.pi/4) + nz * math.cos(math.pi/4) - DebugUtil.drawDebugLine(tx, y + 2, tz, - tx - ncx * 2, y + 2, tz - ncz * 2, unpack(color)) - ncx = nx * math.cos(-math.pi/4) - nz * math.sin(-math.pi/4) - ncz = nx * math.sin(-math.pi/4) + nz * math.cos(-math.pi/4) - DebugUtil.drawDebugLine(tx, y + 2, tz, - tx - ncx * 2, y + 2, tz - ncz * 2, unpack(color)) - elseif self._direction == GraphSegmentDirection.DUAL then - -- x, y, z, radius, steps, color, alignToTerrain, filled - DebugUtil.drawDebugCircle(dx + nx * i, y + 2, dz + nz * i, - 1, 8, color) + local x, y, z = point:getPosition() + if DebugUtil.isPositionInCameraRange(x, y, z, self.DRAW_CAMERA_RANGE) then + point:draw(hoveredNodeID, selectedNodeIDs, isTemporary) + self:drawLineBetween(prevPoint, point) + end + prevPoint = point + end +end + +---@param prevPoint GraphPoint|nil +---@param point GraphPoint +function GraphSegment:drawLineBetween(prevPoint, point) + if prevPoint then + local color = {0, 0.5, 1} + local x, y, z = point:getPosition() + local dx, dy, dz = prevPoint:getPosition() + DebugUtil.drawDebugLine(x, y + 2, z, + dx, dy + 2, dz, unpack(color), 2) + local dist = MathUtil.vector3Length(x - dx, y - dy, z - dz) + if dist > 1 then + local nx, _, nz = MathUtil.vector3Normalize(x - dx, y - dy, z - dz) + local delta = 6 + local numArrows = dist / delta + 1 + local spacing = dist / (numArrows + 1) + if self._direction == GraphSegmentDirection.REVERSE then + nz = -1 * nz + nx = -1 * nx + end + for i = spacing/2, dist, spacing do + if self._direction == GraphSegmentDirection.FORWARD or + self._direction == GraphSegmentDirection.REVERSE then + + local tx, tz = dx + nx * i, dz + nz * i + if self._direction == GraphSegmentDirection.REVERSE then + tx, tz = x + nx * i, z + nz * i end + local ncx = nx * math.cos(math.pi/4) - nz * math.sin(math.pi/4) + local ncz = nx * math.sin(math.pi/4) + nz * math.cos(math.pi/4) + DebugUtil.drawDebugLine(tx, y + 2, tz, + tx - ncx * 2, y + 2, tz - ncz * 2, unpack(color)) + ncx = nx * math.cos(-math.pi/4) - nz * math.sin(-math.pi/4) + ncz = nx * math.sin(-math.pi/4) + nz * math.cos(-math.pi/4) + DebugUtil.drawDebugLine(tx, y + 2, tz, + tx - ncx * 2, y + 2, tz - ncz * 2, unpack(color)) + elseif self._direction == GraphSegmentDirection.DUAL then + -- x, y, z, radius, steps, color, alignToTerrain, filled + DebugUtil.drawDebugCircle(dx + nx * i, y + 2, dz + nz * i, + 1, 8, color) end end end - prevPoint = point end end From 1b1515603cd96e5ff0d22b5fa75d347b9e58868a Mon Sep 17 00:00:00 2001 From: Peter Vaiko Date: Sun, 30 Mar 2025 07:32:50 -0400 Subject: [PATCH 26/73] feat: start/goal can be at any distance Don't limit the distance when finding the graph entry and exit points, start/goal can be arbitrarily far away from the graph. --- scripts/pathfinder/GraphPathfinder.lua | 39 +++-- .../pathfinder/test/GraphPathfinderTest.lua | 144 ++++++++++++------ 2 files changed, 129 insertions(+), 54 deletions(-) diff --git a/scripts/pathfinder/GraphPathfinder.lua b/scripts/pathfinder/GraphPathfinder.lua index 9f4d3ba5a..53d213cad 100644 --- a/scripts/pathfinder/GraphPathfinder.lua +++ b/scripts/pathfinder/GraphPathfinder.lua @@ -90,6 +90,17 @@ function GraphPathfinder.Node:init(x, y, g, pred, d, edge, entry) self.entry = entry end +--- Create a graph pathfinder. +--- +--- Use start(start, goal) as described at PathfinderInterface:start() to run the pathfinder. +--- The entry at the graph will be at the vertex closest to the start location, the exit at the vertex closest +--- to the goal location. (The graph's edges are polylines, consisting of vertices). There is no limitation for the +--- distance between the entry/exit vertices and the start/goal locations. +--- +--- The resulting path will only contain vertices of the edges that are traversed from the entry to the exit, it +--- will not contain the start or the goal. The caller is responsible for creating the sections from the start to the +--- entry (first point of the path) and from the exit (last point of the path) to the goal. +--- ---@param yieldAfter number coroutine yield after so many iterations (number of iterations in one update loop) ---@param maxIterations number maximum iterations before failing ---@param range number when an edge's exit is closer than range to another edge's entry, the @@ -134,22 +145,26 @@ function GraphPathfinder:rollUpPath(lastNode, goal, path) end currentNode = currentNode.pred end - table.insert(path, 1, currentNode) self:debug('Nodes %d, iterations %d, yields %d, deltaTheta %.1f', #path, self.iterations, self.yields, math.deg(self.deltaThetaGoal)) return path end function GraphPathfinder:initRun(start, goal, ...) - self:createGraphEntryAndExit(start, goal) - return HybridAStar.initRun(self, start, goal, ...) + local graphEntry, graphExit = self:createGraphEntryAndExit(start, goal) + return HybridAStar.initRun(self, graphEntry, graphExit, ...) end ---- The start location may not be close to the start or end of an edge. Therefore, ---- we need to look for entries among all the vertices of all edges in the graph. When we find that vertex, and +--- Create the entry and exit vertices for the graph. +--- +--- Look for the vertex closest to the start/end location. When we find that vertex, and --- it isn't the first or last point of the edge, we simply split that edge at that vertex so the parts can ---- be used as entries. ---- We do the same for the goal node to be able to exit the graph at the middle of an edge. +--- be used as entries/exits. +--- +---@param start State3D the start location for the pathfinder +---@param goal State3D the goal location for the pathfinder +---@return State3D the entry vertex of the graph, closest to start +---@return State3D the exit vertex of the graph, closest to goal function GraphPathfinder:createGraphEntryAndExit(start, goal) local function splitClosestEdge(node) local closestEdge, closestVertex @@ -162,6 +177,8 @@ function GraphPathfinder:createGraphEntryAndExit(start, goal) closestVertex = v end end + -- if the vertex is the first or last vertex of the edge, we can use it directly as the entry/exit, + -- otherwise, we split the edge at the vertex so we can use it as an entry/exit point. if closestVertex.ix ~= 1 and closestVertex.ix ~= #closestEdge then self.logger:debug('Graph entry/exit found and split at vertex %d, d: %.1f, x: %.1f y: %.1f', closestVertex.ix, closestDistance, closestVertex.x, closestVertex.y) @@ -173,9 +190,11 @@ function GraphPathfinder:createGraphEntryAndExit(start, goal) table.insert(self.graph, newEdge) closestEdge:cutEndAtIx(closestVertex.ix) end + return State3D(closestVertex.x, closestVertex.y, 0) end - splitClosestEdge(start) - splitClosestEdge(goal) + local entry, exit = splitClosestEdge(start), splitClosestEdge(goal) + self.logger:debug('Graph entry at (%.1f, %.1f), exit at (%.1f, %.1f)', entry.x, entry.y, exit.x, exit.y) + return entry, exit end --- Motion primitives to use with the graph pathfinder, providing the entries @@ -187,7 +206,7 @@ GraphPathfinder.GraphMotionPrimitives = CpObject(HybridAStar.MotionPrimitives) --- two edges are considered as connected (and thus can traverse from one to the other) ---@param graph Vector[] the graph as described in the file header function GraphPathfinder.GraphMotionPrimitives:init(range, graph) - self.logger = Logger('GraphMotionPrimitives', Logger.level.debug, CpDebug.DBG_PATHFINDER) + self.logger = Logger('GraphMotionPrimitives', Logger.level.trace, CpDebug.DBG_PATHFINDER) self.range = range self.graph = graph end diff --git a/scripts/pathfinder/test/GraphPathfinderTest.lua b/scripts/pathfinder/test/GraphPathfinderTest.lua index 441e5340d..090d5cd1d 100644 --- a/scripts/pathfinder/test/GraphPathfinderTest.lua +++ b/scripts/pathfinder/test/GraphPathfinderTest.lua @@ -28,7 +28,7 @@ local TestConstraints = CpObject(PathfinderConstraintInterface) local pathfinder, start, goal, done, path, goalNodeInvalid local function printPath() for _, p in ipairs(path) do - print(p) + print(Vector.__tostring(p)) end end @@ -53,19 +53,17 @@ function testDirection() goal = State3D(130, 105, 0, 0) done, path, goalNodeInvalid = pathfinder:run(start, goal, 1, false, TestConstraints(), 0) lu.assertIsTrue(done) - lu.assertEquals(#path, 4) - -- path contains the start node and all points of the edge it goes through - lu.assertEquals(start, path[1]) - path[2]:assertAlmostEquals(Vector(100, 100)) - path[3]:assertAlmostEquals(Vector(110, 100)) + lu.assertEquals(#path, 3) + -- path contains all points of the edge it goes through + path[1]:assertAlmostEquals(Vector(100, 100)) + path[2]:assertAlmostEquals(Vector(110, 100)) path[#path]:assertAlmostEquals(Vector(120, 100)) start, goal = goal, start done, path, goalNodeInvalid = pathfinder:run(start, goal, 1, false, TestConstraints(), 0) lu.assertIsTrue(done) - lu.assertEquals(#path, 4) - lu.assertEquals(start, path[1]) - path[2]:assertAlmostEquals(Vector(120, 105)) - path[3]:assertAlmostEquals(Vector(110, 105)) + lu.assertEquals(#path, 3) + path[1]:assertAlmostEquals(Vector(120, 105)) + path[2]:assertAlmostEquals(Vector(110, 105)) path[#path]:assertAlmostEquals(Vector(100, 105)) end @@ -90,23 +88,21 @@ function testBidirectional() goal = State3D(130, 105, 0, 0) done, path, goalNodeInvalid = pathfinder:run(start, goal, 1, false, TestConstraints(), 0) lu.assertIsTrue(done) - lu.assertEquals(#path, 4) + lu.assertEquals(#path, 3) -- path contains the start node and all points of the edge it goes through - lu.assertEquals(start, path[1]) - path[2]:assertAlmostEquals(Vector(100, 100)) - path[3]:assertAlmostEquals(Vector(110, 100)) + path[1]:assertAlmostEquals(Vector(100, 100)) + path[2]:assertAlmostEquals(Vector(110, 100)) path[#path]:assertAlmostEquals(Vector(120, 100)) start, goal = goal, start done, path, goalNodeInvalid = pathfinder:run(start, goal, 1, false, TestConstraints(), 0) lu.assertIsTrue(done) - lu.assertEquals(#path, 4) - lu.assertEquals(start, path[1]) + lu.assertEquals(#path, 3) -- TODO: here, it should have taken the other path, over y = 105, as it is slightly shorter since both start and -- goal are on y = 105, but since we reach the goal in a single step, -- it just goes with the first one it finds. This isn't the hill we want to die on, so for now, -- we will just accept this behavior. - path[2]:assertAlmostEquals(Vector(120, 100)) - path[3]:assertAlmostEquals(Vector(110, 100)) + path[1]:assertAlmostEquals(Vector(120, 100)) + path[2]:assertAlmostEquals(Vector(110, 100)) path[#path]:assertAlmostEquals(Vector(100, 100)) end @@ -131,11 +127,9 @@ function testShorterPath() goal = State3D(130, 105, 0, 0) done, path, goalNodeInvalid = pathfinder:run(start, goal, 1, false, TestConstraints(), 0) lu.assertIsTrue(done) - lu.assertEquals(#path, 4) - -- path contains the start node and all points of the edge it goes through - lu.assertEquals(start, path[1]) - path[2]:assertAlmostEquals(Vector(100, 100)) - path[3]:assertAlmostEquals(Vector(110, 100)) + lu.assertEquals(#path, 3) + path[1]:assertAlmostEquals(Vector(100, 100)) + path[2]:assertAlmostEquals(Vector(110, 100)) path[#path]:assertAlmostEquals(Vector(120, 100)) end @@ -166,13 +160,11 @@ function testRange() goal = State3D(150, 105, 0, 0) done, path, goalNodeInvalid = pathfinder:run(start, goal, 1, false, TestConstraints(), 0) lu.assertIsTrue(done) - lu.assertEquals(#path, 7) - -- path contains the start node and all points of the edge it goes through - lu.assertEquals(start, path[1]) - path[2]:assertAlmostEquals(Vector(100, 100)) - path[3]:assertAlmostEquals(Vector(110, 100)) + lu.assertEquals(#path, 6) + path[1]:assertAlmostEquals(Vector(100, 100)) + path[2]:assertAlmostEquals(Vector(110, 100)) path[#path]:assertAlmostEquals(Vector(150, 100)) - pathfinder = GraphPathfinder(math.huge, 500, 10, graph) + pathfinder = GraphPathfinder(math.huge, 500, 9, graph) done, path, goalNodeInvalid = pathfinder:run(start, goal, 1, false, TestConstraints(), 0) lu.assertIsNil(path) end @@ -198,11 +190,9 @@ function testStartInTheMiddle() goal = State3D(95, 95, 0, 0) done, path, goalNodeInvalid = pathfinder:run(start, goal, 1, false, TestConstraints(), 0) lu.assertIsTrue(done) - lu.assertEquals(#path, 3) - -- path contains the start node and all points of the edge it goes through - lu.assertEquals(start, path[1]) - path[2]:assertAlmostEquals(Vector(150, 100)) - path[3]:assertAlmostEquals(Vector(100, 100)) + lu.assertEquals(#path, 2) + path[1]:assertAlmostEquals(Vector(150, 100)) + path[2]:assertAlmostEquals(Vector(100, 100)) graph = { GraphEdge(GraphEdge.BIDIRECTIONAL, { @@ -223,10 +213,9 @@ function testStartInTheMiddle() done, path, goalNodeInvalid = pathfinder:run(start, goal, 1, false, TestConstraints(), 0) printPath() lu.assertIsTrue(done) - lu.assertEquals(#path, 3) - lu.assertEquals(start, path[1]) - path[2]:assertAlmostEquals(Vector(100, 100)) - path[3]:assertAlmostEquals(Vector(150, 100)) + lu.assertEquals(#path, 2) + path[1]:assertAlmostEquals(Vector(100, 100)) + path[2]:assertAlmostEquals(Vector(150, 100)) end function testTwoPointSegments() @@ -248,19 +237,86 @@ function testTwoPointSegments() goal = State3D(130, 105, 0, 0) done, path, goalNodeInvalid = pathfinder:run(start, goal, 1, false, TestConstraints(), 0) lu.assertIsTrue(done) - lu.assertEquals(#path, 3) - -- path contains the start node and all points of the edge it goes through - lu.assertEquals(start, path[1]) - path[2]:assertAlmostEquals(Vector(100, 100)) + lu.assertEquals(#path, 2) + path[1]:assertAlmostEquals(Vector(100, 100)) path[#path]:assertAlmostEquals(Vector(120, 100)) start, goal = goal, start done, path, goalNodeInvalid = pathfinder:run(start, goal, 1, false, TestConstraints(), 0) lu.assertIsTrue(done) + lu.assertEquals(#path, 2) + path[1]:assertAlmostEquals(Vector(120, 105)) + path[#path]:assertAlmostEquals(Vector(100, 105)) +end + +function testEntryExit() + local graph = { + GraphEdge(GraphEdge.UNIDIRECTIONAL, + { + Vertex(100, 100), + Vertex(110, 100), + Vertex(120, 100) + }), + GraphEdge( + GraphEdge.UNIDIRECTIONAL, + { + Vertex(120, 105), + Vertex(110, 105), + Vertex(100, 105), + }), + } + -- range is 5 so goal is never within the range of start, that is in the testGoalWithinRange() function + pathfinder = GraphPathfinder(math.huge, 500, 5, graph) + -- start/goal far away + start = State3D(0, 0, 0, 0) + goal = State3D(130, 0, 0, 0) + done, path, goalNodeInvalid = pathfinder:run(start, goal, 1, false, TestConstraints(), 0) + lu.assertIsTrue(done) + lu.assertEquals(#path, 3) + -- path contains all points of the edge it goes through + path[1]:assertAlmostEquals(Vector(100, 100)) + path[2]:assertAlmostEquals(Vector(110, 100)) + path[#path]:assertAlmostEquals(Vector(120, 100)) + -- start/goal far away + start = State3D(130, 200, 0, 0) + goal = State3D(0, 200, 0, 0) + done, path, goalNodeInvalid = pathfinder:run(start, goal, 1, false, TestConstraints(), 0) + lu.assertIsTrue(done) lu.assertEquals(#path, 3) - lu.assertEquals(start, path[1]) - path[2]:assertAlmostEquals(Vector(120, 105)) + -- path contains all points of the edge it goes through + path[1]:assertAlmostEquals(Vector(120, 105)) + path[2]:assertAlmostEquals(Vector(110, 105)) path[#path]:assertAlmostEquals(Vector(100, 105)) + -- start/goal far away, middle entry + start = State3D(110, 0, 0, 0) + goal = State3D(130, 0, 0, 0) + done, path, goalNodeInvalid = pathfinder:run(start, goal, 1, false, TestConstraints(), 0) + lu.assertIsTrue(done) + lu.assertEquals(#path, 2) + path[1]:assertAlmostEquals(Vector(110, 100)) + path[#path]:assertAlmostEquals(Vector(120, 100)) end +function testGoalWithinRange() + + local graph = { + GraphEdge(GraphEdge.UNIDIRECTIONAL, + { + Vertex(100, 100), + Vertex(120, 100) + }), + } + pathfinder = GraphPathfinder(math.huge, 500, 21, graph) + -- start/goal far away + start = State3D(100, 100, 0, 0) + goal = State3D(120, 100, 0, 0) + print('******** single ******') + done, path, goalNodeInvalid = pathfinder:run(start, goal, 1, false, TestConstraints(), 0) + lu.assertIsTrue(done) + lu.assertEquals(#path, 2) + -- path contains all points of the edge it goes through + path[1]:assertAlmostEquals(Vector(100, 100)) + --path[2]:assertAlmostEquals(Vector(110, 100)) + path[#path]:assertAlmostEquals(Vector(120, 100)) +end os.exit(lu.LuaUnit.run()) From c07d0d30d78df8da7b9dcaa9779c4910d55b0ce7 Mon Sep 17 00:00:00 2001 From: Peter Vaiko Date: Sun, 30 Mar 2025 08:06:11 -0400 Subject: [PATCH 27/73] refactor: big scary pathfinder refactoring Always return PathfinderResult, also in private functions. --- scripts/pathfinder/HybridAStar.lua | 38 ++++++------- .../HybridAStarWithAStarInTheMiddle.lua | 55 +++++++++---------- .../pathfinder/test/GraphPathfinderTest.lua | 41 ++++++++------ 3 files changed, 71 insertions(+), 63 deletions(-) diff --git a/scripts/pathfinder/HybridAStar.lua b/scripts/pathfinder/HybridAStar.lua index e1228be6e..997901468 100644 --- a/scripts/pathfinder/HybridAStar.lua +++ b/scripts/pathfinder/HybridAStar.lua @@ -63,6 +63,7 @@ end -- -- After start(), call resume() until it returns done == true. ---@see PathfinderInterface#run also on how to use. +---@return PathfinderResult function PathfinderInterface:start(...) if not self.coroutine then self.coroutine = coursePlayCoroutine.create(self.run) @@ -83,19 +84,17 @@ function PathfinderInterface:isActive() end --- Resume the pathfinding ----@return boolean true if the pathfinding is done, false if it isn't ready. In this case you'll have to call resume() again ----@return Polyline path if the path found or nil if none found. --- @return array of the points of the grid used for the pathfinding, for test purposes only +---@return PathfinderResult function PathfinderInterface:resume(...) - local ok, done, path, goalNodeInvalid = coursePlayCoroutine.resume(self.coroutine, self, ...) - if not ok or done then + local ok, result = coursePlayCoroutine.resume(self.coroutine, self, ...) + if not ok or result.done then if not ok then - print(done) + print(result.done) end self.coroutine = nil - return true, path, goalNodeInvalid + return result end - return false + return PathfinderResult(false) end function PathfinderInterface:debug(...) @@ -504,7 +503,7 @@ end --- when we search for a valid analytic solution we use this instead of isValidNode() ---@param hitchLength number hitch length of a trailer (length between hitch on the towing vehicle and the --- rear axle of the trailer), can be nil ----@return boolean, {}|nil, boolean done, path, goal node invalid +---@return PathfinderResult function HybridAStar:initRun(start, goal, turnRadius, allowReverse, constraints, hitchLength) self:debug('Start pathfinding between %s and %s', tostring(start), tostring(goal)) self:debug(' turnRadius = %.1f, allowReverse: %s', turnRadius, tostring(allowReverse)) @@ -533,12 +532,12 @@ function HybridAStar:initRun(start, goal, turnRadius, allowReverse, constraints, -- ignore trailer for the first check, we don't know its heading anyway if not constraints:isValidNode(goal, true) then self:debug('Goal node is invalid, abort pathfinding.') - return true, nil, true + return PathfinderResult(true, nil, true) end if not constraints:isValidAnalyticSolutionNode(goal, true) then -- goal node is invalid (for example in fruit), does not make sense to try analytic solutions - self.goalNodeIsInvalid = true + self.goalNodeInvalid = true self:debug('Goal node is invalid for analytical path.') end @@ -548,7 +547,7 @@ function HybridAStar:initRun(start, goal, turnRadius, allowReverse, constraints, if self:isPathValid(analyticPath) then self:debug('Found collision free analytic path (%s) from start to goal', pathType) CourseGenerator.addDebugPolyline(Polyline(analyticPath)) - return true, analyticPath + return PathfinderResult(true, analyticPath, self.goalNodeInvalid) end self:debug('Length of analytic solution is %.1f', analyticSolutionLength) end @@ -561,7 +560,7 @@ function HybridAStar:initRun(start, goal, turnRadius, allowReverse, constraints, self.expansions = 0 self.yields = 0 self.initialized = true - return false + return PathfinderResult(false) end --- Wrap up this run, clean up timer, reset initialized flag so next run will start cleanly @@ -569,15 +568,16 @@ function HybridAStar:finishRun(result, path) self.initialized = false self.constraints:showStatistics() closeIntervalTimer(self.timer) - return result, path + return PathfinderResult(result, path, self.goalNodeInvalid) end --- Reentry-safe pathfinder runner +---@return PathfinderResult function HybridAStar:run(start, goal, turnRadius, allowReverse, constraints, hitchLength) if not self.initialized then - local done, path, goalNodeInvalid = self:initRun(start, goal, turnRadius, allowReverse, constraints, hitchLength) - if done then - return done, path, goalNodeInvalid + local result = self:initRun(start, goal, turnRadius, allowReverse, constraints, hitchLength) + if result.done then + return result end end self.timer = openIntervalTimer() @@ -598,13 +598,13 @@ function HybridAStar:run(start, goal, turnRadius, allowReverse, constraints, hit self.yields = self.yields + 1 closeIntervalTimer(self.timer) -- if we had the coroutine package, we would coursePlayCoroutine.yield(false) here - return false + return PathfinderResult(false) end if not pred:isClosed() then -- analytical expansion: try a Dubins/Reeds-Shepp path from here randomly, more often as we getting closer to the goal -- also, try it before we start with the pathfinding if pred.h then - if self.analyticSolverEnabled and not self.goalNodeIsInvalid and + if self.analyticSolverEnabled and not self.goalNodeInvalid and math.random() > 2 * pred.h / self.distanceToGoal then self:debug('Check analytic solution at iteration %d, %.1f, %.1f', self.iterations, pred.h, pred.h / self.distanceToGoal) local analyticPath, _, pathType = self:getAnalyticPath(pred, self.goal, self.turnRadius, self.allowReverse, self.hitchLength) diff --git a/scripts/pathfinder/HybridAStarWithAStarInTheMiddle.lua b/scripts/pathfinder/HybridAStarWithAStarInTheMiddle.lua index b6064842c..261c3178e 100644 --- a/scripts/pathfinder/HybridAStarWithAStarInTheMiddle.lua +++ b/scripts/pathfinder/HybridAStarWithAStarInTheMiddle.lua @@ -43,12 +43,7 @@ end --- when we search for a valid analytic solution we use this instead of isValidNode() ---@param hitchLength number hitch length of a trailer (length between hitch on the towing vehicle and the --- rear axle of the trailer), can be nil ----@return boolean true if pathfinding is done (success or failure), false means it isn't ready and ---- resume() must be called to continue until this is true ----@return Polyline the path if found ----@return boolean if true, the goal node is invalid (for instance a vehicle or obstacle is there) so ---- the pathfinding can never succeed. ----@return number the furthest distance the pathfinding tried from the start, only when no path found +---@return PathfinderResult function HybridAStarWithAStarInTheMiddle:start(start, goal, turnRadius, allowReverse, constraints, hitchLength) self.startNode, self.goalNode = State3D.copy(start), State3D.copy(goal) self.originalStartNode = State3D.copy(self.startNode) @@ -68,6 +63,7 @@ function HybridAStarWithAStarInTheMiddle:start(start, goal, turnRadius, allowRev end -- distance between start and goal is relatively short, one phase hybrid A* all the way +---@return PathfinderResult function HybridAStarWithAStarInTheMiddle:findHybridStartToEnd() self.phase = self.ALL_HYBRID self:debug('Goal is closer than %d, use one phase pathfinding only', self.hybridRange * 3) @@ -77,6 +73,7 @@ function HybridAStarWithAStarInTheMiddle:findHybridStartToEnd() end -- start and goal far away, this is the hybrid A* from start to the middle section +---@return PathfinderResult function HybridAStarWithAStarInTheMiddle:findPathFromStartToMiddle() self:debug('Finding path between start and middle section...') self.phase = self.START_TO_MIDDLE @@ -88,6 +85,7 @@ function HybridAStarWithAStarInTheMiddle:findPathFromStartToMiddle() end -- start and goal far away, this is the hybrid A* from the middle section to the goal +---@return PathfinderResult function HybridAStarWithAStarInTheMiddle:findPathFromMiddleToEnd() -- generate middle to end self.phase = self.MIDDLE_TO_END @@ -98,47 +96,48 @@ function HybridAStarWithAStarInTheMiddle:findPathFromMiddleToEnd() end --- The resume() of this pathfinder is more complicated as it handles essentially three separate pathfinding runs +---@return PathfinderResult function HybridAStarWithAStarInTheMiddle:resume(...) - local ok, done, path, goalNodeInvalid = coursePlayCoroutine.resume(self.coroutine, self.currentPathfinder, ...) + local ok, result = coursePlayCoroutine.resume(self.coroutine, self.currentPathfinder, ...) if not ok then - print(done) + print(result.done) printCallstack() self:debug('Pathfinding failed') self.coroutine = nil - return PathfinderResult(true, nil, goalNodeInvalid, self.currentPathfinder:getHighestDistance(), + return PathfinderResult(true, nil, result.goalNodeInvalid, self.currentPathfinder:getHighestDistance(), self.constraints) end - if done then + if result.done then self.coroutine = nil if self.phase == self.ALL_HYBRID then - if path then + if result.path then -- start and goal near, just one phase, all hybrid, we are done - return PathfinderResult(true, path) + return PathfinderResult(true, result.path) else self:debug('all hybrid: no path found') - return PathfinderResult(true, nil, goalNodeInvalid, self.currentPathfinder:getHighestDistance(), + return PathfinderResult(true, nil, result.goalNodeInvalid, self.currentPathfinder:getHighestDistance(), self.constraints) end elseif self.phase == self.ASTAR then self.constraints:resetStrictMode() - if not path then + if not result.path then self:debug('fast A*: no path found') - return PathfinderResult(true, nil, goalNodeInvalid, self.currentPathfinder:getHighestDistance(), + return PathfinderResult(true, nil, result.goalNodeInvalid, self.currentPathfinder:getHighestDistance(), self.constraints) end - CourseGenerator.addDebugPolyline(Polyline(path), {1, 0, 0}) - local lMiddlePath = HybridAStar.length(path) + CourseGenerator.addDebugPolyline(Polyline(result.path), {1, 0, 0}) + local lMiddlePath = HybridAStar.length(result.path) self:debug('Direct path is %d m', lMiddlePath) -- do we even need to use the normal A star or the nodes are close enough that the hybrid A star will be fast enough? if lMiddlePath < self.hybridRange * 2 then return self:findHybridStartToEnd() end -- middle part ready, now trim start and end to make room for the hybrid parts - self.middlePath = path + self.middlePath = result.path HybridAStar.shortenStart(self.middlePath, self.hybridRange) HybridAStar.shortenEnd(self.middlePath, self.hybridRange) if #self.middlePath < 2 then - return PathfinderResult(true, nil, goalNodeInvalid, self.currentPathfinder:getHighestDistance(), + return PathfinderResult(true, nil, result.goalNodeInvalid, self.currentPathfinder:getHighestDistance(), self.constraints) end State3D.smooth(self.middlePath) @@ -146,10 +145,10 @@ function HybridAStarWithAStarInTheMiddle:resume(...) State3D.calculateTrailerHeadings(self.middlePath, self.hitchLength, true) return self:findPathFromStartToMiddle() elseif self.phase == self.START_TO_MIDDLE then - if path then - CourseGenerator.addDebugPolyline(Polyline(path), {0, 1, 0}) + if result.path then + CourseGenerator.addDebugPolyline(Polyline(result.path), {0, 1, 0}) -- start and middle sections ready, continue with the piece from the middle to the end - self.path = path + self.path = result.path -- create start point at the last waypoint of middlePath before shortening self.middleToEndStart = State3D.copy(self.middlePath[#self.middlePath]) -- now shorten both ends of middlePath to avoid short fwd/reverse sections due to overlaps (as the @@ -163,26 +162,26 @@ function HybridAStarWithAStarInTheMiddle:resume(...) return self:findPathFromMiddleToEnd() else self:debug('start to middle: no path found') - return PathfinderResult(true, nil, goalNodeInvalid, self.currentPathfinder:getHighestDistance(), + return PathfinderResult(true, nil, result.goalNodeInvalid, self.currentPathfinder:getHighestDistance(), self.constraints) end elseif self.phase == self.MIDDLE_TO_END then - if path then - CourseGenerator.addDebugPolyline(Polyline(path), {0, 0, 1}) + if result.path then + CourseGenerator.addDebugPolyline(Polyline(result.path), {0, 0, 1}) -- last piece is ready, this was generated from the goal point to the end of the middle section so -- first remove the last point of the middle section to make the transition smoother -- and then add the last section in reverse order -- also, for reasons we don't fully understand, this section may have a direction change at the last waypoint, -- so we just ignore the last one - for i = 1, #path do - table.insert(self.path, path[i]) + for i = 1, #result.path do + table.insert(self.path, result.path[i]) end State3D.smooth(self.path) self.constraints:showStatistics() return PathfinderResult(true, self.path) else self:debug('middle to end: no path found') - return PathfinderResult(true, nil, goalNodeInvalid, self.currentPathfinder:getHighestDistance(), + return PathfinderResult(true, nil, result.goalNodeInvalid, self.currentPathfinder:getHighestDistance(), self.constraints) end end diff --git a/scripts/pathfinder/test/GraphPathfinderTest.lua b/scripts/pathfinder/test/GraphPathfinderTest.lua index 090d5cd1d..e7ee504d6 100644 --- a/scripts/pathfinder/test/GraphPathfinderTest.lua +++ b/scripts/pathfinder/test/GraphPathfinderTest.lua @@ -25,13 +25,22 @@ require('GraphPathfinder') local GraphEdge = GraphPathfinder.GraphEdge local TestConstraints = CpObject(PathfinderConstraintInterface) -local pathfinder, start, goal, done, path, goalNodeInvalid +local pathfinder, start, goal, done, path + local function printPath() for _, p in ipairs(path) do print(Vector.__tostring(p)) end end +local function runPathfinder() + local result = pathfinder:start(start, goal, 1, false, TestConstraints(), 0) + while not result.done do + result = pathfinder:resume() + end + return result.done, result.path +end + function testDirection() local graph = { GraphEdge(GraphEdge.UNIDIRECTIONAL, @@ -51,7 +60,7 @@ function testDirection() pathfinder = GraphPathfinder(math.huge, 500, 20, graph) start = State3D(90, 105, 0, 0) goal = State3D(130, 105, 0, 0) - done, path, goalNodeInvalid = pathfinder:run(start, goal, 1, false, TestConstraints(), 0) + done, path, _ = runPathfinder() lu.assertIsTrue(done) lu.assertEquals(#path, 3) -- path contains all points of the edge it goes through @@ -59,7 +68,7 @@ function testDirection() path[2]:assertAlmostEquals(Vector(110, 100)) path[#path]:assertAlmostEquals(Vector(120, 100)) start, goal = goal, start - done, path, goalNodeInvalid = pathfinder:run(start, goal, 1, false, TestConstraints(), 0) + done, path, _ = runPathfinder() lu.assertIsTrue(done) lu.assertEquals(#path, 3) path[1]:assertAlmostEquals(Vector(120, 105)) @@ -86,7 +95,7 @@ function testBidirectional() pathfinder = GraphPathfinder(math.huge, 500, 20, graph) start = State3D(90, 105, 0, 0) goal = State3D(130, 105, 0, 0) - done, path, goalNodeInvalid = pathfinder:run(start, goal, 1, false, TestConstraints(), 0) + done, path, _ = runPathfinder() lu.assertIsTrue(done) lu.assertEquals(#path, 3) -- path contains the start node and all points of the edge it goes through @@ -94,7 +103,7 @@ function testBidirectional() path[2]:assertAlmostEquals(Vector(110, 100)) path[#path]:assertAlmostEquals(Vector(120, 100)) start, goal = goal, start - done, path, goalNodeInvalid = pathfinder:run(start, goal, 1, false, TestConstraints(), 0) + done, path, _ = runPathfinder() lu.assertIsTrue(done) lu.assertEquals(#path, 3) -- TODO: here, it should have taken the other path, over y = 105, as it is slightly shorter since both start and @@ -125,7 +134,7 @@ function testShorterPath() pathfinder = GraphPathfinder(math.huge, 500, 20, graph) start = State3D(90, 105, 0, 0) goal = State3D(130, 105, 0, 0) - done, path, goalNodeInvalid = pathfinder:run(start, goal, 1, false, TestConstraints(), 0) + done, path, _ = runPathfinder() lu.assertIsTrue(done) lu.assertEquals(#path, 3) path[1]:assertAlmostEquals(Vector(100, 100)) @@ -158,14 +167,14 @@ function testRange() pathfinder = GraphPathfinder(math.huge, 500, 20, graph) start = State3D(90, 105, 0, 0) goal = State3D(150, 105, 0, 0) - done, path, goalNodeInvalid = pathfinder:run(start, goal, 1, false, TestConstraints(), 0) + done, path, _ = runPathfinder() lu.assertIsTrue(done) lu.assertEquals(#path, 6) path[1]:assertAlmostEquals(Vector(100, 100)) path[2]:assertAlmostEquals(Vector(110, 100)) path[#path]:assertAlmostEquals(Vector(150, 100)) pathfinder = GraphPathfinder(math.huge, 500, 9, graph) - done, path, goalNodeInvalid = pathfinder:run(start, goal, 1, false, TestConstraints(), 0) + done, path, _ = runPathfinder() lu.assertIsNil(path) end @@ -188,7 +197,7 @@ function testStartInTheMiddle() pathfinder = GraphPathfinder(math.huge, 500, 20, graph) start = State3D(150, 95, 0, 0) goal = State3D(95, 95, 0, 0) - done, path, goalNodeInvalid = pathfinder:run(start, goal, 1, false, TestConstraints(), 0) + done, path, _ = runPathfinder() lu.assertIsTrue(done) lu.assertEquals(#path, 2) path[1]:assertAlmostEquals(Vector(150, 100)) @@ -210,7 +219,7 @@ function testStartInTheMiddle() } pathfinder = GraphPathfinder(math.huge, 500, 10, graph) start, goal = goal, start - done, path, goalNodeInvalid = pathfinder:run(start, goal, 1, false, TestConstraints(), 0) + done, path, _ = runPathfinder() printPath() lu.assertIsTrue(done) lu.assertEquals(#path, 2) @@ -235,13 +244,13 @@ function testTwoPointSegments() pathfinder = GraphPathfinder(math.huge, 500, 20, graph) start = State3D(90, 105, 0, 0) goal = State3D(130, 105, 0, 0) - done, path, goalNodeInvalid = pathfinder:run(start, goal, 1, false, TestConstraints(), 0) + done, path, _ = runPathfinder() lu.assertIsTrue(done) lu.assertEquals(#path, 2) path[1]:assertAlmostEquals(Vector(100, 100)) path[#path]:assertAlmostEquals(Vector(120, 100)) start, goal = goal, start - done, path, goalNodeInvalid = pathfinder:run(start, goal, 1, false, TestConstraints(), 0) + done, path, _ = runPathfinder() lu.assertIsTrue(done) lu.assertEquals(#path, 2) path[1]:assertAlmostEquals(Vector(120, 105)) @@ -269,7 +278,7 @@ function testEntryExit() -- start/goal far away start = State3D(0, 0, 0, 0) goal = State3D(130, 0, 0, 0) - done, path, goalNodeInvalid = pathfinder:run(start, goal, 1, false, TestConstraints(), 0) + done, path, _ = runPathfinder() lu.assertIsTrue(done) lu.assertEquals(#path, 3) -- path contains all points of the edge it goes through @@ -279,7 +288,7 @@ function testEntryExit() -- start/goal far away start = State3D(130, 200, 0, 0) goal = State3D(0, 200, 0, 0) - done, path, goalNodeInvalid = pathfinder:run(start, goal, 1, false, TestConstraints(), 0) + done, path, _ = runPathfinder() lu.assertIsTrue(done) lu.assertEquals(#path, 3) -- path contains all points of the edge it goes through @@ -289,7 +298,7 @@ function testEntryExit() -- start/goal far away, middle entry start = State3D(110, 0, 0, 0) goal = State3D(130, 0, 0, 0) - done, path, goalNodeInvalid = pathfinder:run(start, goal, 1, false, TestConstraints(), 0) + done, path, _ = runPathfinder() lu.assertIsTrue(done) lu.assertEquals(#path, 2) path[1]:assertAlmostEquals(Vector(110, 100)) @@ -310,7 +319,7 @@ function testGoalWithinRange() start = State3D(100, 100, 0, 0) goal = State3D(120, 100, 0, 0) print('******** single ******') - done, path, goalNodeInvalid = pathfinder:run(start, goal, 1, false, TestConstraints(), 0) + done, path, _ = runPathfinder() lu.assertIsTrue(done) lu.assertEquals(#path, 2) -- path contains all points of the edge it goes through From 3e2e5a72ba7d2a1ab597cb8b577934c8c5439c91 Mon Sep 17 00:00:00 2001 From: Peter Vaiko Date: Sun, 30 Mar 2025 12:23:05 -0400 Subject: [PATCH 28/73] feat: return goalNodeInvalid if entry too close to exit If the graph entry (closest vertex to start location) is closer to the graph exit (closest vertex to the goal location) than the range, the GraphPathfinder can't generate a useful path. Return goalNodeInvalid == true in this case and log an error. --- scripts/pathfinder/GraphPathfinder.lua | 7 +++++ .../pathfinder/test/GraphPathfinderTest.lua | 28 ++++++++----------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/scripts/pathfinder/GraphPathfinder.lua b/scripts/pathfinder/GraphPathfinder.lua index 53d213cad..262e76e47 100644 --- a/scripts/pathfinder/GraphPathfinder.lua +++ b/scripts/pathfinder/GraphPathfinder.lua @@ -152,6 +152,13 @@ end function GraphPathfinder:initRun(start, goal, ...) local graphEntry, graphExit = self:createGraphEntryAndExit(start, goal) + local distance = (graphExit - graphEntry):length() + if distance <= self.range then + -- if the distance between the entry and exit is less than the range, we can just return the entry as the exit + self.logger:error('Graph entry and exit are closer than %.1f meters (%.1f), no point in running the pathfinder.', + self.range, distance) + return PathfinderResult(true, nil, true) + end return HybridAStar.initRun(self, graphEntry, graphExit, ...) end diff --git a/scripts/pathfinder/test/GraphPathfinderTest.lua b/scripts/pathfinder/test/GraphPathfinderTest.lua index e7ee504d6..9065c5b32 100644 --- a/scripts/pathfinder/test/GraphPathfinderTest.lua +++ b/scripts/pathfinder/test/GraphPathfinderTest.lua @@ -25,7 +25,7 @@ require('GraphPathfinder') local GraphEdge = GraphPathfinder.GraphEdge local TestConstraints = CpObject(PathfinderConstraintInterface) -local pathfinder, start, goal, done, path +local pathfinder, start, goal, done, path, goalNodeInvalid local function printPath() for _, p in ipairs(path) do @@ -38,7 +38,7 @@ local function runPathfinder() while not result.done do result = pathfinder:resume() end - return result.done, result.path + return result.done, result.path, result.goalNodeInvalid end function testDirection() @@ -57,7 +57,7 @@ function testDirection() Vertex(100, 105), }), } - pathfinder = GraphPathfinder(math.huge, 500, 20, graph) + pathfinder = GraphPathfinder(math.huge, 500, 10, graph) start = State3D(90, 105, 0, 0) goal = State3D(130, 105, 0, 0) done, path, _ = runPathfinder() @@ -92,7 +92,7 @@ function testBidirectional() Vertex(100, 105), }), } - pathfinder = GraphPathfinder(math.huge, 500, 20, graph) + pathfinder = GraphPathfinder(math.huge, 500, 10, graph) start = State3D(90, 105, 0, 0) goal = State3D(130, 105, 0, 0) done, path, _ = runPathfinder() @@ -131,7 +131,7 @@ function testShorterPath() Vertex(120, 105), }), } - pathfinder = GraphPathfinder(math.huge, 500, 20, graph) + pathfinder = GraphPathfinder(math.huge, 500, 10, graph) start = State3D(90, 105, 0, 0) goal = State3D(130, 105, 0, 0) done, path, _ = runPathfinder() @@ -164,7 +164,7 @@ function testRange() Vertex(150, 100) }), } - pathfinder = GraphPathfinder(math.huge, 500, 20, graph) + pathfinder = GraphPathfinder(math.huge, 500, 10, graph) start = State3D(90, 105, 0, 0) goal = State3D(150, 105, 0, 0) done, path, _ = runPathfinder() @@ -194,7 +194,7 @@ function testStartInTheMiddle() Vertex(100, 105), }), } - pathfinder = GraphPathfinder(math.huge, 500, 20, graph) + pathfinder = GraphPathfinder(math.huge, 500, 10, graph) start = State3D(150, 95, 0, 0) goal = State3D(95, 95, 0, 0) done, path, _ = runPathfinder() @@ -241,7 +241,7 @@ function testTwoPointSegments() Vertex(100, 105), }), } - pathfinder = GraphPathfinder(math.huge, 500, 20, graph) + pathfinder = GraphPathfinder(math.huge, 500, 10, graph) start = State3D(90, 105, 0, 0) goal = State3D(130, 105, 0, 0) done, path, _ = runPathfinder() @@ -306,7 +306,7 @@ function testEntryExit() end function testGoalWithinRange() - + -- goal too close to start (graph entry too close to graph exit) local graph = { GraphEdge(GraphEdge.UNIDIRECTIONAL, { @@ -318,14 +318,10 @@ function testGoalWithinRange() -- start/goal far away start = State3D(100, 100, 0, 0) goal = State3D(120, 100, 0, 0) - print('******** single ******') - done, path, _ = runPathfinder() + done, path, goalNodeInvalid = runPathfinder() lu.assertIsTrue(done) - lu.assertEquals(#path, 2) - -- path contains all points of the edge it goes through - path[1]:assertAlmostEquals(Vector(100, 100)) - --path[2]:assertAlmostEquals(Vector(110, 100)) - path[#path]:assertAlmostEquals(Vector(120, 100)) + lu.assertIsTrue(goalNodeInvalid) + lu.assertIsNil(path) end os.exit(lu.LuaUnit.run()) From 31544a671e0f4bf33b3c9f0bb5ad50602bb090d1 Mon Sep 17 00:00:00 2001 From: Peter Vaiko Date: Sun, 30 Mar 2025 12:27:38 -0400 Subject: [PATCH 29/73] fix: pathfinder call I broke --- scripts/graph/Graph.lua | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/scripts/graph/Graph.lua b/scripts/graph/Graph.lua index d98bd957d..123358361 100644 --- a/scripts/graph/Graph.lua +++ b/scripts/graph/Graph.lua @@ -36,16 +36,19 @@ function Graph:consoleCommandFindPathTo(name) if vehicle == nil then return "Must be in a vehicle!" end - local pathfinder = GraphPathfinder(math.huge, 500, 20, edges) + local pathfinder = GraphPathfinder(1000, 500, 20, edges) local start = PathfinderUtil.getVehiclePositionAsState3D(vehicle) local goal = State3D(targetPos.x, targetPos.y, 0, 0) CpUtil.info("Goal: %s", tostring(goal)) local TestConstraints = CpObject(PathfinderConstraintInterface) - local done, path, goalNodeInvalid = pathfinder:run(start, goal, 1, false, TestConstraints(), 0) - if not done or path == nil or #path < 2 then + local result = pathfinder:start(start, goal, 1, false, TestConstraints(), 0) + while not result.done do + result = pathfinder:resume() + end + if not result.done or result.path == nil or #result.path < 2 then return "Pathfinder failed!" end - local course = Course.createFromAnalyticPath(vehicle, path, true) + local course = Course.createFromAnalyticPath(vehicle, result.path, true) vehicle:setFieldWorkCourse(course) end local success, ret = CpUtil.try(cmd) From b8aeff705fc400a4965b1f9ed14f79c6ecc4af40 Mon Sep 17 00:00:00 2001 From: David Schwietering Date: Sun, 30 Mar 2025 19:23:45 +0200 Subject: [PATCH 30/73] WIP parse giants splines to graph --- Courseplay.lua | 1 + scripts/graph/Graph.lua | 100 ++++++++++++++++++++++++++++++++- scripts/graph/GraphPoint.lua | 8 +++ scripts/graph/GraphSegment.lua | 11 +++- 4 files changed, 116 insertions(+), 4 deletions(-) diff --git a/Courseplay.lua b/Courseplay.lua index 244650c97..de473f586 100644 --- a/Courseplay.lua +++ b/Courseplay.lua @@ -134,6 +134,7 @@ function Courseplay:deleteMap() BufferedCourseDisplay.deleteBuffer() g_signPrototypes:delete() g_consoleCommands:delete() + g_graph:delete() end function Courseplay:setupGui() diff --git a/scripts/graph/Graph.lua b/scripts/graph/Graph.lua index 123358361..f3c0a8b24 100644 --- a/scripts/graph/Graph.lua +++ b/scripts/graph/Graph.lua @@ -4,10 +4,19 @@ Graph = CpObject(GraphNode) Graph.XML_KEY = "Graph" function Graph:init() GraphNode.init(self) - g_consoleCommands:registerConsoleCommand("cpGraphFindPathTo", "Tries to find a path to: ", "consoleCommandFindPathTo", self) + g_consoleCommands:registerConsoleCommand("cpGraphFindPathTo", + "Tries to find a path to: ", "consoleCommandFindPathTo", self) + g_consoleCommands:registerConsoleCommand("cpGraphGenerateFromSplines", + "Generates segmenets from traffic splines", + "consoleCommandGenerateSegmentsFromSplines", self) ---@type GraphTarget[] self._targets = {} + self._hasGeneratedSplines = false +end + +function Graph:delete() + end function Graph:consoleCommandFindPathTo(name) @@ -45,7 +54,7 @@ function Graph:consoleCommandFindPathTo(name) while not result.done do result = pathfinder:resume() end - if not result.done or result.path == nil or #result.path < 2 then + if result.path == nil or #result.path < 2 then return "Pathfinder failed!" end local course = Course.createFromAnalyticPath(vehicle, result.path, true) @@ -57,6 +66,86 @@ function Graph:consoleCommandFindPathTo(name) end end +function Graph:consoleCommandGenerateSegmentsFromSplines() + if self._hasGeneratedSplines then + CpUtil.info("Already generated from road splines!") + return + end + local function isSplineEqual(segA, segB) + local snA1 = segA:getChildNodeByIndex(1) + local enA1 = segA:getChildNodeByIndex(segA:getNumChildNodes()) + local snA2 = segA:getChildNodeByIndex(2) + local enA2 = segA:getChildNodeByIndex(segA:getNumChildNodes() - 1) + + local snB1 = segB:getChildNodeByIndex(1) + local enB1 = segB:getChildNodeByIndex(segB:getNumChildNodes()) + local snB2 = segB:getChildNodeByIndex(2) + local enB2 = segB:getChildNodeByIndex(segB:getNumChildNodes() - 1) + local margin = 3 + if (snA1:getDistance2DToPoint(enB1) <= margin or + snA1:getDistance2DToPoint(enB2) <= margin or + snA2:getDistance2DToPoint(enB1) <= margin or + snA2:getDistance2DToPoint(enB2) <= margin) and + (snB1:getDistance2DToPoint(enA1) <= margin or + snB1:getDistance2DToPoint(enA2) <= margin or + snB2:getDistance2DToPoint(enA1) <= margin or + snB2:getDistance2DToPoint(enA2) <= margin) then + + return true + end + end + + local splineToCount = {} + for spline, _ in pairs(g_currentMission.aiSystem:getRoadSplines()) do + splineToCount[spline] = 0 + local sx, _, sz = getSplinePosition(spline, 0) + local ex, _, ez = getSplinePosition(spline, 0) + local length = getSplineLength(spline) + for otherSpline, _ in pairs(g_currentMission.aiSystem:getRoadSplines()) do + if spline ~= otherSpline then + local dsx, _, dsz = getSplinePosition(otherSpline, 0) + local dex, _, dez = getSplinePosition(otherSpline, 0) + if MathUtil.vector2Length(sx - dsx, sz - dsz) < 1 or + MathUtil.vector2Length(sx - dex, sz - dez) < 1 or + MathUtil.vector2Length(ex - dsx, ez - dsz) < 1 or + MathUtil.vector2Length(ex - dex, ez - dez) < 1 then + if length < getSplineLength(otherSpline) then + splineToCount[spline] = splineToCount[spline] + 1 + end + end + end + end + end + local splineSegments = {} + for spline, count in pairs(splineToCount) do + local ignoreSpline = count > 0 + local length = getSplineLength(spline) + local segment = GraphSegment(true) + if not ignoreSpline then + for i = 0, 1, 6/length do + local posX, posY, posZ = getSplinePosition(spline, i) + local point = GraphPoint() + point:setPosition(posX, posY, posZ) + segment:appendChildNode(point) + end + if segment:getNumChildNodes() > 1 then + for _, seg in ipairs(splineSegments) do + if isSplineEqual(segment, seg) then + seg:changeDirection(GraphSegmentDirection.DUAL) + ignoreSpline = true + break + end + end + if not ignoreSpline then + table.insert(splineSegments, segment) + end + end + end + end + self:extendByChildNodes(splineSegments, false) + self._hasGeneratedSplines = true +end + function Graph:setup() ---@type GraphPlot self._ingameMapPlot = GraphPlot(self) @@ -65,6 +154,9 @@ end function Graph.registerXmlSchema(xmlSchema, baseKey) GraphSegment.registerXmlSchema(xmlSchema, baseKey .. Graph.XML_KEY .. ".") + xmlSchema:register(XMLValueType.BOOL, + baseKey .. Graph.XML_KEY .. "#hasGeneratedSplines", + "Has generated splines?", false) end function Graph:loadFromXMLFile(xmlFile, baseKey) @@ -73,6 +165,8 @@ function Graph:loadFromXMLFile(xmlFile, baseKey) segment:loadFromXMLFile(xmlFile, key) self:appendChildNode(segment) end) + self._hasGeneratedSplines = xmlFile:getValue( + baseKey .. self.XML_KEY .. "#hasGeneratedSplines") end function Graph:saveToXMLFile(xmlFile, baseKey) @@ -80,6 +174,8 @@ function Graph:saveToXMLFile(xmlFile, baseKey) segment:saveToXMLFile(xmlFile, string.format("%s.%s(%i)", baseKey .. self.XML_KEY, GraphSegment.XML_KEY, i - 1)) end + xmlFile:setValue(baseKey .. self.XML_KEY .. "#hasGeneratedSplines", + self._hasGeneratedSplines) end ---@param node GraphNode diff --git a/scripts/graph/GraphPoint.lua b/scripts/graph/GraphPoint.lua index bbb6d627b..bac977ebf 100644 --- a/scripts/graph/GraphPoint.lua +++ b/scripts/graph/GraphPoint.lua @@ -173,11 +173,19 @@ function GraphPoint:moveTo2D(dx, dz) end ---@param other GraphPoint +---@return number function GraphPoint:getDistance2DToPoint(other) local dx, dz = other:getPosition2D() return MathUtil.vector2Length(self._x - dx, self._z - dz) end +---@param dx number +---@param dz number +---@return number +function GraphPoint:getDistance2DTo(dx, dz) + return MathUtil.vector2Length(self._x - dx, self._z - dz) +end + function GraphPoint:toVector() return Vector(self._x, -self._z) end diff --git a/scripts/graph/GraphSegment.lua b/scripts/graph/GraphSegment.lua index f083fa133..49d473785 100644 --- a/scripts/graph/GraphSegment.lua +++ b/scripts/graph/GraphSegment.lua @@ -19,10 +19,12 @@ GraphSegmentDirection.DEBUG_TEXTS = { ---@field _childNodes GraphPoint[] GraphSegment = CpObject(GraphNode) GraphSegment.XML_KEY = "Segment" -GraphSegment.DRAW_CAMERA_RANGE = 200 -function GraphSegment:init() +GraphSegment.DRAW_CAMERA_RANGE = 125 +function GraphSegment:init(isGeneratedBySpline) GraphNode.init(self) self._direction = GraphSegmentDirection.FORWARD + ---@type boolean + self._isGeneratedBySpline = isGeneratedBySpline or false end function GraphSegment.registerXmlSchema(xmlSchema, baseKey) @@ -30,11 +32,15 @@ function GraphSegment.registerXmlSchema(xmlSchema, baseKey) xmlSchema:register(XMLValueType.INT, key .. "(?)#direction", "Current direction", GraphSegmentDirection.FORWARD) + xmlSchema:register(XMLValueType.BOOL, + key .. "(?)#isGeneratedBySpline", + "Was generated from splines?", false) GraphPoint.registerXmlSchema(xmlSchema, key .. "(?).") end function GraphSegment:loadFromXMLFile(xmlFile, baseKey) self._direction = xmlFile:getValue(baseKey .. "#direction", GraphSegmentDirection.FORWARD) + self._isGeneratedBySpline = xmlFile:getValue(baseKey .. "isGeneratedBySpline", false) xmlFile:iterate(baseKey .. "." .. GraphPoint.XML_KEY, function (ix, key) local point = GraphPoint() point:loadFromXMLFile(xmlFile, key) @@ -44,6 +50,7 @@ end function GraphSegment:saveToXMLFile(xmlFile, baseKey) xmlFile:setValue(baseKey .. "#direction", self._direction) + xmlFile:setValue(baseKey .. "#isGeneratedBySpline", self._isGeneratedBySpline) for i, point in ipairs(self._childNodes) do local key = string.format("%s.%s(%d)", baseKey, GraphPoint.XML_KEY, i - 1) point:saveToXMLFile(xmlFile, key) From 4850aa11fed75ed805f8f6fc0522047ef1447921 Mon Sep 17 00:00:00 2001 From: David Schwietering Date: Sun, 30 Mar 2025 19:47:42 +0200 Subject: [PATCH 31/73] Split brush fix and hit box improvement --- scripts/editor/EditorGraphWrapper.lua | 8 ++++---- scripts/editor/brushes/graph/GraphBrush.lua | 6 +++--- scripts/graph/GraphNode.lua | 4 ++-- scripts/graph/GraphPoint.lua | 2 +- scripts/graph/GraphSegment.lua | 12 ++++++------ 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/scripts/editor/EditorGraphWrapper.lua b/scripts/editor/EditorGraphWrapper.lua index dac70eb62..3998362a2 100644 --- a/scripts/editor/EditorGraphWrapper.lua +++ b/scripts/editor/EditorGraphWrapper.lua @@ -339,12 +339,12 @@ function EditorGraphWrapper:splitSegment(id) return false, err end local ix = segment:getChildNodeIndex(node) - local postNodes = segment:cloneChildNodesBetweenIndex(ix + 1, segment:getNumChildNodes()) - segment:removeChildNodesBetweenIndex(ix + 1, segment:getNumChildNodes()) - ---@type GraphSegment + local postNodes = segment:getChildNodesBetweenIndex(ix + 1, segment:getNumChildNodes()) + --@type GraphSegment local newSegment = GraphSegment() newSegment:extendByChildNodes(postNodes, false) - self.graph:appendChildNode(segment) + self.graph:appendChildNode(newSegment) + segment:removeChildNodesBetweenIndex(ix + 1, segment:getNumChildNodes()) return true end diff --git a/scripts/editor/brushes/graph/GraphBrush.lua b/scripts/editor/brushes/graph/GraphBrush.lua index eb61473b8..0bb9f6f88 100644 --- a/scripts/editor/brushes/graph/GraphBrush.lua +++ b/scripts/editor/brushes/graph/GraphBrush.lua @@ -3,7 +3,7 @@ ]] ---@class GraphBrush : CpBrush GraphBrush = CpObject(CpBrush) -GraphBrush.radius = 0.5 +GraphBrush.radius = 1 GraphBrush.sizeModifierMax = 10 function GraphBrush:init(...) CpBrush.init(self, ...) @@ -21,11 +21,11 @@ end ---@param x number ---@param y number ---@param z number ----@return boolean +---@return boolean|nil function GraphBrush:isAtPos(point, x, y, z) local dx, dy, dz = point:getPosition() if MathUtil.getPointPointDistance(dx, dz, x, z) < self.radius * self.sizeModifier * (1 + self.camera.zoomFactor) then - return math.abs(dy - y) < 3 + return math.abs(dy + 2 - y) < 3 end end diff --git a/scripts/graph/GraphNode.lua b/scripts/graph/GraphNode.lua index 643dfe0a1..1692501cb 100644 --- a/scripts/graph/GraphNode.lua +++ b/scripts/graph/GraphNode.lua @@ -102,11 +102,11 @@ end ---@param sx number ---@param ex number ---@return GraphNode[] -function GraphNode:cloneChildNodesBetweenIndex(sx, ex) +function GraphNode:getChildNodesBetweenIndex(sx, ex) local nodes = {} for ix, node in ipairs(self._childNodes) do if ix >= sx and ix <= ex then - table.insert(nodes, node:clone(true)) + table.insert(nodes, node) end end return nodes diff --git a/scripts/graph/GraphPoint.lua b/scripts/graph/GraphPoint.lua index bac977ebf..87de1881a 100644 --- a/scripts/graph/GraphPoint.lua +++ b/scripts/graph/GraphPoint.lua @@ -78,7 +78,7 @@ function GraphPoint:draw(hoveredNodeID, selectedNodeIDs, isTemporary) elseif isTemporary then color = Color.new(0, 1, 0) end - DebugUtil.drawDebugSphere(self._x, self._y + 2, self._z, + DebugUtil.drawDebugSphere(self._x, self._y + 0.5, self._z, 1, 6, 6, color, false, false) local data = self:getDebugInfos() local yOffset = 0 diff --git a/scripts/graph/GraphSegment.lua b/scripts/graph/GraphSegment.lua index 49d473785..b6ed4c6b4 100644 --- a/scripts/graph/GraphSegment.lua +++ b/scripts/graph/GraphSegment.lua @@ -96,7 +96,7 @@ function GraphSegment:drawLineBetween(prevPoint, point) local x, y, z = point:getPosition() local dx, dy, dz = prevPoint:getPosition() DebugUtil.drawDebugLine(x, y + 2, z, - dx, dy + 2, dz, unpack(color), 2) + dx, dy + 0.5, dz, unpack(color), 2) local dist = MathUtil.vector3Length(x - dx, y - dy, z - dz) if dist > 1 then local nx, _, nz = MathUtil.vector3Normalize(x - dx, y - dy, z - dz) @@ -117,15 +117,15 @@ function GraphSegment:drawLineBetween(prevPoint, point) end local ncx = nx * math.cos(math.pi/4) - nz * math.sin(math.pi/4) local ncz = nx * math.sin(math.pi/4) + nz * math.cos(math.pi/4) - DebugUtil.drawDebugLine(tx, y + 2, tz, - tx - ncx * 2, y + 2, tz - ncz * 2, unpack(color)) + DebugUtil.drawDebugLine(tx, y + 0.5, tz, + tx - ncx * 2, y + 0.5, tz - ncz * 2, unpack(color)) ncx = nx * math.cos(-math.pi/4) - nz * math.sin(-math.pi/4) ncz = nx * math.sin(-math.pi/4) + nz * math.cos(-math.pi/4) - DebugUtil.drawDebugLine(tx, y + 2, tz, - tx - ncx * 2, y + 2, tz - ncz * 2, unpack(color)) + DebugUtil.drawDebugLine(tx, y + 0.5, tz, + tx - ncx * 2, y + 0.5, tz - ncz * 2, unpack(color)) elseif self._direction == GraphSegmentDirection.DUAL then -- x, y, z, radius, steps, color, alignToTerrain, filled - DebugUtil.drawDebugCircle(dx + nx * i, y + 2, dz + nz * i, + DebugUtil.drawDebugCircle(dx + nx * i, y + 0.5, dz + nz * i, 1, 8, color) end end From d95dc82dd7b7f971b8d3ef67ece1cae8c93628e1 Mon Sep 17 00:00:00 2001 From: David Schwietering Date: Mon, 31 Mar 2025 19:22:40 +0200 Subject: [PATCH 32/73] Fixes save to xml bug --- scripts/graph/GraphSegment.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/graph/GraphSegment.lua b/scripts/graph/GraphSegment.lua index b6ed4c6b4..7ca631382 100644 --- a/scripts/graph/GraphSegment.lua +++ b/scripts/graph/GraphSegment.lua @@ -40,7 +40,7 @@ end function GraphSegment:loadFromXMLFile(xmlFile, baseKey) self._direction = xmlFile:getValue(baseKey .. "#direction", GraphSegmentDirection.FORWARD) - self._isGeneratedBySpline = xmlFile:getValue(baseKey .. "isGeneratedBySpline", false) + self._isGeneratedBySpline = xmlFile:getValue(baseKey .. "#isGeneratedBySpline", false) xmlFile:iterate(baseKey .. "." .. GraphPoint.XML_KEY, function (ix, key) local point = GraphPoint() point:loadFromXMLFile(xmlFile, key) From 1dc3c1d02357104a844945fc6e211dbfce103495 Mon Sep 17 00:00:00 2001 From: Peter Vaiko Date: Wed, 2 Apr 2025 10:02:38 -0400 Subject: [PATCH 33/73] feat: can make left turns to two-lane roads --- scripts/geometry/Polyline.lua | 6 +- scripts/pathfinder/GraphPathfinder.lua | 137 ++++++++++++++---- scripts/pathfinder/HybridAStar.lua | 17 ++- .../pathfinder/test/GraphPathfinderTest.lua | 51 ++++++- 4 files changed, 163 insertions(+), 48 deletions(-) diff --git a/scripts/geometry/Polyline.lua b/scripts/geometry/Polyline.lua index 0292892d5..9ab07227a 100644 --- a/scripts/geometry/Polyline.lua +++ b/scripts/geometry/Polyline.lua @@ -46,8 +46,12 @@ function Polyline:prepend(v) end end +function Polyline:_getNewInstance() + return Polyline() +end + function Polyline:clone() - local clone = Polyline({}) + local clone = self:_getNewInstance() for _, v in ipairs(self) do clone:append(v:clone()) end diff --git a/scripts/pathfinder/GraphPathfinder.lua b/scripts/pathfinder/GraphPathfinder.lua index 262e76e47..29a360bd8 100644 --- a/scripts/pathfinder/GraphPathfinder.lua +++ b/scripts/pathfinder/GraphPathfinder.lua @@ -28,6 +28,17 @@ function GraphPathfinder.GraphEdge:init(direction, vertices) self.direction = direction end +-- allow to inherit clone +function GraphPathfinder.GraphEdge:_getNewInstance() + return GraphPathfinder.GraphEdge() +end + +function GraphPathfinder.GraphEdge:clone() + local clone = Polyline.clone(self) + clone.direction = self.direction + return clone +end + function GraphPathfinder.GraphEdge:getDirection() return self.direction end @@ -76,6 +87,14 @@ function GraphPathfinder.GraphEdge:rollUpIterator(entry) end end +GraphPathfinder.HelperGraphEdge = CpObject(GraphPathfinder.GraphEdge) +--- Helper edges are to entry/exit the graph from the goal/start. We don't want these to be part of the path, so we +--- the roll up iterator returns nothing, automatically skip the vertices of these edges +function GraphPathfinder.HelperGraphEdge:rollUpIterator(entry) + return function() + end +end + --- A pathfinder node, specialized for the GraphPathfinder ---@class GraphPathfinder.Node : State3D GraphPathfinder.Node = CpObject(State3D) @@ -107,10 +126,14 @@ end --- two edges are considered as connected (and thus can traverse from one to the other) ---@param graph GraphPathfinder.GraphEdge[] Array of edges, the graph as described in the file header function GraphPathfinder:init(yieldAfter, maxIterations, range, graph) - self.logger = Logger('GraphPathfinder', Logger.level.debug, CpDebug.DBG_PATHFINDER) HybridAStar.init(self, { }, yieldAfter, maxIterations) + self.logger = Logger('GraphPathfinder', Logger.level.trace, CpDebug.DBG_PATHFINDER) self.range = range - self.graph = graph + -- make a copy of the graph as we'll modify it + self.graph = {} + for _, e in ipairs(graph) do + table.insert(self.graph, e:clone()) + end self.deltaPosGoal = self.range self.deltaThetaDeg = 181 self.deltaThetaGoal = math.rad(self.deltaThetaDeg) @@ -137,9 +160,10 @@ function GraphPathfinder:rollUpPath(lastNode, goal, path) self:debug('Goal node at %.2f/%.2f, cost %.1f (%.1f - %.1f)', goal.x, goal.y, lastNode.cost, self.nodes.lowestCost, self.nodes.highestCost) while currentNode.pred and currentNode ~= currentNode.pred do - if currentNode.edge then - -- add the edge leading to the node - for _, node in currentNode.edge:rollUpIterator(currentNode.entry) do + -- add the edge leading to the node + for _, node in currentNode.edge:rollUpIterator(currentNode.entry) do + if node ~= path[1] then + -- don't insert the same node twice (we'll have the same node twice when we split edges) table.insert(path, 1, node) end end @@ -159,49 +183,97 @@ function GraphPathfinder:initRun(start, goal, ...) self.range, distance) return PathfinderResult(true, nil, true) end - return HybridAStar.initRun(self, graphEntry, graphExit, ...) + return HybridAStar.initRun(self, start, goal, ...) end ---- Create the entry and exit vertices for the graph. +--- Create the entry and exit edges for the graph. +--- +--- The problem we are trying to solve here, is that the start and goal can be in any distance from any edge of the +--- graph, like when the vehicle sits in the middle of a field, surrounded by streets. Any street could be used as +--- an entry, and the closest street may not be the shortest path to the goal. A special case of this is when +--- the street is a two lane, two way street and our goal is to the left, but the closest edge is the right lane, +--- leading away from the goal, forcing us making an unnecessary detour. +--- +--- Therefore, it isn't enough to find the closest vertex of the graph, we need a list of the closest vertices and +--- as long as their distance is not bigger than the distance to the closest one + the range, we can use them as +--- entries/exits. --- ---- Look for the vertex closest to the start/end location. When we find that vertex, and ---- it isn't the first or last point of the edge, we simply split that edge at that vertex so the parts can ---- be used as entries/exits. +--- To make sure that the algorithm actually uses these entry/exit points, we add helper edges to the graph, leading +--- from the start to the entries and from the exits to the goal. +--- +--- When the closest vertex, isn't the first or last point of the edge, we simply split that edge at that vertex so +--- the parts can be used as entries/exits. --- ---@param start State3D the start location for the pathfinder ---@param goal State3D the goal location for the pathfinder ---@return State3D the entry vertex of the graph, closest to start ---@return State3D the exit vertex of the graph, closest to goal function GraphPathfinder:createGraphEntryAndExit(start, goal) - local function splitClosestEdge(node) - local closestEdge, closestVertex - local closestDistance = math.huge - for _, edge in ipairs(self.graph) do - local v, d = edge:findClosestVertexToPoint(node) - if d and d < closestDistance then - closestDistance = d - closestEdge = edge - closestVertex = v - end - end + + local function splitEdgeWhenNeeded(edge, closestVertex) -- if the vertex is the first or last vertex of the edge, we can use it directly as the entry/exit, -- otherwise, we split the edge at the vertex so we can use it as an entry/exit point. - if closestVertex.ix ~= 1 and closestVertex.ix ~= #closestEdge then - self.logger:debug('Graph entry/exit found and split at vertex %d, d: %.1f, x: %.1f y: %.1f', - closestVertex.ix, closestDistance, closestVertex.x, closestVertex.y) - local newEdge = GraphPathfinder.GraphEdge(closestEdge:getDirection()) - for i = closestVertex.ix, #closestEdge do - newEdge:append(closestEdge[i]) + if closestVertex.ix ~= 1 and closestVertex.ix ~= #edge then + self.logger:debug('Graph entry/exit found and split at vertex %d, x: %.1f y: %.1f', + closestVertex.ix, closestVertex.x, closestVertex.y) + local newEdge = GraphPathfinder.GraphEdge(edge:getDirection()) + for j = closestVertex.ix, #edge do + newEdge:append(edge[j]) end newEdge:calculateProperties() table.insert(self.graph, newEdge) - closestEdge:cutEndAtIx(closestVertex.ix) + edge:cutEndAtIx(closestVertex.ix) + return newEdge + end + end + + -- find the edges closest to node. If the closest vertex isn't the first or last vertex of the edge, we split the + -- edge at that vertex so we can use it as an entry/exit point. + local function findClosestEdges(node, isEntry) + local closestEdges = {} + + local function addToClosestEdge(edge, vertex, d) + -- only add the edge if the vertex can be used as an entry/exit point + if edge:isBidirectional() or + (isEntry and vertex.ix == 1) or + (not isEntry and vertex.ix == #edge) then + table.insert(closestEdges, { d = d, edge = edge, vertex = vertex }) + end + end + + -- we'll be adding items to self.graph from within the loop, but that should be ok, because the # is evaluated + -- before the loop starts + for i = 1, #self.graph do + local edge = self.graph[i] + local vertex, d = edge:findClosestVertexToPoint(node) + local newEdge = splitEdgeWhenNeeded(edge, vertex) + if newEdge then + addToClosestEdge(newEdge, newEdge[1], d) + end + addToClosestEdge(edge, vertex, d) + end + table.sort(closestEdges, function(a, b) + return a.d < b.d + end) + return closestEdges + end + + local entryEdges, exitEdges = findClosestEdges(start, true), findClosestEdges(goal, false) + -- only use the edge if it is close enough to the closest + local maxDistance = entryEdges[1].d + self.range + for i = 1, math.min(#entryEdges, 2) do + if entryEdges[i].d <= maxDistance then + table.insert(self.graph, GraphPathfinder.HelperGraphEdge(GraphPathfinder.UNIDIRECTIONAL, { start, entryEdges[i].vertex })) + end + end + maxDistance = exitEdges[1].d + self.range + for i = 1, math.min(#exitEdges, 2) do + if exitEdges[i].d <= maxDistance then + table.insert(self.graph, GraphPathfinder.HelperGraphEdge(GraphPathfinder.UNIDIRECTIONAL, { exitEdges[i].vertex, goal })) end - return State3D(closestVertex.x, closestVertex.y, 0) end - local entry, exit = splitClosestEdge(start), splitClosestEdge(goal) - self.logger:debug('Graph entry at (%.1f, %.1f), exit at (%.1f, %.1f)', entry.x, entry.y, exit.x, exit.y) - return entry, exit + return State3D(entryEdges[1].vertex.x, entryEdges[1].vertex.y, 0, 0), + State3D(exitEdges[1].vertex.x, exitEdges[1].vertex.y, 0, 0) end --- Motion primitives to use with the graph pathfinder, providing the entries @@ -222,6 +294,7 @@ end --- the distance to the entry + the length of the edge function GraphPathfinder.GraphMotionPrimitives:getPrimitives(node) local primitives = {} + self.logger:trace('\tpredecessor: %.1f %.1f (%.1f)', node.x, node.y, node.g) for _, edge in ipairs(self.graph) do local entries = edge:getEntries() for _, entry in ipairs(entries) do diff --git a/scripts/pathfinder/HybridAStar.lua b/scripts/pathfinder/HybridAStar.lua index 997901468..87df50139 100644 --- a/scripts/pathfinder/HybridAStar.lua +++ b/scripts/pathfinder/HybridAStar.lua @@ -455,6 +455,7 @@ HybridAStar.defaultMaxIterations = 40000 ---@param maxIterations number ---@param mustBeAccurate boolean|nil function HybridAStar:init(vehicle, yieldAfter, maxIterations, mustBeAccurate) + self.logger = Logger('HybridAStar', Logger.level.error, CpDebug.DBG_PATHFINDER) self.vehicle = vehicle self.count = 0 self.yields = 0 @@ -585,7 +586,7 @@ function HybridAStar:run(start, goal, turnRadius, allowReverse, constraints, hit -- pop lowest cost node from queue ---@type State3D local pred = State3D.pop(self.openList) - --self:debug('pop %s', tostring(pred)) + self.logger:trace('pop %s', tostring(pred)) if pred:equals(self.goal, self.deltaPosGoal, self.deltaThetaGoal) then -- done! @@ -641,16 +642,16 @@ function HybridAStar:run(start, goal, turnRadius, allowReverse, constraints, hit analyticSolutionCost = analyticSolution:getLength(self.turnRadius) succ:updateH(self.goal, analyticSolutionCost) else - succ:updateH(self.goal, 0, succ:distance(self.goal) * 1.5) + succ:updateH(self.goal, 0, succ:distance(self.goal) * 1.0) end - --self:debug(' %s', tostring(succ)) + self.logger:trace(' %s', tostring(succ)) if existingSuccNode then - --self:debug(' existing node %s', tostring(existingSuccNode)) + self.logger:trace(' existing node %s', tostring(existingSuccNode)) -- there is already a node at this (discretized) position -- add a small number before comparing to adjust for floating point calculation differences if existingSuccNode:getCost() + 0.001 >= succ:getCost() then - --self:debug('%.6f replacing %s with %s', succ:getCost() - existingSuccNode:getCost(), tostring(existingSuccNode), tostring(succ)) + self.logger:trace('%.6f replacing %s with %s', succ:getCost() - existingSuccNode:getCost(), tostring(existingSuccNode), tostring(succ)) if self.openList:valueByPayload(existingSuccNode) then -- existing node is on open list already, remove it here, will replace with existingSuccNode:remove(self.openList) @@ -660,7 +661,7 @@ function HybridAStar:run(start, goal, turnRadius, allowReverse, constraints, hit -- add to open list succ:insert(self.openList) else - --self:debug('insert existing node back %s (iteration %d), diff %s', tostring(succ), self.iterations, tostring(succ:getCost() - existingSuccNode:getCost())) + self.logger:trace('insert existing node back %s (iteration %d), diff %s', tostring(succ), self.iterations, tostring(succ:getCost() - existingSuccNode:getCost())) end else -- successor cell does not yet exist @@ -669,13 +670,13 @@ function HybridAStar:run(start, goal, turnRadius, allowReverse, constraints, hit succ:insert(self.openList) end else - --self:debug('Invalid node %s (iteration %d)', tostring(succ), self.iterations) + self.logger:trace('Invalid node %s (iteration %d)', tostring(succ), self.iterations) succ:close() end -- valid node end end -- node as been expanded, close it to prevent expansion again - --self:debug(tostring(pred)) + self.logger:trace(tostring(pred)) pred:close() self.expansions = self.expansions + 1 end diff --git a/scripts/pathfinder/test/GraphPathfinderTest.lua b/scripts/pathfinder/test/GraphPathfinderTest.lua index 9065c5b32..8ea6aa8d2 100644 --- a/scripts/pathfinder/test/GraphPathfinderTest.lua +++ b/scripts/pathfinder/test/GraphPathfinderTest.lua @@ -106,13 +106,9 @@ function testBidirectional() done, path, _ = runPathfinder() lu.assertIsTrue(done) lu.assertEquals(#path, 3) - -- TODO: here, it should have taken the other path, over y = 105, as it is slightly shorter since both start and - -- goal are on y = 105, but since we reach the goal in a single step, - -- it just goes with the first one it finds. This isn't the hill we want to die on, so for now, - -- we will just accept this behavior. - path[1]:assertAlmostEquals(Vector(120, 100)) - path[2]:assertAlmostEquals(Vector(110, 100)) - path[#path]:assertAlmostEquals(Vector(100, 100)) + path[1]:assertAlmostEquals(Vector(120, 105)) + path[2]:assertAlmostEquals(Vector(110, 105)) + path[#path]:assertAlmostEquals(Vector(100, 105)) end function testShorterPath() @@ -168,6 +164,7 @@ function testRange() start = State3D(90, 105, 0, 0) goal = State3D(150, 105, 0, 0) done, path, _ = runPathfinder() + printPath() lu.assertIsTrue(done) lu.assertEquals(#path, 6) path[1]:assertAlmostEquals(Vector(100, 100)) @@ -324,4 +321,44 @@ function testGoalWithinRange() lu.assertIsNil(path) end +function testTwoWayStreet() + local graph = { + -- lane to the right, closer to the start location + GraphEdge(GraphEdge.UNIDIRECTIONAL, + { + Vertex(-100, 10), + Vertex(0, 10), + Vertex(100, 10) + }), + -- lane to the left, shortest way to the goal + GraphEdge(GraphEdge.UNIDIRECTIONAL, + { + Vertex(100, 15), -- 15 here so we can traverse from the other lane to this at x=100 + Vertex(0, 20), + Vertex(-100, 20) + }), + } + -- Range is 5, so we won't turn right, but take the longer path, to the left, make a U turn and drive back on + -- the lane to the left + pathfinder = GraphPathfinder(math.huge, 500, 5, graph) + start = State3D(0, 0, 0, 0) + -- goal on the left + goal = State3D(-120, 10, 0, 0) + done, path, _ = runPathfinder() + lu.assertIsTrue(done) + lu.assertEquals(#path, 5) + path[1]:assertAlmostEquals(Vector(0, 10)) + path[2]:assertAlmostEquals(Vector(100, 10)) + path[3]:assertAlmostEquals(Vector(100, 15)) + path[4]:assertAlmostEquals(Vector(0, 20)) + path[#path]:assertAlmostEquals(Vector(-100, 20)) + -- with the bigger range, we should turn left, taking the shortest path + pathfinder = GraphPathfinder(math.huge, 500, 10, graph) + done, path, _ = runPathfinder() + lu.assertIsTrue(done) + lu.assertEquals(#path, 2) + path[1]:assertAlmostEquals(Vector(0, 20)) + path[#path]:assertAlmostEquals(Vector(-100, 20)) +end + os.exit(lu.LuaUnit.run()) From d26e4ca83f046a08b3b54054e795965987d1e24b Mon Sep 17 00:00:00 2001 From: David Schwietering Date: Fri, 28 Mar 2025 13:46:48 +0100 Subject: [PATCH 34/73] Started adding street boiler plate ... --- modDesc.xml | 4 +- scripts/ai/jobs/CpAIJobStreet.lua | 85 +++++++++ scripts/ai/jobs/CpJobParameters.lua | 9 + scripts/gui/hud/CpBaseHud.lua | 5 +- scripts/gui/hud/CpStreetWorkerHudPage.lua | 154 ++++++++++++++++ scripts/specializations/CpAIStreetWorker.lua | 176 +++++++++++++++++++ 6 files changed, 431 insertions(+), 2 deletions(-) create mode 100644 scripts/ai/jobs/CpAIJobStreet.lua create mode 100644 scripts/gui/hud/CpStreetWorkerHudPage.lua create mode 100644 scripts/specializations/CpAIStreetWorker.lua diff --git a/modDesc.xml b/modDesc.xml index 6542621be..ccecd483c 100644 --- a/modDesc.xml +++ b/modDesc.xml @@ -267,6 +267,7 @@ Changelog 8.0.0.0: + @@ -305,6 +306,7 @@ Changelog 8.0.0.0: + @@ -360,7 +362,7 @@ Changelog 8.0.0.0: - + diff --git a/scripts/ai/jobs/CpAIJobStreet.lua b/scripts/ai/jobs/CpAIJobStreet.lua new file mode 100644 index 000000000..3149a66a0 --- /dev/null +++ b/scripts/ai/jobs/CpAIJobStreet.lua @@ -0,0 +1,85 @@ +--- Street job. +---@class CpAIJobStreet : CpAIJobFieldWork +---@field selectedFieldPlot FieldPlot +CpAIJobStreet = CpObject(CpAIJob) +CpAIJobStreet.name = "STREET_WORKER_CP" +CpAIJobStreet.jobName = "CP_job_street" +function CpAIJobStreet:init(isServer) + CpAIJob.init(self, isServer) + +end + +function CpAIJobStreet:setupTasks(isServer) + -- CpAIJob.setupTasks(self, isServer) + +end + +function CpAIJobStreet:onPreStart() + --- TODO +end + + +function CpAIJobStreet:setupJobParameters() + CpAIJob.setupJobParameters(self) + self:setupCpJobParameters(CpStreetJobParameters(self)) +end + +function CpAIJobStreet:getIsAvailableForVehicle(vehicle, cpJobsAllowed) + return CpAIJob.getIsAvailableForVehicle(self, vehicle, cpJobsAllowed) +end + +function CpAIJobStreet:getCanStartJob() + return true +end + +function CpAIJobStreet:applyCurrentState(vehicle, mission, farmId, isDirectStart, isStartPositionInvalid) + CpAIJob.applyCurrentState(self, vehicle, mission, farmId, isDirectStart) + self.cpJobParameters:validateSettings() + + -- self:copyFrom(vehicle:getCpBaleFinderJob()) + +end + +function CpAIJobStreet:setValues() + CpAIJob.setValues(self) + local vehicle = self.vehicleParameter:getVehicle() + +end + +--- Called when parameters change, scan field +function CpAIJobStreet:validate(farmId) + local isValid, isRunning, errorMessage = CpAIJob.validate(self, farmId) + if not isValid then + return isValid, errorMessage + end + local vehicle = self.vehicleParameter:getVehicle() + if vehicle then + -- vehicle:applyCpBaleFinderJobParameters(self) + end + + return isValid or isRunning, errorMessage +end + +function CpAIJobStreet:draw(map, isOverviewMap) + CpAIJob.draw(self, map, isOverviewMap) + -- if not isOverviewMap then + -- self.selectedFieldPlot:draw(map) + -- end +end + +--- Gets the additional task description shown. +function CpAIJobStreet:getDescription() + local desc = CpAIJob.getDescription(self) + -- local currentTask = self:getTaskByIndex(self.currentTaskIndex) + -- if currentTask == self.driveToTask then + -- desc = desc .. " - " .. g_i18n:getText("ai_taskDescriptionDriveToField") + -- elseif currentTask == self.baleFinderTask then + -- local vehicle = self:getVehicle() + -- if vehicle and AIUtil.hasChildVehicleWithSpecialization(vehicle, BaleWrapper) then + -- desc = desc .. " - " .. g_i18n:getText("CP_ai_taskDescriptionWrapsBales") + -- else + -- desc = desc .. " - " .. g_i18n:getText("CP_ai_taskDescriptionCollectsBales") + -- end + -- end + return desc +end diff --git a/scripts/ai/jobs/CpJobParameters.lua b/scripts/ai/jobs/CpJobParameters.lua index 9599b77ce..62ddf0d5c 100644 --- a/scripts/ai/jobs/CpJobParameters.lua +++ b/scripts/ai/jobs/CpJobParameters.lua @@ -417,4 +417,13 @@ function CpSiloLoaderJobParameters:generateUnloadingStations(setting, oldIx) end end return unloadingStationIds, texts, oldIx +end + +--- AI parameters for the street driving job. +---@class CpStreetJobParameters : CpJobParameters +CpStreetJobParameters = CpObject(CpJobParameters) + +function CpStreetJobParameters:init(job) + CpJobParameters.init(self, job, + CpStreetJobParameters, "StreetJobParameterSetup.xml") end \ No newline at end of file diff --git a/scripts/gui/hud/CpBaseHud.lua b/scripts/gui/hud/CpBaseHud.lua index 6d4f57ac1..62e7113a2 100644 --- a/scripts/gui/hud/CpBaseHud.lua +++ b/scripts/gui/hud/CpBaseHud.lua @@ -127,6 +127,7 @@ function CpBaseHud:init(vehicle) self.siloLoaderWorkerLayout = self:addHudPage(CpSiloLoaderWorkerHudPageElement, vehicle) + self.streetWorkerLayout = self:addHudPage(CpStreetWorkerHudPageElement, vehicle) -------------------------------------- --- Header -------------------------------------- @@ -473,7 +474,7 @@ function CpBaseHud:getActiveHudPage(vehicle) elseif vehicle:cpIsHudUnloaderJobSelected() then return self.combineUnloaderLayout elseif vehicle:cpIsHudStreetJobSelected() then - -- return self.fieldworkLayout + return self.streetWorkerLayout end end @@ -530,6 +531,8 @@ function CpBaseHud:updateContent(vehicle, status) self.bunkerSiloWorkerLayout:setDisabled(true) self.siloLoaderWorkerLayout:setVisible(false) self.siloLoaderWorkerLayout:setDisabled(true) + self.streetWorkerLayout:setVisible(false) + self.streetWorkerLayout:setDisabled(true) local activeLayout = self:getActiveHudPage(vehicle) if activeLayout then diff --git a/scripts/gui/hud/CpStreetWorkerHudPage.lua b/scripts/gui/hud/CpStreetWorkerHudPage.lua new file mode 100644 index 000000000..ee3e40547 --- /dev/null +++ b/scripts/gui/hud/CpStreetWorkerHudPage.lua @@ -0,0 +1,154 @@ +--- Fieldwork Hud page +---@class CpStreetWorkerHudPageElement : CpHudElement +CpStreetWorkerHudPageElement = {} +local CpStreetWorkerHudPageElement_mt = Class(CpStreetWorkerHudPageElement, CpHudPageElement) + +function CpStreetWorkerHudPageElement.new(overlay, parentHudElement, customMt) + local self = CpHudPageElement.new(overlay, parentHudElement, customMt or CpStreetWorkerHudPageElement_mt) + return self +end + +function CpStreetWorkerHudPageElement:setupElements(baseHud, vehicle, lines, wMargin, hMargin) + + -- local x, y = unpack(lines[5].left) + -- self.loadMultipleFillTypesSetting = CpTextHudElement.new(self, + -- x, y, baseHud.defaultFontSize) + -- x, y = unpack(lines[4].right) + -- local max = CpTextHudElement.new(self, + -- x , y, baseHud.defaultFontSize, RenderText.ALIGN_RIGHT) + -- max:setDisabled(true) + -- max:setTextDetails("max") + -- x = x - max:getTextWidth("100%") - wMargin/2 + -- local min = CpTextHudElement.new(self, + -- x, y, baseHud.defaultFontSize, RenderText.ALIGN_RIGHT) + -- min:setDisabled(true) + -- min:setTextDetails("min") + -- x = x - min:getTextWidth("100%") - wMargin/2 + -- local counter = CpTextHudElement.new(self, + -- x, y, baseHud.defaultFontSize, RenderText.ALIGN_RIGHT) + -- counter:setDisabled(true) + -- counter:setTextDetails("count") + + -- self.fillTypeSettings = { + -- self:addFillTypeSelection(3, baseHud, vehicle, + -- lines, wMargin, hMargin), + -- self:addFillTypeSelection(2, baseHud, vehicle, + -- lines, wMargin, hMargin), + -- self:addFillTypeSelection(1, baseHud, vehicle, + -- lines, wMargin, hMargin) + -- } + + -- CpGuiUtil.addCopyAndPasteButtons(self, baseHud, + -- vehicle, lines, wMargin, hMargin, 1) + + -- self.copyButton:setCallback("onClickPrimary", vehicle, function (vehicle) + -- if not CpBaseHud.copyPasteCache.hasVehicle and vehicle.getCpStreetWorkerJob then + -- CpBaseHud.copyPasteCache.streetWorkerVehicle = vehicle + -- CpBaseHud.copyPasteCache.hasVehicle = true + -- end + -- end) + + + -- self.pasteButton:setCallback("onClickPrimary", vehicle, function (vehicle) + -- if CpBaseHud.copyPasteCache.hasVehicle and not vehicle:getIsCpActive() then + -- if CpBaseHud.copyPasteCache.streetWorkerVehicle then + -- vehicle:applyCpStreetWorkerJobParameters(CpBaseHud.copyPasteCache.streetWorkerVehicle:getCpStreetWorkerJob()) + -- end + -- end + -- end) + + -- self.clearCacheBtn:setCallback("onClickPrimary", vehicle, function (vehicle) + -- CpBaseHud.copyPasteCache.hasVehicle = false + -- CpBaseHud.copyPasteCache.streetWorkerVehicle = nil + -- end) +end + +function CpStreetWorkerHudPageElement:addFillTypeSelection(line, baseHud, vehicle, lines, wMargin, hMargin) + --- Fill type + local x, y = unpack(lines[line].left) + local fillType = CpTextHudElement.new(self, + x , y, baseHud.defaultFontSize) + fillType:setDisabled(true) + x, y = unpack(lines[line].right) + local max = CpTextHudElement.new(self, + x , y, baseHud.defaultFontSize, RenderText.ALIGN_RIGHT) + max:setDisabled(true) + x = x - max:getTextWidth("100%") - wMargin/2 + local min = CpTextHudElement.new(self, + x , y, baseHud.defaultFontSize, RenderText.ALIGN_RIGHT) + min:setDisabled(true) + x = x - min:getTextWidth("100%") - wMargin/2 + local counter = CpTextHudElement.new(self, + x , y, baseHud.defaultFontSize, RenderText.ALIGN_RIGHT) + counter:setDisabled(true) + return { + fillType = fillType, + min = min, + max = max, + counter = counter + } +end + +function CpStreetWorkerHudPageElement:update(dt) + CpStreetWorkerHudPageElement:superClass().update(self, dt) + +end + +---@param vehicle table +---@param status CpStatus +function CpStreetWorkerHudPageElement:updateContent(vehicle, status) + -- local workWidth = vehicle:getCpSettings().bunkerSiloWorkWidth + -- self.workWidthBtn:setTextDetails(workWidth:getTitle(), workWidth:getString()) + -- self.workWidthBtn:setVisible(workWidth:getIsVisible()) + + -- local loadingHeightOffset = vehicle:getCpSettings().loadingShovelHeightOffset + -- self.loadingShovelHeightOffsetBtn:setTextDetails(loadingHeightOffset:getTitle(), loadingHeightOffset:getString()) + -- self.loadingShovelHeightOffsetBtn:setVisible(loadingHeightOffset:getIsVisible()) + -- self.loadingShovelHeightOffsetBtn:setDisabled(loadingHeightOffset:getIsDisabled()) + + -- self.fillLevelProgressText:setTextDetails(status:getSiloFillLevelPercentageLeftOver()) + -- local jobParameters = vehicle:getCpStreetWorkerJob():getCpJobParameters() + -- --self.loadMultipleFillTypesSetting:setTextDetails(jobParameters.loadingMultipleFruitTypesAllowed:getString()) + -- for i, setting in ipairs(jobParameters:getFillTypeSelectionSettings()) do + -- if self.fillTypeSettings[i] and setting.fillType:getValue() > FillType.UNKNOWN then + -- self.fillTypeSettings[i].fillType:setTextDetails(setting.fillType:getString()) + -- self.fillTypeSettings[i].min:setTextDetails(setting.minFillLevel:getString()) + -- self.fillTypeSettings[i].max:setTextDetails(setting.maxFillLevel:getString()) + -- local counter = string.format("%d/%d", + -- setting:getCounter(), setting.counter:getValue()) + -- self.fillTypeSettings[i].counter:setTextDetails(counter) + -- end + -- end + + --- Update copy and paste buttons + -- self:updateCopyButtons(vehicle) +end + + +--- Updates the copy, paste and clear buttons. +function CpStreetWorkerHudPageElement:updateCopyButtons(vehicle) + if CpBaseHud.copyPasteCache.hasVehicle then + self.clearCacheBtn:setVisible(true) + self.pasteButton:setVisible(true) + self.copyButton:setVisible(false) + local copyCacheVehicle = CpBaseHud.copyPasteCache.streetWorkerVehicle + local text = CpUtil.getName(copyCacheVehicle) + self.copyCacheText:setTextDetails(text) + self.copyCacheText:setTextColorChannels(unpack(CpBaseHud.OFF_COLOR)) + self.pasteButton:setColor(unpack(CpBaseHud.OFF_COLOR)) + self.pasteButton:setDisabled(true) + if copyCacheVehicle == vehicle or vehicle:getIsCpActive() then + --- Paste disabled + return + end + self.copyCacheText:setTextColorChannels(unpack(CpBaseHud.WHITE_COLOR)) + self.pasteButton:setColor(unpack(CpBaseHud.ON_COLOR)) + self.pasteButton:setDisabled(false) + + else + self.copyCacheText:setTextDetails("") + self.clearCacheBtn:setVisible(false) + self.pasteButton:setVisible(false) + self.copyButton:setVisible(true) + end +end \ No newline at end of file diff --git a/scripts/specializations/CpAIStreetWorker.lua b/scripts/specializations/CpAIStreetWorker.lua new file mode 100644 index 000000000..069adb328 --- /dev/null +++ b/scripts/specializations/CpAIStreetWorker.lua @@ -0,0 +1,176 @@ +--- This spec is only for overwriting giants function of the AIFieldWorker. +local modName = CpAIStreetWorker and CpAIStreetWorker.MOD_NAME -- for reload + +---@class CpAIStreetWorker +CpAIStreetWorker = {} + +CpAIStreetWorker.startText = g_i18n:getText("CP_fieldWorkJobParameters_startAt_street") + +CpAIStreetWorker.MOD_NAME = g_currentModName or modName +CpAIStreetWorker.NAME = ".cpAIStreetWorker" +CpAIStreetWorker.SPEC_NAME = CpAIStreetWorker.MOD_NAME .. CpAIStreetWorker.NAME +CpAIStreetWorker.KEY = "."..CpAIStreetWorker.MOD_NAME..CpAIStreetWorker.NAME + +function CpAIStreetWorker.initSpecialization() + local schema = Vehicle.xmlSchemaSavegame + local key = "vehicles.vehicle(?)" .. CpAIStreetWorker.KEY + CpJobParameters.registerXmlSchema(schema, key..".cpJob") +end + +function CpAIStreetWorker.prerequisitesPresent(specializations) + return SpecializationUtil.hasSpecialization(CpAIWorker, specializations) +end + +function CpAIStreetWorker.register(typeManager,typeName,specializations) + if CpAIStreetWorker.prerequisitesPresent(specializations) then + typeManager:addSpecialization(typeName, CpAIStreetWorker.SPEC_NAME) + end +end + +function CpAIStreetWorker.registerEventListeners(vehicleType) + SpecializationUtil.registerEventListener(vehicleType, 'onLoad', CpAIStreetWorker) + SpecializationUtil.registerEventListener(vehicleType, 'onLoadFinished', CpAIStreetWorker) + SpecializationUtil.registerEventListener(vehicleType, 'onReadStream', CpAIStreetWorker) + SpecializationUtil.registerEventListener(vehicleType, 'onWriteStream', CpAIStreetWorker) + + SpecializationUtil.registerEventListener(vehicleType, 'onCpADStartedByPlayer', CpAIStreetWorker) + SpecializationUtil.registerEventListener(vehicleType, 'onCpADRestarted', CpAIStreetWorker) +end + +function CpAIStreetWorker.registerFunctions(vehicleType) + SpecializationUtil.registerFunction(vehicleType, "getCanStartCpStreetWorker", CpAIStreetWorker.getCanStartCpStreetWorker) + SpecializationUtil.registerFunction(vehicleType, "getCpStreetWorkerJobParameters", CpAIStreetWorker.getCpStreetWorkerJobParameters) + SpecializationUtil.registerFunction(vehicleType, "getCpStreetWorkerJob", CpAIStreetWorker.getCpStreetWorkerJob) + SpecializationUtil.registerFunction(vehicleType, "applyCpStreetWorkerJobParameters", CpAIStreetWorker.applyCpStreetWorkerJobParameters) + +end + +function CpAIStreetWorker.registerOverwrittenFunctions(vehicleType) + SpecializationUtil.registerOverwrittenFunction(vehicleType, 'getCanStartCp', CpAIStreetWorker.getCanStartCp) + SpecializationUtil.registerOverwrittenFunction(vehicleType, 'getCpStartableJob', CpAIStreetWorker.getCpStartableJob) + + SpecializationUtil.registerOverwrittenFunction(vehicleType, 'startCpAtFirstWp', CpAIStreetWorker.startCpAtFirstWp) + SpecializationUtil.registerOverwrittenFunction(vehicleType, 'startCpAtLastWp', CpAIStreetWorker.startCpAtLastWp) +end + +function CpAIStreetWorker.registerEvents(vehicleType) + -- SpecializationUtil.registerEvent(vehicleType, "onCpWrapTypeSettingChanged") +end + +------------------------------------------------------------------------------------------------------------------------ +--- Event listeners +--------------------------------------------------------------------------------------------------------------------------- +function CpAIStreetWorker:onLoad(savegame) + --- Register the spec: spec_CpAIStreetWorker + self.spec_CpAIStreetWorker = self["spec_" .. CpAIStreetWorker.SPEC_NAME] + local spec = self.spec_CpAIStreetWorker + --- This job is for starting the driving with a key bind or the mini gui. + spec.cpJob = g_currentMission.aiJobTypeManager:createJob(AIJobType.STREET_WORKER_CP) + spec.cpJob:setVehicle(self, true) +end + +function CpAIStreetWorker:onLoadFinished(savegame) + local spec = self.spec_CpAIStreetWorker + if savegame ~= nil then + spec.cpJob:getCpJobParameters():loadFromXMLFile(savegame.xmlFile, savegame.key.. CpAIStreetWorker.KEY..".cpJob") + end +end + +function CpAIStreetWorker:saveToXMLFile(xmlFile, baseKey, usedModNames) + local spec = self.spec_CpAIStreetWorker + spec.cpJob:getCpJobParameters():saveToXMLFile(xmlFile, baseKey.. ".cpJob") +end + +function CpAIStreetWorker:onReadStream(streamId, connection) + local spec = self.spec_CpAIStreetWorker + spec.cpJob:readStream(streamId, connection) +end + +function CpAIStreetWorker:onWriteStream(streamId, connection) + local spec = self.spec_CpAIStreetWorker + spec.cpJob:writeStream(streamId, connection) +end + +function CpAIStreetWorker:getCpStreetWorkerJobParameters() + local spec = self.spec_CpAIStreetWorker + return spec.cpJob:getCpJobParameters() +end + +function CpAIStreetWorker:getCpStreetWorkerJob() + local spec = self.spec_CpAIStreetWorker + return spec.cpJob +end + + +function CpAIStreetWorker:applyCpStreetWorkerJobParameters(job) + local spec = self.spec_CpAIStreetWorker + spec.cpJob:getCpJobParameters():validateSettings() + spec.cpJob:copyFrom(job) +end + +--- Is the bale finder allowed? +function CpAIStreetWorker:getCanStartCpStreetWorker() + return true +end + +function CpAIStreetWorker:getCanStartCp(superFunc) + return superFunc(self) or self:getCanStartCpStreetWorker() and not self:getIsCpCourseRecorderActive() +end + +--- Only use the bale finder, if the cp field work job is not possible. +function CpAIStreetWorker:getCpStartableJob(superFunc, isStartedByHud) + local spec = self.spec_CpAIStreetWorker + if isStartedByHud and self:cpIsHudStreetWorkerJobSelected() then + return self:getCanStartCpStreetWorker() and spec.cpJob + end + return superFunc(self, isStartedByHud) or not isStartedByHud and self:getCanStartCpStreetWorker() and spec.cpJob +end + +--- Starts the cp driver at the first waypoint. +function CpAIStreetWorker:startCpAtFirstWp(superFunc) + -- if not superFunc(self) then + -- if self:getCanStartCpStreetWorker() then + -- local spec = self.spec_CpAIStreetWorker + -- --- Applies the bale wrap type set in the hud, so ad can start with the correct type. + -- --- TODO: This should only be applied, if the driver was started for the first time by ad and not every time. + -- spec.cpJobStartAtLastWp:getCpJobParameters().baleWrapType:setValue(spec.cpJob:getCpJobParameters().baleWrapType:getValue()) + -- spec.cpJob:applyCurrentState(self, g_currentMission, g_currentMission.playerSystem:getLocalPlayer().farmId, true) + -- spec.cpJob:setValues() + -- local success = spec.cpJob:validate(false) + -- if success then + -- g_client:getServerConnection():sendEvent(AIJobStartRequestEvent.new(spec.cpJob, self:getOwnerFarmId())) + -- return true + -- end + -- end + -- else + -- return true + -- end +end + +--- Starts the cp driver at the last driven waypoint. +function CpAIStreetWorker:startCpAtLastWp(superFunc) + -- if not superFunc(self) then + -- if self:getCanStartCpStreetWorker() then + -- local spec = self.spec_CpAIStreetWorker + -- spec.cpJobStartAtLastWp:applyCurrentState(self, g_currentMission, g_currentMission.playerSystem:getLocalPlayer().farmId, true) + -- spec.cpJobStartAtLastWp:setValues() + -- local success = spec.cpJobStartAtLastWp:validate(false) + -- if success then + -- g_client:getServerConnection():sendEvent(AIJobStartRequestEvent.new(spec.cpJobStartAtLastWp, self:getOwnerFarmId())) + -- return true + -- end + -- end + -- else + -- return true + -- end +end + +function CpAIStreetWorker:onCpADStartedByPlayer() + local spec = self.spec_CpAIStreetWorker + --- Applies the bale wrap type set in the hud, so ad can start with the correct type. + spec.cpJobStartAtLastWp:getCpJobParameters().baleWrapType:setValue(spec.cpJob:getCpJobParameters().baleWrapType:getValue()) +end + +function CpAIStreetWorker:onCpADRestarted() + +end From 5e9be3942f9d9f2587725b42d7d30748a66e2dd8 Mon Sep 17 00:00:00 2001 From: David Schwietering Date: Fri, 28 Mar 2025 16:31:38 +0100 Subject: [PATCH 35/73] WIP Some more boiler plate and fixes --- config/HudSettingsSetup.xml | 4 +--- .../jobParameters/StreetJobParameterSetup.xml | 5 +++++ scripts/ai/jobs/CpAIJob.lua | 19 +++++++++++-------- scripts/ai/jobs/CpAIJobStreet.lua | 3 +-- 4 files changed, 18 insertions(+), 13 deletions(-) create mode 100644 config/jobParameters/StreetJobParameterSetup.xml diff --git a/config/HudSettingsSetup.xml b/config/HudSettingsSetup.xml index db6293fd6..d3678b831 100644 --- a/config/HudSettingsSetup.xml +++ b/config/HudSettingsSetup.xml @@ -20,9 +20,7 @@ 5 - CP_job_fieldWork @@ -30,7 +28,7 @@ CP_job_bunkerSilo CP_job_siloLoader CP_job_combineUnload - + CP_job_street diff --git a/config/jobParameters/StreetJobParameterSetup.xml b/config/jobParameters/StreetJobParameterSetup.xml new file mode 100644 index 000000000..e7c1685f5 --- /dev/null +++ b/config/jobParameters/StreetJobParameterSetup.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/scripts/ai/jobs/CpAIJob.lua b/scripts/ai/jobs/CpAIJob.lua index 72fa0eca9..21ca1e20b 100644 --- a/scripts/ai/jobs/CpAIJob.lua +++ b/scripts/ai/jobs/CpAIJob.lua @@ -222,14 +222,16 @@ function CpAIJob:setValues() local vehicle = self.vehicleParameter:getVehicle() - self.driveToTask:setVehicle(vehicle) - - local angle = self.cpJobParameters.startPosition:getAngle() - local x, z = self.cpJobParameters.startPosition:getPosition() - if angle ~= nil and x ~= nil then - local dirX, dirZ = MathUtil.getDirectionFromYRotation(angle) - self.driveToTask:setTargetDirection(dirX, dirZ) - self.driveToTask:setTargetPosition(x, z) + if self.driveToTask then + self.driveToTask:setVehicle(vehicle) + + local angle = self.cpJobParameters.startPosition:getAngle() + local x, z = self.cpJobParameters.startPosition:getPosition() + if angle ~= nil and x ~= nil then + local dirX, dirZ = MathUtil.getDirectionFromYRotation(angle) + self.driveToTask:setTargetDirection(dirX, dirZ) + self.driveToTask:setTargetPosition(x, z) + end end end @@ -525,5 +527,6 @@ function CpAIJob.registerJob(aiJobTypeManager) register(CpAIJobCombineUnloader) register(CpAIJobSiloLoader) register(CpAIJobBunkerSilo) + register(CpAIJobStreet) end diff --git a/scripts/ai/jobs/CpAIJobStreet.lua b/scripts/ai/jobs/CpAIJobStreet.lua index 3149a66a0..9f7d8cc05 100644 --- a/scripts/ai/jobs/CpAIJobStreet.lua +++ b/scripts/ai/jobs/CpAIJobStreet.lua @@ -1,6 +1,5 @@ --- Street job. ----@class CpAIJobStreet : CpAIJobFieldWork ----@field selectedFieldPlot FieldPlot +---@class CpAIJobStreet : CpAIJob CpAIJobStreet = CpObject(CpAIJob) CpAIJobStreet.name = "STREET_WORKER_CP" CpAIJobStreet.jobName = "CP_job_street" From c83d7f03cfc7bc801cbc8fd9e5dc608280804add Mon Sep 17 00:00:00 2001 From: David Schwietering Date: Sun, 30 Mar 2025 13:51:27 +0200 Subject: [PATCH 36/73] WIP Some more boilerplate --- .../FillTypeSelectionParameterSetup.xml | 9 ++ .../jobParameters/StreetJobParameterSetup.xml | 29 +++- modDesc.xml | 2 + scripts/ai/jobs/CpJobParameters.lua | 108 +++++++++++++ scripts/ai/parameters/AIParameterSetting.lua | 25 ++- .../CpAIParameterFillTypeSelection.lua | 150 ++++++++++++++++++ .../parameters/CpAIParameterTargetPoint.lua | 16 ++ scripts/graph/Graph.lua | 4 + scripts/graph/GraphTarget.lua | 11 ++ 9 files changed, 348 insertions(+), 6 deletions(-) create mode 100644 config/jobParameters/FillTypeSelectionParameterSetup.xml create mode 100644 scripts/ai/parameters/CpAIParameterFillTypeSelection.lua create mode 100644 scripts/ai/parameters/CpAIParameterTargetPoint.lua diff --git a/config/jobParameters/FillTypeSelectionParameterSetup.xml b/config/jobParameters/FillTypeSelectionParameterSetup.xml new file mode 100644 index 000000000..4734b8b3a --- /dev/null +++ b/config/jobParameters/FillTypeSelectionParameterSetup.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/config/jobParameters/StreetJobParameterSetup.xml b/config/jobParameters/StreetJobParameterSetup.xml index e7c1685f5..ecd7c9ef3 100644 --- a/config/jobParameters/StreetJobParameterSetup.xml +++ b/config/jobParameters/StreetJobParameterSetup.xml @@ -1,5 +1,32 @@ - + + + + 0 + 1 + 2 + + + driveTo + unloadAtTarget + loadAndUnload + + + + + + + + + + + + + + + + + diff --git a/modDesc.xml b/modDesc.xml index ccecd483c..8baa767db 100644 --- a/modDesc.xml +++ b/modDesc.xml @@ -259,6 +259,8 @@ Changelog 8.0.0.0: + + diff --git a/scripts/ai/jobs/CpJobParameters.lua b/scripts/ai/jobs/CpJobParameters.lua index 62ddf0d5c..da3b0112d 100644 --- a/scripts/ai/jobs/CpJobParameters.lua +++ b/scripts/ai/jobs/CpJobParameters.lua @@ -141,6 +141,32 @@ function CpJobParameters:isCpActive() return self.job:getVehicle() and self.job:getVehicle():getIsCpActive() end +--- Crawls through the parameters and collects all CpAIParameterFillTypeSetting settings. +---@return table +function CpJobParameters:getFillTypeSelectionSettings() + local parameters = {} + for i, setting in ipairs(self.settings) do + if setting:is_a(CpAIParameterFillTypeSetting) then + table.insert(parameters, setting) + end + end + return parameters +end + +function CpJobParameters:generateTargets() + local targets = g_graph:getAllTargets() + local values, texts = {}, {} + for _, t in ipairs(targets) do + table.insert(values, t:getUniqueID()) + table.insert(texts, t:getName()) + end + if #values <=0 then + table.insert(values, -1) + table.insert(texts, "---") + end + return values, texts +end + ---@class CpFieldWorkJobParameters : CpJobParameters CpFieldWorkJobParameters = CpObject(CpJobParameters) function CpFieldWorkJobParameters:init(job) @@ -426,4 +452,86 @@ CpStreetJobParameters = CpObject(CpJobParameters) function CpStreetJobParameters:init(job) CpJobParameters.init(self, job, CpStreetJobParameters, "StreetJobParameterSetup.xml") +end + +function CpStreetJobParameters:hasNoValidTrailerAttached() + local vehicle = self.job:getVehicle() + if vehicle then + return not AIUtil.hasChildVehicleWithSpecialization(vehicle, Dischargeable) + or not AIUtil.hasChildVehicleWithSpecialization(vehicle, Trailer) + end + return false +end + +function CpStreetJobParameters:isUnloadTargetPointDisabled() + return self:hasNoValidTrailerAttached() or self.loadUnloadTargetMode:getValue() == CpStreetJobParameters.DRIVE_TO +end + +function CpStreetJobParameters:isLoadTargetPointDisabled() + return self:hasNoValidTrailerAttached() or self.loadUnloadTargetMode:getValue() ~= CpStreetJobParameters.LOAD_AND_UNLOAD +end + +function CpStreetJobParameters:isRunCounterDisabled() + return false +end +function CpStreetJobParameters:generateFillTypes() + local fillTypes = {} + local texts = {} + -- local vehicle = self.job and self.job:getVehicle() + -- if vehicle then + -- fillTypes = AIUtil.getAllValidFillTypes(vehicle, function() + -- return true + -- end) + -- for _, f in pairs(fillTypes) do + -- table.insert(texts, g_fillTypeManager:getFillTypeTitleByIndex(f)) + -- end + -- else + -- for ix, _ in pairs(g_fillTypeManager:getFillTypes()) do + -- if ix ~= FillType.UNKNOWN then + -- table.insert(fillTypes, ix) + -- table.insert(texts, g_fillTypeManager:getFillTypeTitleByIndex(f)) + -- end + -- end + -- end + table.insert(fillTypes, 1, -1) + table.insert(texts, 1, "---") + return fillTypes, texts +end + +---@param setting CpAIParameterTargetPoint +function CpStreetJobParameters:onChangeTargetPoints(setting) + -- if setting:getIsDisabled() then + -- return + -- end + -- local vehicle = self.job:getVehicle() + -- if not vehicle then + -- return + -- end + -- local startId + -- if self:isLoadTargetPointDisabled() then + -- g_graphCourseManager:generateCourseFromVehicleToStart(vehicle, + -- function(toUnloadCourse) + -- if self:isLoadTargetPointDisabled() then + -- self.job:onCourseGenerated(setting, toUnloadCourse, nil) + -- else + -- g_graphCourseManager:generateCourseBetweenPoints(vehicle, + -- function(fromUnloadCourse) + -- self.job:onCourseGenerated(setting, toUnloadCourse, fromUnloadCourse) + -- end, self.unloadTargetPoint:getValue(), startId) + -- end + -- end, self.unloadTargetPoint:getValue()) + -- else + -- startId = self.loadTargetPoint:getValue() + -- g_graphCourseManager:generateCourseBetweenPoints(vehicle, + -- function(toUnloadCourse) + -- if self:isLoadTargetPointDisabled() then + -- self.job:onCourseGenerated(setting, toUnloadCourse, nil) + -- else + -- g_graphCourseManager:generateCourseBetweenPoints(vehicle, + -- function(fromUnloadCourse) + -- self.job:onCourseGenerated(setting, toUnloadCourse, fromUnloadCourse) + -- end, self.unloadTargetPoint:getValue(), startId) + -- end + -- end, startId, self.unloadTargetPoint:getValue()) + -- end end \ No newline at end of file diff --git a/scripts/ai/parameters/AIParameterSetting.lua b/scripts/ai/parameters/AIParameterSetting.lua index 1fa7e5c04..f1004ff32 100644 --- a/scripts/ai/parameters/AIParameterSetting.lua +++ b/scripts/ai/parameters/AIParameterSetting.lua @@ -2,19 +2,20 @@ ---@class AIParameterSetting : AIParameterSettingInterface AIParameterSetting = CpObject(AIParameterSettingInterface) -function AIParameterSetting:init(name) +function AIParameterSetting:init(data, vehicle, class) AIParameterSettingInterface.init(self) - self.data = nil - self.vehicle = nil - self.class = nil + self.data = data + self.vehicle = vehicle + self.class = class - self.name = name + self.name = "" self.title = "" self.tooltip = "" self.isValid = true self.guiParameterType = nil + self.parent = nil end --- Initialize the setting from config data supplied in CpSettingsUtil @@ -67,10 +68,21 @@ function AIParameterSetting:setIsValid(isValid) self.isValid = isValid end +function AIParameterSetting:setParent(parent) + self.parent = parent +end + +function AIParameterSetting:setClass(class) + self.class = class +end + function AIParameterSetting:getIsDisabled() if self:hasCallback(self.data.isDisabledFunc) then return self:getCallback(self.data.isDisabledFunc) end + if self.parent and self.parent:getIsDisabled() then + return true + end return false end @@ -87,6 +99,9 @@ function AIParameterSetting:getIsVisible() if self:hasCallback(self.data.isVisibleFunc) then return self:getCallback(self.data.isVisibleFunc) end + if self.parent and not self.parent:getIsVisible() then + return false + end return true end diff --git a/scripts/ai/parameters/CpAIParameterFillTypeSelection.lua b/scripts/ai/parameters/CpAIParameterFillTypeSelection.lua new file mode 100644 index 000000000..75008dd38 --- /dev/null +++ b/scripts/ai/parameters/CpAIParameterFillTypeSelection.lua @@ -0,0 +1,150 @@ +---@class CpAIParameterFillTypeSetting : AIParameterSetting +CpAIParameterFillTypeSetting = CpObject(AIParameterSetting) + +function CpAIParameterFillTypeSetting:init(data, vehicle, class) + AIParameterSetting.init(self, data, vehicle, class) + self.guiParameterType = AIParameterType.TEXT --- For the giants gui element. + self:initFromData(data, vehicle, class) + --- Use this hack to load the setting without the need for changing every setting behavior. + local filePath = Utils.getFilename("config/jobParameters/FillTypeSelectionParameterSetup.xml", + g_Courseplay.BASE_DIRECTORY) + local childrenData = { + generateFillTypes = class.generateFillTypes + } + CpSettingsUtil.loadSettingsFromSetup(childrenData, filePath) + + --- Child parameters + ---@type AIParameterSettingList + self.fillType = childrenData.fillType:clone(vehicle, class) + self.fillType:setParent(self) + ---@type AIParameterSettingList + self.maxFillLevel = childrenData.maxFillLevel:clone(vehicle, class) + self.maxFillLevel:setParent(self) + ---@type AIParameterSettingList + self.minFillLevel = childrenData.minFillLevel:clone(vehicle, class) + self.minFillLevel:setParent(self) + ---@type AIParameterSettingList + self.counter = childrenData.counter:clone(vehicle, class) + self.counter:setParent(self) + + self.currentCounterValue = 0 +end + +function CpAIParameterFillTypeSetting:saveToXMLFile(xmlFile, key, usedModNames) + self.fillType:saveToXMLFile(xmlFile, key..".fillType", usedModNames) + self.maxFillLevel:saveToXMLFile(xmlFile, key..".maxFillLevel", usedModNames) + self.minFillLevel:saveToXMLFile(xmlFile, key..".minFillLevel", usedModNames) + self.counter:saveToXMLFile(xmlFile, key..".counter", usedModNames) +end + +function CpAIParameterFillTypeSetting:loadFromXMLFile(xmlFile, key) + self.fillType:loadFromXMLFile(xmlFile, key..".fillType") + self.maxFillLevel:loadFromXMLFile(xmlFile, key..".maxFillLevel") + self.minFillLevel:loadFromXMLFile(xmlFile, key..".minFillLevel") + self.counter:loadFromXMLFile(xmlFile, key..".counter") +end + +function CpAIParameterFillTypeSetting:readStream(streamId, connection) + self.fillType:readStream(streamId, connection) + self.maxFillLevel:readStream(streamId, connection) + self.minFillLevel:readStream(streamId, connection) + self.counter:readStream(streamId, connection) + self.isSynchronized = true +end + +function CpAIParameterFillTypeSetting:writeStream(streamId, connection) + self.fillType:writeStream(streamId, connection) + self.maxFillLevel:writeStream(streamId, connection) + self.minFillLevel:writeStream(streamId, connection) + self.counter:writeStream(streamId, connection) +end + +function CpAIParameterFillTypeSetting:refresh() + self.fillType:refresh() + self.maxFillLevel:refresh() + self.minFillLevel:refresh() + self.counter:refresh() +end + +function CpAIParameterFillTypeSetting:clone(...) + return CpAIParameterFillTypeSetting(self.data,...) +end + +function CpAIParameterFillTypeSetting:copy(otherSetting) + self.fillType:copy(otherSetting.fillType) + self.maxFillLevel:copy(otherSetting.maxFillLevel) + self.minFillLevel:copy(otherSetting.minFillLevel) + self.counter:copy(otherSetting.counter) +end + +function CpAIParameterFillTypeSetting:resetToLoadedValue() + self.fillType:resetToLoadedValue() + self.maxFillLevel:resetToLoadedValue() + self.minFillLevel:resetToLoadedValue() + self.counter:resetToLoadedValue() +end + +function CpAIParameterFillTypeSetting:bindSettingsToGui(lambda, ...) + lambda(self, self.fillType, self.maxFillLevel, self.minFillLevel, self.counter, ...) +end + +function CpAIParameterFillTypeSetting:getNumberOfItemsInSection(list, section) + return self.fillType:getNumberOfElements() +end + +function CpAIParameterFillTypeSetting:getTitleForSectionHeader(list, section) + return nil +end + +function CpAIParameterFillTypeSetting:populateCellForItemInSection(list, section, index, cell) + cell:getAttribute("name"):setText(self.fillType:getTextByIndex(index)) + -- if g_Courseplay.globalSettings:isAutoDriveForStreetActive() then + -- if g_Courseplay.adSortedGroups then + -- cell:getAttribute("name"):setText(g_Courseplay.adSortedGroups[section + g_Courseplay.adOffsetIndex][index].name) + -- cell.target = self.aiFrame + -- cell:setCallback("onClickCallback", "onClickStreetTargetList") + -- else + -- cell:getAttribute("name"):setText("---") + -- cell.target = self.aiFrame + -- cell:setCallback("onClickCallback", "onClickStreetTargetList") + -- end + -- else + -- if self.aiFrame.streetTargetPointParameter then + -- cell:getAttribute("name"):setText(self.aiFrame.streetTargetPointParameter:getTextByIndex(index)) + -- cell.target = self.aiFrame + -- cell:setCallback("onClickCallback", "onClickStreetTargetList") + -- end + -- end +end + +function CpAIParameterFillTypeSetting:onListSelectionChanged(list, section, index) + self.fillType:setValue(self.fillType:getValueByIndex(index)) +end + +function CpAIParameterFillTypeSetting:getString() + return self.fillType:getString() +end + +function CpAIParameterFillTypeSetting:__tostring() + return string.format("CpAIParameterFillTypeSetting(fillType: %s, maxFillLevel: %s, minFillLevel: %s, counter: %s)", + tostring(self.fillType), tostring(self.maxFillLevel), tostring(self.minFillLevel), tostring(self.counter)) +end + +function CpAIParameterFillTypeSetting:getIsValid() + return self.isValid +end + +function CpAIParameterFillTypeSetting:applyCounter(reset) + self.currentCounterValue = self.currentCounterValue + 1 + if reset then + self.currentCounterValue = 0 + end +end + +function CpAIParameterFillTypeSetting:getCounter() + return self.currentCounterValue +end + +function CpAIParameterFillTypeSetting:getIsCounterValid() + return self.currentCounterValue <= self.counter:getValue() +end \ No newline at end of file diff --git a/scripts/ai/parameters/CpAIParameterTargetPoint.lua b/scripts/ai/parameters/CpAIParameterTargetPoint.lua new file mode 100644 index 000000000..1799d36fc --- /dev/null +++ b/scripts/ai/parameters/CpAIParameterTargetPoint.lua @@ -0,0 +1,16 @@ +--- Parameter to selected an unloading station. +---@class CpAIParameterTargetPoint : AIParameterSettingList +CpAIParameterTargetPoint = CpObject(AIParameterSettingList) + +function CpAIParameterTargetPoint:init(data, vehicle, class) + AIParameterSettingList.init(self, data, vehicle, class) + return self +end + +function CpAIParameterTargetPoint:clone(...) + return CpAIParameterTargetPoint(self.data,...) +end + +function CpAIParameterTargetPoint:__tostring() + return string.format("CpAIParameterTargetPoint(name=%s, value=%s, text=%s)", self.name, tostring(self:getValue()), self:getString()) +end \ No newline at end of file diff --git a/scripts/graph/Graph.lua b/scripts/graph/Graph.lua index f3c0a8b24..c3d3e92d9 100644 --- a/scripts/graph/Graph.lua +++ b/scripts/graph/Graph.lua @@ -276,5 +276,9 @@ function Graph:onTargetDeleted(target) table.remove(self._targets, ixToRemove) end +function Graph:getAllTargets() + return self._targets +end + ---@type Graph g_graph = Graph() \ No newline at end of file diff --git a/scripts/graph/GraphTarget.lua b/scripts/graph/GraphTarget.lua index 2c5dda86f..cf9394c55 100644 --- a/scripts/graph/GraphTarget.lua +++ b/scripts/graph/GraphTarget.lua @@ -1,9 +1,16 @@ ---@class GraphTarget GraphTarget = CpObject() +GraphTarget.uniqueID = 0 +function GraphTarget.getNextUniqueID() + GraphTarget.uniqueID = GraphTarget.uniqueID + 1 + return GraphTarget.uniqueID +end + function GraphTarget:init(point, name) ---@type GraphPoint self._point = point self._name = name or "???" + self._uniqueID = GraphTarget.getNextUniqueID() g_graph:onTargetCreated(self) end @@ -28,6 +35,10 @@ function GraphTarget:copyTo(otherTarget) otherTarget._name = self._name end +function GraphTarget:getUniqueID() + return self._uniqueID +end + ---@return string function GraphTarget:getName() return self._name From ccd03f0257f3ad8e3e4e6f860eb08ae6c3054c56 Mon Sep 17 00:00:00 2001 From: David Schwietering Date: Sun, 30 Mar 2025 15:19:17 +0200 Subject: [PATCH 37/73] Add street translations --- config/MasterTranslations.xml | 71 +++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/config/MasterTranslations.xml b/config/MasterTranslations.xml index 8aecdc4a4..aed6f846a 100644 --- a/config/MasterTranslations.xml +++ b/config/MasterTranslations.xml @@ -326,6 +326,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 6cb19bbeeef9dae78412e2faa9aabcabf1c5d123 Mon Sep 17 00:00:00 2001 From: schwiti6190 Date: Sun, 30 Mar 2025 13:19:34 +0000 Subject: [PATCH 38/73] Updated translations --- translations/translation_br.xml | 20 ++++++++++++++++++++ translations/translation_cs.xml | 20 ++++++++++++++++++++ translations/translation_ct.xml | 20 ++++++++++++++++++++ translations/translation_cz.xml | 20 ++++++++++++++++++++ translations/translation_da.xml | 20 ++++++++++++++++++++ translations/translation_de.xml | 20 ++++++++++++++++++++ translations/translation_ea.xml | 20 ++++++++++++++++++++ translations/translation_en.xml | 20 ++++++++++++++++++++ translations/translation_es.xml | 20 ++++++++++++++++++++ translations/translation_fc.xml | 20 ++++++++++++++++++++ translations/translation_fi.xml | 20 ++++++++++++++++++++ translations/translation_fr.xml | 20 ++++++++++++++++++++ translations/translation_hu.xml | 20 ++++++++++++++++++++ translations/translation_id.xml | 20 ++++++++++++++++++++ translations/translation_it.xml | 20 ++++++++++++++++++++ translations/translation_jp.xml | 20 ++++++++++++++++++++ translations/translation_kr.xml | 20 ++++++++++++++++++++ translations/translation_nl.xml | 20 ++++++++++++++++++++ translations/translation_no.xml | 20 ++++++++++++++++++++ translations/translation_pl.xml | 20 ++++++++++++++++++++ translations/translation_pt.xml | 20 ++++++++++++++++++++ translations/translation_ro.xml | 20 ++++++++++++++++++++ translations/translation_ru.xml | 20 ++++++++++++++++++++ translations/translation_sv.xml | 20 ++++++++++++++++++++ translations/translation_tr.xml | 20 ++++++++++++++++++++ translations/translation_uk.xml | 20 ++++++++++++++++++++ translations/translation_vi.xml | 20 ++++++++++++++++++++ 27 files changed, 540 insertions(+) diff --git a/translations/translation_br.xml b/translations/translation_br.xml index be661fded..0499374dc 100644 --- a/translations/translation_br.xml +++ b/translations/translation_br.xml @@ -100,6 +100,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_cs.xml b/translations/translation_cs.xml index c9139e4a5..2640b7692 100644 --- a/translations/translation_cs.xml +++ b/translations/translation_cs.xml @@ -100,6 +100,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_ct.xml b/translations/translation_ct.xml index b134e8f1f..e63d884b9 100644 --- a/translations/translation_ct.xml +++ b/translations/translation_ct.xml @@ -100,6 +100,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_cz.xml b/translations/translation_cz.xml index bf97cb633..ea3f630f6 100644 --- a/translations/translation_cz.xml +++ b/translations/translation_cz.xml @@ -100,6 +100,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_da.xml b/translations/translation_da.xml index 151e55a58..b4ec265d1 100644 --- a/translations/translation_da.xml +++ b/translations/translation_da.xml @@ -100,6 +100,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_de.xml b/translations/translation_de.xml index 66cbbb029..512561cd9 100644 --- a/translations/translation_de.xml +++ b/translations/translation_de.xml @@ -100,6 +100,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_ea.xml b/translations/translation_ea.xml index 8c9bc89df..51336f59c 100644 --- a/translations/translation_ea.xml +++ b/translations/translation_ea.xml @@ -100,6 +100,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_en.xml b/translations/translation_en.xml index 2cabdfb0b..1d6c75966 100644 --- a/translations/translation_en.xml +++ b/translations/translation_en.xml @@ -100,6 +100,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_es.xml b/translations/translation_es.xml index a3fd35b7b..dc2931f88 100644 --- a/translations/translation_es.xml +++ b/translations/translation_es.xml @@ -100,6 +100,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_fc.xml b/translations/translation_fc.xml index aff8d07de..ad0b65a96 100644 --- a/translations/translation_fc.xml +++ b/translations/translation_fc.xml @@ -100,6 +100,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_fi.xml b/translations/translation_fi.xml index e1538048e..714c00905 100644 --- a/translations/translation_fi.xml +++ b/translations/translation_fi.xml @@ -100,6 +100,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_fr.xml b/translations/translation_fr.xml index c218f7d5b..8153e30ca 100644 --- a/translations/translation_fr.xml +++ b/translations/translation_fr.xml @@ -100,6 +100,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_hu.xml b/translations/translation_hu.xml index d8994683d..28c4161be 100644 --- a/translations/translation_hu.xml +++ b/translations/translation_hu.xml @@ -100,6 +100,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_id.xml b/translations/translation_id.xml index cd79bf827..3bd5abfd4 100644 --- a/translations/translation_id.xml +++ b/translations/translation_id.xml @@ -100,6 +100,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_it.xml b/translations/translation_it.xml index ea1170da4..6b18d6124 100644 --- a/translations/translation_it.xml +++ b/translations/translation_it.xml @@ -100,6 +100,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_jp.xml b/translations/translation_jp.xml index da456a5df..5402920aa 100644 --- a/translations/translation_jp.xml +++ b/translations/translation_jp.xml @@ -100,6 +100,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_kr.xml b/translations/translation_kr.xml index e3ba71a3c..0b283faad 100644 --- a/translations/translation_kr.xml +++ b/translations/translation_kr.xml @@ -100,6 +100,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_nl.xml b/translations/translation_nl.xml index 3652e903f..fdb5f8f2d 100644 --- a/translations/translation_nl.xml +++ b/translations/translation_nl.xml @@ -100,6 +100,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_no.xml b/translations/translation_no.xml index 229aa963c..1a00313ed 100644 --- a/translations/translation_no.xml +++ b/translations/translation_no.xml @@ -100,6 +100,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_pl.xml b/translations/translation_pl.xml index 64ffa3156..7427b58bf 100644 --- a/translations/translation_pl.xml +++ b/translations/translation_pl.xml @@ -100,6 +100,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_pt.xml b/translations/translation_pt.xml index 10a75f36d..da3907206 100644 --- a/translations/translation_pt.xml +++ b/translations/translation_pt.xml @@ -100,6 +100,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_ro.xml b/translations/translation_ro.xml index 97fd60c4d..1f22d98d3 100644 --- a/translations/translation_ro.xml +++ b/translations/translation_ro.xml @@ -100,6 +100,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_ru.xml b/translations/translation_ru.xml index 581d882d3..61dbb1759 100644 --- a/translations/translation_ru.xml +++ b/translations/translation_ru.xml @@ -100,6 +100,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_sv.xml b/translations/translation_sv.xml index 720d89a4a..8ce705957 100644 --- a/translations/translation_sv.xml +++ b/translations/translation_sv.xml @@ -100,6 +100,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_tr.xml b/translations/translation_tr.xml index 9c63c597e..bf4890b57 100644 --- a/translations/translation_tr.xml +++ b/translations/translation_tr.xml @@ -100,6 +100,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_uk.xml b/translations/translation_uk.xml index 3cf995449..532fe90e4 100644 --- a/translations/translation_uk.xml +++ b/translations/translation_uk.xml @@ -100,6 +100,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/translations/translation_vi.xml b/translations/translation_vi.xml index e9b2e70dc..b738b2fb8 100644 --- a/translations/translation_vi.xml +++ b/translations/translation_vi.xml @@ -100,6 +100,26 @@ + + + + + + + + + + + + + + + + + + + + From b1bb3869aff46082e9f2158ffbb0641c78ff6355 Mon Sep 17 00:00:00 2001 From: David Schwietering Date: Sun, 30 Mar 2025 20:38:47 +0200 Subject: [PATCH 39/73] WIP adding more boilerplate --- config/VehicleSettingsSetup.xml | 2 + modDesc.xml | 2 + scripts/ai/jobs/CpAIJob.lua | 3 + scripts/ai/jobs/CpAIJobStreet.lua | 8 +- .../AIDriveStrategyStreetDriveToPoint.lua | 207 ++++++++++++++++++ scripts/ai/tasks/CpAITaskDriveToPoint.lua | 31 +++ scripts/graph/Graph.lua | 22 +- 7 files changed, 271 insertions(+), 4 deletions(-) create mode 100644 scripts/ai/strategies/AIDriveStrategyStreetDriveToPoint.lua create mode 100644 scripts/ai/tasks/CpAITaskDriveToPoint.lua diff --git a/config/VehicleSettingsSetup.xml b/config/VehicleSettingsSetup.xml index 4608d3264..19edb5464 100644 --- a/config/VehicleSettingsSetup.xml +++ b/config/VehicleSettingsSetup.xml @@ -147,6 +147,8 @@ + + diff --git a/modDesc.xml b/modDesc.xml index 8baa767db..09c0c05b3 100644 --- a/modDesc.xml +++ b/modDesc.xml @@ -252,6 +252,7 @@ Changelog 8.0.0.0: + @@ -279,6 +280,7 @@ Changelog 8.0.0.0: + diff --git a/scripts/ai/jobs/CpAIJob.lua b/scripts/ai/jobs/CpAIJob.lua index 21ca1e20b..d11c8c256 100644 --- a/scripts/ai/jobs/CpAIJob.lua +++ b/scripts/ai/jobs/CpAIJob.lua @@ -68,6 +68,9 @@ end --- Gets the first task to start with. function CpAIJob:getStartTaskIndex() + if not self.driveToTask then + return 1 + end if self.currentTaskIndex ~= 0 or self.isDirectStart or self:isTargetReached() then -- skip Giants driveTo -- TODO: this isn't very nice as we rely here on the derived classes to add more tasks diff --git a/scripts/ai/jobs/CpAIJobStreet.lua b/scripts/ai/jobs/CpAIJobStreet.lua index 9f7d8cc05..b47645ef2 100644 --- a/scripts/ai/jobs/CpAIJobStreet.lua +++ b/scripts/ai/jobs/CpAIJobStreet.lua @@ -10,11 +10,13 @@ end function CpAIJobStreet:setupTasks(isServer) -- CpAIJob.setupTasks(self, isServer) - + self.driveToPointTask = CpAITaskDriveToPoint(isServer, self) + self:addTask(self.driveToPointTask) end function CpAIJobStreet:onPreStart() - --- TODO + self.driveToPointTask:setTarget( + g_graph:getTargetByUniqueID(self.cpJobParameters.unloadTargetPoint:getValue())) end @@ -42,7 +44,7 @@ end function CpAIJobStreet:setValues() CpAIJob.setValues(self) local vehicle = self.vehicleParameter:getVehicle() - + self.driveToPointTask:setVehicle(vehicle) end --- Called when parameters change, scan field diff --git a/scripts/ai/strategies/AIDriveStrategyStreetDriveToPoint.lua b/scripts/ai/strategies/AIDriveStrategyStreetDriveToPoint.lua new file mode 100644 index 000000000..4e81c8211 --- /dev/null +++ b/scripts/ai/strategies/AIDriveStrategyStreetDriveToPoint.lua @@ -0,0 +1,207 @@ +---@class AIDriveStrategyStreetDriveToPoint : AIDriveStrategyCourse +AIDriveStrategyStreetDriveToPoint = CpObject(AIDriveStrategyCourse) + +AIDriveStrategyStreetDriveToPoint.myStates = { + PREPARE_TO_DRIVE = {}, + PREPARE_FINISHED = {}, + DRIVING_TO_COURSE_START = {}, + DRIVING_COURSE = {} +} +function AIDriveStrategyStreetDriveToPoint:init(task, job) + AIDriveStrategyCourse.init(self, task, job) + AIDriveStrategyCourse.initStates(self, AIDriveStrategyStreetDriveToPoint.myStates) + self.state = self.states.INITIAL + self.prepareState = self.states.PREPARE_TO_DRIVE + self.debugChannel = CpDebug.DBG_FIELDWORK + self.prepareTimeout = 0 + self.emergencyBrake = CpTemporaryObject(true) +end + + +function AIDriveStrategyStreetDriveToPoint:setAllStaticParameters() + AIDriveStrategyCourse.setAllStaticParameters(self) + self.turningRadius = AIUtil.getTurningRadius(self.vehicle) + self:setFrontAndBackMarkers() +end + +function AIDriveStrategyStreetDriveToPoint:initializeImplementControllers(vehicle) + self:addImplementController(vehicle, MotorController, Motorized, {}) + self:addImplementController(vehicle, WearableController, Wearable, {}) +end + +---@param target GraphTarget +function AIDriveStrategyStreetDriveToPoint:setTarget(target) + self.target = target +end + +function AIDriveStrategyStreetDriveToPoint:isGeneratedCourseNeeded() + return false +end + +function AIDriveStrategyStreetDriveToPoint:getProximitySensorWidth() + return AIUtil.getWidth(self.vehicle) * 1.2 +end + +function AIDriveStrategyStreetDriveToPoint:startWithoutCourse(jobParameters) + -- to always have a valid course (for the traffic conflict detector mainly) + local course = Course.createStraightForwardCourse(self.vehicle, 25) + self:startCourse(course, 1) +end + +function AIDriveStrategyStreetDriveToPoint:update(dt) + AIDriveStrategyCourse.update(self, dt) + self:updateImplementControllers(dt) + if self.ppc:getCourse() and CpDebug:isChannelActive(CpDebug.DBG_FIELDWORK, self.vehicle) then + self.ppc:getCourse():draw() + end +end + +function AIDriveStrategyStreetDriveToPoint:getDriveData(dt, vX, vY, vZ) + self:updateLowFrequencyImplementControllers() + self:updateLowFrequencyPathfinder() + + local moveForwards = not self.ppc:isReversing() + local gx, gz, _ + + if not moveForwards then + local maxSpeed + gx, gz, maxSpeed = self:getReverseDriveData() + self:setMaxSpeed(maxSpeed) + else + gx, _, gz = self.ppc:getGoalPointPosition() + end + if self.state == self.states.INITIAL then + self:setMaxSpeed(0) + if self.target then + self.vehicle:prepareForAIDriving() + local pathfinder = GraphPathfinder(1000, 500, 20, g_graph:getGraphEdges()) + local start = PathfinderUtil.getVehiclePositionAsState3D(self.vehicle) + local targetVector = self.target:toVector() + local goal = State3D(targetVector.x, targetVector.y, 0, 0) + CpUtil.info("Goal: %s", tostring(goal)) + local TestConstraints = CpObject(PathfinderConstraintInterface) + local result = pathfinder:start(start, goal, 1, false, TestConstraints(), 0) + while not result.done do + result = pathfinder:resume() + end + local course = Course.createFromAnalyticPath(self.vehicle, result.path, true) + local isNeeded, ix = self:isPathFindingNeeded(course) + if isNeeded then + self:startPathfindingToStart(course, ix) + else + self.state = self.states.DRIVING_COURSE + self:startCourse(course, ix) + end + else + self:debug("Skipping drive to start point strategy, as no course was given!") + self:setCurrentTaskFinished() + end + elseif self.state == self.states.DRIVING_TO_COURSE_START then + self:setMaxSpeed(self.settings.streetSpeed:getValue()) + elseif self.state == self.states.DRIVING_COURSE then + self:drivingCourse() + self:setMaxSpeed(self.settings.streetSpeed:getValue()) + end + + if self.prepareState == self.states.PREPARE_TO_DRIVE then + self:setMaxSpeed(0) + local isReadyToDrive, blockingVehicle = self.vehicle:getIsAIReadyToDrive() + if isReadyToDrive then + self.prepareState = self.states.PREPARE_FINISHED + self:debug('Ready to drive') + else + self:debugSparse('Not ready to drive because of %s, preparing ...', CpUtil.getName(blockingVehicle)) + if not self.vehicle:getIsAIPreparingToDrive() then + self.prepareTimeout = self.prepareTimeout + dt + if 2000 < self.prepareTimeout then + self:debug('Timeout preparing, continue anyway') + self.prepareState = self.states.PREPARE_FINISHED + end + end + end + end + + self:checkProximitySensors(moveForwards) + return gx, gz, moveForwards, self.maxSpeed, 100 +end + +function AIDriveStrategyStreetDriveToPoint:drivingCourse() + --- override +end + +function AIDriveStrategyStreetDriveToPoint:onCourseEndReached() + self:setCurrentTaskFinished() +end + +----------------------------------------------------------------------------------------------------------------------- +--- Event listeners +----------------------------------------------------------------------------------------------------------------------- +---@param course Course +function AIDriveStrategyStreetDriveToPoint:onWaypointPassed(ix, course) + if course:isLastWaypointIx(ix) then + if self.state == self.states.DRIVING_TO_COURSE_START then + local course, ix = self:getRememberedCourseAndIx() + self:startCourse(course, ix) + self.state = self.states.DRIVING_COURSE + elseif self.state == self.states.DRIVING_COURSE then + self:onCourseEndReached() + end + end +end + +-------------------------------------------------------- +--- Pathfinding +-------------------------------------------------------- + +---@param course Course +function AIDriveStrategyStreetDriveToPoint:isPathFindingNeeded(course) + local ixClosest, distanceClosest, ixClosestRightDirection, distanceClosestRightDirection = course:getNearestWaypoints(self.vehicle:getAIDirectionNode()) + if distanceClosestRightDirection - distanceClosest > 25 then + return true, ixClosest + end + return distanceClosestRightDirection > 10, ixClosestRightDirection +end + +--- Pathfinding has finished +---@param controller PathfinderController +---@param success boolean +---@param course Course|nil +---@param goalNodeInvalid boolean|nil +function AIDriveStrategyStreetDriveToPoint:onPathfindingFinished(controller, + success, course, goalNodeInvalid) + if not success then + self:debug('Pathfinding failed, giving up!') + self.vehicle:stopCurrentAIJob(AIMessageCpErrorNoPathFound.new()) + return + end + if self.state == self.states.DRIVING_TO_COURSE_START then + self:startCourse(course, 1) + end +end + +--- Pathfinding failed, but a retry attempt is leftover. +---@param controller PathfinderController +---@param lastContext PathfinderContext +---@param wasLastRetry boolean +---@param currentRetryAttempt number +function AIDriveStrategyStreetDriveToPoint:onPathfindingRetry(controller, + lastContext, wasLastRetry, currentRetryAttempt) + --- TODO: Think of possible points of failures, that could be adjusted here. + --- Maybe a small reverse course might help to avoid a deadlock + --- after one pathfinder failure based on proximity sensor data and so on .. + if self.state == self.states.DRIVING_TO_COURSE_START then + local course = self:getRememberedCourseAndIx() + controller:findPathToWaypoint(lastContext, course, + 1, 0, 0, 1) + end +end + +---@param course Course +---@param ix number +function AIDriveStrategyStreetDriveToPoint:startPathfindingToStart(course, ix) + self.state = self.states.DRIVING_TO_COURSE_START + self:rememberCourse(course, ix) + local context = PathfinderContext(self.vehicle):allowReverse(false):ignoreFruit():vehiclesToIgnore({self.vehicle}) + self.pathfinderController:findPathToWaypoint(context, course, + ix, 0, 0, 1) +end \ No newline at end of file diff --git a/scripts/ai/tasks/CpAITaskDriveToPoint.lua b/scripts/ai/tasks/CpAITaskDriveToPoint.lua new file mode 100644 index 000000000..d8d86d74e --- /dev/null +++ b/scripts/ai/tasks/CpAITaskDriveToPoint.lua @@ -0,0 +1,31 @@ +---@class CpAITaskDriveToPoint : CpAITask +CpAITaskDriveToPoint = CpObject(CpAITask) + +function CpAITaskDriveToPoint:reset() + CpAITask.reset(self) + self.target = nil +end + +---@param target GraphTarget +function CpAITaskDriveToPoint:setTarget(target) + self.target = target +end + +function CpAITaskDriveToPoint:start() + if self.isServer then + self:debug('CP drive to target point task started') + local strategy = AIDriveStrategyStreetDriveToPoint(self, self.job) + strategy:setTarget(self.target) + strategy:setAIVehicle(self.vehicle, self.job:getCpJobParameters()) + self.vehicle:startCpWithStrategy(strategy) + end + CpAITask.start(self) +end + +function CpAITaskDriveToPoint:stop(wasJobStopped) + if self.isServer then + self:debug('CP drive to target point task stopped') + self.vehicle:stopCpDriver(wasJobStopped) + end + CpAITask.stop(self) +end \ No newline at end of file diff --git a/scripts/graph/Graph.lua b/scripts/graph/Graph.lua index c3d3e92d9..ee770fc21 100644 --- a/scripts/graph/Graph.lua +++ b/scripts/graph/Graph.lua @@ -35,7 +35,6 @@ function Graph:consoleCommandFindPathTo(name) end end local edge = seg:toGraphEdge() - print(tostring(edge)) table.insert(edges, edge) end if targetPos == nil or targetPos.x == nil or targetPos.y == nil then @@ -280,5 +279,26 @@ function Graph:getAllTargets() return self._targets end +---@param id number +---@return GraphTarget|nil +function Graph:getTargetByUniqueID(id) + for _, target in ipairs(self._targets) do + if target:getUniqueID() == id then + return target + end + end +end + +---@return GraphPathfinder.GraphEdge[] +function Graph:getGraphEdges() + local edges = {} + for _, seg in ipairs(self._childNodes) do + local edge = seg:toGraphEdge() + table.insert(edges, edge) + end + return edges +end + + ---@type Graph g_graph = Graph() \ No newline at end of file From 7bac917f3db49999c0083f05cdabed3937c4c5cd Mon Sep 17 00:00:00 2001 From: David Schwietering Date: Sun, 30 Mar 2025 21:23:47 +0200 Subject: [PATCH 40/73] Multiple bug fixes .. --- Courseplay.lua | 1 + config/MasterTranslations.xml | 4 + scripts/ai/jobs/CpAIJobStreet.lua | 4 +- scripts/ai/jobs/CpJobParameters.lua | 19 +++-- scripts/specializations/CpAIBaleFinder.lua | 2 +- .../specializations/CpAICombineUnloader.lua | 2 +- scripts/specializations/CpAIFieldWorker.lua | 2 +- scripts/specializations/CpAIStreetWorker.lua | 81 +++++++------------ scripts/specializations/CpAIWorker.lua | 4 + 9 files changed, 56 insertions(+), 63 deletions(-) diff --git a/Courseplay.lua b/Courseplay.lua index de473f586..dd5ee438e 100644 --- a/Courseplay.lua +++ b/Courseplay.lua @@ -296,6 +296,7 @@ function Courseplay.register(typeManager) CpAICombineUnloader.register(typeManager, typeName, typeEntry.specializations) CpAISiloLoaderWorker.register(typeManager, typeName, typeEntry.specializations) CpAIBunkerSiloWorker.register(typeManager, typeName, typeEntry.specializations) + CpAIStreetWorker.register(typeManager, typeName, typeEntry.specializations) -- TODO 25 CpGamePadHud.register(typeManager, typeName,typeEntry.specializations) CpHud.register(typeManager, typeName, typeEntry.specializations) CpInfoTexts.register(typeManager, typeName, typeEntry.specializations) diff --git a/config/MasterTranslations.xml b/config/MasterTranslations.xml index aed6f846a..837c3335a 100644 --- a/config/MasterTranslations.xml +++ b/config/MasterTranslations.xml @@ -418,6 +418,10 @@ + + + + diff --git a/scripts/ai/jobs/CpAIJobStreet.lua b/scripts/ai/jobs/CpAIJobStreet.lua index b47645ef2..a4a501390 100644 --- a/scripts/ai/jobs/CpAIJobStreet.lua +++ b/scripts/ai/jobs/CpAIJobStreet.lua @@ -37,7 +37,7 @@ function CpAIJobStreet:applyCurrentState(vehicle, mission, farmId, isDirectStart CpAIJob.applyCurrentState(self, vehicle, mission, farmId, isDirectStart) self.cpJobParameters:validateSettings() - -- self:copyFrom(vehicle:getCpBaleFinderJob()) + self:copyFrom(vehicle:getCpStreetWorkerJob()) end @@ -55,7 +55,7 @@ function CpAIJobStreet:validate(farmId) end local vehicle = self.vehicleParameter:getVehicle() if vehicle then - -- vehicle:applyCpBaleFinderJobParameters(self) + vehicle:applyCpStreetWorkerJobParameters(self) end return isValid or isRunning, errorMessage diff --git a/scripts/ai/jobs/CpJobParameters.lua b/scripts/ai/jobs/CpJobParameters.lua index da3b0112d..855f0438f 100644 --- a/scripts/ai/jobs/CpJobParameters.lua +++ b/scripts/ai/jobs/CpJobParameters.lua @@ -455,20 +455,23 @@ function CpStreetJobParameters:init(job) end function CpStreetJobParameters:hasNoValidTrailerAttached() - local vehicle = self.job:getVehicle() - if vehicle then - return not AIUtil.hasChildVehicleWithSpecialization(vehicle, Dischargeable) - or not AIUtil.hasChildVehicleWithSpecialization(vehicle, Trailer) - end - return false + return true + -- local vehicle = self.job:getVehicle() + -- if vehicle then + -- return not AIUtil.hasChildVehicleWithSpecialization(vehicle, Dischargeable) + -- or not AIUtil.hasChildVehicleWithSpecialization(vehicle, Trailer) + -- end + -- return false end function CpStreetJobParameters:isUnloadTargetPointDisabled() - return self:hasNoValidTrailerAttached() or self.loadUnloadTargetMode:getValue() == CpStreetJobParameters.DRIVE_TO + return false +-- return self:hasNoValidTrailerAttached() or self.loadUnloadTargetMode:getValue() == CpStreetJobParameters.DRIVE_TO end function CpStreetJobParameters:isLoadTargetPointDisabled() - return self:hasNoValidTrailerAttached() or self.loadUnloadTargetMode:getValue() ~= CpStreetJobParameters.LOAD_AND_UNLOAD + return true + -- return self:hasNoValidTrailerAttached() or self.loadUnloadTargetMode:getValue() ~= CpStreetJobParameters.LOAD_AND_UNLOAD end function CpStreetJobParameters:isRunCounterDisabled() diff --git a/scripts/specializations/CpAIBaleFinder.lua b/scripts/specializations/CpAIBaleFinder.lua index afd6573cd..8a5cefb2a 100644 --- a/scripts/specializations/CpAIBaleFinder.lua +++ b/scripts/specializations/CpAIBaleFinder.lua @@ -125,7 +125,7 @@ function CpAIBaleFinder:getCanStartCpBaleFinder() end function CpAIBaleFinder:getCanStartCp(superFunc) - return superFunc(self) or self:getCanStartCpBaleFinder() and not self:getIsCpCourseRecorderActive() + return superFunc(self) or self:getCanStartCpBaleFinder() end --- Only use the bale finder, if the cp field work job is not possible. diff --git a/scripts/specializations/CpAICombineUnloader.lua b/scripts/specializations/CpAICombineUnloader.lua index e729c6883..65cebe39d 100644 --- a/scripts/specializations/CpAICombineUnloader.lua +++ b/scripts/specializations/CpAICombineUnloader.lua @@ -242,7 +242,7 @@ function CpAICombineUnloader:getCanStartCpCombineUnloader() end function CpAICombineUnloader:getCanStartCp(superFunc) - return superFunc(self) or self:getCanStartCpCombineUnloader() and not self:getIsCpCourseRecorderActive() + return superFunc(self) or self:getCanStartCpCombineUnloader() end function CpAICombineUnloader:getCpStartableJob(superFunc, isStartedByHud) diff --git a/scripts/specializations/CpAIFieldWorker.lua b/scripts/specializations/CpAIFieldWorker.lua index a0c14f502..4848a23fa 100644 --- a/scripts/specializations/CpAIFieldWorker.lua +++ b/scripts/specializations/CpAIFieldWorker.lua @@ -296,7 +296,7 @@ end --- Only allow the basic field work job to start, if a course is assigned. function CpAIFieldWorker:getCanStartCp(superFunc) - return not self:getIsCpCourseRecorderActive() and self:hasCpCourse() and self:getCanStartCpFieldWork() or superFunc(self) + return self:hasCpCourse() and self:getCanStartCpFieldWork() or superFunc(self) end --- Gets the field work job for the hud or start action event. diff --git a/scripts/specializations/CpAIStreetWorker.lua b/scripts/specializations/CpAIStreetWorker.lua index 069adb328..63409b4cf 100644 --- a/scripts/specializations/CpAIStreetWorker.lua +++ b/scripts/specializations/CpAIStreetWorker.lua @@ -32,9 +32,6 @@ function CpAIStreetWorker.registerEventListeners(vehicleType) SpecializationUtil.registerEventListener(vehicleType, 'onLoadFinished', CpAIStreetWorker) SpecializationUtil.registerEventListener(vehicleType, 'onReadStream', CpAIStreetWorker) SpecializationUtil.registerEventListener(vehicleType, 'onWriteStream', CpAIStreetWorker) - - SpecializationUtil.registerEventListener(vehicleType, 'onCpADStartedByPlayer', CpAIStreetWorker) - SpecializationUtil.registerEventListener(vehicleType, 'onCpADRestarted', CpAIStreetWorker) end function CpAIStreetWorker.registerFunctions(vehicleType) @@ -61,66 +58,66 @@ end --- Event listeners --------------------------------------------------------------------------------------------------------------------------- function CpAIStreetWorker:onLoad(savegame) - --- Register the spec: spec_CpAIStreetWorker - self.spec_CpAIStreetWorker = self["spec_" .. CpAIStreetWorker.SPEC_NAME] - local spec = self.spec_CpAIStreetWorker + --- Register the spec: spec_cpAIStreetWorker + self.spec_cpAIStreetWorker = self["spec_" .. CpAIStreetWorker.SPEC_NAME] + local spec = self.spec_cpAIStreetWorker --- This job is for starting the driving with a key bind or the mini gui. spec.cpJob = g_currentMission.aiJobTypeManager:createJob(AIJobType.STREET_WORKER_CP) spec.cpJob:setVehicle(self, true) end function CpAIStreetWorker:onLoadFinished(savegame) - local spec = self.spec_CpAIStreetWorker + local spec = self.spec_cpAIStreetWorker if savegame ~= nil then spec.cpJob:getCpJobParameters():loadFromXMLFile(savegame.xmlFile, savegame.key.. CpAIStreetWorker.KEY..".cpJob") end end function CpAIStreetWorker:saveToXMLFile(xmlFile, baseKey, usedModNames) - local spec = self.spec_CpAIStreetWorker + local spec = self.spec_cpAIStreetWorker spec.cpJob:getCpJobParameters():saveToXMLFile(xmlFile, baseKey.. ".cpJob") end function CpAIStreetWorker:onReadStream(streamId, connection) - local spec = self.spec_CpAIStreetWorker + local spec = self.spec_cpAIStreetWorker spec.cpJob:readStream(streamId, connection) end function CpAIStreetWorker:onWriteStream(streamId, connection) - local spec = self.spec_CpAIStreetWorker + local spec = self.spec_cpAIStreetWorker spec.cpJob:writeStream(streamId, connection) end function CpAIStreetWorker:getCpStreetWorkerJobParameters() - local spec = self.spec_CpAIStreetWorker + local spec = self.spec_cpAIStreetWorker return spec.cpJob:getCpJobParameters() end function CpAIStreetWorker:getCpStreetWorkerJob() - local spec = self.spec_CpAIStreetWorker + local spec = self.spec_cpAIStreetWorker return spec.cpJob end function CpAIStreetWorker:applyCpStreetWorkerJobParameters(job) - local spec = self.spec_CpAIStreetWorker + local spec = self.spec_cpAIStreetWorker spec.cpJob:getCpJobParameters():validateSettings() spec.cpJob:copyFrom(job) end ---- Is the bale finder allowed? +--- Is the Street job allowed? function CpAIStreetWorker:getCanStartCpStreetWorker() return true end function CpAIStreetWorker:getCanStartCp(superFunc) - return superFunc(self) or self:getCanStartCpStreetWorker() and not self:getIsCpCourseRecorderActive() + return superFunc(self) or self:getCanStartCpStreetWorker() end --- Only use the bale finder, if the cp field work job is not possible. function CpAIStreetWorker:getCpStartableJob(superFunc, isStartedByHud) - local spec = self.spec_CpAIStreetWorker - if isStartedByHud and self:cpIsHudStreetWorkerJobSelected() then + local spec = self.spec_cpAIStreetWorker + if isStartedByHud and self:cpIsHudStreetJobSelected() then return self:getCanStartCpStreetWorker() and spec.cpJob end return superFunc(self, isStartedByHud) or not isStartedByHud and self:getCanStartCpStreetWorker() and spec.cpJob @@ -128,47 +125,31 @@ end --- Starts the cp driver at the first waypoint. function CpAIStreetWorker:startCpAtFirstWp(superFunc) - -- if not superFunc(self) then - -- if self:getCanStartCpStreetWorker() then - -- local spec = self.spec_CpAIStreetWorker - -- --- Applies the bale wrap type set in the hud, so ad can start with the correct type. - -- --- TODO: This should only be applied, if the driver was started for the first time by ad and not every time. - -- spec.cpJobStartAtLastWp:getCpJobParameters().baleWrapType:setValue(spec.cpJob:getCpJobParameters().baleWrapType:getValue()) - -- spec.cpJob:applyCurrentState(self, g_currentMission, g_currentMission.playerSystem:getLocalPlayer().farmId, true) - -- spec.cpJob:setValues() - -- local success = spec.cpJob:validate(false) - -- if success then - -- g_client:getServerConnection():sendEvent(AIJobStartRequestEvent.new(spec.cpJob, self:getOwnerFarmId())) - -- return true - -- end - -- end - -- else - -- return true - -- end + if not superFunc(self) then + if self:getCanStartCpStreetWorker() then + local spec = self.spec_cpAIStreetWorker + spec.cpJob:applyCurrentState(self, g_currentMission, g_currentMission.playerSystem:getLocalPlayer().farmId, true) + spec.cpJob:setValues() + local success = spec.cpJob:validate(false) + if success then + g_client:getServerConnection():sendEvent(AIJobStartRequestEvent.new(spec.cpJob, self:getOwnerFarmId())) + return true + end + end + else + return true + end end --- Starts the cp driver at the last driven waypoint. function CpAIStreetWorker:startCpAtLastWp(superFunc) - -- if not superFunc(self) then - -- if self:getCanStartCpStreetWorker() then - -- local spec = self.spec_CpAIStreetWorker - -- spec.cpJobStartAtLastWp:applyCurrentState(self, g_currentMission, g_currentMission.playerSystem:getLocalPlayer().farmId, true) - -- spec.cpJobStartAtLastWp:setValues() - -- local success = spec.cpJobStartAtLastWp:validate(false) - -- if success then - -- g_client:getServerConnection():sendEvent(AIJobStartRequestEvent.new(spec.cpJobStartAtLastWp, self:getOwnerFarmId())) - -- return true - -- end - -- end - -- else - -- return true - -- end + CpAIStreetWorker.startCpAtFirstWp(self, superFunc) end function CpAIStreetWorker:onCpADStartedByPlayer() - local spec = self.spec_CpAIStreetWorker + local spec = self.spec_cpAIStreetWorker --- Applies the bale wrap type set in the hud, so ad can start with the correct type. - spec.cpJobStartAtLastWp:getCpJobParameters().baleWrapType:setValue(spec.cpJob:getCpJobParameters().baleWrapType:getValue()) + end function CpAIStreetWorker:onCpADRestarted() diff --git a/scripts/specializations/CpAIWorker.lua b/scripts/specializations/CpAIWorker.lua index c4bc4313d..1556af1dd 100644 --- a/scripts/specializations/CpAIWorker.lua +++ b/scripts/specializations/CpAIWorker.lua @@ -259,6 +259,10 @@ function CpAIWorker:cpStartStopDriver(isStartedByHud) CpUtil.debugVehicle(CpDebug.DBG_FIELDWORK, self, "Could not find a CP job to start!") return end + if self:getIsCpCourseRecorderActive() then + CpUtil.debugVehicle(CpDebug.DBG_FIELDWORK, self, "Could not start cp, as the course recorder is active!") + return + end if self:getCanStartCp() and job then job:applyCurrentState(self, g_currentMission, g_currentMission.playerSystem:getLocalPlayer().farmId, true, true) From d39cb6715033d3c87913809bcf2e2dadb79878b9 Mon Sep 17 00:00:00 2001 From: schwiti6190 Date: Sun, 30 Mar 2025 19:24:04 +0000 Subject: [PATCH 41/73] Updated translations --- translations/translation_br.xml | 1 + translations/translation_cs.xml | 1 + translations/translation_ct.xml | 1 + translations/translation_cz.xml | 1 + translations/translation_da.xml | 1 + translations/translation_de.xml | 1 + translations/translation_ea.xml | 1 + translations/translation_en.xml | 1 + translations/translation_es.xml | 1 + translations/translation_fc.xml | 1 + translations/translation_fi.xml | 1 + translations/translation_fr.xml | 1 + translations/translation_hu.xml | 1 + translations/translation_id.xml | 1 + translations/translation_it.xml | 1 + translations/translation_jp.xml | 1 + translations/translation_kr.xml | 1 + translations/translation_nl.xml | 1 + translations/translation_no.xml | 1 + translations/translation_pl.xml | 1 + translations/translation_pt.xml | 1 + translations/translation_ro.xml | 1 + translations/translation_ru.xml | 1 + translations/translation_sv.xml | 1 + translations/translation_tr.xml | 1 + translations/translation_uk.xml | 1 + translations/translation_vi.xml | 1 + 27 files changed, 27 insertions(+) diff --git a/translations/translation_br.xml b/translations/translation_br.xml index 0499374dc..8b998e337 100644 --- a/translations/translation_br.xml +++ b/translations/translation_br.xml @@ -127,6 +127,7 @@ + diff --git a/translations/translation_cs.xml b/translations/translation_cs.xml index 2640b7692..8b89ae25f 100644 --- a/translations/translation_cs.xml +++ b/translations/translation_cs.xml @@ -127,6 +127,7 @@ + diff --git a/translations/translation_ct.xml b/translations/translation_ct.xml index e63d884b9..347553f44 100644 --- a/translations/translation_ct.xml +++ b/translations/translation_ct.xml @@ -127,6 +127,7 @@ + diff --git a/translations/translation_cz.xml b/translations/translation_cz.xml index ea3f630f6..4d8b10fe8 100644 --- a/translations/translation_cz.xml +++ b/translations/translation_cz.xml @@ -127,6 +127,7 @@ + diff --git a/translations/translation_da.xml b/translations/translation_da.xml index b4ec265d1..f04080697 100644 --- a/translations/translation_da.xml +++ b/translations/translation_da.xml @@ -127,6 +127,7 @@ + diff --git a/translations/translation_de.xml b/translations/translation_de.xml index 512561cd9..31f5fa178 100644 --- a/translations/translation_de.xml +++ b/translations/translation_de.xml @@ -127,6 +127,7 @@ + diff --git a/translations/translation_ea.xml b/translations/translation_ea.xml index 51336f59c..429ff7fdd 100644 --- a/translations/translation_ea.xml +++ b/translations/translation_ea.xml @@ -127,6 +127,7 @@ + diff --git a/translations/translation_en.xml b/translations/translation_en.xml index 1d6c75966..18e923618 100644 --- a/translations/translation_en.xml +++ b/translations/translation_en.xml @@ -127,6 +127,7 @@ + diff --git a/translations/translation_es.xml b/translations/translation_es.xml index dc2931f88..96d4f828a 100644 --- a/translations/translation_es.xml +++ b/translations/translation_es.xml @@ -127,6 +127,7 @@ + diff --git a/translations/translation_fc.xml b/translations/translation_fc.xml index ad0b65a96..d753cb146 100644 --- a/translations/translation_fc.xml +++ b/translations/translation_fc.xml @@ -127,6 +127,7 @@ + diff --git a/translations/translation_fi.xml b/translations/translation_fi.xml index 714c00905..657b7141f 100644 --- a/translations/translation_fi.xml +++ b/translations/translation_fi.xml @@ -127,6 +127,7 @@ + diff --git a/translations/translation_fr.xml b/translations/translation_fr.xml index 8153e30ca..f182729d4 100644 --- a/translations/translation_fr.xml +++ b/translations/translation_fr.xml @@ -127,6 +127,7 @@ + diff --git a/translations/translation_hu.xml b/translations/translation_hu.xml index 28c4161be..0b6675bf7 100644 --- a/translations/translation_hu.xml +++ b/translations/translation_hu.xml @@ -127,6 +127,7 @@ + diff --git a/translations/translation_id.xml b/translations/translation_id.xml index 3bd5abfd4..ff907d835 100644 --- a/translations/translation_id.xml +++ b/translations/translation_id.xml @@ -127,6 +127,7 @@ + diff --git a/translations/translation_it.xml b/translations/translation_it.xml index 6b18d6124..4071284ea 100644 --- a/translations/translation_it.xml +++ b/translations/translation_it.xml @@ -127,6 +127,7 @@ + diff --git a/translations/translation_jp.xml b/translations/translation_jp.xml index 5402920aa..bdbdf240d 100644 --- a/translations/translation_jp.xml +++ b/translations/translation_jp.xml @@ -127,6 +127,7 @@ + diff --git a/translations/translation_kr.xml b/translations/translation_kr.xml index 0b283faad..386ada766 100644 --- a/translations/translation_kr.xml +++ b/translations/translation_kr.xml @@ -127,6 +127,7 @@ + diff --git a/translations/translation_nl.xml b/translations/translation_nl.xml index fdb5f8f2d..98e0995c5 100644 --- a/translations/translation_nl.xml +++ b/translations/translation_nl.xml @@ -127,6 +127,7 @@ + diff --git a/translations/translation_no.xml b/translations/translation_no.xml index 1a00313ed..3f3e5a255 100644 --- a/translations/translation_no.xml +++ b/translations/translation_no.xml @@ -127,6 +127,7 @@ + diff --git a/translations/translation_pl.xml b/translations/translation_pl.xml index 7427b58bf..2bcfc61ae 100644 --- a/translations/translation_pl.xml +++ b/translations/translation_pl.xml @@ -127,6 +127,7 @@ + diff --git a/translations/translation_pt.xml b/translations/translation_pt.xml index da3907206..9184c0055 100644 --- a/translations/translation_pt.xml +++ b/translations/translation_pt.xml @@ -127,6 +127,7 @@ + diff --git a/translations/translation_ro.xml b/translations/translation_ro.xml index 1f22d98d3..1a0b42356 100644 --- a/translations/translation_ro.xml +++ b/translations/translation_ro.xml @@ -127,6 +127,7 @@ + diff --git a/translations/translation_ru.xml b/translations/translation_ru.xml index 61dbb1759..e7385591f 100644 --- a/translations/translation_ru.xml +++ b/translations/translation_ru.xml @@ -127,6 +127,7 @@ + diff --git a/translations/translation_sv.xml b/translations/translation_sv.xml index 8ce705957..02df8019d 100644 --- a/translations/translation_sv.xml +++ b/translations/translation_sv.xml @@ -127,6 +127,7 @@ + diff --git a/translations/translation_tr.xml b/translations/translation_tr.xml index bf4890b57..0d0a3cf0a 100644 --- a/translations/translation_tr.xml +++ b/translations/translation_tr.xml @@ -127,6 +127,7 @@ + diff --git a/translations/translation_uk.xml b/translations/translation_uk.xml index 532fe90e4..5c22094da 100644 --- a/translations/translation_uk.xml +++ b/translations/translation_uk.xml @@ -127,6 +127,7 @@ + diff --git a/translations/translation_vi.xml b/translations/translation_vi.xml index b738b2fb8..e90fcd395 100644 --- a/translations/translation_vi.xml +++ b/translations/translation_vi.xml @@ -127,6 +127,7 @@ + From be7515b38ba75bd4f204d3bc86af1b297d306a02 Mon Sep 17 00:00:00 2001 From: David Schwietering Date: Tue, 1 Apr 2025 21:56:57 +0200 Subject: [PATCH 42/73] WIP some more gui boiler plate --- config/MasterTranslations.xml | 62 +++++--- .../BaleFinderJobParameterSetup.xml | 14 +- .../BunkerSiloJobParameterSetup.xml | 6 +- .../CombineUnloaderJobParameterSetup.xml | 40 +++-- .../FieldWorkJobParameterSetup.xml | 22 ++- .../SiloLoaderJobParameterSetup.xml | 9 +- scripts/ai/jobs/CpAIJobCombineUnloader.lua | 24 ++- scripts/ai/jobs/CpAIJobStreet.lua | 8 +- scripts/ai/jobs/CpJobParameters.lua | 142 +++++++++++++++--- scripts/ai/util/FillLevelUtil.lua | 46 ++++++ scripts/gui/pages/CpCourseGeneratorFrame.lua | 2 + 11 files changed, 302 insertions(+), 73 deletions(-) diff --git a/config/MasterTranslations.xml b/config/MasterTranslations.xml index 837c3335a..69fe1e740 100644 --- a/config/MasterTranslations.xml +++ b/config/MasterTranslations.xml @@ -91,8 +91,20 @@ + + + + + + + + + + + + - + @@ -207,6 +219,14 @@ + + + + + + + + @@ -225,26 +245,10 @@ - - - - - - - - - - - - - - - - @@ -253,6 +257,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/jobParameters/BaleFinderJobParameterSetup.xml b/config/jobParameters/BaleFinderJobParameterSetup.xml index 136ebcdbe..211873d74 100644 --- a/config/jobParameters/BaleFinderJobParameterSetup.xml +++ b/config/jobParameters/BaleFinderJobParameterSetup.xml @@ -7,13 +7,23 @@ - + + + + + - + 1 2 diff --git a/config/jobParameters/BunkerSiloJobParameterSetup.xml b/config/jobParameters/BunkerSiloJobParameterSetup.xml index 07ddc98ee..f45cbf036 100644 --- a/config/jobParameters/BunkerSiloJobParameterSetup.xml +++ b/config/jobParameters/BunkerSiloJobParameterSetup.xml @@ -7,7 +7,11 @@ - + + diff --git a/config/jobParameters/CombineUnloaderJobParameterSetup.xml b/config/jobParameters/CombineUnloaderJobParameterSetup.xml index bbc44d6c1..bb4f6fda2 100644 --- a/config/jobParameters/CombineUnloaderJobParameterSetup.xml +++ b/config/jobParameters/CombineUnloaderJobParameterSetup.xml @@ -7,7 +7,11 @@ - + + @@ -24,15 +28,29 @@ - - - - - - - - - - + + + + 1 + 2 + 3 + 4 + + + CP_deactivated + giants + autoDrive + field + + + + + + diff --git a/config/jobParameters/FieldWorkJobParameterSetup.xml b/config/jobParameters/FieldWorkJobParameterSetup.xml index 3e8f5b711..a69f3287e 100644 --- a/config/jobParameters/FieldWorkJobParameterSetup.xml +++ b/config/jobParameters/FieldWorkJobParameterSetup.xml @@ -7,7 +7,11 @@ - + + @@ -30,4 +34,20 @@ + + + + + + + diff --git a/config/jobParameters/SiloLoaderJobParameterSetup.xml b/config/jobParameters/SiloLoaderJobParameterSetup.xml index 21e3e0122..6751ee8a0 100644 --- a/config/jobParameters/SiloLoaderJobParameterSetup.xml +++ b/config/jobParameters/SiloLoaderJobParameterSetup.xml @@ -7,7 +7,11 @@ - + + @@ -24,6 +28,7 @@ - + diff --git a/scripts/ai/jobs/CpAIJobCombineUnloader.lua b/scripts/ai/jobs/CpAIJobCombineUnloader.lua index e45693c2d..7c0051368 100644 --- a/scripts/ai/jobs/CpAIJobCombineUnloader.lua +++ b/scripts/ai/jobs/CpAIJobCombineUnloader.lua @@ -42,7 +42,6 @@ function CpAIJobCombineUnloader:setupJobParameters() CpAIJob.setupJobParameters(self) self:setupCpJobParameters(CpCombineUnloaderJobParameters(self)) self.cpJobParameters.fieldUnloadPosition:setSnappingAngle(math.pi/8) -- AI menu snapping angle of 22.5 degree. - --- Giants unload self.unloadingStationParameter = self.cpJobParameters.unloadingStation self.waitForFillingTask = self.combineUnloaderTask @@ -120,10 +119,15 @@ function CpAIJobCombineUnloader:validate(farmId) if vehicle then vehicle:applyCpCombineUnloaderJobParameters(self) end + + local useGiantsUnload = self.cpJobParameters.unloadWith:getValue() == CpCombineUnloaderJobParameters.UNLOAD_WITH_GIANTS + local useFieldUnload = self.cpJobParameters.unloadWith:getValue() == CpCombineUnloaderJobParameters.UNLOAD_ON_FIELD + local useStreetModeUnload = self.cpJobParameters.unloadWith:getValue() == CpCombineUnloaderJobParameters.UNLOAD_WITH_STREET_MODE + ------------------------------------ --- Validate giants unload if needed ------------------------------------- - if not self.cpJobParameters.useGiantsUnload:getIsDisabled() and self.cpJobParameters.useGiantsUnload:getValue() then + if useGiantsUnload then isValid, errorMessage = self.cpJobParameters.unloadingStation:validateUnloadingStation() if not isValid then @@ -160,15 +164,14 @@ function CpAIJobCombineUnloader:onFieldBoundaryDetectionFinished(vehicle, fieldP self.selectedFieldPlot:setWaypoints(fieldPolygon) self.selectedFieldPlot:setVisible(true) end + local useGiantsUnload = self.cpJobParameters.unloadWith:getValue() == CpCombineUnloaderJobParameters.UNLOAD_WITH_GIANTS + local useFieldUnload = self.cpJobParameters.unloadWith:getValue() == CpCombineUnloaderJobParameters.UNLOAD_ON_FIELD + local useStreetModeUnload = self.cpJobParameters.unloadWith:getValue() == CpCombineUnloaderJobParameters.UNLOAD_WITH_STREET_MODE ------------------------------------ --- Validate start distance to field ------------------------------------- - local useGiantsUnload = false - if not self.cpJobParameters.useGiantsUnload:getIsDisabled() then - useGiantsUnload = self.cpJobParameters.useGiantsUnload:getValue() - end - local isValid, errorMessage = true - if fieldPolygon and self.isDirectStart then + local isValid, errorMessage = true, "" + if not useStreetModeUnload and fieldPolygon and self.isDirectStart then --- Checks the distance for starting with the hud, as a safety check. --- Firstly check, if the vehicle is near the field. local x, _, z = getWorldTranslation(vehicle.rootNode) @@ -190,12 +193,7 @@ function CpAIJobCombineUnloader:onFieldBoundaryDetectionFinished(vehicle, fieldP ------------------------------------ --- Validate field unload if needed ------------------------------------- - local useFieldUnload = false - if not self.cpJobParameters.useFieldUnload:getIsDisabled() then - useFieldUnload = self.cpJobParameters.useFieldUnload:getValue() - end if useFieldUnload then - local x, z = self.cpJobParameters.fieldUnloadPosition:getPosition() isValid = CpMathUtil.isPointInPolygon(fieldPolygon, x, z) or CpMathUtil.isWithinDistanceToPolygon(fieldPolygon, x, z, self.minFieldUnloadDistanceToField) diff --git a/scripts/ai/jobs/CpAIJobStreet.lua b/scripts/ai/jobs/CpAIJobStreet.lua index a4a501390..fdf029ead 100644 --- a/scripts/ai/jobs/CpAIJobStreet.lua +++ b/scripts/ai/jobs/CpAIJobStreet.lua @@ -25,10 +25,6 @@ function CpAIJobStreet:setupJobParameters() self:setupCpJobParameters(CpStreetJobParameters(self)) end -function CpAIJobStreet:getIsAvailableForVehicle(vehicle, cpJobsAllowed) - return CpAIJob.getIsAvailableForVehicle(self, vehicle, cpJobsAllowed) -end - function CpAIJobStreet:getCanStartJob() return true end @@ -57,7 +53,9 @@ function CpAIJobStreet:validate(farmId) if vehicle then vehicle:applyCpStreetWorkerJobParameters(self) end - + if self.cpJobParameters.unloadTargetPoint:getValue() < 0 then + return false, g_i18n:getText("CP_error_no_target_selected") + end return isValid or isRunning, errorMessage end diff --git a/scripts/ai/jobs/CpJobParameters.lua b/scripts/ai/jobs/CpJobParameters.lua index 855f0438f..b301776a3 100644 --- a/scripts/ai/jobs/CpJobParameters.lua +++ b/scripts/ai/jobs/CpJobParameters.lua @@ -119,6 +119,14 @@ function CpJobParameters:isFieldWorkHudModeDisabled() return false end +function CpJobParameters:isStreetModeActive() + return true +end + +function CpJobParameters:isGiantsForStreetActive() + return false +end + --- Callback raised by a setting and executed as an vehicle event. ---@param callbackStr string event to be raised ---@param setting AIParameterSettingList setting that raised the callback. @@ -167,6 +175,19 @@ function CpJobParameters:generateTargets() return values, texts end +--- Are the setting values roughly equal. +---@param otherParameters CpJobParameters +---@return boolean +function CpJobParameters:areAlmostEqualTo(otherParameters) + for i, param in pairs(self.settings) do + if not param:isAlmostEqualTo(otherParameters[param:getName()]) then + CpUtil.debugFormat(CpDebug.DBG_HUD, "Parameter: %s not equal!", param:getName()) + return false + end + end + return true +end + ---@class CpFieldWorkJobParameters : CpJobParameters CpFieldWorkJobParameters = CpObject(CpJobParameters) function CpFieldWorkJobParameters:init(job) @@ -252,19 +273,93 @@ function CpFieldWorkJobParameters:isLaneOffsetDisabled() return self:noMultiToolsCourseSelected() or vehicle and vehicle:getIsCpActive() end ---- Are the setting values roughly equal. ----@param otherParameters CpJobParameters ----@return boolean -function CpJobParameters:areAlmostEqualTo(otherParameters) - for i, param in pairs(self.settings) do - if not param:isAlmostEqualTo(otherParameters[param:getName()]) then - CpUtil.debugFormat(CpDebug.DBG_HUD, "Parameter: %s not equal!", param:getName()) - return false +function CpFieldWorkJobParameters:isUnloadRefillTargetDisabled() + return true --- TODO: Added check for refill/unload possibilities ... +end + + +function CpFieldWorkJobParameters:generateFillTypes() + local fillTypes = {} + local texts = {} + local vehicle = self.job and self.job:getVehicle() + if vehicle then + fillTypes = FillLevelUtil.getAllValidFillTypes(vehicle, function() + return true + end) + for _, f in pairs(fillTypes) do + table.insert(texts, g_fillTypeManager:getFillTypeTitleByIndex(f)) end + else + for ix, _ in pairs(g_fillTypeManager:getFillTypes()) do + if ix ~= FillType.UNKNOWN then + table.insert(fillTypes, ix) + table.insert(texts, g_fillTypeManager:getFillTypeTitleByIndex(f)) + end + end + end + table.insert(fillTypes, 1, -1) + table.insert(texts, 1, "---") + return fillTypes, texts +end + +function CpFieldWorkJobParameters:isLoadingDisabled() + if self:isUnloadRefillTargetDisabled() then + return true + end + + local vehicle = self.job:getVehicle() + if not vehicle then + return false + end + return not AIUtil.hasChildVehicleWithSpecialization(vehicle, Sprayer) and not + AIUtil.hasChildVehicleWithSpecialization(vehicle, SowingMachine) +end + +function CpFieldWorkJobParameters:isUnloadingDisabled() + if self:isUnloadRefillTargetDisabled() then + return true end + local vehicle = self.job:getVehicle() + if not vehicle then + return false + end + return not AIUtil.hasChildVehicleWithSpecialization(vehicle, ForageWagon) +end + +function CpFieldWorkJobParameters:isRunCounterDisabled() return true end +function CpFieldWorkJobParameters:isUnloadRefillDisabled() + + local vehicle = self.job:getVehicle() + if not vehicle then + return false + end + if self:isLoadingDisabled() and self:isUnloadingDisabled() then + return true + end + return not self.unloadRefillActivated:getValue() +end + +function CpFieldWorkJobParameters:isUnloadRefillFillTypeDisabled() + return self:isUnloadRefillDisabled() or self:isLoadingDisabled() +end + +function CpFieldWorkJobParameters:isUnloadRefillExtraDisabled() + local vehicle = self.job:getVehicle() + if not vehicle then + return false + end + return self:isUnloadRefillDisabled() or + not (AIUtil.hasChildVehicleWithSpecialization(vehicle, Sprayer) and + AIUtil.hasChildVehicleWithSpecialization(vehicle, SowingMachine)) +end + +function CpFieldWorkJobParameters:onChangeStartUnloadRefillTargetPoint(setting) + +end + --- AI parameters for the bale finder job. ---@class CpBaleFinderJobParameters : CpJobParameters CpBaleFinderJobParameters = CpObject(CpJobParameters) @@ -286,6 +381,14 @@ function CpBaleFinderJobParameters:hasBaleLoader() return true end +function CpBaleFinderJobParameters:isUnloadTargetPointDisabled() + return not self:hasBaleLoader() +end + +function CpBaleFinderJobParameters:isBaleWrapSettingVisible() + return self:hasBaleLoader() +end + --- AI parameters for the bale finder job. ---@class CpCombineUnloaderJobParameters : CpJobParameters ---@field useGiantsUnload AIParameterBooleanSetting @@ -298,24 +401,20 @@ function CpCombineUnloaderJobParameters:init(job) end function CpCombineUnloaderJobParameters:isGiantsUnloadDisabled() - return self:hasPipe() or self.useFieldUnload:getValue() -end - -function CpCombineUnloaderJobParameters:isFieldUnloadDisabled() - return self.useGiantsUnload:getValue() + return self:hasPipe() or + self.unloadWith:getValue() ~= CpCombineUnloaderJobParameters.UNLOAD_WITH_GIANTS end -function CpCombineUnloaderJobParameters:isUnloadStationSelectorVisible() - return not self:isGiantsUnloadDisabled() and self.useGiantsUnload:getValue() +function CpCombineUnloaderJobParameters:isUnloadWithStreetModeDisabled() + return self:hasPipe() or + self.unloadWith:getValue() ~= CpCombineUnloaderJobParameters.UNLOAD_WITH_STREET_MODE end -function CpCombineUnloaderJobParameters:isFieldUnloadPositionSelectorDisabled() - return self:isFieldUnloadDisabled() or not self.useFieldUnload:getValue() +function CpCombineUnloaderJobParameters:isFieldUnloadDisabled() + return self.unloadWith:getValue() ~= CpCombineUnloaderJobParameters.UNLOAD_ON_FIELD end - - function CpCombineUnloaderJobParameters:isFieldUnloadTipSideDisabled() - return self:isFieldUnloadDisabled() or self:hasPipe() or not self.useFieldUnload:getValue() + return self:isFieldUnloadDisabled() or self:hasPipe() end function CpCombineUnloaderJobParameters:hasPipe() @@ -323,6 +422,7 @@ function CpCombineUnloaderJobParameters:hasPipe() if vehicle then return AIUtil.hasChildVehicleWithSpecialization(vehicle, Pipe) end + return false end --- Inserts the current available unloading stations into the setting values/texts. @@ -482,7 +582,7 @@ function CpStreetJobParameters:generateFillTypes() local texts = {} -- local vehicle = self.job and self.job:getVehicle() -- if vehicle then - -- fillTypes = AIUtil.getAllValidFillTypes(vehicle, function() + -- fillTypes = FillLevelUtil.getAllValidFillTypes(vehicle, function() -- return true -- end) -- for _, f in pairs(fillTypes) do diff --git a/scripts/ai/util/FillLevelUtil.lua b/scripts/ai/util/FillLevelUtil.lua index 1a4ca55f0..4dc7db39c 100644 --- a/scripts/ai/util/FillLevelUtil.lua +++ b/scripts/ai/util/FillLevelUtil.lua @@ -248,4 +248,50 @@ function FillLevelUtil.getTrailerFillLevels(trailer) end end return totalFillLevel, totalCapacity, totalFreeCapacity +end + + +--- Gets all currently possible fill types. +---@param vehicle table +---@param lambda function|nil +---@return table[] all fill types +---@return table fill types as keys +function FillLevelUtil.getAllValidFillTypes(vehicle, lambda, ...) + local fillTypes, fillTypesByIndex = {}, {} + for _, v in pairs(vehicle:getChildVehicles()) do + if v.getFillUnits then + for ix, _ in pairs(v:getFillUnits()) do + for fillType, state in pairs(v:getFillUnitSupportedFillTypes(ix)) do + if state then + if not fillTypesByIndex[fillType] and + (lambda == nil or lambda(fillType, ...)) then + fillTypesByIndex[fillType] = true + table.insert(fillTypes, fillType) + end + end + end + end + end + end + return fillTypes, fillTypesByIndex +end + +--- Gets all discharge nodes. +---@param vehicle table +---@param lambda function|nil +---@return table[] +---@return table +function FillLevelUtil.getAllDischargeNodes(vehicle, lambda, ...) + local dischargeNodes, dischargeNodeToObject = {}, {} + for _, v in pairs(vehicle:getChildVehicles()) do + if v.spec_dischargeable then + for _, node in pairs(v.spec_dischargeable.dischargeNodes) do + if lambda == nil or lambda(node, ...) then + dischargeNodeToObject[node] = v + table.insert(dischargeNodes, node) + end + end + end + end + return dischargeNodes, dischargeNodeToObject end \ No newline at end of file diff --git a/scripts/gui/pages/CpCourseGeneratorFrame.lua b/scripts/gui/pages/CpCourseGeneratorFrame.lua index d33b6e1e1..bea5a0be3 100644 --- a/scripts/gui/pages/CpCourseGeneratorFrame.lua +++ b/scripts/gui/pages/CpCourseGeneratorFrame.lua @@ -930,6 +930,7 @@ function CpCourseGeneratorFrame:updateParameterValueTexts() invalidElement:setVisible(not parameter:getIsValid() and parameter:getCanBeChanged()) end element:setDisabled(not parameter:getCanBeChanged()) + element:setVisible(parameter.getIsVisible == nil or parameter:getIsVisible()) local parameterType = parameter:getType() if parameterType == AIParameterType.TEXT then local title = element:getDescendantByName("title") @@ -1661,6 +1662,7 @@ function CpCourseGeneratorFrame:setActiveJobTypeSelection(jobTypeIndex) element:updateTitle() end element:setDisabled(not item:getCanBeChanged()) + element:setVisible(item.getIsVisible == nil or item:getIsVisible()) table.insert(self.currentJobElements, element) end end From 0e92cfede87fb2dab5790ee9d91ca94f3bf88fb7 Mon Sep 17 00:00:00 2001 From: schwiti6190 Date: Tue, 1 Apr 2025 19:57:18 +0000 Subject: [PATCH 43/73] Updated translations --- translations/translation_br.xml | 17 ++++++++++++----- translations/translation_cs.xml | 17 ++++++++++++----- translations/translation_ct.xml | 17 ++++++++++++----- translations/translation_cz.xml | 17 ++++++++++++----- translations/translation_da.xml | 17 ++++++++++++----- translations/translation_de.xml | 17 ++++++++++++----- translations/translation_ea.xml | 17 ++++++++++++----- translations/translation_en.xml | 17 ++++++++++++----- translations/translation_es.xml | 17 ++++++++++++----- translations/translation_fc.xml | 17 ++++++++++++----- translations/translation_fi.xml | 17 ++++++++++++----- translations/translation_fr.xml | 17 ++++++++++++----- translations/translation_hu.xml | 17 ++++++++++++----- translations/translation_id.xml | 17 ++++++++++++----- translations/translation_it.xml | 17 ++++++++++++----- translations/translation_jp.xml | 17 ++++++++++++----- translations/translation_kr.xml | 17 ++++++++++++----- translations/translation_nl.xml | 17 ++++++++++++----- translations/translation_no.xml | 17 ++++++++++++----- translations/translation_pl.xml | 17 ++++++++++++----- translations/translation_pt.xml | 17 ++++++++++++----- translations/translation_ro.xml | 17 ++++++++++++----- translations/translation_ru.xml | 17 ++++++++++++----- translations/translation_sv.xml | 17 ++++++++++++----- translations/translation_tr.xml | 17 ++++++++++++----- translations/translation_uk.xml | 17 ++++++++++++----- translations/translation_vi.xml | 17 ++++++++++++----- 27 files changed, 324 insertions(+), 135 deletions(-) diff --git a/translations/translation_br.xml b/translations/translation_br.xml index 8b998e337..7de83f472 100644 --- a/translations/translation_br.xml +++ b/translations/translation_br.xml @@ -28,8 +28,11 @@ + + + - + @@ -62,6 +65,8 @@ + + @@ -69,13 +74,15 @@ - - - - + + + + + + diff --git a/translations/translation_cs.xml b/translations/translation_cs.xml index 8b89ae25f..10e8b4739 100644 --- a/translations/translation_cs.xml +++ b/translations/translation_cs.xml @@ -28,8 +28,11 @@ + + + - + @@ -62,6 +65,8 @@ + + @@ -69,13 +74,15 @@ - - - - + + + + + + diff --git a/translations/translation_ct.xml b/translations/translation_ct.xml index 347553f44..5b32247e9 100644 --- a/translations/translation_ct.xml +++ b/translations/translation_ct.xml @@ -28,8 +28,11 @@ + + + - + @@ -62,6 +65,8 @@ + + @@ -69,13 +74,15 @@ - - - - + + + + + + diff --git a/translations/translation_cz.xml b/translations/translation_cz.xml index 4d8b10fe8..4ee32bd60 100644 --- a/translations/translation_cz.xml +++ b/translations/translation_cz.xml @@ -28,8 +28,11 @@ + + + - + @@ -62,6 +65,8 @@ + + @@ -69,13 +74,15 @@ - - - - + + + + + + diff --git a/translations/translation_da.xml b/translations/translation_da.xml index f04080697..e3c32e8bb 100644 --- a/translations/translation_da.xml +++ b/translations/translation_da.xml @@ -28,8 +28,11 @@ + + + - + @@ -62,6 +65,8 @@ + + @@ -69,13 +74,15 @@ - - - - + + + + + + diff --git a/translations/translation_de.xml b/translations/translation_de.xml index 31f5fa178..302d7b1d6 100644 --- a/translations/translation_de.xml +++ b/translations/translation_de.xml @@ -28,8 +28,11 @@ + + + - + @@ -62,6 +65,8 @@ + + @@ -69,13 +74,15 @@ - - - - + + + + + + diff --git a/translations/translation_ea.xml b/translations/translation_ea.xml index 429ff7fdd..41372840c 100644 --- a/translations/translation_ea.xml +++ b/translations/translation_ea.xml @@ -28,8 +28,11 @@ + + + - + @@ -62,6 +65,8 @@ + + @@ -69,13 +74,15 @@ - - - - + + + + + + diff --git a/translations/translation_en.xml b/translations/translation_en.xml index 18e923618..62080f69b 100644 --- a/translations/translation_en.xml +++ b/translations/translation_en.xml @@ -28,8 +28,11 @@ + + + - + @@ -62,6 +65,8 @@ + + @@ -69,13 +74,15 @@ - - - - + + + + + + diff --git a/translations/translation_es.xml b/translations/translation_es.xml index 96d4f828a..632a7b149 100644 --- a/translations/translation_es.xml +++ b/translations/translation_es.xml @@ -28,8 +28,11 @@ + + + - + @@ -62,6 +65,8 @@ + + @@ -69,13 +74,15 @@ - - - - + + + + + + diff --git a/translations/translation_fc.xml b/translations/translation_fc.xml index d753cb146..de968b230 100644 --- a/translations/translation_fc.xml +++ b/translations/translation_fc.xml @@ -28,8 +28,11 @@ + + + - + @@ -62,6 +65,8 @@ + + @@ -69,13 +74,15 @@ - - - - + + + + + + diff --git a/translations/translation_fi.xml b/translations/translation_fi.xml index 657b7141f..aa2a1e261 100644 --- a/translations/translation_fi.xml +++ b/translations/translation_fi.xml @@ -28,8 +28,11 @@ + + + - + @@ -62,6 +65,8 @@ + + @@ -69,13 +74,15 @@ - - - - + + + + + + diff --git a/translations/translation_fr.xml b/translations/translation_fr.xml index f182729d4..de7051eaa 100644 --- a/translations/translation_fr.xml +++ b/translations/translation_fr.xml @@ -28,8 +28,11 @@ + + + - + @@ -62,6 +65,8 @@ + + @@ -69,13 +74,15 @@ - - - - + + + + + + diff --git a/translations/translation_hu.xml b/translations/translation_hu.xml index 0b6675bf7..c0aa25809 100644 --- a/translations/translation_hu.xml +++ b/translations/translation_hu.xml @@ -28,8 +28,11 @@ + + + - + @@ -62,6 +65,8 @@ + + @@ -69,13 +74,15 @@ - - - - + + + + + + diff --git a/translations/translation_id.xml b/translations/translation_id.xml index ff907d835..e5cc698b5 100644 --- a/translations/translation_id.xml +++ b/translations/translation_id.xml @@ -28,8 +28,11 @@ + + + - + @@ -62,6 +65,8 @@ + + @@ -69,13 +74,15 @@ - - - - + + + + + + diff --git a/translations/translation_it.xml b/translations/translation_it.xml index 4071284ea..4f5757a43 100644 --- a/translations/translation_it.xml +++ b/translations/translation_it.xml @@ -28,8 +28,11 @@ + + + - + @@ -62,6 +65,8 @@ + + @@ -69,13 +74,15 @@ - - - - + + + + + + diff --git a/translations/translation_jp.xml b/translations/translation_jp.xml index bdbdf240d..6b485220a 100644 --- a/translations/translation_jp.xml +++ b/translations/translation_jp.xml @@ -28,8 +28,11 @@ + + + - + @@ -62,6 +65,8 @@ + + @@ -69,13 +74,15 @@ - - - - + + + + + + diff --git a/translations/translation_kr.xml b/translations/translation_kr.xml index 386ada766..16bd301e2 100644 --- a/translations/translation_kr.xml +++ b/translations/translation_kr.xml @@ -28,8 +28,11 @@ + + + - + @@ -62,6 +65,8 @@ + + @@ -69,13 +74,15 @@ - - - - + + + + + + diff --git a/translations/translation_nl.xml b/translations/translation_nl.xml index 98e0995c5..5c9b959e1 100644 --- a/translations/translation_nl.xml +++ b/translations/translation_nl.xml @@ -28,8 +28,11 @@ + + + - + @@ -62,6 +65,8 @@ + + @@ -69,13 +74,15 @@ - - - - + + + + + + diff --git a/translations/translation_no.xml b/translations/translation_no.xml index 3f3e5a255..f94cfb4d9 100644 --- a/translations/translation_no.xml +++ b/translations/translation_no.xml @@ -28,8 +28,11 @@ + + + - + @@ -62,6 +65,8 @@ + + @@ -69,13 +74,15 @@ - - - - + + + + + + diff --git a/translations/translation_pl.xml b/translations/translation_pl.xml index 2bcfc61ae..a51065d53 100644 --- a/translations/translation_pl.xml +++ b/translations/translation_pl.xml @@ -28,8 +28,11 @@ + + + - + @@ -62,6 +65,8 @@ + + @@ -69,13 +74,15 @@ - - - - + + + + + + diff --git a/translations/translation_pt.xml b/translations/translation_pt.xml index 9184c0055..a2bccdf0d 100644 --- a/translations/translation_pt.xml +++ b/translations/translation_pt.xml @@ -28,8 +28,11 @@ + + + - + @@ -62,6 +65,8 @@ + + @@ -69,13 +74,15 @@ - - - - + + + + + + diff --git a/translations/translation_ro.xml b/translations/translation_ro.xml index 1a0b42356..c93bc148a 100644 --- a/translations/translation_ro.xml +++ b/translations/translation_ro.xml @@ -28,8 +28,11 @@ + + + - + @@ -62,6 +65,8 @@ + + @@ -69,13 +74,15 @@ - - - - + + + + + + diff --git a/translations/translation_ru.xml b/translations/translation_ru.xml index e7385591f..fc4807a17 100644 --- a/translations/translation_ru.xml +++ b/translations/translation_ru.xml @@ -28,8 +28,11 @@ + + + - + @@ -62,6 +65,8 @@ + + @@ -69,13 +74,15 @@ - - - - + + + + + + diff --git a/translations/translation_sv.xml b/translations/translation_sv.xml index 02df8019d..4cb7a4ebb 100644 --- a/translations/translation_sv.xml +++ b/translations/translation_sv.xml @@ -28,8 +28,11 @@ + + + - + @@ -62,6 +65,8 @@ + + @@ -69,13 +74,15 @@ - - - - + + + + + + diff --git a/translations/translation_tr.xml b/translations/translation_tr.xml index 0d0a3cf0a..21492df5d 100644 --- a/translations/translation_tr.xml +++ b/translations/translation_tr.xml @@ -28,8 +28,11 @@ + + + - + @@ -62,6 +65,8 @@ + + @@ -69,13 +74,15 @@ - - - - + + + + + + diff --git a/translations/translation_uk.xml b/translations/translation_uk.xml index 5c22094da..f948bc13e 100644 --- a/translations/translation_uk.xml +++ b/translations/translation_uk.xml @@ -28,8 +28,11 @@ + + + - + @@ -62,6 +65,8 @@ + + @@ -69,13 +74,15 @@ - - - - + + + + + + diff --git a/translations/translation_vi.xml b/translations/translation_vi.xml index e90fcd395..cfc24e274 100644 --- a/translations/translation_vi.xml +++ b/translations/translation_vi.xml @@ -28,8 +28,11 @@ + + + - + @@ -62,6 +65,8 @@ + + @@ -69,13 +74,15 @@ - - - - + + + + + + From 3fd613ef947a6e724a84374ae2076ec114222991 Mon Sep 17 00:00:00 2001 From: David Schwietering Date: Wed, 2 Apr 2025 22:16:24 +0200 Subject: [PATCH 44/73] WIP some more ingame menu gui improvements --- config/gui/pages/CourseGeneratorFrame.xml | 9 +- .../CombineUnloaderJobParameterSetup.xml | 4 +- .../jobParameters/StreetJobParameterSetup.xml | 8 +- modDesc.xml | 3 +- scripts/CpSettingsUtil.lua | 2 +- scripts/ai/jobs/CpAIJob.lua | 2 +- scripts/ai/jobs/CpJobParameters.lua | 2 +- scripts/ai/parameters/AIParameterSetting.lua | 4 + .../CpAIParameterFillTypeSelection.lua | 9 +- scripts/ai/parameters/CpAIParameterGroup.lua | 37 ++++++ scripts/graph/Graph.lua | 4 +- scripts/gui/pages/CpCourseGeneratorFrame.lua | 124 ++++++++++++------ 12 files changed, 147 insertions(+), 61 deletions(-) create mode 100644 scripts/ai/parameters/CpAIParameterGroup.lua diff --git a/config/gui/pages/CourseGeneratorFrame.xml b/config/gui/pages/CourseGeneratorFrame.xml index da191c2f0..6a04a684a 100644 --- a/config/gui/pages/CourseGeneratorFrame.xml +++ b/config/gui/pages/CourseGeneratorFrame.xml @@ -72,13 +72,14 @@ - + - 1 - 2 - 3 + 2 + 3 4 diff --git a/config/jobParameters/StreetJobParameterSetup.xml b/config/jobParameters/StreetJobParameterSetup.xml index ecd7c9ef3..0e436185d 100644 --- a/config/jobParameters/StreetJobParameterSetup.xml +++ b/config/jobParameters/StreetJobParameterSetup.xml @@ -24,9 +24,9 @@ - - - - + + + + diff --git a/modDesc.xml b/modDesc.xml index 09c0c05b3..263b7481e 100644 --- a/modDesc.xml +++ b/modDesc.xml @@ -262,7 +262,8 @@ Changelog 8.0.0.0: - + + diff --git a/scripts/CpSettingsUtil.lua b/scripts/CpSettingsUtil.lua index d14791152..598967fb9 100644 --- a/scripts/CpSettingsUtil.lua +++ b/scripts/CpSettingsUtil.lua @@ -385,7 +385,7 @@ end ---@param class table function CpSettingsUtil.generateAiJobGuiElementsFromSettingsTable(settingsBySubTitle, class, settings) for _, data in ipairs(settingsBySubTitle) do - local parameterGroup = AIParameterGroup.new(g_i18n:getText(data.title)) + local parameterGroup = CpAIParameterGroup(settings, data) for _, setting in ipairs(data.elements) do local s = settings[setting:getName()] parameterGroup:addParameter(s) diff --git a/scripts/ai/jobs/CpAIJob.lua b/scripts/ai/jobs/CpAIJob.lua index d11c8c256..49cfedef5 100644 --- a/scripts/ai/jobs/CpAIJob.lua +++ b/scripts/ai/jobs/CpAIJob.lua @@ -46,7 +46,7 @@ end function CpAIJob:setupJobParameters() self.vehicleParameter = AIParameterVehicle.new() self:addNamedParameter("vehicle", self.vehicleParameter) - local vehicleGroup = AIParameterGroup.new(g_i18n:getText("ai_parameterGroupTitleVehicle")) + local vehicleGroup = CpAIParameterGroup(nil, {title = "ai_parameterGroupTitleVehicle"}) vehicleGroup:addParameter(self.vehicleParameter) table.insert(self.groupedParameters, vehicleGroup) end diff --git a/scripts/ai/jobs/CpJobParameters.lua b/scripts/ai/jobs/CpJobParameters.lua index b301776a3..1ff71a7b0 100644 --- a/scripts/ai/jobs/CpJobParameters.lua +++ b/scripts/ai/jobs/CpJobParameters.lua @@ -274,7 +274,7 @@ function CpFieldWorkJobParameters:isLaneOffsetDisabled() end function CpFieldWorkJobParameters:isUnloadRefillTargetDisabled() - return true --- TODO: Added check for refill/unload possibilities ... + return false --- TODO: Added check for refill/unload possibilities ... end diff --git a/scripts/ai/parameters/AIParameterSetting.lua b/scripts/ai/parameters/AIParameterSetting.lua index f1004ff32..0f3d9ae0d 100644 --- a/scripts/ai/parameters/AIParameterSetting.lua +++ b/scripts/ai/parameters/AIParameterSetting.lua @@ -60,6 +60,10 @@ function AIParameterSetting:getString() return "" end +function AIParameterSetting:getCustomIconFilename() + return nil +end + function AIParameterSetting:getIsValid() return self.isValid end diff --git a/scripts/ai/parameters/CpAIParameterFillTypeSelection.lua b/scripts/ai/parameters/CpAIParameterFillTypeSelection.lua index 75008dd38..de34a6033 100644 --- a/scripts/ai/parameters/CpAIParameterFillTypeSelection.lua +++ b/scripts/ai/parameters/CpAIParameterFillTypeSelection.lua @@ -1,9 +1,9 @@ ---@class CpAIParameterFillTypeSetting : AIParameterSetting CpAIParameterFillTypeSetting = CpObject(AIParameterSetting) - +AIParameterType.TEXT_BUTTON = 99 function CpAIParameterFillTypeSetting:init(data, vehicle, class) AIParameterSetting.init(self, data, vehicle, class) - self.guiParameterType = AIParameterType.TEXT --- For the giants gui element. + self.guiParameterType = AIParameterType.TEXT_BUTTON --- For the giants gui element. self:initFromData(data, vehicle, class) --- Use this hack to load the setting without the need for changing every setting behavior. local filePath = Utils.getFilename("config/jobParameters/FillTypeSelectionParameterSetup.xml", @@ -147,4 +147,9 @@ end function CpAIParameterFillTypeSetting:getIsCounterValid() return self.currentCounterValue <= self.counter:getValue() +end + +function CpAIParameterFillTypeSetting:getCustomIconFilename() + local fillType = g_fillTypeManager:getFillTypeByIndex(self.fillType:getValue()) + return fillType and fillType.hudOverlayFilename end \ No newline at end of file diff --git a/scripts/ai/parameters/CpAIParameterGroup.lua b/scripts/ai/parameters/CpAIParameterGroup.lua new file mode 100644 index 000000000..bf3625c2a --- /dev/null +++ b/scripts/ai/parameters/CpAIParameterGroup.lua @@ -0,0 +1,37 @@ +---@class CpAIParameterGroup +CpAIParameterGroup = CpObject() +function CpAIParameterGroup:init(class, data) + self.class = class + self.data = data + self.parameters = {} +end + +---@return string +function CpAIParameterGroup:getTitle() + return g_i18n:getText(self.data.title) +end + +---@param parameter AIParameterSetting +function CpAIParameterGroup:addParameter(parameter) + table.insert(self.parameters, parameter) +end + +---@return AIParameterSetting[] +function CpAIParameterGroup:getParameters() + return self.parameters +end + +function CpAIParameterGroup:getIsDisabled() + return self:getCallback(self.data.isDisabledFunc) +end + +function CpAIParameterGroup:getIsVisible() + return self.data.isVisibleFunc == nil or self:getCallback(self.data.isVisibleFunc) +end + +--- Gets the result from a class callback. +function CpAIParameterGroup:getCallback(callbackStr, ...) + if callbackStr and self.class and self.class[callbackStr] then + return self.class[callbackStr](self.class, self, ...) + end +end \ No newline at end of file diff --git a/scripts/graph/Graph.lua b/scripts/graph/Graph.lua index ee770fc21..8367f4fb0 100644 --- a/scripts/graph/Graph.lua +++ b/scripts/graph/Graph.lua @@ -165,7 +165,7 @@ function Graph:loadFromXMLFile(xmlFile, baseKey) self:appendChildNode(segment) end) self._hasGeneratedSplines = xmlFile:getValue( - baseKey .. self.XML_KEY .. "#hasGeneratedSplines") + baseKey .. self.XML_KEY .. "#hasGeneratedSplines", false) end function Graph:saveToXMLFile(xmlFile, baseKey) @@ -174,7 +174,7 @@ function Graph:saveToXMLFile(xmlFile, baseKey) baseKey .. self.XML_KEY, GraphSegment.XML_KEY, i - 1)) end xmlFile:setValue(baseKey .. self.XML_KEY .. "#hasGeneratedSplines", - self._hasGeneratedSplines) + self._hasGeneratedSplines or false) end ---@param node GraphNode diff --git a/scripts/gui/pages/CpCourseGeneratorFrame.lua b/scripts/gui/pages/CpCourseGeneratorFrame.lua index bea5a0be3..eb932a95d 100644 --- a/scripts/gui/pages/CpCourseGeneratorFrame.lua +++ b/scripts/gui/pages/CpCourseGeneratorFrame.lua @@ -776,8 +776,17 @@ function CpCourseGeneratorFrame:onClickMultiTextOptionParameter(index, element) self:validateParameters() end -function CpCourseGeneratorFrame:onClickMultiTextOptionCenterParameter() - +function CpCourseGeneratorFrame:onClickMultiTextOptionCenterParameter(element) + local param = element.aiParameter + if self.currentJob ~= nil then + if param and param:getType() == AIParameterType.TEXT_BUTTON then + param.fillType:setNextItem() + print("onclick") + end + self.currentJob:onParameterValueChanged(element.aiParameter) + self:updateParameterValueTexts() + end + self:validateParameters() end function CpCourseGeneratorFrame:executePickingCallback(...) @@ -909,11 +918,13 @@ function CpCourseGeneratorFrame:onFieldBoundaryDetectionFinished(isValid, errorT end function CpCourseGeneratorFrame:updateWarnings() - for _, element in ipairs(self.currentJobElements) do - local parameter = element.aiParameter - local invalidElement = element:getDescendantByName("invalid") - if invalidElement ~= nil then - invalidElement:setVisible(not parameter:getIsValid() and parameter:getCanBeChanged()) + for _, group in ipairs(self.currentJobElements) do + for _, element in ipairs(group.elements) do + local parameter = element.aiParameter + local invalidElement = element:getDescendantByName("invalid") + if invalidElement ~= nil then + invalidElement:setVisible(not parameter:getIsValid() and parameter:getCanBeChanged()) + end end end end @@ -923,40 +934,54 @@ function CpCourseGeneratorFrame:updateParameterValueTexts() g_currentMission:removeMapHotspot(self.fieldSiloAiTargetMapHotspot) g_currentMission:removeMapHotspot(self.unloadAiTargetMapHotspot) g_currentMission:removeMapHotspot(self.loadAiTargetMapHotspot) - for _, element in ipairs(self.currentJobElements) do - local parameter = element.aiParameter - local invalidElement = element:getDescendantByName("invalid") - if invalidElement ~= nil then - invalidElement:setVisible(not parameter:getIsValid() and parameter:getCanBeChanged()) - end - element:setDisabled(not parameter:getCanBeChanged()) - element:setVisible(parameter.getIsVisible == nil or parameter:getIsVisible()) - local parameterType = parameter:getType() - if parameterType == AIParameterType.TEXT then - local title = element:getDescendantByName("title") - - title:setText(parameter:getString()) - elseif parameter.is_a and parameter:is_a(CpAIParameterPosition) then - element:setText(parameter:getString()) - if parameter:getPositionType() == CpAIParameterPositionAngle.POSITION_TYPES.DRIVE_TO then - if parameter:applyToMapHotspot(self.driveToAiTargetMapHotspot) then - g_currentMission:addMapHotspot(self.driveToAiTargetMapHotspot) - end - elseif parameter:getPositionType() == CpAIParameterPositionAngle.POSITION_TYPES.FIELD_OR_SILO then - if parameter:applyToMapHotspot(self.fieldSiloAiTargetMapHotspot) then - g_currentMission:addMapHotspot(self.fieldSiloAiTargetMapHotspot) - end - elseif parameter:getPositionType() == CpAIParameterPositionAngle.POSITION_TYPES.UNLOAD then - if parameter:applyToMapHotspot(self.unloadAiTargetMapHotspot) then - g_currentMission:addMapHotspot(self.unloadAiTargetMapHotspot) + for _, groupElement in ipairs(self.currentJobElements) do + groupElement.title:setVisible(groupElement.group:getIsVisible()) + for _, element in ipairs(groupElement.elements) do + local parameter = element.aiParameter + local invalidElement = element:getDescendantByName("invalid") + if invalidElement ~= nil then + invalidElement:setVisible(not parameter:getIsValid() and parameter:getCanBeChanged()) + end + local icon = element:getDescendantByName("icon") + if icon ~= nil and parameter.getCustomIconFilename then + local filename = parameter:getCustomIconFilename() + if filename then + icon:setImageFilename(parameter:getCustomIconFilename()) + icon:setImageUVs(0,0,0,0,1,1,0,1,1) end - elseif parameter:getPositionType() == CpAIParameterPositionAngle.POSITION_TYPES.LOAD then - if parameter:applyToMapHotspot(self.loadAiTargetMapHotspot) then - g_currentMission:addMapHotspot(self.loadAiTargetMapHotspot) + icon:setVisible(filename ~= nil) + end + element:setDisabled(groupElement.group:getIsDisabled() or not parameter:getCanBeChanged()) + element:setVisible(groupElement.group:getIsVisible() and (parameter.getIsVisible == nil or parameter:getIsVisible())) + local parameterType = parameter:getType() + if parameterType == AIParameterType.TEXT then + local title = element:getDescendantByName("title") + + title:setText(parameter:getString()) + elseif parameterType == AIParameterType.TEXT_BUTTON then + element:setText(parameter:getString()) + elseif parameter.is_a and parameter:is_a(CpAIParameterPosition) then + element:setText(parameter:getString()) + if parameter:getPositionType() == CpAIParameterPositionAngle.POSITION_TYPES.DRIVE_TO then + if parameter:applyToMapHotspot(self.driveToAiTargetMapHotspot) then + g_currentMission:addMapHotspot(self.driveToAiTargetMapHotspot) + end + elseif parameter:getPositionType() == CpAIParameterPositionAngle.POSITION_TYPES.FIELD_OR_SILO then + if parameter:applyToMapHotspot(self.fieldSiloAiTargetMapHotspot) then + g_currentMission:addMapHotspot(self.fieldSiloAiTargetMapHotspot) + end + elseif parameter:getPositionType() == CpAIParameterPositionAngle.POSITION_TYPES.UNLOAD then + if parameter:applyToMapHotspot(self.unloadAiTargetMapHotspot) then + g_currentMission:addMapHotspot(self.unloadAiTargetMapHotspot) + end + elseif parameter:getPositionType() == CpAIParameterPositionAngle.POSITION_TYPES.LOAD then + if parameter:applyToMapHotspot(self.loadAiTargetMapHotspot) then + g_currentMission:addMapHotspot(self.loadAiTargetMapHotspot) + end end + elseif element.updateTitle then + element:updateTitle() end - elseif element.updateTitle then - element:updateTitle() end end end @@ -1633,9 +1658,12 @@ function CpCourseGeneratorFrame:setActiveJobTypeSelection(jobTypeIndex) self.currentJobElements = {} for _, group in ipairs(self.currentJob:getGroupedParameters()) do local titleElement = self.createTitleTemplate:clone(self.jobMenuLayout) - titleElement:setText(group:getTitle()) - + local groupElements = { + group = group, + title = titleElement, + elements = {} + } for _, item in ipairs(group:getParameters()) do local element = nil local parameterType = item:getType() @@ -1651,21 +1679,31 @@ function CpCourseGeneratorFrame:setActiveJobTypeSelection(jobTypeIndex) parameterType == AIParameterType.FILLTYPE then element = self.createMultiOptionTemplate:clone(self.jobMenuLayout) - element:setDataSource(item) + elseif parameterType == AIParameterType.TEXT_BUTTON then + element = self.createButtonTemplate:clone(self.jobMenuLayout) + element:setText(item:getString()) end if element then FocusManager:loadElementFromCustomValues(element) - element.aiParameter = item if element.updateTitle then element:updateTitle() end element:setDisabled(not item:getCanBeChanged()) element:setVisible(item.getIsVisible == nil or item:getIsVisible()) - table.insert(self.currentJobElements, element) + local icon = element:getDescendantByName("icon") + if icon ~= nil and item.getCustomIconFilename then + local filename = item:getCustomIconFilename() + if filename then + icon:setImageFilename(filename) + end + icon:setVisible(filename ~= nil) + end + table.insert(groupElements.elements, element) end end + table.insert(self.currentJobElements, groupElements) end self:validateParameters() self:updateParameterValueTexts() From 39f8890430ecaf354829f78fd3117bcbb0d79068 Mon Sep 17 00:00:00 2001 From: Tensuko Date: Wed, 2 Apr 2025 19:57:50 +0200 Subject: [PATCH 45/73] max iteration and radius increase --- scripts/graph/Graph.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/graph/Graph.lua b/scripts/graph/Graph.lua index 8367f4fb0..ead3f1c87 100644 --- a/scripts/graph/Graph.lua +++ b/scripts/graph/Graph.lua @@ -44,7 +44,7 @@ function Graph:consoleCommandFindPathTo(name) if vehicle == nil then return "Must be in a vehicle!" end - local pathfinder = GraphPathfinder(1000, 500, 20, edges) + local pathfinder = GraphPathfinder(1000, 800, 25, edges) local start = PathfinderUtil.getVehiclePositionAsState3D(vehicle) local goal = State3D(targetPos.x, targetPos.y, 0, 0) CpUtil.info("Goal: %s", tostring(goal)) From 5a973e161a7ad7e8851a65357964e30a5cb6ace2 Mon Sep 17 00:00:00 2001 From: Tensuko Date: Thu, 3 Apr 2025 19:17:43 +0200 Subject: [PATCH 46/73] Same as in Graph.lua --- scripts/ai/strategies/AIDriveStrategyStreetDriveToPoint.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ai/strategies/AIDriveStrategyStreetDriveToPoint.lua b/scripts/ai/strategies/AIDriveStrategyStreetDriveToPoint.lua index 4e81c8211..75f670b54 100644 --- a/scripts/ai/strategies/AIDriveStrategyStreetDriveToPoint.lua +++ b/scripts/ai/strategies/AIDriveStrategyStreetDriveToPoint.lua @@ -74,7 +74,7 @@ function AIDriveStrategyStreetDriveToPoint:getDriveData(dt, vX, vY, vZ) self:setMaxSpeed(0) if self.target then self.vehicle:prepareForAIDriving() - local pathfinder = GraphPathfinder(1000, 500, 20, g_graph:getGraphEdges()) + local pathfinder = GraphPathfinder(1000, 800, 25, g_graph:getGraphEdges()) local start = PathfinderUtil.getVehiclePositionAsState3D(self.vehicle) local targetVector = self.target:toVector() local goal = State3D(targetVector.x, targetVector.y, 0, 0) From 2ee21ec942a107e6bb3b046f523ca26a5bf2ebc5 Mon Sep 17 00:00:00 2001 From: David Schwietering Date: Sat, 5 Apr 2025 10:13:26 +0200 Subject: [PATCH 47/73] Added target point selection dialog --- Courseplay.lua | 2 +- .../gui/dialog/TargetPointSelectionDialog.xml | 178 ++++++++++++++++++ .../BaleFinderJobParameterSetup.xml | 6 +- .../BunkerSiloJobParameterSetup.xml | 3 +- .../CombineUnloaderJobParameterSetup.xml | 6 +- .../FieldWorkJobParameterSetup.xml | 9 +- .../SiloLoaderJobParameterSetup.xml | 5 +- .../jobParameters/StreetJobParameterSetup.xml | 21 ++- modDesc.xml | 1 + scripts/ai/jobs/CpJobParameters.lua | 21 +-- .../ai/parameters/AIParameterSettingList.lua | 17 ++ .../parameters/CpAIParameterTargetPoint.lua | 44 ++++- scripts/graph/Graph.lua | 21 +++ .../gui/dialog/TargetPointSelectionDialog.lua | 119 ++++++++++++ scripts/gui/pages/CpCourseGeneratorFrame.lua | 19 +- 15 files changed, 423 insertions(+), 49 deletions(-) create mode 100644 config/gui/dialog/TargetPointSelectionDialog.xml create mode 100644 scripts/gui/dialog/TargetPointSelectionDialog.lua diff --git a/Courseplay.lua b/Courseplay.lua index dd5ee438e..d1efb8d67 100644 --- a/Courseplay.lua +++ b/Courseplay.lua @@ -140,7 +140,7 @@ end function Courseplay:setupGui() CpInGameMenu.setupGui(self.courseStorage) self.infoTextsHud = CpHudInfoTexts() - + TargetPointSelectionDialog.register() --- Adding Player input bindings local function addPlayerActionEvents(self, superFunc, ...) superFunc(self, ...) diff --git a/config/gui/dialog/TargetPointSelectionDialog.xml b/config/gui/dialog/TargetPointSelectionDialog.xml new file mode 100644 index 000000000..a7ce4304b --- /dev/null +++ b/config/gui/dialog/TargetPointSelectionDialog.xml @@ -0,0 +1,178 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +