23 | function GroundAdjustedNodes.initSpecialization() |
24 | local schema = Vehicle.xmlSchema |
25 | schema:setXMLSpecializationType("GroundAdjustedNodes") |
26 | |
27 | local basePath = GroundAdjustedNodes.GROUND_ADJUSTED_NODE_XML_KEY |
28 | |
29 | schema:register(XMLValueType.NODE_INDEX, basePath .. "#node", "Ground adjusted node") |
30 | schema:register(XMLValueType.NODE_INDEX, basePath .. ".raycastNode(?)#node", "Ground adjusted raycast node") |
31 | schema:register(XMLValueType.FLOAT, basePath .. ".raycastNode(?)#distance", "Ground adjusted raycast distance", 4) |
32 | schema:register(XMLValueType.INT, basePath .. ".raycastNode(?)#updateFrame", "Defines the frame delay between two raycasts", "Number of raycasts") |
33 | |
34 | schema:register(XMLValueType.FLOAT, basePath .. "#minY", "Min. Y translation", "translation in i3d - 1") |
35 | schema:register(XMLValueType.FLOAT, basePath .. "#maxY", "Max. Y translation", "minY + 1") |
36 | schema:register(XMLValueType.FLOAT, basePath .. "#yOffset", "Y translation offset", 0) |
37 | schema:register(XMLValueType.FLOAT, basePath .. "#moveSpeed", "Move speed", 1) |
38 | schema:register(XMLValueType.BOOL, basePath .. "#resetIfNotActive", "Reset node to start translation if not active", true) |
39 | schema:register(XMLValueType.FLOAT, basePath .. "#activationTime", "In this time after the activation of the node the #moveSpeedStateChange will be used", 0) |
40 | schema:register(XMLValueType.FLOAT, basePath .. "#moveSpeedStateChange", "Move speed while node is inactive or active an in range of #activationTime", "#moveSpeed") |
41 | schema:register(XMLValueType.FLOAT, basePath .. "#updateThreshold", "Position of node will be updated if change is greater than this value", 0.002) |
42 | schema:register(XMLValueType.BOOL, basePath .. "#averageInActivePosY", "While nodes are turned off the average Y position will be used as target for all nodes", false) |
43 | |
44 | schema:setXMLSpecializationType() |
45 | end |
138 | function GroundAdjustedNodes:loadGroundAdjustedNodeFromXML(xmlFile, key, adjustedNode) |
139 | |
140 | XMLUtil.checkDeprecatedXMLElements(xmlFile, self.configFileName, key.."#index", key.."#node") --FS17 to FS19 |
141 | |
142 | local node = xmlFile:getValue(key.."#node", nil, self.components, self.i3dMappings) |
143 | if node == nil then |
144 | Logging.xmlWarning(self.xmlFile, "Missing 'node' for groundAdjustedNode '%s'!", key) |
145 | return false |
146 | end |
147 | |
148 | local x, y, z = getTranslation(node) |
149 | adjustedNode.node = node |
150 | adjustedNode.x = x |
151 | adjustedNode.y = y |
152 | adjustedNode.z = z |
153 | adjustedNode.raycastNodes = {} |
154 | |
155 | local j = 0 |
156 | while true do |
157 | local raycastKey = string.format("%s.raycastNode(%d)", key, j) |
158 | if not self.xmlFile:hasProperty(raycastKey) then |
159 | break |
160 | end |
161 | |
162 | local raycastNode = {} |
163 | if self:loadGroundAdjustedRaycastNodeFromXML(xmlFile, raycastKey, adjustedNode, raycastNode) then |
164 | table.insert(adjustedNode.raycastNodes, raycastNode) |
165 | end |
166 | |
167 | j = j + 1 |
168 | end |
169 | |
170 | if #adjustedNode.raycastNodes > 0 then |
171 | adjustedNode.minY = self.xmlFile:getValue(key.."#minY", y - 1) |
172 | adjustedNode.maxY = self.xmlFile:getValue(key.."#maxY", adjustedNode.minY + 1) |
173 | adjustedNode.yOffset = self.xmlFile:getValue(key.."#yOffset", 0) |
174 | adjustedNode.moveSpeed = (self.xmlFile:getValue(key.."#moveSpeed", 1)) / 1000 |
175 | adjustedNode.moveSpeedStateChange = (self.xmlFile:getValue(key.."#moveSpeedStateChange", 1)) / 1000 |
176 | adjustedNode.activationTime = self.xmlFile:getValue(key.."#activationTime", 0) * 1000 |
177 | adjustedNode.activationTimer = 0 |
178 | adjustedNode.resetIfNotActive = self.xmlFile:getValue(key.."#resetIfNotActive", true) |
179 | adjustedNode.updateThreshold = self.xmlFile:getValue(key.."#updateThreshold", 0.002) |
180 | |
181 | adjustedNode.inActiveY = y |
182 | adjustedNode.averageInActivePosY = self.xmlFile:getValue(key.."#averageInActivePosY", false) |
183 | |
184 | adjustedNode.targetY = y |
185 | adjustedNode.curY = y |
186 | adjustedNode.lastY = y |
187 | adjustedNode.isActive = false |
188 | else |
189 | Logging.xmlWarning(self.xmlFile, "No raycastNodes defined for groundAdjustedNode '%s'!", key) |
190 | return false |
191 | end |
192 | |
193 | return true |
194 | end |
198 | function GroundAdjustedNodes:loadGroundAdjustedRaycastNodeFromXML(xmlFile, key, groundAdjustedNode, raycastNode) |
199 | XMLUtil.checkDeprecatedXMLElements(xmlFile, self.configFileName, key.."#index", key.."#node") --FS17 to FS19 |
200 | |
201 | local node = self.xmlFile:getValue(key.."#node", nil, self.components, self.i3dMappings) |
202 | if node == nil then |
203 | Logging.xmlWarning(self.xmlFile, "Missing 'node' for groundAdjustedNodes raycast '%s'!", key) |
204 | return false |
205 | end |
206 | |
207 | if getParent(groundAdjustedNode.node) ~= getParent(node) then |
208 | Logging.xmlWarning(self.xmlFile, "Raycast node is not on the same hierarchy level as the groundAdjustedNode (%s)!", key) |
209 | return false |
210 | end |
211 | |
212 | local _,y1,_ = getTranslation(node) |
213 | raycastNode.node = node |
214 | raycastNode.yDiff = y1 - groundAdjustedNode.y |
215 | raycastNode.distance = self.xmlFile:getValue(key.."#distance", 4) |
216 | |
217 | raycastNode.history = {} |
218 | for i=1, 2 do |
219 | raycastNode.history[i] = {0, 0, 0, 0} |
220 | end |
221 | raycastNode.lastRaycastPos = {0, 0, 0, 0} |
222 | |
223 | raycastNode.updateFrame = self.xmlFile:getValue(key.."#updateFrame", -1) |
224 | raycastNode.frameCount = 1 |
225 | |
226 | return true |
227 | end |
66 | function GroundAdjustedNodes:onLoad(savegame) |
67 | local spec = self.spec_groundAdjustedNodes |
68 | |
69 | self.raycastMask = CollisionFlag.TERRAIN + CollisionFlag.STATIC_OBJECT |
70 | |
71 | spec.groundAdjustedNodes = {} |
72 | local i = 0 |
73 | while true do |
74 | local key = string.format("vehicle.groundAdjustedNodes.groundAdjustedNode(%d)", i) |
75 | if not self.xmlFile:hasProperty(key) then |
76 | break |
77 | end |
78 | |
79 | local node = {} |
80 | if self:loadGroundAdjustedNodeFromXML(self.xmlFile, key, node) then |
81 | table.insert(spec.groundAdjustedNodes, node) |
82 | end |
83 | |
84 | i = i + 1 |
85 | end |
86 | |
87 | for j=1, #spec.groundAdjustedNodes do |
88 | local adjustedNode = spec.groundAdjustedNodes[j] |
89 | for l=1, #adjustedNode.raycastNodes do |
90 | local raycastNode = adjustedNode.raycastNodes[l] |
91 | if raycastNode.updateFrame < 0 then |
92 | raycastNode.updateFrame = #spec.groundAdjustedNodes |
93 | end |
94 | raycastNode.frameCount = j % raycastNode.updateFrame + 1 |
95 | end |
96 | end |
97 | |
98 | self.lastRaycastDistance = 0 |
99 | self.lastRaycastGroundPos = {0, 0, 0} |
100 | |
101 | if #spec.groundAdjustedNodes == 0 then |
102 | SpecializationUtil.removeEventListener(self, "onUpdate", GroundAdjustedNodes) |
103 | end |
104 | end |
108 | function GroundAdjustedNodes:onUpdate(dt, isActiveForInput, isActiveForInputIgnoreSelection, isSelected) |
109 | local spec = self.spec_groundAdjustedNodes |
110 | |
111 | for _, adjustedNode in pairs(spec.groundAdjustedNodes) do |
112 | self:updateGroundAdjustedNode(adjustedNode, dt) |
113 | |
114 | if adjustedNode.targetY ~= adjustedNode.curY then |
115 | local stateChangeActive = not adjustedNode.isActive or adjustedNode.activationTimer > 0 |
116 | local moveSpeed = stateChangeActive and adjustedNode.moveSpeedStateChange or adjustedNode.moveSpeed |
117 | |
118 | if adjustedNode.targetY > adjustedNode.curY then |
119 | adjustedNode.curY = math.min(adjustedNode.curY + moveSpeed*dt, adjustedNode.targetY) |
120 | else |
121 | adjustedNode.curY = math.max(adjustedNode.curY - moveSpeed*dt, adjustedNode.targetY) |
122 | end |
123 | |
124 | if math.abs(adjustedNode.lastY - adjustedNode.curY) > adjustedNode.updateThreshold then |
125 | setTranslation(adjustedNode.node, adjustedNode.x, adjustedNode.curY, adjustedNode.z) |
126 | adjustedNode.lastY = adjustedNode.curY |
127 | |
128 | if self.setMovingToolDirty ~= nil then |
129 | self:setMovingToolDirty(adjustedNode.node) |
130 | end |
131 | end |
132 | end |
133 | end |
134 | end |
231 | function GroundAdjustedNodes:updateGroundAdjustedNode(adjustedNode, dt) |
232 | local wasActive = adjustedNode.isActive |
233 | adjustedNode.isActive = self:getIsGroundAdjustedNodeActive(adjustedNode) |
234 | if adjustedNode.isActive then |
235 | adjustedNode.activationTimer = math.max(adjustedNode.activationTimer - dt, 0) |
236 | |
237 | for i=1, #adjustedNode.raycastNodes do |
238 | local raycastNode = adjustedNode.raycastNodes[i] |
239 | |
240 | local distance |
241 | local rx, ry, rz |
242 | if raycastNode.frameCount == raycastNode.updateFrame then |
243 | local x,y,z = localToWorld(raycastNode.node, 0, adjustedNode.yOffset, 0) |
244 | local dx,dy,dz = localDirectionToWorld(raycastNode.node, 0, -1, 0) |
245 | |
246 | self.lastRaycastDistance = 0 |
247 | raycastAll(x, y, z, dx, dy, dz, "groundAdjustRaycastCallback", raycastNode.distance, self, self.raycastMask) |
248 | distance = self.lastRaycastDistance |
249 | |
250 | --#debug drawDebugLine(x, y, z, 0, 1, 0, x+dx*raycastNode.distance, y+dy*raycastNode.distance, z+dz*raycastNode.distance, 1, 0, 0, true) |
251 | |
252 | if raycastNode.updateFrame > 1 and distance ~= 0 then |
253 | local oldData = raycastNode.history[2] |
254 | oldData[1], oldData[2], oldData[3], oldData[4] = self.lastRaycastGroundPos[1], self.lastRaycastGroundPos[2], self.lastRaycastGroundPos[3], self.lastRaycastDistance |
255 | raycastNode.history[2] = raycastNode.history[1] |
256 | raycastNode.history[1] = oldData |
257 | |
258 | rx, ry, rz = oldData[1], oldData[2], oldData[3] |
259 | end |
260 | else |
261 | local history1 = raycastNode.history[1] |
262 | local history2 = raycastNode.history[2] |
263 | local x1, y1, z1 = history1[1], history1[2], history1[3] |
264 | local x2, y2, z2 = history2[1], history2[2], history2[3] |
265 | |
266 | if raycastNode.lastRaycastPos[1] ~= nil then |
267 | rx = raycastNode.lastRaycastPos[1] + (x1 - x2) / raycastNode.updateFrame |
268 | ry = raycastNode.lastRaycastPos[2] + (y1 - y2) / raycastNode.updateFrame |
269 | rz = raycastNode.lastRaycastPos[3] + (z1 - z2) / raycastNode.updateFrame |
270 | |
271 | local x, y, z = localToWorld(raycastNode.node, 0, adjustedNode.yOffset, 0) |
272 | distance = MathUtil.vector3Length(x-rx, y-ry, z-rz) |
273 | else |
274 | distance = 0 |
275 | end |
276 | end |
277 | |
278 | if raycastNode.updateFrame > 1 then |
279 | raycastNode.lastRaycastPos[1] = rx |
280 | raycastNode.lastRaycastPos[2] = ry |
281 | raycastNode.lastRaycastPos[3] = rz |
282 | raycastNode.lastRaycastPos[4] = distance |
283 | |
284 | raycastNode.frameCount = raycastNode.frameCount + 1 |
285 | if raycastNode.frameCount > raycastNode.updateFrame then |
286 | raycastNode.frameCount = 1 |
287 | end |
288 | end |
289 | |
290 | local newY |
291 | if distance ~= 0 then |
292 | newY = adjustedNode.y + adjustedNode.yOffset - distance + raycastNode.yDiff |
293 | else |
294 | -- if we did not hit the ground we use the last target value, since we don't know where we exceeded the limit |
295 | newY = adjustedNode.targetY |
296 | end |
297 | |
298 | newY = MathUtil.clamp(newY, adjustedNode.minY, adjustedNode.maxY) |
299 | adjustedNode.targetY = newY |
300 | local _ |
301 | _, adjustedNode.curY, _ = getTranslation(adjustedNode.node) |
302 | end |
303 | else |
304 | if adjustedNode.averageInActivePosY and wasActive then |
305 | local groundAdjustedNodes = self.spec_groundAdjustedNodes.groundAdjustedNodes |
306 | local inActiveY, numNodes = 0, 0 |
307 | for _, _adjustedNode in pairs(groundAdjustedNodes) do |
308 | if _adjustedNode.averageInActivePosY then |
309 | inActiveY = inActiveY + _adjustedNode.curY |
310 | numNodes = numNodes + 1 |
311 | end |
312 | end |
313 | |
314 | if numNodes > 0 then |
315 | adjustedNode.inActiveY = inActiveY / numNodes |
316 | |
317 | -- reapply to all since some could have already changed |
318 | for _, _adjustedNode in pairs(groundAdjustedNodes) do |
319 | if _adjustedNode.averageInActivePosY then |
320 | _adjustedNode.inActiveY = inActiveY / numNodes |
321 | end |
322 | end |
323 | end |
324 | end |
325 | |
326 | if adjustedNode.resetIfNotActive then |
327 | adjustedNode.targetY = adjustedNode.inActiveY |
328 | end |
329 | |
330 | adjustedNode.activationTimer = adjustedNode.activationTime |
331 | end |
332 | end |