3 name:
"World Entities Statistics",
4 description:
"Get Prefabs usage and Entities statistics in the currently opened world",
5 wbModules: {
"WorldEditor" },
7 awesomeFontCode: 0xF1FE)]
8class SCR_WorldEntitiesStatisticsPlugin : WorkbenchPlugin
14 [
Attribute(defvalue:
"0",
desc:
"Output result in storage (txt and edds), otherwise Log Console",
category:
"General")]
15 protected bool m_bOutputToFile;
21 [
Attribute(defvalue:
"0",
desc:
"Only analyse Entities/Prefabs from the active layer",
category:
"Text Output")]
22 protected bool m_bActiveLayerOnly;
25 protected int m_iMaxDisplayedEntries;
28 protected int m_iDisplayThreshold;
34 [
Attribute(defvalue:
"1",
desc:
"Output terrain entities's density map (requires Output To File)",
category:
"Heatmap")]
35 protected bool m_bOutputHeatmap;
37 [
Attribute(defvalue:
"0", uiwidget:
UIWidgets.ComboBox,
desc:
"Heat Map Type:\n- density = entity count per pixel\n- variety = different model (not entities/Prefabs) count per pixel", enums:
SCR_ParamEnumArray.
FromString(
"Entity density,Number of entities per pixel;Model variety,Number of different models per pixel"),
category:
"Heatmap")]
38 protected int m_iHeatmapType;
42 protected bool m_bOutputHeatmapForAllLayers;
44 [
Attribute(defvalue:
"0", uiwidget:
UIWidgets.ComboBox,
desc:
"- Greyscale: from black to white\n- Thermal: from blue to green to red\n- Alpha: from transparent to white", enums:
SCR_ParamEnumArray.
FromString(
"Greyscale,From black to white;Thermal,From blue to green to red;Alpha,From transparent to white"),
category:
"Heatmap")]
45 protected int m_iHeatmapColourMode;
47 [
Attribute(defvalue:
"0",
desc:
"Invert pixel value (except for max value), e.g black = most dense, white = least dense instead of the opposite",
category:
"Heatmap")]
48 protected bool m_bHeatmapValueInversion;
50 [
Attribute(defvalue:
"1", uiwidget:
UIWidgets.ComboBox,
desc:
"This setting can prevent a density peak from \"darkening\" the image everywhere else - it also offers to highlight pixels above the 0..2×median range", enums:
SCR_ParamEnumArray.
FromString(
"Raw value;2 × average;2 × median"),
category:
"Heatmap")]
51 protected int m_iHeatmapMaxValueMode;
53 [
Attribute(defvalue:
"1",
desc:
"Highlight values above max (when Heat Map Max Value Mode is not Raw)\n- Greyscale: red pixels\n- Thermal: white pixels",
category:
"Heatmap")]
54 protected bool m_bHeatmapHighlightValuesAboveMax;
56 [
Attribute(defvalue:
"512", uiwidget:
UIWidgets.ComboBox,
desc:
"Number of detection squares in terrain height/width", enums:
SCR_ParamEnumArray.
FromString(
"16,16×16,;32,32×32,;64,64×64,;128,128×128,;256,256×256,;512,512×512,;1024,1024×1024,;2048,2048×2048,;4096,4096×4096,;1080,1080×1080 (wallpaper),;1440,1440×1440 (wallpaper),;2160,2160×2160 (wallpaper),"),
category:
"Heatmap")]
57 protected int m_iHeatmapDefinition;
60 protected int m_iHeatmapResolutionFactor;
69 [
Attribute(defvalue:
"0",
desc:
"Visual shapes debug - display e.g land surface vs water surface",
category:
"Debug")]
70 protected bool m_bDebug;
74 protected static const string WORLD_DIRECTORY_FORMAT =
"WorldStatistics_%1";
75 protected static const string LAYERS_SUBDIR =
"Layers";
76 protected static const string OUTPUT_FILE_NAME =
"PrefabStatistics.txt";
77 protected static const string LAYER_OUTPUT_FILE_NAME =
"%1_%2_PrefabStatistics.txt";
78 protected static const string OUTPUT_MAP_NAME =
"Heatmap_%1_%2.dds";
79 protected static const string LAYER_OUTPUT_MAP_NAME =
"%1_%2_Heatmap_%3_%4.dds";
81 protected static const string NO_MAP_CONTENT_FORMAT =
"No %1 found";
82 protected static const int MIN_TERRAIN_DEBUG_RESOLUTION = 50;
83 protected static const float SQM_TO_SQKM = 0.000001;
85 protected static const int TYPE_DENSITY = 0;
86 protected static const int TYPE_VARIETY = 1;
88 protected static const int MIN_ENTITIES_RGB = 8;
91 protected override void Run()
93 if (!Workbench.ScriptDialog(
"",
"Get world entities statistics (or close the window)",
this))
96 Debug.BeginTimeMeasure();
97 int scannedEntitiesCount = ScanWorld();
98 if (scannedEntitiesCount < 0)
99 Debug.EndTimeMeasure(
"World entity scan error");
101 Debug.EndTimeMeasure(
string.Format(
"World scan (%1 entities)", scannedEntitiesCount));
106 protected int ScanWorld()
108 WorldEditorAPI worldEditorAPI = SCR_WorldEditorToolHelper.GetWorldEditorAPI();
115 string worldPath = SCR_WorldEditorToolHelper.GetWorldPath();
118 Workbench.Dialog(
"",
"Be sure to load a world (or have it saved) before using the World Entities Statistics plugin.");
122 IEntitySource terrainEntity = SCR_WorldEditorToolHelper.GetTerrainEntitySource();
125 Workbench.Dialog(
"",
"Be sure to have a terrain entity (of type GenericTerrainEntity) before using the World Entities Statistics plugin.");
130 if (!terrainEntity.Get(
"coords", terrainOrigin))
133 float terrainUnitScale = worldEditorAPI.GetTerrainUnitScale();
134 float terrainX = terrainUnitScale * worldEditorAPI.GetTerrainResolutionX();
135 float terrainZ = terrainX;
136 vector terrainSize = { terrainX, 0, terrainZ };
138 float landRatio = GetLandAboveWaterRatio(worldEditorAPI, terrainOrigin, terrainSize, terrainUnitScale);
141 "%1km² (%2%% land, %3%% water)",
142 (terrainX * terrainZ * SQM_TO_SQKM).
ToString(lenDec: 2),
143 (landRatio * 100).
ToString(lenDec: 2),
144 ((1 - landRatio) * 100).
ToString(lenDec: 2));
150 int sourcesWithoutEntity;
154 int subScenesCount = worldEditorAPI.GetNumSubScenes();
157 SCR_WorldEntitiesStatisticsPlugin_Report currentLayerReport;
158 SCR_WorldEntitiesStatisticsPlugin_Report mainReport;
160 SCR_WorldEntitiesStatisticsPlugin_Report_World worldInfo =
new SCR_WorldEntitiesStatisticsPlugin_Report_World();
161 worldInfo.m_fTerrainSurface = terrainX * terrainZ;
162 worldInfo.m_fTerrainSurfaceASLRatio = landRatio;
163 worldInfo.m_vTerrainMin = terrainOrigin;
164 worldInfo.m_vTerrainMax = terrainSize;
166 if (m_bActiveLayerOnly)
168 currentSceneId = worldEditorAPI.GetCurrentSubScene();
169 currentLayerId = worldEditorAPI.GetCurrentEntityLayerId();
173 mainReport =
new SCR_WorldEntitiesStatisticsPlugin_Report();
174 mainReport.m_WorldInfo = worldInfo;
177 Debug.BeginTimeMeasure();
179 int editorEntitiesCount = worldEditorAPI.GetEditorEntityCount();
180 for (
int i = editorEntitiesCount - 1; i >= 0; --i)
182 entitySource = worldEditorAPI.GetEditorEntity(i);
189 int entitySceneId = entitySource.GetSubScene();
190 int entityLayerId = entitySource.GetLayerID();
191 if (m_bActiveLayerOnly && (entityLayerId != currentLayerId || entitySceneId != currentSceneId))
194 if (!subSceneLayerReports.Find(entitySceneId, layerReports))
197 subSceneLayerReports.Set(entitySceneId, layerReports);
200 if (!layerReports.Find(entityLayerId, currentLayerReport))
202 currentLayerReport =
new SCR_WorldEntitiesStatisticsPlugin_Report();
203 currentLayerReport.m_WorldInfo = worldInfo;
205 currentLayerReport.m_iSubSceneId = entitySceneId;
206 currentLayerReport.m_sSubSceneName =
string.Format(
"SubScene #%1/%2", entitySceneId + 1, subScenesCount);
207 currentLayerReport.m_iLayerId = entityLayerId;
208 currentLayerReport.m_sLayerName = worldEditorAPI.GetSubsceneLayerPath(entitySceneId, entityLayerId);
209 layerReports.Insert(entityLayerId, currentLayerReport);
213 entity = worldEditorAPI.SourceToEntity(entitySource);
218 currentLayerReport.m_aAllEntityPositions.Insert(worldPos);
220 mainReport.m_aAllEntityPositions.Insert(worldPos);
224 ++sourcesWithoutEntity;
227 ancestor = entitySource.GetAncestor();
239 if (componentSource.Get(
"Object", model) && model)
242 currentLayerReport.m_aModelPositions.Insert(worldPos);
252 mainReport.m_aModelPositions.Insert(worldPos);
261 currentLayerReport.m_mPrefabNoAncestorResult.Set(
resourceName, currentLayerReport.m_mPrefabNoAncestorResult.Get(
resourceName) + 1);
267 string className = ancestor.GetClassName();
268 currentLayerReport.m_mEmptyResult.Set(className, currentLayerReport.m_mEmptyResult.Get(className) + 1);
270 mainReport.m_mEmptyResult.Set(className, mainReport.m_mEmptyResult.Get(className) + 1);
276 string className = entitySource.GetClassName();
277 currentLayerReport.m_mGenericResult.Set(className, currentLayerReport.m_mGenericResult.Get(className) + 1);
279 mainReport.m_mGenericResult.Set(className, mainReport.m_mGenericResult.Get(className) + 1);
283 Debug.EndTimeMeasure(
"Sorting " + editorEntitiesCount +
" entities");
285 if (noSourceFound > 0)
286 PrintFormat(
"%1 entities do NOT have an Entity Source!", noSourceFound, level:
LogLevel.WARNING);
288 if (sourcesWithoutEntity > 0)
289 PrintFormat(
"%1 entity sources do NOT have an Entity!", sourcesWithoutEntity, level:
LogLevel.WARNING);
295 string worldName = SCR_WorldEditorToolHelper.GetWorldName();
296 string worldDirectory =
string.Format(WORLD_DIRECTORY_FORMAT, worldName);
297 string layersDirectory =
FilePath.Concat(worldDirectory, LAYERS_SUBDIR);
303 foreach (
int layerId, SCR_WorldEntitiesStatisticsPlugin_Report layerReport : layersReports)
307 filePath =
FilePath.Concat(layersDirectory,
string.Format(LAYER_OUTPUT_FILE_NAME, layerReport.m_iSubSceneId, layerReport.m_sLayerName));
308 string fileDir =
FilePath.StripFileName(filePath);
309 if (!
FileIO.MakeDirectory(fileDir))
331 OutputLines(GetReportLines(layerReport), filePath);
333 if (m_bActiveLayerOnly && filePath)
336 if (Workbench.GetAbsolutePath(filePath, absPath,
true))
337 Workbench.RunCmd(
string.Format(
"notepad \"%1\"", absPath));
344 filePath =
FilePath.Concat(worldDirectory, OUTPUT_FILE_NAME);
345 OutputLines(GetReportLines(mainReport), filePath);
349 if (Workbench.GetAbsolutePath(filePath, absPath,
true))
351 string cmd =
string.Format(
"explorer \"%1\"",
FilePath.StripFileName(absPath));
353 Workbench.RunCmd(cmd);
357 if (m_bOutputToFile && m_bOutputHeatmap)
360 if (m_iHeatmapType == TYPE_DENSITY)
374 colourMode =
"Alpha";
376 if (m_bHeatmapValueInversion)
379 filePath =
FilePath.Concat(worldDirectory,
string.Format(OUTPUT_MAP_NAME,
type, colourMode));
380 if (m_iHeatmapType == TYPE_DENSITY)
382 if (!CreateDensityImage(
384 m_iHeatmapDefinition,
385 mainReport.m_WorldInfo.m_vTerrainMin,
386 mainReport.m_WorldInfo.m_vTerrainMax,
387 mainReport.m_aAllEntityPositions))
395 if (!CreateVarietyImage(
397 m_iHeatmapDefinition,
398 mainReport.m_WorldInfo.m_vTerrainMin,
399 mainReport.m_WorldInfo.m_vTerrainMax,
400 mainReport.m_aModels,
401 mainReport.m_aModelPositions))
409 return editorEntitiesCount;
418 protected float GetLandAboveWaterRatio(notnull
WorldEditorAPI worldEditorAPI,
vector terrainOrigin,
vector terrainSize,
float terrainUnitScale)
432 float oceanLevel = worldEditorAPI.GetWorld().GetOceanBaseHeight();
436 if (m_bDebug && terrainUnitScale < MIN_TERRAIN_DEBUG_RESOLUTION)
438 stepX = MIN_TERRAIN_DEBUG_RESOLUTION;
439 stepZ = MIN_TERRAIN_DEBUG_RESOLUTION;
440 PrintFormat(
"Debug is enabled - terrain surface tracing is every %1m instead of %2m, losing precision", MIN_TERRAIN_DEBUG_RESOLUTION, terrainUnitScale, level:
LogLevel.WARNING);
444 stepX = terrainUnitScale;
445 stepZ = terrainUnitScale;
450 for (
float x = terrainOrigin[0]; x < terrainSize[0]; x += stepX)
452 for (
float z = terrainOrigin[2]; z < terrainSize[2]; z += stepZ)
454 float y = worldEditorAPI.GetTerrainSurfaceY(x, z);
455 bool isAboveOrEqualWaterLevel = y >= oceanLevel;
456 if (isAboveOrEqualWaterLevel)
463 vector pos2D = { x, 0, z };
464 vector pos3D = { x, y, z };
471 if (isAboveOrEqualWaterLevel)
479 if (totalMeasures < 1 || aslMeasures == totalMeasures)
485 return aslMeasures / totalMeasures;
491 protected array<string> GetReportLines(notnull SCR_WorldEntitiesStatisticsPlugin_Report report)
493 array<string> lines = {};
495 lines.InsertAll(GetFormattedMapLines(
"Prefab entities", report.m_mPrefabResult, report));
496 lines.InsertAll(GetFormattedMapLines(
"entities with Prefab path on EntitySource", report.m_mPrefabNoAncestorResult, report));
497 lines.InsertAll(GetFormattedMapLines(
"entities with empty Prefab path", report.m_mEmptyResult, report));
498 lines.InsertAll(GetFormattedMapLines(
"generic entities", report.m_mGenericResult, report));
507 protected void OutputLines(notnull array<string> lines,
string filePath)
512 if (m_bOutputToFile && filePath)
520 Print(
"Export failed: cannot write to file - printing instead",
LogLevel.WARNING);
523 foreach (
string line : lines)
535 protected array<string> GetFormattedMapLines(
string description, notnull
map<ResourceName, int> resultMap, notnull SCR_WorldEntitiesStatisticsPlugin_Report report)
537 int count = resultMap.Count();
539 return {
string.Format(NO_MAP_CONTENT_FORMAT, description) };
541 array<string> resultLines = {};
543 array<int> values = {};
544 values.Reserve(count);
549 foreach (
string key,
int value : resultMap)
551 values.Insert(value);
552 entitiesCount += value;
556 if (report.m_WorldInfo.m_fTerrainSurface > 0)
557 entityDensity = entitiesCount / (report.m_WorldInfo.m_fTerrainSurface * SQM_TO_SQKM);
561 "Found and processed %1 %2 (density: %3/km² on land, %4/km² in water, %5/km² total)",
564 (entityDensity * report.m_WorldInfo.m_fTerrainSurfaceASLRatio).ToString(lenDec: 2),
565 (entityDensity * (1 - report.m_WorldInfo.m_fTerrainSurfaceASLRatio)).ToString(lenDec: 2),
566 (entityDensity).ToString(lenDec: 2)
570 if (m_iMaxDisplayedEntries > 0 && values.Count() > m_iMaxDisplayedEntries)
571 values.Resize(m_iMaxDisplayedEntries);
573 foreach (
int value : values)
575 if (value < m_iDisplayThreshold)
581 if (mapValue == value)
584 resultMap.Remove(key);
585 resultLines.Insert(
string.Format(
"%1× %2", value, key));
605 protected bool CreateDensityImage(
string imagePath,
int resolution,
vector terrainMin,
vector terrainMax, notnull array<vector> entityPositions)
613 if (terrainMin[0] >= terrainMax[0]
615 || terrainMin[2] >= terrainMax[2])
617 PrintFormat(
"Invalid terrain min/max (%1 & %2)", terrainMin, terrainMax, level:
LogLevel.WARNING);
621 float pixelWidth = (terrainMax[0] - terrainMin[0]) / resolution;
622 float pixelHeight = (terrainMax[2] - terrainMin[2]) / resolution;
624 if (pixelWidth < 0.1 || pixelHeight < 0.1)
626 PrintFormat(
"Invalid pixel size (%1×%2)", pixelWidth, pixelHeight, level:
LogLevel.WARNING);
630 array<int> imageData = {};
631 imageData.Resize(resolution * resolution);
633 Debug.BeginTimeMeasure();
635 foreach (
vector entityPos : entityPositions)
639 entityPos -= terrainMin;
641 int x = entityPos[0] / pixelWidth;
642 if (x < 0 || x >= resolution)
645 int z = resolution - entityPos[2] / pixelHeight;
646 if (z < 0 || z >= resolution)
649 imageData[x + resolution * z] = imageData[x + resolution * z] + 1;
652 Debug.EndTimeMeasure(
654 "Processing all %1 entity positions for image creation (%2×%2 = %3 pixels)",
655 entityPositions.Count(),
657 resolution * resolution));
671 protected bool CreateVarietyImage(
string imagePath,
int resolution,
vector terrainMin,
vector terrainMax, notnull array<ResourceName> models, notnull array<vector> prefabPositions)
679 if (terrainMin[0] >= terrainMax[0]
681 || terrainMin[2] >= terrainMax[2])
683 PrintFormat(
"Invalid terrain min/max (%1 & %2)", terrainMin, terrainMax, level:
LogLevel.WARNING);
687 float pixelWidth = (terrainMax[0] - terrainMin[0]) / resolution;
688 float pixelHeight = (terrainMax[2] - terrainMin[2]) / resolution;
690 if (pixelWidth < 0.1 || pixelHeight < 0.1)
692 PrintFormat(
"Invalid pixel size (%1×%2)", pixelWidth, pixelHeight, level:
LogLevel.WARNING);
696 array<int> imageData = {};
697 array<ref set<ResourceName>> pixelPrefabs = {};
698 imageData.Resize(resolution * resolution);
699 pixelPrefabs.Resize(resolution * resolution);
701 Debug.BeginTimeMeasure();
703 set<ResourceName> modelsSet;
704 foreach (
int i,
vector prefabPosition : prefabPositions)
708 prefabPosition -= terrainMin;
710 int x = prefabPosition[0] / pixelWidth;
711 if (x < 0 || x >= resolution)
714 int z = resolution - prefabPosition[2] / pixelHeight;
715 if (z < 0 || z >= resolution)
718 modelsSet = pixelPrefabs[x + resolution * z];
721 modelsSet =
new set<ResourceName>();
722 pixelPrefabs[x + resolution * z] = modelsSet;
725 modelsSet.Insert(models[i]);
727 int value = modelsSet.Count();
728 imageData[x + resolution * z] = value;
731 Debug.EndTimeMeasure(
733 "Processing all %1 entity positions for image creation (%2×%2 = %3 pixels)",
734 prefabPositions.Count(),
736 resolution * resolution));
743 protected int ButtonAnalyse()
750 protected int ButtonClose()
756class SCR_WorldEntitiesStatisticsPlugin_Report_World
758 float m_fTerrainSurface;
759 float m_fTerrainSurfaceASLRatio;
760 vector m_vTerrainMin;
761 vector m_vTerrainMax;
764class SCR_WorldEntitiesStatisticsPlugin_Report
766 ref SCR_WorldEntitiesStatisticsPlugin_Report_World m_WorldInfo;
769 string m_sSubSceneName;
775 ref array<vector> m_aAllEntityPositions = {};
778 ref array<ResourceName> m_aModels = {};
779 ref array<vector> m_aModelPositions = {};
782 ref map<ResourceName, int> m_mPrefabResult =
new map<ResourceName, int>();
783 ref map<ResourceName, int> m_mPrefabNoAncestorResult =
new map<ResourceName, int>();
784 ref map<ResourceName, int> m_mEmptyResult =
new map<ResourceName, int>();
785 ref map<ResourceName, int> m_mGenericResult =
new map<ResourceName, int>();
791class SCR_WorldEntitiesStatisticsPlugin_Item
ResourceName resourceName
UI Textures DeployMenu Briefing conflict_HintBanner_1_UI desc
ref SCR_DebugShapeManager m_DebugShapeManager
ResourceName m_sResourceName
class WorkbenchDialog_AbortRetryIgnore ButtonAttribute("OK", true)
proto external vector GetOrigin()
static bool WriteFileContent(string filePath, notnull array< string > lines)
static string GetResolutionFactorEnum(array< int > multipliers=null)
static bool CreateHeatmapImageFromData(string imagePath, notnull array< int > imageData, int colourMode=COLOUR_MODE_GREYSCALE, bool invertColour=false, int maxValueMode=MAX_MODE_RAW, bool highlightValuesAboveMax=false, int resolutionFactor=1)
static ParamEnumArray FromString(string input)
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
proto external string ToString()
Plain C++ pointer, no weak pointers, no memory management.