328 | function PlaceablePlacement:getHasOverlap(x, y, z, rotY, checkFunc) |
329 | local spec = self.spec_placement |
330 | |
331 | local callbackTarget = {hasOverlap = false} |
332 | callbackTarget.overlapCallback = function(target, hitObjectId, hitX, hitY, hitZ, distance) |
333 | if checkFunc ~= nil then |
334 | if checkFunc(hitObjectId) then |
335 | callbackTarget.hasOverlap = true |
336 | callbackTarget.node = hitObjectId |
337 | return false |
338 | end |
339 | else |
340 | if hitObjectId ~= g_currentMission.terrainRootNode then |
341 | callbackTarget.hasOverlap = true |
342 | callbackTarget.node = hitObjectId |
343 | return false |
344 | end |
345 | end |
346 | |
347 | return true |
348 | end |
349 | |
350 | for _, area in ipairs(spec.testAreas) do |
351 | local size = area.size |
352 | local center = area.center |
353 | |
354 | local dirX, dirZ = MathUtil.getDirectionFromYRotation(rotY) |
355 | local normX, _, normZ = MathUtil.crossProduct(0, 1, 0, dirX, 0, dirZ) |
356 | |
357 | local posX = x + dirX * center.z + normX * center.x |
358 | local posY = y + center.y |
359 | local posZ = z + dirZ * center.z + normZ * center.x |
360 | |
361 | local sizeXHalf, sizeZHalf = size.x*0.5, size.z*0.5 |
362 | local frontLeftX, frontLeftZ = posX + dirX*sizeZHalf - normX*sizeXHalf, posZ + dirZ*sizeZHalf - normZ*sizeXHalf |
363 | local frontRightX, frontRightZ = posX + dirX*sizeZHalf + normX*sizeXHalf, posZ + dirZ*sizeZHalf + normZ*sizeXHalf |
364 | local backLeftX, backLeftZ = posX - dirX*sizeZHalf - normX*sizeXHalf, posZ - dirZ*sizeZHalf - normZ*sizeXHalf |
365 | local backRightX, backRightZ = posX - dirX*sizeZHalf + normX*sizeXHalf, posZ - dirZ*sizeZHalf + normZ*sizeXHalf |
366 | |
367 | local frontLeftY = getTerrainHeightAtWorldPos(g_currentMission.terrainRootNode, frontLeftX, 0, frontLeftZ) |
368 | local frontRightY = getTerrainHeightAtWorldPos(g_currentMission.terrainRootNode, frontRightX, 0, frontRightZ) |
369 | local backLeftY = getTerrainHeightAtWorldPos(g_currentMission.terrainRootNode, backLeftX, 0, backLeftZ) |
370 | local backRightY = getTerrainHeightAtWorldPos(g_currentMission.terrainRootNode, backRightX, 0, backRightZ) |
371 | local centerY = getTerrainHeightAtWorldPos(g_currentMission.terrainRootNode, posX, 0, posZ) |
372 | |
373 | local terrainBasedCenterY = math.min(frontLeftY, frontRightY, backLeftY, backRightY, centerY) + size.y*0.5 - 0.5 -- we want to check 0.5m below terrain |
374 | posY = math.max(terrainBasedCenterY, posY) |
375 | |
376 | overlapBox(posX, posY, posZ, 0, rotY + area.rotYOffset, 0, size.x*0.5, size.y*0.5, size.z*0.5, "overlapCallback", callbackTarget, nil, true, true, true, false) |
377 | |
378 | --#debug DebugUtil.drawDebugGizmoAtWorldPos(frontLeftX, frontLeftY, frontLeftZ, dirX*5, 0, dirZ*5, 0, 1, 0, "frontLeft", false, {1, 1, 1, 1}) |
379 | --#debug DebugUtil.drawDebugGizmoAtWorldPos(frontRightX, frontRightY, frontRightZ, dirX, 0, dirZ, 0, 1, 0, "frontLeft", false, {1, 1, 1, 1}) |
380 | --#debug DebugUtil.drawDebugGizmoAtWorldPos(backLeftX, backLeftY, backLeftZ, dirX, 0, dirZ, 0, 1, 0, "frontLeft", false, {1, 1, 1, 1}) |
381 | --#debug DebugUtil.drawDebugGizmoAtWorldPos(backRightX, backRightY, backRightZ, dirX, 0, dirZ, 0, 1, 0, "frontLeft", false, {1, 1, 1, 1}) |
382 | --#debug DebugUtil.drawOverlapBox(posX, posY, posZ, 0, rotY + area.rotYOffset, 0, size.x*0.5, size.y*0.5, size.z*0.5, 1, 0, 0) |
383 | |
384 | --#debug if area.debugTestBox then |
385 | --#debug area.debugTestBox:createWithStartEnd(area.startNode, area.endNode) |
386 | --#debug g_debugManager:addFrameElement(area.debugTestBox) |
387 | --#debug area.debugStartNode:createWithNode(area.startNode, getName(area.startNode), false, nil) |
388 | --#debug g_debugManager:addFrameElement(area.debugStartNode) |
389 | --#debug area.debugEndNode:createWithNode(area.endNode, getName(area.endNode), false, nil) |
390 | --#debug g_debugManager:addFrameElement(area.debugEndNode) |
391 | --#debug end |
392 | |
393 | if callbackTarget.hasOverlap then |
394 | return true, callbackTarget.node |
395 | end |
396 | |
397 | local startX,startZ, widthX,widthZ, heightX,heightZ = self:getTestParallelogramAtWorldPosition(area, x, z, rotY) |
398 | |
399 | --#debug area.debugArea:createWithStartEnd(area.startNode, area.endNode) |
400 | --#debug g_debugManager:addFrameElement(area.debugArea) |
401 | |
402 | local density = DensityMapHeightUtil.getValueAtArea(startX, startZ, widthX, widthZ, heightX, heightZ, true) |
403 | if density > 0 then |
404 | return true, nil |
405 | end |
406 | end |
407 | |
408 | return false, nil |
409 | end |
437 | function PlaceablePlacement:getHasOverlapWithZones(zones, x, y, z, rotY) |
438 | local spec = self.spec_placement |
439 | |
440 | for _, area in ipairs(spec.testAreas) do |
441 | local x1, z1, x2, z2, x3, z3, x4, z4 = self:getTestParallelogramAtWorldPosition(area, x, z, rotY) |
442 | |
443 | --#debug DebugUtil.drawDebugGizmoAtWorldPos(x1, y, z1, 0, 0, 1, 0, 1, 0, "P1", true) |
444 | --#debug DebugUtil.drawDebugGizmoAtWorldPos(x2, y, z2, 0, 0, 1, 0, 1, 0, "P2", true) |
445 | --#debug DebugUtil.drawDebugGizmoAtWorldPos(x3, y, z3, 0, 0, 1, 0, 1, 0, "P3", true) |
446 | --#debug DebugUtil.drawDebugGizmoAtWorldPos(x4, y, z4, 0, 0, 1, 0, 1, 0, "P4", true) |
447 | |
448 | if PlacementUtil.isInsideRestrictedZone(zones, x1, y, z1, false) then |
449 | return true |
450 | end |
451 | if PlacementUtil.isInsideRestrictedZone(zones, x2, y, z2, false) then |
452 | return true |
453 | end |
454 | if PlacementUtil.isInsideRestrictedZone(zones, x3, y, z3, false) then |
455 | return true |
456 | end |
457 | if PlacementUtil.isInsideRestrictedZone(zones, x4, y, z4, false) then |
458 | return true |
459 | end |
460 | |
461 | end |
462 | |
463 | return false |
464 | end |
303 | function PlaceablePlacement:getIsAreaOwned(farmId) |
304 | local spec = self.spec_placement |
305 | local halfX = spec.testSizeX * 0.5 |
306 | local halfZ = spec.testSizeZ * 0.5 |
307 | local offsetX = spec.testSizeOffsetX |
308 | local offsetZ = spec.testSizeOffsetZ |
309 | |
310 | local x1, _, z1 = localToWorld(self.rootNode, -halfX + offsetX, 0, -halfZ + offsetZ) |
311 | local x2, _, z2 = localToWorld(self.rootNode, halfX + offsetX, 0, -halfZ + offsetZ) |
312 | local x3, _, z3 = localToWorld(self.rootNode, -halfX + offsetX, 0, halfZ + offsetZ) |
313 | local x4, _, z4 = localToWorld(self.rootNode, halfX + offsetX, 0, halfZ + offsetZ) |
314 | |
315 | return g_farmlandManager:getIsOwnedByFarmAtWorldPosition(farmId, x1, z1) and |
316 | g_farmlandManager:getIsOwnedByFarmAtWorldPosition(farmId, x2, z2) and |
317 | g_farmlandManager:getIsOwnedByFarmAtWorldPosition(farmId, x3, z3) and |
318 | g_farmlandManager:getIsOwnedByFarmAtWorldPosition(farmId, x4, z4) |
319 | end |
468 | function PlaceablePlacement:getTestParallelogramAtWorldPosition(testArea, x, z, rotY) |
469 | local dirX, dirZ = MathUtil.getDirectionFromYRotation(rotY) |
470 | local normX, _, normZ = MathUtil.crossProduct(0, 1, 0, dirX, 0, dirZ) |
471 | |
472 | local centerXOffset = testArea.center.x |
473 | local centerZOffset = testArea.center.z |
474 | local centerX = x + dirX * centerZOffset + normX * centerXOffset |
475 | local centerZ = z + dirZ * centerZOffset + normZ * centerXOffset |
476 | |
477 | dirX, dirZ = MathUtil.getDirectionFromYRotation(rotY + testArea.rotYOffset) |
478 | normX, _, normZ = MathUtil.crossProduct(0, 1, 0, dirX, 0, dirZ) |
479 | |
480 | local startOffsetX = testArea.size.x * 0.5 |
481 | local startOffsetZ = testArea.size.z * 0.5 |
482 | local startX = centerX - dirX * startOffsetZ - normX * startOffsetX |
483 | local startZ = centerZ - dirZ * startOffsetZ - normZ * startOffsetX |
484 | |
485 | local widthOffset = testArea.size.x |
486 | local widthX = startX + normX * widthOffset |
487 | local widthZ = startZ + normZ * widthOffset |
488 | |
489 | local heightOffset = testArea.size.z |
490 | local heightX = startX + dirX * heightOffset |
491 | local heightZ = startZ + dirZ * heightOffset |
492 | |
493 | local heightX2 = widthX + dirX * heightOffset |
494 | local heightZ2 = widthZ + dirZ * heightOffset |
495 | |
496 | return startX, startZ, widthX, widthZ, heightX, heightZ, heightX2, heightZ2 |
497 | end |
176 | function PlaceablePlacement:loadTestArea(xmlFile, key, area) |
177 | local startNode = xmlFile:getValue(key .. "#startNode", nil, self.components, self.i3dMappings) |
178 | local endNode = xmlFile:getValue(key .. "#endNode", nil, self.components, self.i3dMappings) |
179 | |
180 | if startNode == nil then |
181 | Logging.xmlWarning(xmlFile, "Missing test area start node for '%s'", key) |
182 | return false |
183 | end |
184 | |
185 | if endNode == nil then |
186 | Logging.xmlWarning(xmlFile, "Missing test area end node for '%s'", key) |
187 | return false |
188 | end |
189 | |
190 | if getParent(endNode) ~= startNode then |
191 | Logging.xmlWarning(xmlFile, "Test area end node is not a direct child of startNode for '%s'", key) |
192 | return false |
193 | end |
194 | |
195 | area.startNode = startNode |
196 | area.endNode = endNode |
197 | |
198 | local ySafetyOffset = 0.05 -- shift all test areas down by 5cm to avoid stacking of placables with testArea start at y=0 |
199 | local offsetX, offsetY, offsetZ = localToLocal(endNode, startNode, 0, -ySafetyOffset, 0) |
200 | local centerX, centerY, centerZ = localToLocal(startNode, self.rootNode, offsetX*0.5, offsetY*0.5, offsetZ*0.5) |
201 | local sizeX, sizeY, sizeZ = math.abs(offsetX), math.abs(offsetY+ySafetyOffset), math.abs(offsetZ) |
202 | |
203 | if offsetY < 0.01 then |
204 | Logging.xmlDevWarning(xmlFile, "TestArea '%s 'has no height (endNode has same y as startNode)", key) |
205 | end |
206 | |
207 | area.size = {} |
208 | area.size.x = math.abs(sizeX) |
209 | area.size.y = math.abs(sizeY) |
210 | area.size.z = math.abs(sizeZ) |
211 | |
212 | area.center = {} |
213 | area.center.x = centerX |
214 | area.center.y = centerY |
215 | area.center.z = centerZ |
216 | |
217 | local dirX, _, dirZ = localDirectionToLocal(startNode, self.rootNode, 0, 0, 1) |
218 | local rotY = MathUtil.getYRotationFromDirection(dirX, dirZ) |
219 | area.rotYOffset = rotY |
220 | |
221 | area.debugTestBox = DebugCube.new() |
222 | area.debugStartNode = DebugGizmo.new() |
223 | area.debugEndNode = DebugGizmo.new() |
224 | area.debugArea = Debug2DArea.new(true, false, {1, 0, 0, 0.3}) |
225 | |
226 | return true |
227 | end |
76 | function PlaceablePlacement:onLoad(savegame) |
77 | local spec = self.spec_placement |
78 | local xmlFile = self.xmlFile |
79 | |
80 | spec.testAreas = {} |
81 | xmlFile:iterate("placeable.placement.testAreas.testArea", function(_, key) |
82 | local testArea = {} |
83 | if self:loadTestArea(xmlFile, key, testArea) then |
84 | table.insert(spec.testAreas, testArea) |
85 | end |
86 | end) |
87 | |
88 | XMLUtil.checkDeprecatedXMLElements(self.xmlFile, "placeable.placement#sizeX") -- FS19 to FS22 |
89 | XMLUtil.checkDeprecatedXMLElements(self.xmlFile, "placeable.placement#sizeZ") -- FS19 to FS22 |
90 | XMLUtil.checkDeprecatedXMLElements(self.xmlFile, "placeable.placement#sizeOffsetX") -- FS19 to FS22 |
91 | XMLUtil.checkDeprecatedXMLElements(self.xmlFile, "placeable.placement#sizeOffsetZ") -- FS19 to FS22 |
92 | XMLUtil.checkDeprecatedXMLElements(self.xmlFile, "placeable.placement#testSizeX", "placeable.placement.testAreas.testArea") -- FS19 to FS22 |
93 | XMLUtil.checkDeprecatedXMLElements(self.xmlFile, "placeable.placement#testSizeZ", "placeable.placement.testAreas.testArea") -- FS19 to FS22 |
94 | XMLUtil.checkDeprecatedXMLElements(self.xmlFile, "placeable.placement#testSizeOffsetX", "placeable.placement.testAreas.testArea") -- FS19 to FS22 |
95 | XMLUtil.checkDeprecatedXMLElements(self.xmlFile, "placeable.placement#testSizeOffsetZ", "placeable.placement.testAreas.testArea") -- FS19 to FS22 |
96 | |
97 | -- fallback to legacy xmls |
98 | -- TODO: remove |
99 | local testSizeX = xmlFile:getFloat("placeable.placement#testSizeX") |
100 | local testSizeZ = xmlFile:getFloat("placeable.placement#testSizeZ") |
101 | if testSizeX ~= nil and testSizeZ ~= nil then |
102 | local testSizeOffsetX = xmlFile:getFloat("placeable.placement#testSizeOffsetX", 0) |
103 | local testSizeOffsetZ = xmlFile:getFloat("placeable.placement#testSizeOffsetZ", 0) |
104 | |
105 | local startNode = createTransformGroup("legacyTestAreaStartNode") |
106 | local endNode = createTransformGroup("legacyTestAreaEndNode") |
107 | link(self.rootNode, startNode) |
108 | link(startNode, endNode) |
109 | |
110 | setTranslation(startNode, -testSizeX * 0.5 + testSizeOffsetX, 0, -testSizeZ * 0.5 + testSizeOffsetZ) |
111 | setTranslation(endNode, testSizeX, 2, testSizeZ) |
112 | |
113 | local testArea = {} |
114 | testArea.startNode = startNode |
115 | testArea.endNode = endNode |
116 | |
117 | testArea.size = {} |
118 | testArea.size.x = math.abs(testSizeX) |
119 | testArea.size.y = 2 |
120 | testArea.size.z = math.abs(testSizeZ) |
121 | |
122 | testArea.center = {} |
123 | testArea.center.x = testSizeOffsetX |
124 | testArea.center.y = testArea.size.y * 0.5 |
125 | testArea.center.z = testSizeOffsetZ |
126 | |
127 | testArea.debugTestBox = DebugCube.new() |
128 | testArea.debugStartNode = DebugGizmo.new() |
129 | testArea.debugEndNode = DebugGizmo.new() |
130 | testArea.debugArea = Debug2DArea.new(true, false, {1, 0, 0, 0.3}) |
131 | |
132 | testArea.rotYOffset = 0 |
133 | table.insert(spec.testAreas, testArea) |
134 | end |
135 | |
136 | spec.useRandomYRotation = xmlFile:getValue("placeable.placement#useRandomYRotation", spec.useRandomYRotation) |
137 | spec.useManualYRotation = xmlFile:getValue("placeable.placement#useManualYRotation", spec.useManualYRotation) |
138 | spec.positionSnapSize = math.abs(xmlFile:getValue("placeable.placement#placementPositionSnapSize", 0.0)) |
139 | spec.positionSnapOffset = math.abs(xmlFile:getValue("placeable.placement#placementPositionSnapOffset", 0.0)) |
140 | spec.rotationSnapAngle = math.abs(xmlFile:getValue("placeable.placement#placementRotationSnapAngle", 0.0)) |
141 | |
142 | spec.alignToWorldY = xmlFile:getValue("placeable.placement#alignToWorldY", true) |
143 | if not spec.alignToWorldY then |
144 | spec.pos1Node = xmlFile:getValue("placeable.placement#pos1Node", nil, self.components, self.i3dMappings) |
145 | spec.pos2Node = xmlFile:getValue("placeable.placement#pos2Node", nil, self.components, self.i3dMappings) |
146 | spec.pos3Node = xmlFile:getValue("placeable.placement#pos3Node", nil, self.components, self.i3dMappings) |
147 | if spec.pos1Node == nil or spec.pos2Node == nil or spec.pos3Node == nil then |
148 | spec.alignToWorldY = true |
149 | Logging.xmlWarning(xmlFile, "pos1Node, pos2Node and pos3Node has to be set when alignToWorldY is false!") |
150 | end |
151 | end |
152 | |
153 | if self.isClient then |
154 | spec.samples = {} |
155 | spec.samples.place = g_soundManager:loadSample2DFromXML(xmlFile.handle, "placeable.placement.sounds", "place", self.baseDirectory, 1, AudioGroup.GUI) |
156 | spec.samples.placeLayered = g_soundManager:loadSample2DFromXML(xmlFile.handle, "placeable.placement.sounds", "placeLayered", self.baseDirectory, 1, AudioGroup.GUI) |
157 | spec.samples.destroy = g_soundManager:loadSample2DFromXML(xmlFile.handle, "placeable.placement.sounds", "destroy", self.baseDirectory, 1, AudioGroup.GUI) |
158 | end |
159 | end |
276 | function PlaceablePlacement:onPreFinalizePlacement() |
277 | local spec = self.spec_placement |
278 | -- only (re-)align if the property is set and we're on the server, clients will get the correct transform in sync. |
279 | -- This also avoids wrong positioning of placeables on modified terrain. |
280 | if not spec.alignToWorldY and self.isServer then |
281 | local x1,y1,z1 = getWorldTranslation(self.rootNode) |
282 | y1 = getTerrainHeightAtWorldPos(g_currentMission.terrainRootNode, x1,y1,z1) |
283 | setTranslation(self.rootNode, x1,y1,z1) |
284 | local x2,y2,z2 = getWorldTranslation(spec.pos1Node) |
285 | y2 = getTerrainHeightAtWorldPos(g_currentMission.terrainRootNode, x2,y2,z2) |
286 | local x3,y3,z3 = getWorldTranslation(spec.pos2Node) |
287 | y3 = getTerrainHeightAtWorldPos(g_currentMission.terrainRootNode, x3,y3,z3) |
288 | local x4,y4,z4 = getWorldTranslation(spec.pos3Node) |
289 | y4 = getTerrainHeightAtWorldPos(g_currentMission.terrainRootNode, x4,y4,z4) |
290 | local dirX = x2 - x1 |
291 | local dirY = y2 - y1 |
292 | local dirZ = z2 - z1 |
293 | local dir2X = x3 - x4 |
294 | local dir2Y = y3 - y4 |
295 | local dir2Z = z3 - z4 |
296 | local upX,upY,upZ = MathUtil.crossProduct(dir2X, dir2Y, dir2Z, dirX, dirY, dirZ) |
297 | setDirection(self.rootNode, dirX, dirY, dirZ, upX,upY,upZ) |
298 | end |
299 | end |
50 | function PlaceablePlacement.registerXMLPaths(schema, basePath) |
51 | schema:setXMLSpecializationType("Placement") |
52 | schema:register(XMLValueType.NODE_INDEX, basePath .. ".placement#pos1Node", "Position node 1 (Required if alignToWorldY is false to calculate the terrain alignment)") |
53 | schema:register(XMLValueType.NODE_INDEX, basePath .. ".placement#pos2Node", "Position node 2 (Required if alignToWorldY is false to calculate the terrain alignment)") |
54 | schema:register(XMLValueType.NODE_INDEX, basePath .. ".placement#pos3Node", "Position node 3 (Required if alignToWorldY is false to calculate the terrain alignment)") |
55 | |
56 | schema:register(XMLValueType.NODE_INDEX, basePath .. ".placement.testAreas.testArea(?)#startNode", "Start node of box for testing overlap") |
57 | schema:register(XMLValueType.NODE_INDEX, basePath .. ".placement.testAreas.testArea(?)#endNode", "End node of box for testing overlap") |
58 | |
59 | schema:register(XMLValueType.BOOL, basePath .. ".placement#useRandomYRotation", "Use random Y rotation", false) |
60 | schema:register(XMLValueType.BOOL, basePath .. ".placement#useManualYRotation", "Use manual Y rotation", false) |
61 | schema:register(XMLValueType.BOOL, basePath .. ".placement#alignToWorldY", "Placeable is aligned to world Y instead of terrain", true) |
62 | schema:register(XMLValueType.FLOAT, basePath .. ".placement#placementPositionSnapSize", "Position snap size", 0) |
63 | schema:register(XMLValueType.FLOAT, basePath .. ".placement#placementPositionSnapOffset", "Position snap offset", 0) |
64 | schema:register(XMLValueType.ANGLE, basePath .. ".placement#placementRotationSnapAngle", "Rotation snap angle", 0) |
65 | |
66 | SoundManager.registerSampleXMLPaths(schema, basePath .. ".placement.sounds", "place") |
67 | SoundManager.registerSampleXMLPaths(schema, basePath .. ".placement.sounds", "placeLayered") |
68 | SoundManager.registerSampleXMLPaths(schema, basePath .. ".placement.sounds", "destroy") |
69 | |
70 | schema:setXMLSpecializationType() |
71 | end |