3 name:
"Floaters Finder",
5 wbModules: {
"WorldEditor" },
6 shortcut:
"Ctrl+Alt+Page Up",
7 awesomeFontCode: 0xF338)]
8class SCR_FloatersFinderPlugin : WorkbenchPlugin
15 protected bool m_bActiveLayerOnly;
17 [
Attribute(defvalue:
"1",
desc:
"Check for entities entirely below terrain by bounding box vertices' altitude",
category:
"Search")]
18 protected bool m_bSearchForEntitiesBelowTerrain;
20 [
Attribute(defvalue:
"1",
desc:
"Look for misplaced vegetation (trees & bushes) - floating up or too deep in the ground, etc.",
category:
"Search")]
21 protected bool m_bSearchForVegetation;
30 [
Attribute(defvalue:
"false",
desc:
"If set, uses the Prefab's random angle as acceptable margin; otherwise, uses below setting",
category:
"Angle")]
31 protected bool m_bUsePrefabAngles;
33 [
Attribute(defvalue:
"180", uiwidget: UIWidgets.Slider,
desc:
"Maximum allowed angle to not consider the tree as fallen. 0 = fully vertical tree, 90 = horizontal tree, 180 = no angle check (needs \"Use Prefab Angles\" unchecked)",
params:
"0 180 0.5",
category:
"Angle",
precision: 1)]
34 protected float m_fMaxTreeAngle;
40 [
Attribute(defvalue:
"1",
desc:
"Check object's altitude from OBJECTS below it (2× slower as it uses Trace)",
category:
"Vertical Offset")]
41 protected bool m_bCheckAboveEntitiesSurface;
43 [
Attribute(defvalue:
"0",
desc:
"If set, uses the Prefab's vertical offset range; otherwise, uses below settings",
category:
"Vertical Offset")]
44 protected bool m_bUsePrefabVerticalOffset;
46 [
Attribute(defvalue:
"-1",
desc:
"Minimum (included) vertical offset (needs \"Use Prefab Vertical Offset\" unchecked)",
params:
"-100 100 0.1",
category:
"Vertical Offset",
precision: 1)]
47 protected float m_fMinVerticalOffset;
49 [
Attribute(defvalue:
"0.5",
desc:
"Maximum (included) vertical offset (needs \"Use Prefab Vertical Offset\" unchecked)",
params:
"-100 100 0.1",
category:
"Vertical Offset",
precision: 1)]
50 protected float m_fMaxVerticalOffset;
52 [
Attribute(defvalue:
"0",
desc:
"Check for items being below water level (Ocean only for now). If under water, whatever the offset, the entity will be selected",
category:
"Vertical Offset")]
53 protected bool m_bCheckBelowWater;
59 [
Attribute(defvalue:
"1000", uiwidget: UIWidgets.Slider,
desc:
"Search radius around camera position - 0 = all entities",
params:
"0 5000 1",
category:
"Performance")]
60 protected int m_iCameraSearchRadius;
63 protected bool m_bShowSearchRadiusSphere;
65 [
Attribute(defvalue:
"2000", uiwidget: UIWidgets.Slider,
desc:
"Maximum number of selected entities (UI performance)",
params:
"1 5000 1",
category:
"Performance")]
66 protected int m_iMaxSelectedEntities;
68 [
Attribute(defvalue:
"1",
desc:
"[requires Search For Vegetation] Trace only checks for the vegetation's full height, allowing it to be below a bridge but may be missing a small tree entirely inside a rock",
category:
"Performance")]
69 protected bool m_bTraceVegetationPrecisely;
71 [
Attribute(defvalue:
"50", uiwidget: UIWidgets.Slider,
desc:
"Trace origin's distance (in metres) from above the entity to determine if it is below another entity AND Trace Vegetation Precisely is unchecked",
params:
"10 500 10",
category:
"Performance")]
72 protected int m_iTraceOriginDistance;
78 [
Attribute(defvalue:
"0",
desc:
"If ticked, write a file with links to all findings.",
category:
"Output")]
79 protected bool m_bOutputFindingsToFile;
82 protected bool m_bUseWebPrefix;
84 protected static const ref array<IEntitySource> WORLD_ENTITIES = {};
85 protected static ref Shape s_DetectionRadiusSphere;
87 protected static const int DEBUG_COLOUR = 0x99025D00;
88 protected static const int DEBUG_DURATION = 333;
89 protected static const string OUTPUT_FILENAME =
"FoundFloatingEntities.txt";
97 Print(
"Floaters Finder - Run method started",
LogLevel.NORMAL);
100 WORLD_ENTITIES.Clear();
102 WorldEditorAPI worldEditorAPI = SCR_WorldEditorToolHelper.GetWorldEditorAPI();
103 BaseWorld baseWorld = worldEditorAPI.GetWorld();
104 bool useSelectedEntities = worldEditorAPI.GetSelectedEntity() != null;
105 if (useSelectedEntities)
107 for (
int i, cnt = worldEditorAPI.GetSelectedEntitiesCount(); i < cnt; i++)
109 WORLD_ENTITIES.Insert(worldEditorAPI.GetSelectedEntity(i));
112 PrintFormat(
"Going with the current selection of %1 entities", WORLD_ENTITIES.Count(), level:
LogLevel.NORMAL);
116 firstTick = System.GetTickCount();
117 GetEntities(baseWorld);
118 PrintFormat(
"Entity getter duration: %1ms for %2 entities", System.GetTickCount() - firstTick, WORLD_ENTITIES.Count(), level:
LogLevel.NORMAL);
122 array<IEntitySource> filteredEntities = {};
123 firstTick = System.GetTickCount();
124 FilterEntities(baseWorld, filteredEntities);
125 PrintFormat(
"Entity filter duration: %1ms for %2 entities (output: %3 entities)", System.GetTickCount() - firstTick, WORLD_ENTITIES.Count(), filteredEntities.Count(), level:
LogLevel.NORMAL);
128 SelectEntities(filteredEntities);
131 if (m_bShowSearchRadiusSphere && !useSelectedEntities && m_iCameraSearchRadius > 0)
133 vector cameraMatrix[4];
134 baseWorld.GetCurrentCamera(cameraMatrix);
135 s_DetectionRadiusSphere = Shape.CreateSphere(DEBUG_COLOUR,
ShapeFlags.BACKFACE |
ShapeFlags.NOOUTLINE |
ShapeFlags.TRANSP, cameraMatrix[3], m_iCameraSearchRadius);
139 if (worldEditorAPI.GetSelectedEntitiesCount() != filteredEntities.Count())
143 "Cannot select all entities: Treated %1, Detected %2, Selected %3",
144 WORLD_ENTITIES.Count(),
145 filteredEntities.Count(),
146 worldEditorAPI.GetSelectedEntitiesCount()),
153 "%1 entities treated, %2 selected properly",
154 WORLD_ENTITIES.Count(),
155 filteredEntities.Count()),
160 if (m_bOutputFindingsToFile)
162 firstTick = System.GetTickCount();
163 OutputEntitiesToFile(filteredEntities);
166 "%1 entities successfully written to file %2 in %3ms",
167 filteredEntities.Count(),
168 FilePath.ToSystemFormat(OUTPUT_FILENAME),
169 System.GetTickCount() - firstTick),
173 Print(
"Floaters Finder - Run method ended",
LogLevel.NORMAL);
176 if (s_DetectionRadiusSphere)
178 Sleep(DEBUG_DURATION);
179 s_DetectionRadiusSphere = null;
184 protected bool Init()
186 if (!SCR_Global.IsEditMode())
188 Print(
"Floaters Finder - Run method stopped because non-Workbench run",
LogLevel.NORMAL);
192 WorldEditor worldEditor = Workbench.GetModule(WorldEditor);
193 if (worldEditor.IsPrefabEditMode())
195 Print(
"Floaters Finder - Run method stopped because World Editor is in Prefab edit mode",
LogLevel.NORMAL);
199 WorldEditorAPI worldEditorAPI = SCR_WorldEditorToolHelper.GetWorldEditorAPI();
202 Print(
"Floaters Finder - Run method stopped because World Editor API was not found",
LogLevel.WARNING);
206 BaseWorld baseWorld = worldEditorAPI.GetWorld();
209 Print(
"Floaters Finder - Run method stopped because base world was not found",
LogLevel.WARNING);
217 protected void GetEntities(BaseWorld baseWorld)
219 if (m_iCameraSearchRadius > 0)
221 vector cameraMatrix[4];
222 baseWorld.GetCurrentCamera(cameraMatrix);
223 baseWorld.QueryEntitiesBySphere(cameraMatrix[3], m_iCameraSearchRadius, InsertEntity);
227 vector minPos, maxPos;
228 baseWorld.GetBoundBox(minPos, maxPos);
229 baseWorld.QueryEntitiesByAABB(minPos, maxPos, InsertEntity);
234 protected bool InsertEntity(notnull
IEntity entity)
236 WorldEditorAPI worldEditorAPI = SCR_WorldEditorToolHelper.GetWorldEditorAPI();
237 WORLD_ENTITIES.Insert(worldEditorAPI.EntityToSource(entity));
242 protected void FilterEntities(notnull BaseWorld baseWorld, notnull out array<IEntitySource> filteredEntities)
244 BaseContainerList editorData;
245 BaseContainer firstEditorData;
246 BaseContainer ancestorContainer;
247 vector randomVerticalOffset;
253 bool isOcean = baseWorld.IsOcean();
254 float oceanHeight = baseWorld.GetOceanBaseHeight();
256 vector entityPos, bboxMin, bboxMax, tempPos;
257 vector bboxCorners[15];
258 float altitude, terrainY, tempTerrainY;
260 TraceParam traceParam =
new TraceParam();
262 float minVerticalOffset = m_fMinVerticalOffset;
263 float maxVerticalOffset = m_fMaxVerticalOffset;
264 float maxPitch = m_fMaxTreeAngle;
265 float maxRoll = m_fMaxTreeAngle;
267 WorldEditorAPI worldEditorAPI = SCR_WorldEditorToolHelper.GetWorldEditorAPI();
269 bool manyEntities = WORLD_ENTITIES.Count() > 10;
270 int currentLayerId = worldEditorAPI.GetCurrentEntityLayerId();
271 int underWaterNb, verticalOffsetNb, angleOffsetNb, fullyUndergroundNb;
273 filteredEntities.Clear();
274 foreach (IEntitySource entitySource : WORLD_ENTITIES)
276 if (!m_bOutputFindingsToFile && filteredEntities.Count() >= m_iMaxSelectedEntities)
282 if (m_bActiveLayerOnly && entitySource.GetLayerID() != currentLayerId)
285 IEntity entity = worldEditorAPI.SourceToEntity(entitySource);
286 isVegetation =
Tree.Cast(entity) != null;
288 if (!(m_bSearchForEntitiesBelowTerrain || isVegetation))
291 if (isVegetation && entitySource.Get(
"placement", placementMode) && placementMode == 1)
294 minVerticalOffset = m_fMinVerticalOffset;
295 maxVerticalOffset = m_fMaxVerticalOffset;
296 maxPitch = m_fMaxTreeAngle;
297 maxRoll = m_fMaxTreeAngle;
299 editorData = entitySource.GetObjectArray(
"editorData");
300 if (editorData && editorData.Count() > 0)
302 firstEditorData = editorData.Get(0);
304 if (m_bUsePrefabVerticalOffset)
306 firstEditorData.Get(
"randomVertOffset", randomVerticalOffset);
307 minVerticalOffset = randomVerticalOffset[0];
308 maxVerticalOffset = randomVerticalOffset[1];
311 if (m_bUsePrefabAngles)
313 firstEditorData.Get(
"randomPitchAngle", maxPitch);
314 firstEditorData.Get(
"randomRollAngle", maxRoll);
318 insertEntity =
false;
321 terrainY = worldEditorAPI.GetTerrainSurfaceY(entityPos[0], entityPos[2]);
324 if (!insertEntity && m_bCheckBelowWater && isOcean && oceanHeight > entityPos[1])
334 if (!insertEntity && isVegetation && (m_bUsePrefabAngles || m_fMaxTreeAngle < 180))
337 if (entitySource.Get(
"angles",
angles)
352 altitude = entityPos[1] - terrainY;
358 m_bSearchForEntitiesBelowTerrain &&
360 (bboxMin != vector.Zero || bboxMax != vector.Zero) &&
361 !LakeGeneratorEntity.Cast(entity) &&
362 !ShapeEntity.Cast(entity) &&
366 !entity.
GetParent().IsInherited(BaseBuilding) &&
367 !entity.
GetParent().IsInherited(StaticModelEntity)
373 bboxCorners[0] =
Vector(bboxMin[0], bboxMax[1], bboxMax[2]);
374 bboxCorners[1] =
Vector(bboxMax[0], bboxMax[1], bboxMax[2]);
375 bboxCorners[2] =
Vector(bboxMin[0], bboxMax[1], bboxMin[2]);
376 bboxCorners[3] =
Vector(bboxMax[0], bboxMax[1], bboxMin[2]);
377 bboxCorners[4] =
Vector(bboxMin[0], bboxMin[1], bboxMax[2]);
378 bboxCorners[5] =
Vector(bboxMax[0], bboxMin[1], bboxMax[2]);
379 bboxCorners[6] =
Vector(bboxMin[0], bboxMin[1], bboxMin[2]);
380 bboxCorners[7] =
Vector(bboxMax[0], bboxMin[1], bboxMin[2]);
383 bboxCorners[14] =
Vector((bboxMax[0] + bboxMin[1]) * 0.5, (bboxMax[1] + bboxMin[1]) * 0.5, (bboxMax[2] + bboxMin[2]) * 0.5);
386 for (
int i = 8; i < 14; i++)
388 bboxCorners[i] = bboxCorners[14];
391 bboxCorners[08][2] = bboxMax[2];
392 bboxCorners[09][0] = bboxMax[0];
393 bboxCorners[10][2] = bboxMin[2];
394 bboxCorners[11][0] = bboxMin[0];
395 bboxCorners[12][1] = bboxMax[1];
396 bboxCorners[13][1] = bboxMin[1];
399 for (
int i; i < 15; i++)
402 tempTerrainY = baseWorld.GetSurfaceY(tempPos[0], tempPos[2]);
403 if (tempPos[1] > tempTerrainY)
405 insertEntity =
false;
411 fullyUndergroundNb++;
414 if (!insertEntity && isVegetation && m_bSearchForVegetation)
416 if (m_bCheckAboveEntitiesSurface && altitude >= minVerticalOffset)
419 traceParam.Start = entityPos;
421 if (m_bTraceVegetationPrecisely || bboxMax[1] > m_iTraceOriginDistance)
422 traceParam.Start[1] = traceParam.Start[1] + bboxMax[1];
424 traceParam.Start[1] = traceParam.Start[1] + m_iTraceOriginDistance;
426 if (traceParam.Start[1] > terrainY)
428 traceParam.End = entityPos;
429 traceParam.End[1] = terrainY;
431 traceParam.Exclude = entity;
432 traceRatio = baseWorld.TraceMove(traceParam, NoVegetationFilterCallback);
440 "DONE %1pct (%2m/%3m)",
441 Math.Round(traceRatio * 10000) * 0.01,
442 vector.Distance(traceParam.Start, traceParam.End) * traceRatio,
443 vector.Distance(traceParam.Start, traceParam.End),
447 altitude -= (traceParam.Start[1] - traceParam.End[1]) * (1 - traceRatio);
451 insertEntity = altitude > maxVerticalOffset || altitude < minVerticalOffset || traceParam.Start[1] <= terrainY;
455 PrintFormat(
"altitude %1 DOES NOT MATCH the [%2, %3] range", altitude, minVerticalOffset, maxVerticalOffset, level:
LogLevel.NORMAL);
464 filteredEntities.Insert(entitySource);
469 "Filtered: %1 below water level, %2 vertical offset, %3 entirely underground, ignored %4 angle offset entities",
478 protected bool NoVegetationFilterCallback(notnull
IEntity entity, vector start =
"0 0 0", vector dir =
"0 0 0")
480 return Tree.Cast(entity) == null;
484 protected void SelectEntities(notnull array<IEntitySource> entities)
486 WorldEditorAPI worldEditorAPI = SCR_WorldEditorToolHelper.GetWorldEditorAPI();
487 worldEditorAPI.ClearEntitySelection();
488 for (
int i, cnt = Math.Min(m_iMaxSelectedEntities, entities.Count()); i < cnt; i++)
490 worldEditorAPI.AddToEntitySelection(entities[i]);
493 worldEditorAPI.UpdateSelectionGui();
497 protected void OutputEntitiesToFile(notnull array<IEntitySource> filteredEntities)
499 WorldEditorAPI worldEditorAPI = SCR_WorldEditorToolHelper.GetWorldEditorAPI();
502 worldEditorAPI.GetWorldPath(worldName);
505 if (filteredEntities.IsEmpty())
510 FileHandle fileHandle = FileIO.OpenFile(OUTPUT_FILENAME,
FileMode.WRITE);
511 fileHandle.WriteLine(
"===========================================================================");
512 fileHandle.WriteLine(
"===== File generated by Floaters Finder Plugin on " + SCR_DateTimeHelper.GetDateTimeLocal() +
" =====");
513 fileHandle.WriteLine(
"===========================================================================");
514 fileHandle.WriteLine(
string.Empty);
515 fileHandle.WriteLine(filteredEntities.Count().ToString() +
" misplaced entities were found on " + worldName +
" terrain" + link);
516 if (!filteredEntities.IsEmpty())
517 fileHandle.WriteLine(
string.Empty);
520 vector transformation[4];
521 transformation[0] = vector.Right;
522 transformation[1] = vector.Up;
523 transformation[2] = vector.Forward;
524 vector bboxMin, bboxMax, centre;
525 foreach (IEntitySource entitySource : filteredEntities)
527 entity = worldEditorAPI.SourceToEntity(entitySource);
529 float diagonal = (bboxMax - bboxMin).Length();
533 centre = entity.
CoordToParent((vector)(bboxMax + bboxMin) * 0.5);
534 transformation[3] = centre - 1.25 * diagonal * vector.Forward;
536 fileHandle.WriteLine(
537 SCR_WorldEditorToolHelper.GetCurrentWorldEditorLink(
539 Math3D.MatrixToAngles(transformation),
549 Workbench.ScriptDialog(
"Configure 'Floaters Finder' plugin",
"",
this);
ref array< string > angles
UI Textures DeployMenu Briefing conflict_HintBanner_1_UI desc
enum EVehicleType IEntity
class WorkbenchDialog_AbortRetryIgnore ButtonAttribute("OK", true)
proto external vector GetOrigin()
proto external void GetBounds(out vector mins, out vector maxs)
proto external IEntity GetParent()
proto external vector CoordToParent(vector coord)
proto void Print(void var, LogLevel level=LogLevel.NORMAL)
Prints content of variable to console/log.
LogLevel
Enum with severity of the logging message.
proto void PrintFormat(string fmt, void param1=NULL, void param2=NULL, void param3=NULL, void param4=NULL, void param5=NULL, void param6=NULL, void param7=NULL, void param8=NULL, void param9=NULL, LogLevel level=LogLevel.NORMAL)
SCR_FieldOfViewSettings Attribute
FileMode
Mode for opening file. See FileSystem::Open.
proto native vector Vector(float x, float y, float z)