Add main mesh fixes

This commit is contained in:
Toomas Tamm
2021-02-23 02:13:14 +02:00
parent c89f326c37
commit 989424e659
139 changed files with 8101 additions and 2033 deletions

View File

@@ -0,0 +1,15 @@
{
"name": "AssetCleaner",
"references": [],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": false,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: de141fd48bce7594e9474fe440f5ea10
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7f4dfe9d348c0ff4cb525a0b2c48fc34
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 0
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f45f9c9770a8371428fce5031e1c7c5c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View File

@@ -0,0 +1,110 @@
fileFormatVersion: 2
guid: bfec65de7ee2e114199f83507d375a6f
TextureImporter:
fileIDToRecycleName: {}
externalObjects: {}
serializedVersion: 9
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: -1
aniso: 1
mipBias: -100
wrapU: 1
wrapV: 1
wrapW: -1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 2
textureShape: 1
singleChannelComponent: 0
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
platformSettings:
- serializedVersion: 2
buildTarget: DefaultTexturePlatform
maxTextureSize: 32
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 2
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
- serializedVersion: 2
buildTarget: Standalone
maxTextureSize: 32
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 2
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
- serializedVersion: 2
buildTarget: Android
maxTextureSize: 32
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 2
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
vertices: []
indices:
edges: []
weights: []
spritePackingTag:
pSDRemoveMatte: 0
pSDShowRemoveMatteOption: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View File

@@ -0,0 +1,110 @@
fileFormatVersion: 2
guid: f4b3e5a29ec20194a8ba1d6cf7b1b037
TextureImporter:
fileIDToRecycleName: {}
externalObjects: {}
serializedVersion: 9
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: -1
aniso: 1
mipBias: -100
wrapU: 1
wrapV: 1
wrapW: -1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 2
textureShape: 1
singleChannelComponent: 0
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
platformSettings:
- serializedVersion: 2
buildTarget: DefaultTexturePlatform
maxTextureSize: 32
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 2
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
- serializedVersion: 2
buildTarget: Standalone
maxTextureSize: 32
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 2
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
- serializedVersion: 2
buildTarget: Android
maxTextureSize: 32
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 2
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID:
vertices: []
indices:
edges: []
weights: []
spritePackingTag:
pSDRemoveMatte: 0
pSDShowRemoveMatteOption: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View File

@@ -0,0 +1,110 @@
fileFormatVersion: 2
guid: 3fec3bf4c4f5e2d48835dbe2f21aa7d0
TextureImporter:
fileIDToRecycleName: {}
externalObjects: {}
serializedVersion: 9
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: -1
aniso: 1
mipBias: -100
wrapU: 1
wrapV: 1
wrapW: -1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 2
textureShape: 1
singleChannelComponent: 0
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
platformSettings:
- serializedVersion: 2
buildTarget: DefaultTexturePlatform
maxTextureSize: 32
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 2
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
- serializedVersion: 2
buildTarget: Standalone
maxTextureSize: 32
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 2
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
- serializedVersion: 2
buildTarget: Android
maxTextureSize: 32
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 2
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID:
vertices: []
indices:
edges: []
weights: []
spritePackingTag:
pSDRemoveMatte: 0
pSDShowRemoveMatteOption: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View File

@@ -0,0 +1,110 @@
fileFormatVersion: 2
guid: 07bb28c80f378c64194dd46c34b2d6c1
TextureImporter:
fileIDToRecycleName: {}
externalObjects: {}
serializedVersion: 9
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: -1
aniso: 1
mipBias: -100
wrapU: 1
wrapV: 1
wrapW: -1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 2
textureShape: 1
singleChannelComponent: 0
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
platformSettings:
- serializedVersion: 2
buildTarget: DefaultTexturePlatform
maxTextureSize: 32
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 2
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
- serializedVersion: 2
buildTarget: Standalone
maxTextureSize: 32
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 2
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
- serializedVersion: 2
buildTarget: Android
maxTextureSize: 32
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 2
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
vertices: []
indices:
edges: []
weights: []
spritePackingTag:
pSDRemoveMatte: 0
pSDShowRemoveMatteOption: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View File

@@ -0,0 +1,110 @@
fileFormatVersion: 2
guid: c5fd88366c67daf4585631953fa79779
TextureImporter:
fileIDToRecycleName: {}
externalObjects: {}
serializedVersion: 9
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: -1
aniso: 1
mipBias: -100
wrapU: 1
wrapV: 1
wrapW: -1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 2
textureShape: 1
singleChannelComponent: 0
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
platformSettings:
- serializedVersion: 2
buildTarget: DefaultTexturePlatform
maxTextureSize: 32
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 2
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
- serializedVersion: 2
buildTarget: Standalone
maxTextureSize: 32
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 2
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
- serializedVersion: 2
buildTarget: Android
maxTextureSize: 32
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 2
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID:
vertices: []
indices:
edges: []
weights: []
spritePackingTag:
pSDRemoveMatte: 0
pSDShowRemoveMatteOption: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View File

@@ -0,0 +1,110 @@
fileFormatVersion: 2
guid: e0208e3a37267be46ba2d10fad510cef
TextureImporter:
fileIDToRecycleName: {}
externalObjects: {}
serializedVersion: 9
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: -1
aniso: 1
mipBias: -100
wrapU: 1
wrapV: 1
wrapW: -1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 2
textureShape: 1
singleChannelComponent: 0
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
platformSettings:
- serializedVersion: 2
buildTarget: DefaultTexturePlatform
maxTextureSize: 32
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
- serializedVersion: 2
buildTarget: Standalone
maxTextureSize: 32
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
- serializedVersion: 2
buildTarget: Android
maxTextureSize: 32
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID:
vertices: []
indices:
edges: []
weights: []
spritePackingTag:
pSDRemoveMatte: 0
pSDShowRemoveMatteOption: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View File

@@ -0,0 +1,110 @@
fileFormatVersion: 2
guid: e755b5ef4cb427f469ba580ae9956237
TextureImporter:
fileIDToRecycleName: {}
externalObjects: {}
serializedVersion: 9
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: -1
aniso: 1
mipBias: -100
wrapU: 1
wrapV: 1
wrapW: -1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 2
textureShape: 1
singleChannelComponent: 0
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
platformSettings:
- serializedVersion: 2
buildTarget: DefaultTexturePlatform
maxTextureSize: 32
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 2
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
- serializedVersion: 2
buildTarget: Standalone
maxTextureSize: 32
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 2
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
- serializedVersion: 2
buildTarget: Android
maxTextureSize: 32
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 2
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID:
vertices: []
indices:
edges: []
weights: []
spritePackingTag:
pSDRemoveMatte: 0
pSDShowRemoveMatteOption: 0
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4848aa5846cd6264c984dcc6cd9d72af
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,74 @@
using System;
using System.Linq;
namespace Asset_Cleaner {
[Serializable]
class AufSerializableData {
public const int CurrentVersion = 1;
public int Version;
public bool MarkRed;
public int RebuildCacheOnDemand;
public bool ShowInfoBox;
public string IgnorePathContainsCombined;
public bool IgnoreMaterial;
public bool IgnoreScriptable;
static int BoolToInt(bool val) {
return val ? 2 : 1;
}
static bool IntToBool(int val, bool defaultVal) {
switch (val) {
case 2:
return true;
case 1:
return false;
default:
return defaultVal;
}
}
public static AufSerializableData Default() {
return new AufSerializableData {
Version = CurrentVersion,
MarkRed = true,
ShowInfoBox = true,
IgnorePathContainsCombined = "Gizmos;Resources;Editor;Asset Cleaner;Asset Usage Finder;",
IgnoreMaterial = false,
IgnoreScriptable = true,
RebuildCacheOnDemand = 2,
};
}
public static void OnSerialize(in Config src, out AufSerializableData result) {
result = new AufSerializableData();
result.Version = CurrentVersion;
result.MarkRed = src.MarkRed;
result.ShowInfoBox = src.ShowInfoBox;
result.IgnorePathContainsCombined = src.IgnorePathContainsCombined;
result.IgnoreMaterial = src.IgnoreMaterial;
result.IgnoreScriptable = src.IgnoreScriptable;
result.RebuildCacheOnDemand = BoolToInt(src.RebuildCacheOnDemand);
}
public static void OnDeserialize(in AufSerializableData src, ref Config result) {
var def = Default();
result.MarkRed = src.MarkRed;
result.IgnorePathContainsCombined = src.IgnorePathContainsCombined;
result.ShowInfoBox = src.ShowInfoBox;
result.IgnorePathContains = result.IgnorePathContainsCombined
.Split(';')
.Select(s => s.Trim())
.Where(s => !string.IsNullOrWhiteSpace(s))
.ToArray();
result.IgnoreMaterial = src.IgnoreMaterial;
result.IgnoreScriptable = src.IgnoreScriptable;
result.RebuildCacheOnDemand = IntToBool(src.RebuildCacheOnDemand, def.RebuildCacheOnDemand == 2);
}
public bool Valid() {
return Version == CurrentVersion || IgnorePathContainsCombined == null;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 14980ecc573d44c59b5f9323603c7bdf
timeCreated: 1588518687

View File

@@ -0,0 +1,105 @@
using System;
using System.Linq;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;
#pragma warning disable 649
namespace Asset_Cleaner {
class CleanerStyleAsset : ScriptableObject {
[Serializable]
public class Style {
public Color RedHighlight = new Color(1, 0, 0, 1f);
public GUIContent Lock;
public GUIStyle LockBtn = new GUIStyle();
public GUIStyle SampleBtn = new GUIStyle();
public GUIContent Unlock;
public GUIStyle UnlockBtn = new GUIStyle();
public GUIContent RemoveFile;
public GUIContent RemoveScene;
public GUIStyle RowMainAssetBtn = new GUIStyle();
public GUIStyle RemoveUnusedBtn = new GUIStyle();
public GUIStyle CurrentBtn = new GUIStyle();
public GUIContent ArrowL;
public GUIContent ArrowR;
public GUIStyle ArrowBtn = new GUIStyle();
public float SceneIndent1 = 20f;
public float SceneIndent2 = 20f;
public GUIStyle ProjectViewCounterLabel;
public GUIContent MultiSelect;
public static bool TryFindSelf(out Style value) {
const string typeName = nameof(CleanerStyleAsset);
var guids = AssetDatabase.FindAssets($"t:{typeName}");
if (!guids.Any()) {
AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate);
}
Asr.IsTrue(guids.Length > 0, $"No '{typeName}' assets found");
var res = guids.Select(AssetDatabase.GUIDToAssetPath).Select(t => (CleanerStyleAsset) AssetDatabase.LoadAssetAtPath(t, typeof(CleanerStyleAsset))).FirstOrDefault();
if (res == null) {
value = default;
return false;
}
value = EditorGUIUtility.isProSkin ? res.Pro : res.Personal;
return value != null;
}
}
#pragma warning disable 0649
public Style Pro;
public Style Personal;
#pragma warning restore
[CustomEditor(typeof(CleanerStyleAsset))]
class Editor : UnityEditor.Editor {
public override void OnInspectorGUI() {
#if false
if (GUILayout.Button("Update Btn backgrounds")) {
var targ = (CleanerStyleAsset) target;
Set(targ.Pro);
}
#endif
EditorGUI.BeginChangeCheck();
base.OnInspectorGUI();
if (EditorGUI.EndChangeCheck())
UnityEditorInternal.InternalEditorUtility.RepaintAllViews();
}
#if false
static void Set(Style style) {
var st = style;
var sample = st.SampleBtn;
foreach (var btn in new[] {
st.LockBtn,
st.UnlockBtn,
st.RowMainAssetBtn,
st.RemoveUnusedBtn,
st.CurrentBtn,
st.ArrowBtn,
}) {
btn.normal = sample.normal;
btn.hover = sample.hover;
btn.active = sample.active;
btn.focused = sample.focused;
btn.onNormal = sample.onNormal;
btn.onHover = sample.onHover;
btn.onActive = sample.onActive;
btn.onFocused = sample.onFocused;
}
}
#endif
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 16578cbec6a24ac4d951b5f328a970ca
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
namespace Asset_Cleaner {
enum FindModeEnum {
None = 0,
File = 1,
Scene = 2,
Stage = 3,
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 34e14d6333c649f4fb74ccdb6c86d8b6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f94bcb0962724eeea17b007a6ddfd885
timeCreated: 1596213773

View File

@@ -0,0 +1,286 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEditorInternal;
using Debug = UnityEngine.Debug;
using Object = UnityEngine.Object;
namespace Asset_Cleaner {
class BacklinkStore {
public bool Initialized { get; private set; }
public Dictionary<string, long> UnusedFiles { get; private set; }
public Dictionary<string, long> UnusedScenes { get; private set; }
public Dictionary<string, BwMeta> Backward { get; private set; }
public Dictionary<string, UnusedQty> FoldersWithQty { get; private set; }
Dictionary<string, FwMeta> _forward;
List<string> Folders { get; set; }
public void Init() {
FoldersWithQty = new Dictionary<string, UnusedQty>();
_forward = new Dictionary<string, FwMeta>();
Backward = new Dictionary<string, BwMeta>();
var defaultAss = typeof(DefaultAsset);
var asmdefAss = typeof(AssemblyDefinitionAsset);
var paths = AssetDatabase.GetAllAssetPaths()
.Distinct()
.Where(s => s.StartsWith("Assets") || s.StartsWith("ProjectSettings"))
.Where(p => {
var t = AssetDatabase.GetMainAssetTypeAtPath(p);
return !t.IsAssignableFromInverse(defaultAss) && !t.IsAssignableFromInverse(asmdefAss);
})
.ToArray();
var i = 0f;
var total = (float) paths.Length;
foreach (var path in paths) {
_FillFwAndBacklinks(path);
var percent = i * 100f / total;
if (Math.Abs(percent % 5f) < 0.01f) {
if (EditorUtility.DisplayCancelableProgressBar(
"Please wait...",
"Building the cache...", percent))
Debug.LogError("Cache build aborted");
}
i++;
}
EditorUtility.ClearProgressBar();
// FillFoldersWithQtyByPaths
List<string> foldersAll = new List<string>();
foreach (var path in paths) {
var folders = GetAllFoldersFromPath(path);
foldersAll.AddRange(folders);
}
Folders = foldersAll.Distinct().OrderBy(p => p).ToList();
UpdateUnusedAssets();
Initialized = true;
}
void _FillFwAndBacklinks(string path) {
var dependencies = _Dependencies(path);
var hs = new FwMeta {Dependencies = new HashSet<string>(dependencies)};
_forward.Add(path, hs);
foreach (var backPath in dependencies) {
if (!Backward.TryGetValue(backPath, out var val)) {
val = new BwMeta();
val.Lookup = new HashSet<string>();
Backward.Add(backPath, val);
}
val.Lookup.Add(path);
}
}
void UpdateFoldersWithQtyByPath(string path) {
var folders = GetAllFoldersFromPath(path);
foreach (var folder in folders) {
if (!Folders.Exists(p => p == folder))
Folders.Add(folder);
}
}
static List<string> GetAllFoldersFromPath(string p) {
var result = new List<string>();
var i = p.IndexOf('/', 0);
while (i > 0) {
var item = p.Substring(0, i);
result.Add(item);
i = p.IndexOf('/', i + 1);
}
return result.Distinct().ToList();
}
public void UpdateUnusedAssets() {
var all = new HashSet<string>(_forward.Keys);
var withBacklinks =
new HashSet<string>(Backward.Where(kv => kv.Value.Lookup.Count > 0).Select(kv => kv.Key));
all.ExceptWith(withBacklinks);
all.RemoveWhere(SearchUtils.IsFileIgrnoredBySettings);
var unusedAssets = all;
var scenes = unusedAssets.Where(s =>
AssetDatabase.GetMainAssetTypeAtPath(s).IsAssignableFromInverse(typeof(SceneAsset))).ToArray();
unusedAssets.ExceptWith(scenes);
var files = unusedAssets;
UnusedFiles = new Dictionary<string, long>();
foreach (var file in files) UnusedFiles[file] = CommonUtils.Size(file);
UnusedScenes = new Dictionary<string, long>();
foreach (var scene in scenes) UnusedScenes[scene] = CommonUtils.Size(scene);
// UpdateFoldersWithQty();
foreach (var folder in Folders) {
var unusedFilesQty = UnusedFiles.Count(p => p.Key.StartsWith(folder));
var unusedScenesQty = UnusedScenes.Count(p => p.Key.StartsWith(folder));
long size = 0;
size = UnusedFiles.Where((p => p.Key.StartsWith(folder))).Sum(p => p.Value);
size += UnusedScenes.Where(p => p.Key.StartsWith(folder)).Sum(p => p.Value);
FoldersWithQty.TryGetValue(folder, out var folderWithQty);
if (folderWithQty == null) {
FoldersWithQty.Add(folder, new UnusedQty(unusedFilesQty, unusedScenesQty, size));
}
else {
folderWithQty.UnusedFilesQty = unusedFilesQty;
folderWithQty.UnusedScenesQty = unusedScenesQty;
folderWithQty.UnusedSize = size;
}
}
}
public void Remove(string path) {
if (!_forward.TryGetValue(path, out var fwMeta))
return;
foreach (var dependency in fwMeta.Dependencies) {
if (!Backward.TryGetValue(dependency, out var dep)) continue;
dep.Lookup.Remove(path);
}
_forward.Remove(path);
UpdateFoldersWithQtyByPath(path);
}
public void Replace(string src, string dest) {
_Upd(_forward);
_Upd(Backward);
UpdateFoldersWithQtyByPath(dest);
void _Upd<T>(Dictionary<string, T> dic) {
if (!dic.TryGetValue(src, out var refs)) return;
dic.Remove(src);
dic.Add(dest, refs);
}
}
public void RebuildFor(string path, bool remove) {
if (!_forward.TryGetValue(path, out var fwMeta)) {
fwMeta = new FwMeta();
_forward.Add(path, fwMeta);
}
else if (remove) {
foreach (var dependency in fwMeta.Dependencies) {
if (!Backward.TryGetValue(dependency, out var backDep)) continue;
backDep.Lookup.Remove(path);
}
fwMeta.Dependencies = null;
}
var dependencies = _Dependencies(path);
fwMeta.Dependencies = new HashSet<string>(dependencies);
foreach (var backPath in dependencies) {
if (!Backward.TryGetValue(backPath, out var bwMeta)) {
bwMeta = new BwMeta {Lookup = new HashSet<string>()};
Backward.Add(backPath, bwMeta);
}
else if (remove)
bwMeta.Lookup.Remove(path);
bwMeta.Lookup.Add(path);
}
if (!remove) {
UpdateFoldersWithQtyByPath(path);
}
}
static string[] _Dependencies(string s) {
if (s[0] == 'A')
return AssetDatabase.GetDependencies(s, false);
var obj = LoadAllOrMain(s)[0];
return GetDependenciesManualPaths().ToArray();
Object[] LoadAllOrMain(string assetPath) {
// prevents error "Do not use readobjectthreaded on scene objects!"
return typeof(SceneAsset) == AssetDatabase.GetMainAssetTypeAtPath(assetPath)
? new[] {AssetDatabase.LoadMainAssetAtPath(assetPath)}
: AssetDatabase.LoadAllAssetsAtPath(assetPath);
}
IEnumerable<string> GetDependenciesManualPaths() {
if (obj is EditorBuildSettings) {
foreach (var scene in EditorBuildSettings.scenes)
yield return scene.path;
}
using (var so = new SerializedObject(obj)) {
var props = so.GetIterator();
while (props.Next(true)) {
switch (props.propertyType) {
case SerializedPropertyType.ObjectReference:
var propsObjectReferenceValue = props.objectReferenceValue;
if (!propsObjectReferenceValue) continue;
var assetPath = AssetDatabase.GetAssetPath(propsObjectReferenceValue);
yield return assetPath;
break;
#if later
case SerializedPropertyType.Generic:
case SerializedPropertyType.ExposedReference:
case SerializedPropertyType.ManagedReference:
break;
#endif
default:
continue;
}
}
}
}
}
class FwMeta {
public HashSet<string> Dependencies;
}
public class BwMeta {
public HashSet<string> Lookup;
}
public class UnusedQty {
public int UnusedFilesQty;
public int UnusedScenesQty;
public long UnusedSize;
public UnusedQty() {
Init(0, 0, 0);
}
public UnusedQty(int unusedFilesQty, int unusedScenesQty, long unusedSize) {
Init(unusedFilesQty, unusedScenesQty, unusedSize);
}
private void Init(int unusedFilesQty, int unusedScenesQty, long unusedSize) {
UnusedFilesQty = unusedFilesQty;
UnusedScenesQty = unusedScenesQty;
UnusedSize = unusedSize;
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 004e5680782f4e679ae93027d2d40023
timeCreated: 1577106223

View File

@@ -0,0 +1,20 @@
namespace Asset_Cleaner {
class Config {
// serialized
public bool MarkRed;
public string IgnorePathContainsCombined;
public bool ShowInfoBox;
public bool RebuildCacheOnDemand;
// todo make type array
public bool IgnoreMaterial;
public bool IgnoreScriptable;
// serialized only while window is opened
public bool Locked;
// non-serialized
public string[] IgnorePathContains;
public string InitializationTime;
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a6ff796e4dab4324887a303cba29eed4
timeCreated: 1577117228

View File

@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
namespace Asset_Cleaner {
[Serializable]
class PersistentUndoRedoState {
public List<SelectionEntry> History = new List<SelectionEntry>();
public int Id;
public void Deconstruct(out List<SelectionEntry> list, out int id) {
id = Id;
list = History;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5414db21dfea4d7eaff8ce4c8f106c8d
timeCreated: 1596213717

View File

@@ -0,0 +1,6 @@
namespace Asset_Cleaner {
class UndoRedoState {
public bool UndoEnabled;
public bool RedoEnabled;
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c3ac9efa32c8491aa8a11749f8020d96
timeCreated: 1596213776

View File

@@ -0,0 +1,14 @@
using UnityEngine;
namespace Asset_Cleaner {
class WindowData {
public bool ExpandFiles;
public bool ExpandScenes;
public Vector2 ScrollPos;
public CleanerStyleAsset.Style Style;
public GUIContent SceneFoldout;
public PrevClick Click;
public AufWindow Window;
public FindModeEnum FindFrom;
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 8481f043b2414c08a45957dcd6035fbe
timeCreated: 1575305740

View File

@@ -0,0 +1,11 @@
using Leopotam.Ecs;
namespace Asset_Cleaner {
class InSceneResult : IEcsAutoReset {
public string ScenePath;
public void Reset() {
ScenePath = default;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 013577ffef1140109e8367f2a5f75fa6
timeCreated: 1577269723

View File

@@ -0,0 +1,18 @@
using UnityEngine;
namespace Asset_Cleaner {
public struct PrevClick {
const float DoubleClickTime = 0.5f;
Object _target;
float _timeClicked;
public PrevClick(Object target) {
_target = target;
_timeClicked = Time.realtimeSinceStartup;
}
public bool IsDoubleClick(Object o) {
return _target == o && Time.realtimeSinceStartup - _timeClicked < DoubleClickTime;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 31c0f204d1b044caaa02a0726f11a270
timeCreated: 1576087327

View File

@@ -0,0 +1,19 @@
using Leopotam.Ecs;
using UnityEngine;
using Object = UnityEngine.Object;
namespace Asset_Cleaner {
class Result : IEcsAutoReset {
public string FilePath;
public Object File;
public Object MainFile;
public GameObject RootGo;
public void Reset() {
FilePath = default;
File = default;
MainFile = default;
RootGo = default;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 46f4fd24243adc749b377209f744d628
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,20 @@
using Leopotam.Ecs;
using UnityEngine.SceneManagement;
namespace Asset_Cleaner {
class SceneDetails : IEcsAutoReset {
public string Path;
public Scene Scene;
public bool SearchRequested;
public bool SearchDone;
public bool WasOpened;
public void Reset() {
Path = default;
Scene = default;
SearchRequested = default;
SearchDone = default;
WasOpened = default;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 14749f86c2c14febb592442508f8c23a
timeCreated: 1577266613

View File

@@ -0,0 +1,11 @@
using Leopotam.Ecs;
namespace Asset_Cleaner {
class SceneResult : IEcsAutoReset {
public string PathNicified;
public void Reset() {
PathNicified = default;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 338e9e86c9274f91808429753a912d4b
timeCreated: 1576083610

View File

@@ -0,0 +1,26 @@
using System.Collections.Generic;
using Leopotam.Ecs;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace Asset_Cleaner {
class SearchArg : IEcsAutoReset {
public Object Target;
public Object Main;
public string FilePath;
public Option<Object[]> SubAssets;
public Scene Scene;
public List<string> UnusedAssetsFiltered;
public List<string> UnusedScenesFiltered;
public void Reset() {
UnusedAssetsFiltered = default;
UnusedScenesFiltered = default;
Target = default;
Main = default;
SubAssets = default;
Scene = default;
FilePath = default;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4a6b25ca9296c9e41affadc9ccecb1f1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,32 @@
using System.Collections.Generic;
using Leopotam.Ecs;
using UnityEditor;
using UnityEngine;
namespace Asset_Cleaner {
class SearchResultGui : IEcsAutoReset {
public SerializedObject SerializedObject;
public List<PropertyData> Properties;
public GUIContent Label;
public string TransformPath;
public void Reset() {
SerializedObject?.Dispose();
SerializedObject = default;
if (Properties != default)
foreach (var propertyData in Properties) {
propertyData.Property.Dispose();
}
Properties = default;
Label = default;
TransformPath = default;
}
public class PropertyData {
public SerializedProperty Property;
public GUIContent Content;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 14f26ff078c04c5685fdbb3ec3f92382
timeCreated: 1576168925

View File

@@ -0,0 +1,17 @@
using Leopotam.Ecs;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace Asset_Cleaner {
class SelectionChanged : IEcsAutoReset {
public Object Target;
public Scene Scene;
public FindModeEnum From;
public void Reset() {
Target = default;
Scene = default;
From = default;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1d0dc4d007bb4ebbbeea6626197d17b9
timeCreated: 1575365553

View File

@@ -0,0 +1,31 @@
using System;
using UnityEditor;
using Object = UnityEngine.Object;
namespace Asset_Cleaner {
[Serializable]
class SelectionEntry {
public bool IsGuids;
public string[] Guids;
public Object[] SceneObjects;
public bool Valid() {
if (IsGuids) {
foreach (var guid in Guids) {
var path = AssetDatabase.GUIDToAssetPath(guid);
if (!string.IsNullOrEmpty(path))
return true;
}
return false;
}
foreach (var sceneObject in SceneObjects)
if (sceneObject)
return true;
return false;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2b4476be23474ea4b20262c0ee59983c
timeCreated: 1596139978

View File

@@ -0,0 +1,34 @@
using System;
using UnityEditor;
using UnityEditorInternal;
using UnityEngine;
namespace Asset_Cleaner {
static class IgnoreTypes {
public static bool Check(string path, out Type type) {
type = AssetDatabase.GetMainAssetTypeAtPath(path);
if (type == null) return false;
var conf = Globals<Config>.Value;
if (type.IsAssignableFromInverse(typeof(MonoScript))) return true;
if (type.IsAssignableFromInverse(typeof(DefaultAsset))) return true;
if (conf.IgnoreScriptable && type.IsAssignableFromInverse(typeof(ScriptableObject))) return true;
if (type.IsAssignableFromInverse(typeof(Shader))) return true;
if (type.IsAssignableFromInverse(typeof(ComputeShader))) return true;
if (type.IsAssignableFromInverse(typeof(ShaderVariantCollection))) return true;
#if UNITY_2019_3_OR_NEWER
if (type.IsAssignableFromInverse(typeof(UnityEngine.Experimental.Rendering.RayTracingShader))) return true; // todo: track of Experimental namespace
#endif
if (type.IsAssignableFromInverse(typeof(TextAsset))) return true;
if (type.IsAssignableFromInverse(typeof(AssemblyDefinitionAsset))) return true;
if (type.IsAssignableFromInverse(typeof(UnityEngine.U2D.SpriteAtlas))) return true;
if (conf.IgnoreMaterial && type.IsAssignableFromInverse(typeof(Material))) return true;
return false;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 34f1d98d6e464e5ebee0df9de721af39
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 4b15d89b453047218955641734d4ec9e
timeCreated: 1596213484

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: cbedea52d931409a8b62256873637225
timeCreated: 1596214557

View File

@@ -0,0 +1,94 @@
using System.Diagnostics;
using UnityEditor;
using UnityEngine;
using Debug = UnityEngine.Debug;
namespace Asset_Cleaner {
class AufWindow : EditorWindow {
[SerializeField] PersistentUndoRedoState _persistentUndo;
[MenuItem("Window/- Asset Cleaner %L")]
static void OpenActiveWindow() {
GetWindow<AufWindow>();
}
// restore window state after recompilation
void OnEnable() {
var wd = Globals<WindowData>.Value = new WindowData();
wd.Window = this;
var firstTime = _persistentUndo == null;
Globals<PersistentUndoRedoState>.Value = _persistentUndo ?? new PersistentUndoRedoState();
Globals<BacklinkStore>.Value = new BacklinkStore();
var config = Globals<Config>.Value = new Config();
PersistenceUtils.Load(ref config);
if (firstTime || !config.RebuildCacheOnDemand)
Globals<BacklinkStore>.Value.Init();
EditorApplication.update += Upd;
EditorApplication.projectWindowItemOnGUI += ProjectViewGui.OnProjectWindowItemOnGui;
AufCtx.TryInitWorld();
// need to close window in case of Asset Cleaner uninstalled
if (!CleanerStyleAsset.Style.TryFindSelf(out wd.Style))
ForceClose();
}
void OnGUI() {
var store = Globals<BacklinkStore>.Value;
if (!store.Initialized) {
// prevent further window GUI rendering
if (!GUILayout.Button("Initialize Cache")) return;
var stopwatch = new Stopwatch();
stopwatch.Start();
store.Init();
stopwatch.Stop();
Globals<Config>.Value.InitializationTime = $"Initialized in {stopwatch.Elapsed.TotalSeconds:N} s";
AufCtx.World.NewEntityWith(out RequestRepaintEvt _);
}
AufCtx.OnGuiGroup.Run();
}
static void Upd() {
if (AufCtx.World == null) {
AufCtx.DestroyWorld();
return;
}
if (!Globals<BacklinkStore>.Value.Initialized) return;
AufCtx.UpdateGroup.Run();
}
bool _closing;
void ForceClose() {
if (_closing) return;
_closing = true;
Close();
EditorWindow.DestroyImmediate(this);
}
void OnDisable() {
if (AufCtx.Destroyed) return;
_persistentUndo = Globals<PersistentUndoRedoState>.Value;
AufCtx.UpdateGroup.Destroy();
AufCtx.OnGuiGroup.Destroy();
AufCtx.DestroyWorld();
Globals<Config>.Value = default;
Globals<PersistentUndoRedoState>.Value = default;
Globals<WindowData>.Value = default;
EditorApplication.update -= Upd;
EditorApplication.projectWindowItemOnGUI -= ProjectViewGui.OnProjectWindowItemOnGui;
// need to close window in case of Asset Cleaner uninstalled
if (!CleanerStyleAsset.Style.TryFindSelf(out _))
ForceClose();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 44ebd7bbb2bb41b4ba0187901e8d583f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,23 @@
using UnityEditor;
namespace Asset_Cleaner {
class ProcessAllAssets : AssetPostprocessor {
static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths) {
if (!AufCtx.InitStarted) return;
if (!Globals<BacklinkStore>.Value.Initialized) return;
var store = Globals<BacklinkStore>.Value;
var length = movedAssets.Length;
for (var i = 0; i < length; i++)
store.Replace(movedFromAssetPaths[i], movedAssets[i]);
foreach (var path in deletedAssets)
store.Remove(path);
foreach (var path in importedAssets)
store.RebuildFor(path, true);
store.UpdateUnusedAssets();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 17c1f845fa88d38448c4cf65e9745f30
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,47 @@
using UnityEditor;
using UnityEngine;
namespace Asset_Cleaner {
static class ProjectViewGui {
static CleanerStyleAsset.Style _style = Globals<WindowData>.Value.Style;
public static void OnProjectWindowItemOnGui(string guid, Rect rect) {
if (!Globals<Config>.Value.MarkRed) return;
var store = Globals<BacklinkStore>.Value;
if (!store.Initialized) return;
var path = AssetDatabase.GUIDToAssetPath(guid);
ShowRowQuantity(rect, path, store);
long size = 0;
var _ = store.UnusedFiles.TryGetValue(path, out size) || store.UnusedScenes.TryGetValue(path, out size);
if (SearchUtils.IsUnused(path)) {
var buf = GUI.color;
{
GUI.color = _style.RedHighlight;
GUI.Box(rect, string.Empty);
}
GUI.color = buf;
GUI.Label(rect, CommonUtils.BytesToString(size), _style.ProjectViewCounterLabel);
}
}
static void ShowRowQuantity(Rect rect, string path, BacklinkStore backlinkStore) {
if (!AssetDatabase.IsValidFolder(path))
return;
backlinkStore.FoldersWithQty.TryGetValue(path, out var folderWithQty);
var cntFiles = folderWithQty?.UnusedFilesQty ?? 0;
var cntScenes = folderWithQty?.UnusedScenesQty ?? 0;
long size = folderWithQty?.UnusedSize ?? 0;
if (cntFiles == 0 && cntScenes == 0) return;
var countStr = cntFiles + cntScenes > 0 ? $"{cntFiles} | {cntScenes} ({CommonUtils.BytesToString(size)})" : "";
GUI.Label(rect, countStr, _style.ProjectViewCounterLabel);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a17718cc645348b0bc4ed6d7e514ab30
timeCreated: 1589032770

View File

@@ -0,0 +1,56 @@
using System;
using Leopotam.Ecs;
using UnityEngine;
using static Asset_Cleaner.AufCtx;
namespace Asset_Cleaner {
class SysProcessSearch : IEcsRunSystem {
EcsFilter<SelectionChanged> _from = null;
EcsFilter<Result, SearchResultGui, InSceneResult> SceneResultRows = null;
EcsFilter<SceneResult, SceneDetails> ScenePaths = null;
EcsFilter<SearchArg>.Exclude<InSceneResult> SearchArgMain = null;
EcsFilter<Result, SearchResultGui, FileResultTag> FileResultRows = null;
public void Run() {
if (_from.IsEmpty())
return;
SearchArgMain.AllDestroy();
ScenePaths.AllDestroy();
FileResultRows.AllDestroy();
SceneResultRows.AllDestroy();
var wd = Globals<WindowData>.Value;
if (wd.Window)
wd.Window.Repaint();
foreach (var i in _from.Out(out var get1, out _)) {
var t1 = get1[i];
if (!t1.Target) continue;
wd.FindFrom = t1.From;
try {
switch (t1.From) {
case FindModeEnum.Scene:
World.NewEntityWith(out SearchArg st);
SearchUtils.Init(st, t1.Target, t1.Scene);
SearchUtils.InScene(st, t1.Scene);
break;
case FindModeEnum.File:
World.NewEntityWith(out SearchArg arg);
SearchUtils.Init(arg, t1.Target);
SearchUtils.FilesThatReference(arg);
SearchUtils.ScenesThatContain(t1.Target);
break;
}
}
catch (Exception e) {
Debug.LogException(e);
}
}
_from.AllUnset<SelectionChanged>();
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: afd561b0c1384bcc82976f2624afa895
timeCreated: 1577287503

View File

@@ -0,0 +1,30 @@
using Leopotam.Ecs;
using UnityEditor;
using UnityEditorInternal;
using UnityEngine;
namespace Asset_Cleaner {
class RequestRepaintEvt { }
class SysRepaintWindow : IEcsRunSystem, IEcsInitSystem {
EcsFilter<RequestRepaintEvt> Repaint = null;
public void Init() {
var wd = Globals<WindowData>.Value;
wd.SceneFoldout = new GUIContent(AssetPreview.GetMiniTypeThumbnail(typeof(SceneAsset)));
wd.ExpandScenes = true;
wd.ExpandFiles = true;
wd.ScrollPos = Vector2.zero;
wd.Window.titleContent = new GUIContent("Asset Cleaner v1.0");
}
public void Run() {
var wd = Globals<WindowData>.Value;
if (Repaint.IsEmpty()) return;
wd.Window.Repaint();
InternalEditorUtility.RepaintAllViews();
Repaint.AllDestroy();
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 673a2486762b4e3da784f1657b90bb93
timeCreated: 1577288020

View File

@@ -0,0 +1,44 @@
using Leopotam.Ecs;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine.SceneManagement;
namespace Asset_Cleaner {
class SceneToClose : IEcsAutoReset {
public Scene Scene;
public int SelectionId;
public bool ForceClose;
public void Reset() {
ForceClose = default;
Scene = default;
SelectionId = default;
}
}
class SysSceneCleanup : IEcsRunSystem, IEcsDestroySystem {
EcsFilter<SceneToClose> ScenesToClose = default;
public void Run() {
if (ScenesToClose.IsEmpty()) return;
var selectionId = Globals<PersistentUndoRedoState>.Value.Id;
foreach (var i in ScenesToClose.Out(out var g1, out var entities)) {
var s = g1[i].Scene;
if (g1[i].SelectionId == selectionId && !g1[i].ForceClose) continue;
if (Selection.activeGameObject && Selection.activeGameObject.scene == s) continue;
if (s.isLoaded) EditorSceneManager.CloseScene(s, removeScene: true);
entities[i].Destroy();
}
}
// close scenes on window close
public void Destroy() {
foreach (var i in ScenesToClose.Out(out var g1, out _)) {
var s = g1[i].Scene;
if (s.isLoaded) EditorSceneManager.CloseScene(s, removeScene: true);
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d42cae4aa3ff4500948ddd623a270120
timeCreated: 1596142629

View File

@@ -0,0 +1,190 @@
using System.Linq;
using System.Runtime.InteropServices;
using Leopotam.Ecs;
using UnityEditor;
using UnityEngine;
using static Asset_Cleaner.AufCtx;
namespace Asset_Cleaner {
class CleanupPrevArg { }
class UndoEvt { }
class RedoEvt { }
class SysUndoRedoSelection : IEcsRunSystem, IEcsInitSystem, IEcsDestroySystem {
EcsFilter<UndoEvt> UndoEvt = default;
EcsFilter<RedoEvt> RedoEvt = default;
bool _preventHistoryInsert;
bool _preventSelectionSet;
public void Init() {
Undo.undoRedoPerformed += OnUndoRedoPerformed;
Selection.selectionChanged += OnSelectionChanged;
Globals<UndoRedoState>.Value = new UndoRedoState();
if (Globals<PersistentUndoRedoState>.Value.History.Count > 0)
_preventHistoryInsert = true;
OnSelectionChanged(); //init selection
}
public void Destroy() {
Undo.undoRedoPerformed -= OnUndoRedoPerformed;
Selection.selectionChanged -= OnSelectionChanged;
Globals<UndoRedoState>.Value = default;
}
public void Run() {
MouseInput();
if (UndoEvt.IsEmpty() && RedoEvt.IsEmpty()) return;
Counters(undo: !UndoEvt.IsEmpty(), redo: !RedoEvt.IsEmpty(), false);
_preventHistoryInsert = true;
if (!_preventSelectionSet) {
(var history, var id) = Globals<PersistentUndoRedoState>.Value;
SelectionEntry entry = history[id];
if (entry.Valid())
Selection.objects = entry.IsGuids
? entry.Guids.Select(AssetDatabase.GUIDToAssetPath).Select(AssetDatabase.LoadAssetAtPath<Object>).Where(obj => obj).ToArray()
: entry.SceneObjects;
}
_preventSelectionSet = false;
UndoEvt.AllDestroy();
RedoEvt.AllDestroy();
}
static void Counters(bool undo, bool redo, bool insertToHistory) {
var state = Globals<PersistentUndoRedoState>.Value;
World.NewEntityWith(out RequestRepaintEvt _);
const int MinId = 0;
if (insertToHistory) {
var entry = new SelectionEntry();
var count = state.History.Count - 1 - state.Id;
if (count > 0)
state.History.RemoveRange(state.Id + 1, count);
state.History.Add(entry);
state.Id = MaxId();
if (Selection.assetGUIDs.Length > 0) {
entry.IsGuids = true;
entry.Guids = Selection.assetGUIDs;
}
else {
entry.SceneObjects = Selection.objects;
}
}
if (undo) {
// loop to skip invalid
while (true) {
state.Id -= 1;
if (state.Id < MinId) break;
if (state.History[state.Id].Valid()) break;
}
}
if (redo) {
// loop to skip invalid
while (true) {
state.Id += 1;
if (state.Id > MaxId()) break;
if (state.History[state.Id].Valid()) break;
}
}
state.Id = Mathf.Clamp(state.Id, MinId, MaxId());
var undoRedoState = Globals<UndoRedoState>.Value;
undoRedoState.UndoEnabled = state.Id != MinId;
undoRedoState.RedoEnabled = state.Id != MaxId();
int MaxId() => Mathf.Max(0, state.History.Count - 1);
}
void OnSelectionChanged() {
World.NewEntityWith(out RequestRepaintEvt _);
if (Globals<Config>.Value.Locked) return;
Counters(undo: false, redo: false, insertToHistory: !_preventHistoryInsert);
_preventHistoryInsert = false;
World.NewEntityWith(out SelectionChanged comp);
World.NewEntityWith(out CleanupPrevArg _);
var go = Selection.activeGameObject;
if (go && go.scene.IsValid()) {
comp.From = FindModeEnum.Scene;
comp.Target = go;
comp.Scene = go.scene;
}
else {
var guids = Selection.assetGUIDs;
// comp.Guids = Selection.assetGUIDs;
bool any = guids != null && guids.Length > 0;
if (any) {
comp.From = FindModeEnum.File;
var path = AssetDatabase.GUIDToAssetPath(guids[0]);
comp.Target = AssetDatabase.LoadAssetAtPath<Object>(path);
}
else {
comp.From = FindModeEnum.None;
comp.Target = null;
}
}
}
// prevents selection history flooding
void OnUndoRedoPerformed() {
// below is a hackish way to catch Undo/Redo from editor
if (!Undo.GetCurrentGroupName().Equals("Selection Change")) return;
var evt = Event.current;
if (evt == null) return;
if (evt.rawType != EventType.KeyDown) return;
switch (evt.keyCode) {
case KeyCode.Z:
World.NewEntityWith(out UndoEvt _);
_preventSelectionSet = true; // prevent manual Selection set
break;
case KeyCode.Y:
World.NewEntityWith(out RedoEvt _);
_preventSelectionSet = true;
break;
}
}
void MouseInput() {
if (_nextClick > EditorApplication.timeSinceStartup) return;
var any = false;
if (Pressed(0x5)) {
World.NewEntityWith(out UndoEvt _);
any = true;
}
if (Pressed(0x6)) {
World.NewEntityWith(out RedoEvt _);
any = true;
}
if (any)
_nextClick = EditorApplication.timeSinceStartup + 0.25;
}
#if UNITY_EDITOR_WIN
[DllImport("USER32.dll")]
static extern short GetKeyState(int keycode);
#else
static short GetKeyState(int keycode) => 0;
#endif
double _nextClick;
// 5 back, 6 fw
static bool Pressed(int keyCode) => (GetKeyState(keyCode) & 0x100) != 0;
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6e6a2f49ce174cabbc560ea8e3e9975c
timeCreated: 1596008857

View File

@@ -0,0 +1,970 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Leopotam.Ecs;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEditorInternal;
using UnityEngine;
using UnityEngine.SceneManagement;
using Object = UnityEngine.Object;
namespace Asset_Cleaner {
class FileResultTag { }
enum TargetTypeEnum {
File = 0,
Directory = 1,
Scene = 2,
ObjectInScene = 3,
ObjectInStage = 4
}
class SysWindowGui : IEcsRunSystem, IEcsInitSystem {
EcsFilter<Result, SearchResultGui, InSceneResult> SceneResultRows = null;
EcsFilter<SceneResult, SceneDetails> ScenePaths = null;
EcsFilter<SearchArg>.Exclude<InSceneResult> SearchArgMain = null;
EcsFilter<Result, SearchResultGui, FileResultTag> FileResultRows = null;
public void Init() {
BacklinkStoreDirty(true);
VisualSettingDirty(true);
}
public void Run() {
var windowData = Globals<WindowData>.Value;
_toolbarSelection = GUILayout.Toolbar(_toolbarSelection, _toolbarStrings, GUILayout.ExpandWidth(false));
var conf = Globals<Config>.Value;
switch (_toolbarSelection) {
case 0: {
ShowTabMain(conf, windowData);
break;
}
case 1: {
ShowTabSettings(conf);
break;
}
}
}
string[] _toolbarStrings = {"Main", "Settings"};
const int _progressBarShowFromLevel = 10;
int _toolbarSelection = 0;
int _settingIgnoredPathsHash1;
bool BacklinkStoreDirty(bool set) {
var res = Hash() != _settingIgnoredPathsHash1;
if (set) _settingIgnoredPathsHash1 = Hash();
return res;
int Hash() {
var conf = Globals<Config>.Value;
return DirtyUtils.HashCode(conf.IgnorePathContainsCombined,
conf.IgnoreMaterial,
conf.IgnoreScriptable);
}
}
int _settingCodeHash1;
bool VisualSettingDirty(bool set) {
var res = Hash() != _settingCodeHash1;
if (set) _settingCodeHash1 = Hash();
return res;
int Hash() {
var conf = Globals<Config>.Value;
return DirtyUtils.HashCode(
conf.MarkRed,
conf.ShowInfoBox,
conf.RebuildCacheOnDemand);
}
}
void ShowTabSettings(Config conf) {
using (new EditorGUILayout.VerticalScope()) {
EditorGUILayout.Space();
var enabled = GUI.enabled;
GUI.enabled = true;
using (new EditorGUILayout.VerticalScope()) {
conf.MarkRed = GUILayout.Toggle(conf.MarkRed, "Display counters and red overlay in Project View");
conf.ShowInfoBox = GUILayout.Toggle(conf.ShowInfoBox, "Help suggestions");
conf.RebuildCacheOnDemand = GUILayout.Toggle(conf.RebuildCacheOnDemand, "Rebuild cache on demand (when scripts are updated often)");
EditorGUILayout.Space();
conf.IgnoreMaterial = GUILayout.Toggle(conf.IgnoreMaterial, "Ignore Materials");
conf.IgnoreScriptable = GUILayout.Toggle(conf.IgnoreScriptable, "Ignore ScriptableObjects");
}
EditorGUILayout.Space();
GUI.enabled = enabled;
EditorGUILayout.LabelField("Ignore Path(s) contains:");
conf.IgnorePathContainsCombined = GUILayout.TextArea(conf.IgnorePathContainsCombined);
using (new EditorGUILayout.HorizontalScope(GUILayout.ExpandWidth(false))) {
EditorGUILayout.Space();
var previous = GUI.enabled;
GUI.enabled = BacklinkStoreDirty(false) || VisualSettingDirty(false);
if (GUILayout.Button("Apply")) {
conf.IgnorePathContains = conf.IgnorePathContainsCombined.Split(';')
.Where(s => !string.IsNullOrWhiteSpace(s)).ToArray();
Apply();
}
GUI.enabled = previous;
var selectedGuids = Selection.assetGUIDs;
var assetPaths = new List<string>();
if (selectedGuids.Length > 0) {
foreach (var guid in selectedGuids) {
var realAssetPath = AssetDatabase.GUIDToAssetPath(guid);
var obj = AssetDatabase.LoadAssetAtPath<Object>(realAssetPath);
var assetPath = realAssetPath.Replace("Assets/", string.Empty);
if (obj is DefaultAsset &&
!conf.IgnorePathContains.Any(p => (StringComparer.Ordinal.Equals(p, assetPath)))) {
assetPaths.Add(assetPath);
}
}
}
GUI.enabled = (assetPaths.Count > 0);
var foldersList = string.Join(", ", assetPaths);
if (GUILayout.Button("Add Selected Path")) {
var choice = EditorUtility.DisplayDialog(
title: "Asset Cleaner",
message:
$"Do you really want to add these folder(s) to ignored list: \"{foldersList}\"?",
ok: "Ignore",
cancel: "Cancel");
if (choice) {
conf.IgnorePathContainsCombined += $"{foldersList};";
conf.IgnorePathContains = conf.IgnorePathContainsCombined.Split(';')
.Where(s => !string.IsNullOrWhiteSpace(s)).ToArray();
Apply();
}
}
GUI.enabled = true;
if (GUILayout.Button("Reset")) {
var choice = EditorUtility.DisplayDialog(
title: "Asset Cleaner",
message:
$"Do you really want to reset to the factory settings?",
ok: "Reset",
cancel: "Cancel");
if (choice) {
var serializable = AufSerializableData.Default();
AufSerializableData.OnDeserialize(in serializable, ref conf);
Apply();
}
}
GUI.enabled = previous;
}
EditorGUILayout.Space();
EditorGUILayout.LabelField(conf.InitializationTime);
var buf = GUI.enabled;
GUI.enabled = Selection.objects.Length > 0;
if (GUILayout.Button("Reserialize selected assets", GUILayout.ExpandWidth(false))) {
var paths = Selection.objects.Select(AssetDatabase.GetAssetPath);
AssetDatabase.ForceReserializeAssets(paths);
EditorApplication.ExecuteMenuItem("File/Save Project");
AssetDatabase.Refresh();
}
GUI.enabled = buf;
EditorGUILayout.Space();
}
void Apply() {
var rebuild = BacklinkStoreDirty(true);
VisualSettingDirty(true);
PersistenceUtils.Save(in conf);
AufCtx.World.NewEntityWith(out RequestRepaintEvt _);
if (rebuild)
Globals<BacklinkStore>.Value.UpdateUnusedAssets();
InternalEditorUtility.RepaintAllViews();
}
}
void ShowTabMain(Config conf, WindowData windowData) {
var store = Globals<BacklinkStore>.Value;
EditorGUIUtility.labelWidth = windowData.Window.position.width * .7f;
int Hash() => DirtyUtils.HashCode(conf.Locked);
var active = SearchArgMain.Get1[0];
if (conf.Locked && (windowData.FindFrom == FindModeEnum.File &&
(active == null || active.Main == null || !AssetDatabase.Contains(active.Main)))) {
conf.Locked = false;
AufCtx.World.NewEntityWith(out RequestRepaintEvt _);
}
var style = windowData.Style;
var hash = Hash();
if (hash != Hash()) {
PersistenceUtils.Save(in conf);
AufCtx.World.NewEntityWith(out RequestRepaintEvt _);
}
// if (Globals<WindowData>.Get() == null) return;
EditorGUILayout.Space();
SearchArg arg = default;
foreach (var i in SearchArgMain) {
arg = SearchArgMain.Get1[i];
if (arg != null && arg.Main != null) {
break;
}
}
if (arg == default)
return;
var targetTypeEnum = GetTargetType(windowData, arg?.Main);
BacklinkStore.UnusedQty unusedQty = new BacklinkStore.UnusedQty(0, 0, 0);
using (new EditorGUILayout.HorizontalScope()) {
var enabledBuf = GUI.enabled;
var selectedGuids = Selection.assetGUIDs;
var undoRedoState = Globals<UndoRedoState>.Value;
GUI.enabled = selectedGuids != null && !conf.Locked && undoRedoState.UndoEnabled;
if (GUILayout.Button(style.ArrowL, style.ArrowBtn)) {
AufCtx.World.NewEntityWith(out UndoEvt _);
}
GUI.enabled = selectedGuids != null && !conf.Locked && undoRedoState.RedoEnabled;
if (GUILayout.Button(style.ArrowR, style.ArrowBtn)) {
AufCtx.World.NewEntityWith(out RedoEvt _);
}
GUI.enabled = enabledBuf;
if (conf.Locked) {
if (GUILayout.Button(style.Lock, style.LockBtn)) {
AufCtx.World.NewEntityWith(out SelectionChanged selectionChanged);
conf.Locked = false;
if (Selection.activeObject != arg.Target) {
selectionChanged.From = FindModeEnum.Scene;
selectionChanged.Scene = SceneManager.GetActiveScene();
selectionChanged.Target = Selection.activeObject;
}
else if (Selection.assetGUIDs is string[] guids) {
// todo show info box multiple selection is unsupported
if (guids.Length > 0) {
var path = AssetDatabase.GUIDToAssetPath(guids[0]);
selectionChanged.Target = AssetDatabase.LoadAssetAtPath<Object>(path);
switch (Selection.selectionChanged.Target) {
case DefaultAsset _:
selectionChanged.From = FindModeEnum.File;
break;
case GameObject go when go.scene.isLoaded:
selectionChanged.From = FindModeEnum.Scene;
selectionChanged.Scene = SceneManager.GetActiveScene();
break;
default:
selectionChanged.From = FindModeEnum.File;
break;
}
}
else if (Selection.activeObject is GameObject go && go.scene.isLoaded) {
selectionChanged.From = FindModeEnum.Scene;
selectionChanged.Target = Selection.activeObject;
selectionChanged.Scene = SceneManager.GetActiveScene();
}
}
}
}
else {
var enabled = GUI.enabled;
GUI.enabled = selectedGuids != null && selectedGuids.Length == 1;
if (GUILayout.Button(style.Unlock, style.UnlockBtn)) {
conf.Locked = true;
}
GUI.enabled = enabled;
}
unusedQty = ShowObjectName(store, windowData, targetTypeEnum, arg, selectedGuids);
}
bool isMultiSelect = Selection.assetGUIDs != null && Selection.assetGUIDs.Length > 1;
if (conf.ShowInfoBox) {
if (isMultiSelect && (unusedQty.UnusedFilesQty + unusedQty.UnusedScenesQty > 0)) {
var msgUnusedFiles = (unusedQty.UnusedFilesQty > 0)
? $"unused files ({unusedQty.UnusedFilesQty}),"
: "";
var msgUnusedScenes = (unusedQty.UnusedScenesQty > 0)
? $"unused scenes ({unusedQty.UnusedScenesQty}),"
: "";
var msgMultiSelect = $"This multi-selection contains: " +
msgUnusedFiles + msgUnusedScenes +
$"\nYou could delete them pressing corresponding button to the right.";
EditorGUILayout.HelpBox(msgMultiSelect, MessageType.Info);
}
else if (TryGetHelpInfo(arg, out var msg, out var msgType)) {
EditorGUILayout.HelpBox(msg, msgType);
}
}
if (targetTypeEnum != TargetTypeEnum.Directory && !isMultiSelect) {
var windowData2 = Globals<WindowData>.Value;
EditorGUILayout.BeginVertical();
{
windowData2.ScrollPos = EditorGUILayout.BeginScrollView(windowData2.ScrollPos);
{
RenderRows(windowData2); //TODO?
EditorGUILayout.Space();
}
EditorGUILayout.EndScrollView();
}
EditorGUILayout.EndVertical();
EditorGUILayout.Space();
}
}
static bool TryGetHelpInfo(SearchArg arg, out string msg, out MessageType msgType) {
msgType = MessageType.Info;
if (arg == null) {
msg = default;
return false;
}
var path = arg.FilePath;
if (string.IsNullOrEmpty(path)) {
msg = default;
return false;
}
if (SearchUtils.IgnoredPaths(path, out var subPath)) {
msg = $"Paths containing '{subPath}' are ignored. You could add or remove it in Settings tab";
return true;
}
if (SearchUtils.IgnoredNonAssets(path) && !path.Eq("Assets")) {
msg = $"Asset is outside of Assets folder";
return true;
}
if (IgnoreTypes.Check(path, out var type)) {
if (AssetDatabase.IsValidFolder(path)) {
var scenes = arg.UnusedScenesFiltered?.Count;
var files = arg.UnusedAssetsFiltered?.Count;
if (scenes == 0 && files == 0) {
msg = default;
return false;
}
var b = new StringBuilder();
b.Append("This directory contains: ");
var any = false;
if (files > 0) {
any = true;
b.Append($"unused files ({files})");
}
if (scenes > 0) {
if (any)
b.Append(", ");
b.Append($"unused scenes ({scenes})");
}
b.Append(
".\nYou could delete them pressing corresponding button to the right.\nIf you don't want it to be inspected, please add it to Ignore list in the Settings tab");
msg = b.ToString();
return true;
}
msg = $"Assets of type '{type.Name}' are ignored. Please contact support if you need to change it";
return true;
}
// if (Filters.ScenePaths.GetEntitiesCount() == 0 && Filters.FileResultRows.GetEntitiesCount() == 0) {
if (SearchUtils.IsUnused(path)) {
type = AssetDatabase.GetMainAssetTypeAtPath(path);
var name = type.IsAssignableFromInverse(typeof(SceneAsset)) ? "scene" : "file";
msg =
$"This {name} has no explicit serialized usages and potentially could be removed. If you don't want it to be inspected, please add the containing folder to ignore list";
return true;
}
msgType = default;
msg = default;
return false;
}
static TargetTypeEnum GetTargetType(WindowData windowData1, Object obj) {
if (obj == null) return TargetTypeEnum.File;
var targetTypeEnum = TargetTypeEnum.Directory;
var path = AssetDatabase.GetAssetPath(obj);
switch (windowData1.FindFrom) {
case FindModeEnum.File when obj is DefaultAsset:
targetTypeEnum = TargetTypeEnum.Directory;
break;
case FindModeEnum.File when path.LastIndexOf(".unity", StringComparison.Ordinal) != -1:
targetTypeEnum = TargetTypeEnum.Scene;
break;
case FindModeEnum.File:
targetTypeEnum = TargetTypeEnum.File;
break;
case FindModeEnum.Scene:
targetTypeEnum = TargetTypeEnum.ObjectInScene;
break;
case FindModeEnum.Stage:
targetTypeEnum = TargetTypeEnum.ObjectInStage;
break;
}
return targetTypeEnum;
}
GUIContent _contentBuf = new GUIContent();
GUIContent _buf2 = new GUIContent();
BacklinkStore.UnusedQty ShowObjectName(BacklinkStore store, WindowData windowData, TargetTypeEnum targetTypeEnum, SearchArg arg, string[] selectedGuids) {
float TextWidth() {
_buf2.text = _contentBuf.text;
return 20f + GUI.skin.button.CalcSize(_buf2).x;
}
if (arg == null || arg.Main == null) return new BacklinkStore.UnusedQty();
bool isMultiSelect = selectedGuids != null && selectedGuids.Length > 1;
if (_contentBuf == null) {
_contentBuf = new GUIContent {tooltip = $"Click to ping"};
}
_contentBuf.image = isMultiSelect
? windowData.Style.MultiSelect.image
: AssetPreview.GetMiniThumbnail(arg.Target);
_contentBuf.text = string.Empty;
_contentBuf.tooltip = string.Empty;
if (!isMultiSelect) {
switch (targetTypeEnum) {
case TargetTypeEnum.Directory:
if (!SearchArgMain.IsEmpty()) {
_contentBuf.text = $"{arg.Main.name} (Folder)";
if (GUILayout.Button(_contentBuf, windowData.Style.CurrentBtn,
GUILayout.MinWidth(TextWidth()))) {
EditorGUIUtility.PingObject(arg.Main);
}
if (AskDeleteUnusedFiles(arg, arg.UnusedAssetsFiltered, windowData))
return new BacklinkStore.UnusedQty();
if (AskDeleteUnusedScenes(arg, arg.UnusedScenesFiltered, windowData))
return new BacklinkStore.UnusedQty();
}
break;
case TargetTypeEnum.File:
_contentBuf.text = $"{arg.Main.name} (File Asset)";
if (GUILayout.Button(_contentBuf, windowData.Style.CurrentBtn,
GUILayout.MinWidth(TextWidth()))) {
EditorGUIUtility.PingObject(arg.Main);
}
bool hasUnusedFile = SearchUtils.IsUnused(arg.FilePath);
var previous = GUI.enabled;
GUI.enabled = hasUnusedFile;
if (GUILayout.Button(windowData.Style.RemoveFile,
windowData.Style.RemoveUnusedBtn)) {
var choice = EditorUtility.DisplayDialog(
title: "Asset Cleaner",
message:
$"Do you really want to remove file: \"{arg.Main.name}\"?",
ok: "Remove",
cancel: "Cancel");
if (choice) {
EditorApplication.ExecuteMenuItem("File/Save Project");
DeleteWithMeta(arg.FilePath);
AssetDatabase.Refresh();
SearchUtils.Upd(arg);
}
}
GUI.enabled = previous;
break;
case TargetTypeEnum.Scene:
_contentBuf.text = $"{arg.Main.name} (Scene)";
if (GUILayout.Button(_contentBuf, windowData.Style.CurrentBtn,
GUILayout.MinWidth(TextWidth()))) {
EditorGUIUtility.PingObject(arg.Main);
}
bool hasUnusedScene = SearchUtils.IsUnused(arg.FilePath);
previous = GUI.enabled;
GUI.enabled = hasUnusedScene;
if (GUILayout.Button(windowData.Style.RemoveScene,
windowData.Style.RemoveUnusedBtn)) {
var choice = EditorUtility.DisplayDialog(
title: "Asset Cleaner",
message:
$"Do you really want to remove scene: {arg.Main.name}?",
ok: "Remove",
cancel: "Cancel");
if (choice) {
EditorApplication.ExecuteMenuItem("File/Save Project");
DeleteWithMeta(arg.FilePath);
AssetDatabase.Refresh();
SearchUtils.Upd(arg);
}
}
GUI.enabled = previous;
break;
case TargetTypeEnum.ObjectInScene:
_contentBuf.text = $"{arg.Main.name} (Object in Scene)";
if (GUILayout.Button(_contentBuf, windowData.Style.CurrentBtn,
GUILayout.MinWidth(TextWidth()))) {
EditorGUIUtility.PingObject(arg.Main);
}
break;
case TargetTypeEnum.ObjectInStage:
_contentBuf.image = AssetPreview.GetMiniThumbnail(arg.Target);
_contentBuf.text = $"{arg.Main.name} (Object in Staging)";
if (GUILayout.Button(_contentBuf,
windowData.Style.RemoveUnusedBtn)) {
EditorGUIUtility.PingObject(arg.Main);
}
break;
default:
if (GUILayout.Button($"{arg.Main.name} (Unknown Object Type)",
windowData.Style.RemoveUnusedBtn)) {
EditorGUIUtility.PingObject(arg.Main);
}
break;
}
}
else {
var unusedAssets = new List<string>();
var unusedScenes = new List<string>();
foreach (var guid in selectedGuids) {
var path = AssetDatabase.GUIDToAssetPath(guid);
if (store.UnusedFiles.TryGetValue(path, out _))
unusedAssets.Add(path);
else if (store.UnusedScenes.TryGetValue(path, out _))
unusedScenes.Add(path);
if (store.FoldersWithQty.TryGetValue(path, out _)) {
SearchArg searchArg = new SearchArg() {
FilePath = path,
Target = AssetDatabase.LoadAssetAtPath<DefaultAsset>(path),
Main = AssetDatabase.LoadAssetAtPath<DefaultAsset>(path)
};
SearchUtils.Upd(searchArg);
foreach (var unusedAssetPath in searchArg.UnusedAssetsFiltered)
if (store.UnusedFiles.TryGetValue(unusedAssetPath, out _))
unusedAssets.Add(unusedAssetPath);
foreach (var unusedScenePath in searchArg.UnusedScenesFiltered)
if (store.UnusedScenes.TryGetValue(unusedScenePath, out _))
unusedScenes.Add(unusedScenePath);
}
}
unusedAssets = unusedAssets.Distinct().ToList();
unusedScenes = unusedScenes.Distinct().ToList();
var assetSize = unusedAssets.Sum(CommonUtils.Size);
var sceneSize = unusedScenes.Sum(CommonUtils.Size);
_contentBuf.text =
$"Assets: {unusedAssets.Count} ({CommonUtils.BytesToString(assetSize)}), Scenes: {unusedScenes.Count} ({CommonUtils.BytesToString(sceneSize)})";
;
GUILayout.Button(_contentBuf, windowData.Style.CurrentBtn, GUILayout.MinWidth(TextWidth()));
if (AskDeleteUnusedFiles(arg, unusedAssets, windowData))
return new BacklinkStore.UnusedQty();
if (AskDeleteUnusedScenes(arg, unusedScenes, windowData))
return new BacklinkStore.UnusedQty();
return new BacklinkStore.UnusedQty(unusedAssets.Count, unusedScenes.Count, assetSize + sceneSize);
}
return new BacklinkStore.UnusedQty();
}
static bool AskDeleteUnusedFiles(SearchArg arg, List<string> unusedAssets, WindowData windowData) {
if (arg == null || unusedAssets == null) return false;
var hasUnusedAssets = unusedAssets.Count > 0;
var previous = GUI.enabled;
GUI.enabled = hasUnusedAssets;
var guiContentRemoveAssets = windowData.Style.RemoveFile;
if (GUILayout.Button(guiContentRemoveAssets,
windowData.Style.RemoveUnusedBtn)) {
var choice = EditorUtility.DisplayDialog(
title: "Asset Cleaner",
message:
$"Do you really want to remove {unusedAssets.Count} asset(s)?",
ok: "Remove",
cancel: "Cancel");
if (choice) {
EditorApplication.ExecuteMenuItem("File/Save Project");
var i = 0f;
var total = (float) unusedAssets.Count;
foreach (var f in unusedAssets) {
var path = Application.dataPath.Replace("Assets", f);
DeleteWithMeta(path);
var percent = i * 100 / total;
if (total >= _progressBarShowFromLevel) {
if (Math.Abs(percent % 5f) < 0.01f) {
if (EditorUtility.DisplayCancelableProgressBar(
"Please wait...",
"Deleting assets...", percent))
throw new Exception("Deleting aborted");
}
i++;
}
}
if (total >= _progressBarShowFromLevel) {
EditorUtility.ClearProgressBar();
}
AssetDatabase.Refresh();
SearchUtils.Upd(arg);
}
GUI.enabled = previous;
return true;
}
GUI.enabled = previous;
return false;
}
static void DeleteWithMeta(string path) {
FileUtil.DeleteFileOrDirectory(path);
var metaPath = AssetDatabase.GetTextMetaFilePathFromAssetPath(path);
if (!string.IsNullOrEmpty(metaPath))
FileUtil.DeleteFileOrDirectory(metaPath);
}
static bool AskDeleteUnusedScenes(SearchArg arg, List<string> unusedScenes, WindowData windowData) {
if (arg == null || unusedScenes == null) return false;
var hasUnusedScenes = unusedScenes.Count > 0;
var previous = GUI.enabled;
GUI.enabled = hasUnusedScenes;
var guiContentRemoveScenes = windowData.Style.RemoveScene;
if (GUILayout.Button(guiContentRemoveScenes,
windowData.Style.RemoveUnusedBtn)) {
var choice = EditorUtility.DisplayDialog(
title: "Asset Cleaner",
message:
$"Do you really want to remove {unusedScenes.Count} scene(s)?",
ok: "Remove",
cancel: "Cancel");
if (choice) {
EditorApplication.ExecuteMenuItem("File/Save Project");
var i = 0f;
var total = (float) unusedScenes.Count;
foreach (var scene in unusedScenes) {
var path = Application.dataPath.Replace("Assets", scene);
DeleteWithMeta(path);
if (total >= _progressBarShowFromLevel) {
var percent = i * 100 / total;
if (Math.Abs(percent % 5f) < 0.01f) {
if (EditorUtility.DisplayCancelableProgressBar(
"Please wait...",
"Deleting scenes...", percent))
throw new Exception("Deleting aborted");
}
i++;
}
}
if (total >= _progressBarShowFromLevel) {
EditorUtility.ClearProgressBar();
}
AssetDatabase.Refresh();
SearchUtils.Upd(arg);
}
GUI.enabled = previous;
return true;
}
GUI.enabled = previous;
return false;
}
void RenderRows(WindowData windowData) {
// todo show spinner until scene is loaded,
if (FileResultRows.GetEntitiesCount() > 0) {
windowData.ExpandFiles =
EditorGUILayout.Foldout(windowData.ExpandFiles,
$"Usages in Project: {FileResultRows.GetEntitiesCount()}");
}
if (SearchArgMain.IsEmpty())
return;
if (windowData.ExpandFiles && windowData.FindFrom == FindModeEnum.File)
foreach (var i in FileResultRows.Out(out var get1, out var get2, out _, out _))
DrawRowFile(get1[i], get2[i], windowData);
var sceneMessage = $"Usages in Scenes: {ScenePaths.GetEntitiesCount()}";
if (ScenePaths.GetEntitiesCount() > 0) {
windowData.ExpandScenes =
EditorGUILayout.Foldout(windowData.ExpandScenes, sceneMessage);
}
if (!windowData.ExpandScenes) return;
if (windowData.ExpandScenes && windowData.FindFrom == FindModeEnum.Scene) {
foreach (var (grp, indices) in SceneResultRows.Out(out _, out var get2, out _, out _)
.GroupBy1(ResultComp.Instance)) {
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox)) {
var count = 0;
foreach (var i in indices) {
if (count++ == 0)
if (GUILayout.Button(get2[i].Label, windowData.Style.RowMainAssetBtn)) {
if (windowData.Click.IsDoubleClick(grp.RootGo)) {
// _selectionChangedByArrows = false;
Selection.activeObject = grp.RootGo;
}
else
EditorGUIUtility.PingObject(grp.RootGo);
windowData.Click = new PrevClick(grp.RootGo);
}
DrawRowScene(get2[i]);
}
}
}
}
using (new GUILayout.HorizontalScope()) {
GUILayout.Space(windowData.Style.SceneIndent1);
using (new EditorGUILayout.VerticalScope()) {
foreach (var i1 in ScenePaths.Out(out var get1, out var get2, out _)) {
windowData.SceneFoldout.text = get1[i1].PathNicified;
var details = get2[i1];
details.SearchRequested = details.Scene.isLoaded;
details.SearchRequested = EditorGUILayout.Foldout(details.SearchRequested,
windowData.SceneFoldout, EditorStyles.foldout);
if (details.SearchRequested && details.Scene.isLoaded && !details.SearchDone) {
var mainArg = SearchArgMain.GetSingle();
mainArg.Scene = SceneManager.GetSceneByPath(details.Scene.path);
SearchUtils.InScene(mainArg, details.Scene);
details.SearchDone = true;
}
if (!details.SearchRequested) {
if (!details.Scene.isLoaded) continue;
if (!details.WasOpened) {
// to clean up on selection change
AufCtx.World.NewEntityWith(out SceneToClose comp);
comp.Scene = details.Scene;
comp.ForceClose = true;
}
foreach (var row in SceneResultRows.Out(out _, out _, out var get3, out var entities)) {
if (!get3[row].ScenePath.Eq(details.Path))
continue;
entities[row].Destroy();
}
details.SearchDone = false;
}
else {
if (!details.Scene.isLoaded) {
details.Scene = EditorSceneManager.OpenScene(details.Path, OpenSceneMode.Additive);
// to clean up on selection change
AufCtx.World.NewEntityWith(out SceneToClose comp);
comp.Scene = details.Scene;
comp.SelectionId = Globals<PersistentUndoRedoState>.Value.Id;
#if UNITY_2019_1_OR_NEWER
EditorSceneManager.SetSceneCullingMask(details.Scene, 0);
#endif
details.SearchRequested = true;
// todo Scope component
#if later
if (details.Scene.isLoaded)
EditorSceneManager.CloseScene(details.Scene, false);
#endif
}
else if (SceneResultRows.IsEmpty())
EditorGUILayout.LabelField("No in-scene dependencies found.");
else
using (new GUILayout.HorizontalScope()) {
GUILayout.Space(windowData.Style.SceneIndent2);
using (new EditorGUILayout.VerticalScope())
foreach (var (grp, indices) in SceneResultRows
.Out(out var g1, out var g2, out var g3, out _)
.GroupBy1(ResultComp.Instance)) {
var any = false;
foreach (var i3 in indices) {
if (!g3[i3].ScenePath.Eq(details.Path))
continue;
any = true;
break;
}
if (!any)
continue;
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox)) {
var count = 0;
foreach (var i2 in indices) {
if (!g3[i2].ScenePath.Eq(details.Path))
continue;
if (count++ == 0) {
Result comp = g1[i2];
if (GUILayout.Button(g2[i2].Label,
windowData.Style.RowMainAssetBtn)) {
if (windowData.Click.IsDoubleClick(grp.RootGo)) {
// _selectionChangedByArrows = false;
Selection.activeObject = comp.RootGo;
}
else
EditorGUIUtility.PingObject(comp.RootGo);
windowData.Click = new PrevClick(comp.RootGo);
}
}
DrawRowScene(g2[i2]);
}
}
}
}
}
}
}
}
}
class ResultComp : IEqualityComparer<Result> {
public static ResultComp Instance { get; } = new ResultComp();
public bool Equals(Result x, Result y) => GetHashCode(x) == GetHashCode(y);
public int GetHashCode(Result obj) => obj.RootGo.GetInstanceID();
}
static void DrawRowScene(SearchResultGui gui) {
EditorGUI.BeginChangeCheck();
// if (data.TargetGo || data.TargetComponent)
foreach (var prop in gui.Properties) {
{
var locked = prop.Property.objectReferenceValue is MonoScript;
var f = GUI.enabled;
if (locked) GUI.enabled = false;
EditorGUILayout.PropertyField(prop.Property, prop.Content, false);
if (locked) GUI.enabled = f;
}
}
if (EditorGUI.EndChangeCheck())
gui.SerializedObject.ApplyModifiedProperties();
}
static void DrawRowFile(Result data, SearchResultGui gui, WindowData windowData) {
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox)) {
var buf = GUI.color;
var pingGo = data.MainFile == null ? data.RootGo : data.MainFile;
if (GUILayout.Button(gui.Label, windowData.Style.RowMainAssetBtn)) {
if (windowData.Click.IsDoubleClick(pingGo)) {
// _selectionChangedByArrows = false;
Selection.activeObject = pingGo;
}
else {
EditorGUIUtility.PingObject(pingGo);
}
windowData.Click = new PrevClick(pingGo);
}
GUI.color = buf;
EditorGUI.BeginChangeCheck();
if (data.File) {
foreach (var prop in gui.Properties) {
using (new EditorGUILayout.HorizontalScope()) {
var locked = prop.Property.objectReferenceValue is MonoScript;
var f = GUI.enabled;
if (locked) GUI.enabled = false;
EditorGUILayout.PropertyField(prop.Property, prop.Content, false);
if (locked) GUI.enabled = f;
}
}
}
if (EditorGUI.EndChangeCheck()) {
gui.SerializedObject.ApplyModifiedProperties();
// dependency.SerializedObject.Update();
}
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 87ac41283aa347c4aece096c90076c7f
timeCreated: 1596213420

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 8f9623ba76cbf214994b7b865c65f362
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,34 @@
using System.Diagnostics;
using UnityEngine.Assertions;
namespace Asset_Cleaner {
static class FLAGS {
// cleanup in release
public const string DEBUG = "DEBUG1";
public const string M_DISABLE_POOLING = "M_DISABLE_POOLING";
}
static class Asr {
#line hidden
[Conditional(FLAGS.DEBUG)]
public static void AreEqual(int a, int b) {
Assert.AreEqual(a, b);
}
[Conditional(FLAGS.DEBUG)]
public static void IsTrue(bool b, string format = null) {
Assert.IsTrue(b, format);
}
[Conditional(FLAGS.DEBUG)]
public static void IsFalse(bool b, string format = null) {
Assert.IsFalse(b, format);
}
[Conditional(FLAGS.DEBUG)]
public static void IsNotNull(object target, string format = null) {
Assert.IsNotNull(target, format);
}
#line default
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 53c5eeeb89d541a5b26f906f93ffe650
timeCreated: 1581424097

View File

@@ -0,0 +1,37 @@
using Leopotam.Ecs;
namespace Asset_Cleaner {
static class AufCtx {
public static EcsWorld World;
public static EcsSystems UpdateGroup;
public static EcsSystems OnGuiGroup;
internal static bool InitStarted { get; private set; }
internal static bool Destroyed { get; private set; }
internal static void TryInitWorld() {
if (InitStarted) return;
InitStarted = true;
World = new EcsWorld();
(OnGuiGroup = new EcsSystems(World)
.Add(new SysWindowGui())).Init();
(UpdateGroup = new EcsSystems(World)
.Add(new SysRepaintWindow())
.Add(new SysUndoRedoSelection())
.Add(new SysProcessSearch())
.Add(new SysSceneCleanup())
).Init();
}
internal static void DestroyWorld() {
if (!InitStarted) return;
InitStarted = false;
Destroyed = true;
Asr.IsFalse(__GlobalsCounter.HasAnyValue());
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 999cc1329c8dd5743b39fe17eb02a7aa
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,40 @@
using System;
using System.IO;
namespace Asset_Cleaner {
static class CommonUtils {
static string[] _suffix = {"B", "KB", "MB", "GB", "TB"};
public static string BytesToString(long byteCount) {
if (byteCount == 0)
return $"0 {_suffix[0]}";
var bytes = Math.Abs(byteCount);
var place = Convert.ToInt32(Math.Floor(Math.Log(bytes, 1024)));
double num;
if (place == 0 || place == 1) { // display B, KB in MB
num = Math.Round(bytes / Math.Pow(1024, 2), 4);
return $"{Math.Sign(byteCount) * num:N} {_suffix[2]}";
}
num = Math.Round(bytes / Math.Pow(1024, place), 1);
return $"{Math.Sign(byteCount) * num:F0} {_suffix[place]}";
}
// todo
public static long Size(string path) {
return TryGetSize(path, out var res) ? res : 0L;
}
public static bool TryGetSize(string path, out long result) {
if (!File.Exists(path)) {
result = default;
return false;
}
var fi = new FileInfo(path);
result = fi.Length;
return true;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 4a0fa79cb05e495a8dc80a251a33970c
timeCreated: 1595072385

View File

@@ -0,0 +1,46 @@
using System.Runtime.CompilerServices;
namespace Asset_Cleaner {
static class DirtyUtils {
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int HashCode<T1>(in T1 v1) {
var hash = v1.GetHashCode();
hash = (hash * 397);
return hash;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int HashCode<T1, T2>(in T1 v1, in T2 v2) {
var hash = v1.GetHashCode();
hash = (hash * 397) ^ v2.GetHashCode();
return hash;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int HashCode<T1, T2, T3>(in T1 v1, in T2 v2, in T3 v3) {
var hash = v1.GetHashCode();
hash = (hash * 397) ^ v2.GetHashCode();
hash = (hash * 397) ^ v3.GetHashCode();
return hash;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int HashCode<T1, T2, T3, T4>(in T1 v1, in T2 v2, in T3 v3, in T4 v4) {
var hash = v1.GetHashCode();
hash = (hash * 397) ^ v2.GetHashCode();
hash = (hash * 397) ^ v3.GetHashCode();
hash = (hash * 397) ^ v4.GetHashCode();
return hash;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int HashCode<T1, T2, T3, T4, T5>(in T1 v1, in T2 v2, in T3 v3, in T4 v4, in T5 v5) {
var hash = v1.GetHashCode();
hash = (hash * 397) ^ v2.GetHashCode();
hash = (hash * 397) ^ v3.GetHashCode();
hash = (hash * 397) ^ v4.GetHashCode();
hash = (hash * 397) ^ v5.GetHashCode();
return hash;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 3fd8e20db38143e3a3c51ea733bd0bab
timeCreated: 1577270369

View File

@@ -0,0 +1,55 @@
using System.Collections.Generic;
using System.Linq;
using Leopotam.Ecs;
namespace Asset_Cleaner {
static class EcsUtils {
public static IEnumerable<(T Group, IEnumerable<int> Indices)> GroupBy1<T, T1, T2>(this EcsFilter<T, T1, T2> f, IEqualityComparer<T> comp)
where T : class
where T1 : class
where T2 : class {
foreach (var group in Inner().GroupBy(tuple => tuple.Group, comp))
yield return (group.Key, group.Select(g => g.EcsIndex));
IEnumerable<(T Group, int EcsIndex)> Inner() {
var get1 = f.Get1;
foreach (var i in f) yield return (get1[i], i);
}
}
public static EcsFilter<T> Out<T>(this EcsFilter<T> filter, out T[] get1, out EcsEntity[] entities) where T : class {
get1 = filter.Get1;
entities = filter.Entities;
return filter;
}
public static EcsFilter<T1, T2> Out<T1, T2>(this EcsFilter<T1, T2> filter, out T1[] get1, out T2[] get2, out EcsEntity[] entities)
where T1 : class where T2 : class {
get1 = filter.Get1;
get2 = filter.Get2;
entities = filter.Entities;
return filter;
}
public static EcsFilter<T1, T2, T3> Out<T1, T2, T3>(this EcsFilter<T1, T2, T3> filter, out T1[] get1, out T2[] get2, out T3[] get3, out EcsEntity[] entities)
where T1 : class where T2 : class where T3 : class {
get1 = filter.Get1;
get2 = filter.Get2;
get3 = filter.Get3;
entities = filter.Entities;
return filter;
}
public static void AllDestroy(this EcsFilter f) {
var ecsEntities = f.Entities;
foreach (var i in f)
ecsEntities[i].Destroy();
}
public static void AllUnset<T>(this EcsFilter f) where T : class {
var e = f.Entities;
foreach (var i in f)
e[i].Unset<T>();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3d81e9bb0744bc4448ddb9bbd93bffd3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,17 @@
using System.Runtime.CompilerServices;
using Leopotam.Ecs;
namespace Asset_Cleaner {
static class Ext {
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool Eq(this string s1, string s2) => (s1 == s2);
// public static bool Eq(this string s1, string s2) => StringComparer.Ordinal.Equals(s1, s2);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T GetSingle<T>(this EcsFilter<T> f) where T : class {
Asr.AreEqual(f.GetEntitiesCount(), 1);
return f.Get1[0];
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 66f6b6922016ea04797deac8d082bc80
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,29 @@
namespace Asset_Cleaner {
static class Globals<T> where T : class {
static T _instance;
public static T Value {
get {
Asr.IsFalse(_instance == null);
return _instance;
}
set {
var was = HasValue();
_instance = value;
// keep counter to check during deinitialization if all Globals are cleared
if (was && !HasValue())
__GlobalsCounter.Counter -= 1;
if (!was && HasValue())
__GlobalsCounter.Counter += 1;
bool HasValue() => _instance != null;
}
}
}
static class __GlobalsCounter {
internal static int Counter;
public static bool HasAnyValue() => Counter > 0;
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: dd0464bfdb1b4253ba131c7140510c81
timeCreated: 1596137163

View File

@@ -0,0 +1,124 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
namespace Asset_Cleaner {
readonly struct Option<T> : IEquatable<Option<T>>, IComparable<Option<T>> {
// ReSharper disable once StaticMemberInGenericType
static readonly bool IsValueType;
public bool HasValue { get; }
T Value { get; }
public static implicit operator Option<T>(T arg) {
if (!IsValueType) return ReferenceEquals(arg, null) ? new Option<T>() : new Option<T>(arg, true);
#if M_WARN
if (arg.Equals(default(T)))
Warn.Warning($"{arg} has default value");
#endif
return new Option<T>(arg, true);
}
static Option() {
IsValueType = typeof(T).IsValueType;
}
public void GetOrFail(out T value) {
if (!TryGet(out value))
Fail($"Option<{typeof(T).Name}> has no value");
}
public T GetOrFail() {
if (!TryGet(out var value))
Fail($"Option<{typeof(T).Name}> has no value");
return value;
}
[Conditional("DEBUG1")]
static void Fail(string format = null) {
throw new Exception(format);
}
public bool TryGet(out T value) {
if (!HasValue) {
value = default(T);
return false;
}
value = Value;
return true;
}
internal Option(T value, bool hasValue) {
Value = value;
HasValue = hasValue;
}
public T ValueOr(T alternative) {
return HasValue ? Value : alternative;
}
// for debug purposes
public override string ToString() {
if (!HasValue) return "None";
return Value == null ? "Some(null)" : $"Some({Value})";
}
#region eq comparers boilerplate
public bool Equals(Option<T> other) {
if (!HasValue && !other.HasValue)
return true;
if (HasValue && other.HasValue)
return EqualityComparer<T>.Default.Equals(Value, other.Value);
return false;
}
public override bool Equals(object obj) {
return obj is Option<T> && Equals((Option<T>) obj);
}
public static bool operator ==(Option<T> left, Option<T> right) {
return left.Equals(right);
}
public static bool operator !=(Option<T> left, Option<T> right) {
return !left.Equals(right);
}
public override int GetHashCode() {
if (!HasValue) return 0;
return IsValueType || Value != null ? Value.GetHashCode() : 1;
}
public int CompareTo(Option<T> other) {
if (HasValue && !other.HasValue) return 1;
if (!HasValue && other.HasValue) return -1;
return Comparer<T>.Default.Compare(Value, other.Value);
}
public static bool operator <(Option<T> left, Option<T> right) {
return left.CompareTo(right) < 0;
}
public static bool operator <=(Option<T> left, Option<T> right) {
return left.CompareTo(right) <= 0;
}
public static bool operator >(Option<T> left, Option<T> right) {
return left.CompareTo(right) > 0;
}
public static bool operator >=(Option<T> left, Option<T> right) {
return left.CompareTo(right) >= 0;
}
#endregion
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 098c45ccebe14f5caa28635386021c94
timeCreated: 1591801098

View File

@@ -0,0 +1,57 @@
using System.IO;
using UnityEngine;
namespace Asset_Cleaner {
static class PersistenceUtils {
public static void Load(ref Config result) {
var serializable = Deserialize();
AufSerializableData.OnDeserialize(in serializable, ref result);
}
public static void Save(in Config src) {
AufSerializableData.OnSerialize(in src, out var serializable);
var json = JsonUtility.ToJson(serializable);
File.WriteAllText(Path, json);
}
static AufSerializableData Deserialize() {
AufSerializableData serializableData;
string json;
if (!File.Exists(Path)) {
// not exists - write new
serializableData = AufSerializableData.Default();
json = JsonUtility.ToJson(serializableData);
File.WriteAllText(Path, json);
}
else {
// exists
json = File.ReadAllText(Path);
if (string.IsNullOrEmpty(json)) {
// but corrupted - overwrite with new
serializableData = AufSerializableData.Default();
json = JsonUtility.ToJson(serializableData);
File.WriteAllText(Path, json);
}
serializableData = JsonUtility.FromJson<AufSerializableData>(json);
if (serializableData.Valid())
return serializableData;
serializableData = AufSerializableData.Default();
json = JsonUtility.ToJson(serializableData);
File.WriteAllText(Path, json);
}
return serializableData;
}
static string Path => $"{Application.temporaryCachePath}/AssetCleaner_{AufSerializableData.CurrentVersion}.json";
// [MenuItem("Tools/LogPath")]
static void Log() {
Debug.Log(Application.temporaryCachePath);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 764a6ef52ca2456eb7c276bbcd0929f0
timeCreated: 1589466213

View File

@@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
namespace Asset_Cleaner {
class Pool<T> : IDisposable where T : class {
Func<T> _ctor;
readonly Stack<T> _stack;
// todo place asserts on app quit
Action<T> _reset;
Action<T> _destroy;
static Action<T> Empty = _ => { };
public Pool(Func<T> ctor, Action<T> reset, Action<T> destroy = null) {
_ctor = ctor;
#if !M_DISABLE_POOLING
_destroy = destroy ?? Empty;
_reset = reset;
_stack = new Stack<T>();
#endif
}
public T Get() {
#if M_DISABLE_POOLING
return _ctor.Invoke();
#else
T element;
if (_stack.Count == 0) {
element = _ctor();
}
else {
element = _stack.Pop();
}
return element;
#endif
}
public void Release(ref T element) {
#if !M_DISABLE_POOLING
Asr.IsFalse(_stack.Count > 0 && ReferenceEquals(_stack.Peek(), element),
"Internal error. Trying to release object that is already released to pool. ");
_reset.Invoke(element);
_stack.Push(element);
#endif
element = null;
}
public void Dispose() {
#if !M_DISABLE_POOLING
while (_stack.Count > 0) {
var t = _stack.Pop();
_destroy.Invoke(t);
}
#endif
}
public _Scope GetScoped(out T tmp) {
tmp = Get();
return new _Scope(this, ref tmp);
}
public struct _Scope : IDisposable {
Pool<T> _pool;
T _val;
internal _Scope(Pool<T> pool, ref T val) {
_pool = pool;
_val = val;
}
public void Dispose() {
_pool.Release(ref _val);
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b5a618b193c34a5cba77e7cc0df122f1
timeCreated: 1591254176

View File

@@ -0,0 +1,458 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Leopotam.Ecs;
using UnityEditor;
using UnityEditor.Animations;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
using static Asset_Cleaner.AufCtx;
using Object = UnityEngine.Object;
namespace Asset_Cleaner {
static class SearchUtils {
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsAssignableFromInverse(this Type lhs, Type rhs) {
if (lhs == null || rhs == null)
return false;
return rhs.IsAssignableFrom(lhs);
}
static Queue<SerializedProperty> _tmp = new Queue<SerializedProperty>();
public static void Upd(SearchArg arg) {
if (arg.Target is DefaultAsset folder) {
var path = AssetDatabase.GetAssetPath(folder);
var store = Globals<BacklinkStore>.Value;
arg.UnusedAssetsFiltered = store.UnusedFiles.Where(p => p.Key.StartsWith(path)).Select(p => p.Key).ToList();
arg.UnusedScenesFiltered = store.UnusedScenes.Where(p => p.Key.StartsWith(path)).Select(p => p.Key).ToList();
}
}
public static void Init(SearchArg arg, Object target, Scene scene = default) {
Asr.IsNotNull(target, "Asset you're trying to search is corrupted");
arg.Target = target;
arg.FilePath = AssetDatabase.GetAssetPath(arg.Target);
if (!scene.IsValid()) {
Upd(arg);
arg.Main = AssetDatabase.LoadMainAssetAtPath(arg.FilePath);
if (AssetDatabase.IsSubAsset(arg.Target)) { }
else {
switch (target) {
case SceneAsset _:
// todo support cross-scene references?
// nested = all assets
break;
default:
// AssetDatabase.IsMainAssetAtPathLoaded()
var subAssets = AssetDatabase.LoadAllAssetsAtPath(arg.FilePath).Where(Predicate).ToArray();
arg.SubAssets = subAssets.Length == 0 ? default(Option<Object[]>) : subAssets;
bool Predicate(Object s) {
if (!s)
return false;
return s.GetInstanceID() != arg.Target.GetInstanceID();
}
break;
}
}
}
else {
switch (arg.Target) {
case GameObject gg:
arg.Main = gg;
arg.Scene = scene;
arg.SubAssets = gg.GetComponents<Component>().OfType<Object>().ToArray();
break;
case Component component: {
// treat like subAsset
arg.Main = component.gameObject;
arg.Scene = scene;
break;
}
default:
// project asset such as Material
arg.Main = arg.Target;
arg.Scene = scene;
break;
}
}
}
static bool SearchInChildProperties(SearchArg arg, Object suspect, bool scene, out EcsEntity entity) {
if (IsTargetOrNested(arg, suspect)) {
entity = default;
return false;
}
if (!suspect) {
entity = default;
return false;
}
var so = new SerializedObject(suspect);
_tmp.Clear();
var queue = _tmp;
var propIterator = so.GetIterator();
var prefabInstance = false;
if (scene && !string.IsNullOrEmpty(arg.FilePath) && PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(suspect) == arg.FilePath) {
prefabInstance = true;
while (propIterator.NextVisible(true)) {
if (propIterator.propertyType != SerializedPropertyType.ObjectReference)
continue;
if (!IsTargetOrNested(arg, propIterator.objectReferenceValue))
continue;
queue.Enqueue(propIterator.Copy());
}
}
else {
while (propIterator.Next(true)) {
if (propIterator.propertyType != SerializedPropertyType.ObjectReference)
continue;
if (!IsTargetOrNested(arg, propIterator.objectReferenceValue))
continue;
queue.Enqueue(propIterator.Copy());
}
}
if (queue.Count == 0 && !prefabInstance) {
entity = default;
return false;
}
entity = World.NewEntityWith(out Result data);
var gui = entity.Set<SearchResultGui>();
gui.Properties = new List<SearchResultGui.PropertyData>();
gui.SerializedObject = so;
gui.Label = new GUIContent();
// init header
Texture2D miniTypeThumbnail = null;
if (scene) {
switch (suspect) {
case Component component:
data.RootGo = component.gameObject;
gui.TransformPath = AnimationUtility.CalculateTransformPath(component.transform, null);
gui.Label.image = AssetPreview.GetMiniThumbnail(data.RootGo);
gui.Label.text = gui.TransformPath;
break;
case GameObject go:
data.RootGo = go;
gui.Label.image = AssetPreview.GetMiniThumbnail(data.RootGo);
gui.TransformPath = AnimationUtility.CalculateTransformPath(go.transform, null);
gui.Label.text = gui.TransformPath;
break;
default:
throw new NotImplementedException();
}
miniTypeThumbnail = data.RootGo.GetInstanceID() == suspect.GetInstanceID()
? null
: AssetPreview.GetMiniThumbnail(suspect);
}
else {
data.File = suspect;
data.FilePath = AssetDatabase.GetAssetPath(data.File);
data.MainFile = AssetDatabase.LoadMainAssetAtPath(data.FilePath);
// todo
var prefabInstanceStatus = PrefabUtility.GetPrefabInstanceStatus(data.MainFile);
switch (prefabInstanceStatus) {
case PrefabInstanceStatus.Connected:
case PrefabInstanceStatus.Disconnected:
switch (data.File) {
case Component comp:
// transformPath = $"{AnimationUtility.CalculateTransformPath(comp.transform, null)}/".Replace("/", "/\n");
gui.TransformPath =
$"{AnimationUtility.CalculateTransformPath(comp.transform, null)}";
break;
case GameObject go:
// transformPath = $"{AnimationUtility.CalculateTransformPath(go.transform, null)}/".Replace("/", "/\n");
gui.TransformPath =
$"{AnimationUtility.CalculateTransformPath(go.transform, null)}";
break;
default:
// Assert.Fail("Not a component"); //todo
break;
}
break;
case PrefabInstanceStatus.NotAPrefab:
case PrefabInstanceStatus.MissingAsset:
if (!AssetDatabase.IsMainAsset(data.File)) {
// {row.Main.name}
gui.TransformPath = $"/{data.File.name}";
}
break;
}
gui.Label.text = data.FilePath.Replace(AssetsRootPath, string.Empty);
gui.Label.image = AssetDatabase.GetCachedIcon(data.FilePath);
}
gui.Label.tooltip = gui.TransformPath;
// init properties (footer)
while (queue.Count > 0) {
var prop = queue.Dequeue();
var targetObject = prop.serializedObject.targetObject;
var item = new SearchResultGui.PropertyData {
Property = prop,
Content = new GUIContent()
};
item.Content.image = miniTypeThumbnail;
item.Content.text = Nicify(prop, targetObject, gui.TransformPath);
item.Content.tooltip = gui.TransformPath;
var typeName = targetObject.GetType().Name;
if (StringComparer.Ordinal.Equals(typeName, targetObject.name))
item.Content.tooltip = $"{gui.TransformPath}.{prop.propertyPath}";
else
item.Content.tooltip = $"{gui.TransformPath}({typeName}).{prop.propertyPath}";
gui.Properties.Add(item: item);
}
return true;
}
public static bool IsFileIgrnoredBySettings(string path) {
if (IgnoreTypes.Check(path, out _)) return true;
if (IgnoredNonAssets(path)) return true;
if (IgnoredPaths(path, out _)) return true;
return false;
}
public static bool IgnoredPaths(string path, out string str) {
var conf = Globals<Config>.Value;
foreach (var substr in conf.IgnorePathContains) {
Asr.IsNotNull(path);
Asr.IsNotNull(substr);
if (!path.Contains(substr)) continue;
str = substr;
return true;
}
str = default;
return false;
}
public static bool IgnoredNonAssets(string path) {
return !path.Contains("Assets/");
}
#region Project
public static bool IsUnused(string path) {
if (IsFileIgrnoredBySettings(path))
return false;
return !AnyDependencies(path);
}
static bool AnyDependencies(string path) {
var store = Globals<BacklinkStore>.Value;
if (store.UnusedFiles.Select(p => p.Key).Contains(path))
return false;
if (store.UnusedScenes.Select(p => p.Key).Contains(path))
return false;
return true;
}
public static void FilesThatReference(SearchArg arg) {
var store = Globals<BacklinkStore>.Value;
var path1 = AssetDatabase.GetAssetPath(arg.Target);
if (!store.Backward.TryGetValue(path1, out var dep))
return;
foreach (var path in dep.Lookup) {
var mainAsset = AssetDatabase.GetMainAssetTypeAtPath(path);
if (mainAsset.IsAssignableFromInverse(typeof(SceneAsset)))
continue;
var any = false;
if (mainAsset.IsAssignableFromInverse(typeof(GameObject))) { }
else {
var allAssetsAtPath = AssetDatabase.LoadAllAssetsAtPath(path);
foreach (var suspect in allAssetsAtPath) {
if (suspect is DefaultAsset || suspect is Transform || !suspect) continue;
if (!SearchInChildProperties(arg, suspect, false, out var entity))
continue;
entity.Set<FileResultTag>();
any = true;
}
}
if (any) continue;
// failed to find any property - just show main asset
var e = World.NewEntity();
var gui = e.Set<SearchResultGui>();
gui.Properties = new List<SearchResultGui.PropertyData>();
var main = AssetDatabase.LoadMainAssetAtPath(path);
gui.Label = new GUIContent() {
image = AssetPreview.GetMiniThumbnail(main),
text = path.Replace(AssetsRootPath, string.Empty)
};
var res = e.Set<Result>();
res.MainFile = main;
e.Set<FileResultTag>();
}
}
public static void ScenesThatContain(Object activeObject) {
var store = Globals<BacklinkStore>.Value;
var path1 = AssetDatabase.GetAssetPath(activeObject);
if (!store.Backward.TryGetValue(path1, out var dep))
return;
foreach (var path in dep.Lookup) {
if (!AssetDatabase.GetMainAssetTypeAtPath(path).IsAssignableFromInverse(typeof(SceneAsset)))
continue;
World.NewEntityWith(out SceneResult sp, out SceneDetails sceneDetails);
sp.PathNicified = path.Replace("Assets/", string.Empty);
// heavy
sceneDetails.Path = path;
var alreadyOpened = false;
for (var i = 0; i < EditorSceneManager.loadedSceneCount; i++) {
var cur = SceneManager.GetSceneAt(i);
if (!cur.path.Eq(sceneDetails.Path)) continue;
alreadyOpened = true;
sceneDetails.Scene = cur;
}
sceneDetails.WasOpened = alreadyOpened;
}
}
#endregion
#region Scene
static Pool<List<Component>> ComponentListPool = new Pool<List<Component>>(() => new List<Component>(), list => list.Clear());
// todo provide explicit scene arg
public static void InScene(SearchArg arg, Scene currentScene) {
var rootGameObjects = currentScene.GetRootGameObjects();
foreach (var suspect in Traverse(rootGameObjects)) {
if (!SearchInChildProperties(arg, suspect, scene: true, out var entity))
continue;
var s = entity.Set<InSceneResult>();
s.ScenePath = arg.Scene.path;
}
IEnumerable<Object> Traverse(GameObject[] roots) {
foreach (var rootGo in roots)
foreach (var comp in GoAndChildComps(rootGo)) {
yield return comp;
}
}
IEnumerable<Object> GoAndChildComps(GameObject rr) {
using (ComponentListPool.GetScoped(out var pooled)) {
rr.GetComponents(pooled);
foreach (var component in pooled) {
if (component is Transform)
continue;
yield return component;
}
}
var transform = rr.transform;
var childCount = transform.childCount;
for (int i = 0; i < childCount; i++) {
var g = transform.GetChild(i).gameObject;
foreach (var res in GoAndChildComps(g))
yield return res;
}
}
}
#if backup
static void InScene(SearchArg arg, Scene currentScene) {
var allObjects = currentScene
.GetRootGameObjects()
.SelectMany(g => g.GetComponentsInChildren<Component>(true)
.Where(c => c && !(c is Transform))
.Union(Enumerable.Repeat(g as Object, 1))
)
.ToArray();
var total = allObjects.Length;
for (var i = 0; i < total; i++) {
var suspect = allObjects[i];
if (SearchInChildProperties(arg, suspect, true, out var entity)) {
var s = entity.Set<InSceneResult>();
s.ScenePath = arg.Scene.path;
}
}
}
#endif
static bool IsTargetOrNested(SearchArg target, Object suspect) {
if (!suspect)
return false;
if (target.Target.GetInstanceID() == suspect.GetInstanceID() || target.Main.GetInstanceID() == (suspect).GetInstanceID())
return true;
if (target.SubAssets.TryGet(out var subassets))
foreach (var asset in subassets) {
if (asset.GetInstanceID() == (suspect).GetInstanceID())
return true;
}
return false;
}
static string Nicify(SerializedProperty sp, Object o, string transformPath) {
// return sp.propertyPath;
string nice;
switch (o) {
case AnimatorState animatorState:
return animatorState.name;
case Material material:
nice = material.name;
break;
default: {
nice = sp.propertyPath.Replace(".Array.data", string.Empty);
if (nice.IndexOf(".m_PersistentCalls.m_Calls", StringComparison.Ordinal) > 0) {
nice = nice.Replace(".m_PersistentCalls.m_Calls", string.Empty)
.Replace(".m_Target", string.Empty);
}
nice = nice.Split('.').Select(t => ObjectNames.NicifyVariableName(t).Replace(" ", string.Empty))
.Aggregate((a, b) => a + "." + b);
break;
}
}
// nice = $"{transformPath}({o.GetType().Name}).{nice}";
nice = $"({o.GetType().Name}).{nice}";
return nice;
}
const string AssetsRootPath = "Assets/";
#endregion
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 66f3f357bedc44e2907d7e5f48de45d3
timeCreated: 1576134438

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: cf3dfe88eca681d449c3f24f79d64b4c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,408 @@
[![discord](https://img.shields.io/discord/404358247621853185.svg?label=discord)](https://discord.gg/5GZVde6)
[![license](https://img.shields.io/github/license/Leopotam/ecs.svg)](https://github.com/Leopotam/ecs/blob/develop/LICENSE)
# LeoECS - Simple lightweight C# Entity Component System framework
Performance, zero/small memory allocations/footprint, no dependencies on any game engine - main goals of this project.
> C#7.3 or above required for this framework.
> Tested on unity 2019.1 (not dependent on it) and contains assembly definition for compiling to separate assembly file for performance reason.
> **Important!** Dont forget to use `DEBUG` builds for development and `RELEASE` builds in production: all internal error checks / exception throwing works only in `DEBUG` builds and eleminated for performance reasons in `RELEASE`.
# Installation
## As unity module
This repository can be installed as unity module directly from git url. In this way new line should be added to `Packages/manifest.json`:
```
"com.leopotam.ecs": "https://github.com/Leopotam/ecs.git",
```
By default last released version will be used. If you need trunk / developing version then `develop` name of branch should be added after hash:
```
"com.leopotam.ecs": "https://github.com/Leopotam/ecs.git#develop",
```
## As unity module from npm registry (Experimental)
This repository can be installed as unity module from external npm registry with support of different versions. In this way new block should be added to `Packages/manifest.json` right after opening `{` bracket:
```
"scopedRegistries": [
{
"name": "Leopotam",
"url": "https://npm.leopotam.com",
"scopes": [
"com.leopotam"
]
}
],
```
After this operation registry can be installed from list of packages from standard unity module manager.
> **Important!** Url can be changed later, check actual url at `README`.
## As source
If you can't / don't want to use unity modules, code can be downloaded as sources archive of required release from [Releases page](`https://github.com/Leopotam/ecs/releases`).
# Main parts of ecs
## Component
Container for user data without / with small logic inside. Can be used any user class without any additional inheritance:
```csharp
class WeaponComponent {
public int Ammo;
public string GunName;
}
```
> **Important!** Dont forget to manually init all fields of new added component. Default value initializers will not work due all components can be reused automatically multiple times through builtin pooling mechanism (no destroying / creating new instance for each request for performance reason).
> **Important!** Dont forget to cleanup reference links to instances of any classes before removing components from entity, otherwise it can lead to memory leaks.
>
> By default all `marshal-by-reference` typed fields of component (classes in common case) will be checked for null on removing attempt in `DEBUG`-mode. If you know that you have object instance that should be not null (preinited collections for example) - `[EcsIgnoreNullCheck]` attribute can be used for disabling these checks.
## Entity
Сontainer for components. Implemented as `EcsEntity` for wrapping internal identifiers:
```csharp
EcsEntity entity = _world.NewEntity ();
Component1 c1 = entity.Set<Component1> ();
Component2 c2 = entity.Set<Component2> ();
```
There are some helpers to simplify creation of entity with multiple components:
```csharp
EcsEntity entity = _world.NewEntityWith<Component1, Component2> (out Component1 c1, out Component2 c2);
```
> **Important!** Entities without components on them will be automatically removed on last `EcsEntity.Unset()` call.
## System
Сontainer for logic for processing filtered entities. User class should implements `IEcsInitSystem`, `IEcsDestroySystem`, `IEcsRunSystem` (or other supported) interfaces:
```csharp
class WeaponSystem : IEcsPreInitSystem, IEcsInitSystem, IEcsDestroySystem, IEcsPreDestroySystem {
public void PreInit () {
// Will be called once during EcsSystems.Init() call and before IEcsInitSystem.Init.
}
public void Init () {
// Will be called once during EcsSystems.Init() call.
}
public void Destroy () {
// Will be called once during EcsSystems.Destroy() call.
}
public void AfterDestroy () {
// Will be called once during EcsSystems.Destroy() call and after IEcsDestroySystem.Destroy.
}
}
```
```csharp
class HealthSystem : IEcsRunSystem {
public void Run () {
// Will be called on each EcsSystems.Run() call.
}
}
```
# Data injection
All compatible `EcsWorld` and `EcsFilter<T>` fields of ecs-system will be auto-initialized (auto-injected):
```csharp
class HealthSystem : IEcsSystem {
// auto-injected fields.
EcsWorld _world = null;
EcsFilter<WeaponComponent> _weaponFilter = null;
}
```
Instance of any custom type can be injected to all systems through `EcsSystems.Inject()` method:
```csharp
var systems = new EcsSystems (world)
.Add (new TestSystem1 ())
.Add (new TestSystem2 ())
.Add (new TestSystem3 ())
.Inject (a)
.Inject (b)
.Inject (c)
.Inject (d);
systems.Init ();
```
Each system will be scanned for compatible fields (can contains all of them or no one) with proper initialization.
> **Important!** Data injection for any user type can be used for sharing external data between systems.
## Data Injection with multiple EcsSystems
If you want to use multiple `EcsSystems` you can find strange behaviour with DI:
```csharp
class Component1 { }
class System1 : IEcsInitSystem {
EcsWorld _world = null;
public void Init () {
_world.NewEntity ().Set<Component1> ();
}
}
class System2 : IEcsInitSystem {
EcsFilter<Component1> _filter = null;
public void Init () {
Debug.Log (_filter.GetEntitiesCount ());
}
}
var systems1 = new EcsSystems (world);
var systems2 = new EcsSystems (world);
systems1.Add (new System1 ());
systems2.Add (new System2 ());
systems1.Init ();
systems2.Init ();
```
You will get "0" at console. Problem is that DI starts at `Init` method inside each `EcsSystems`. It means that any new `EcsFilter` instance (with lazy initialization) will be correctly injected only at current `EcsSystems`.
To fix this behaviour startup code should be modified in this way:
```csharp
var systems1 = new EcsSystems (world);
var systems2 = new EcsSystems (world);
systems1.Add (new System1 ());
systems2.Add (new System2 ());
systems1.ProcessInjects ();
systems2.ProcessInjects ();
systems1.Init ();
systems2.Init ();
```
You should get "1" at console after fix.
# Special classes
## EcsFilter<T>
Container for keeping filtered entities with specified component list:
```csharp
class WeaponSystem : IEcsInitSystem, IEcsRunSystem {
// auto-injected fields: EcsWorld instance and EcsFilter.
EcsWorld _world = null;
// We wants to get entities with "WeaponComponent" and without "HealthComponent".
EcsFilter<WeaponComponent>.Exclude<HealthComponent> _filter = null;
public void Init () {
// new C# syntax can be used if component instance not required right now.
_world.NewEntityWith<WeaponComponent> (out _);
}
public void Run () {
foreach (var i in _filter) {
// entity that contains WeaponComponent.
// Performance hint: use 'ref' prefixes for disable copying entity structure.
// Important: dont use `ref` on filter data outside of foreach-loop over this filter.
ref var entity = ref _filter.Entities[i];
// Get1 will return link to attached "WeaponComponent".
var weapon = _filter.Get1[i];
weapon.Ammo = System.Math.Max (0, weapon.Ammo - 1);
}
}
}
```
> **Important!** You should not use `ref` modifier for any filter data outside of foreach-loop over this filter if you want to destroy part of this data (entity or component) - it will break memory integrity.
All components from filter `Include` constraint can be fast accessed through `filter.Get1()`, `filter.Get2()`, etc - in same order as they were used in filter type declaration.
If fast access not required (for example, for flag-based components without data), component can implements `IEcsIgnoreInFilter` interface for decrease memory usage and increase performance:
```csharp
class Component1 { }
class Component2 : IEcsIgnoreInFilter { }
class TestSystem : IEcsRunSystem {
EcsFilter<Component1, Component2> _filter = null;
public void Run () {
foreach (var i in _filter) {
// its valid code.
var component1 = _filter.Get1[i];
// its invalid code due to cache for _filter.Get2[] is null for memory / performance reasons.
var component2 = _filter.Get2[i];
}
}
}
```
> Important: Any filter supports up to 4 component types as "include" constraints and up to 2 component types as "exclude" constraints. Shorter constraints - better performance.
> Important: If you will try to use 2 filters with same components but in different order - you will get exception with detailed info about conflicted types, but only in `DEBUG` mode. In `RELEASE` mode all checks will be skipped.
## EcsWorld
Root level container for all entities / components, works like isolated environment.
> Important: Do not forget to call `EcsWorld.Destroy()` method when instance will not be used anymore.
## EcsSystems
Group of systems to process `EcsWorld` instance:
```csharp
class Startup : MonoBehaviour {
EcsWorld _world;
EcsSystems _systems;
void Start () {
// create ecs environment.
_world = new EcsWorld ();
_systems = new EcsSystems (_world)
.Add (new WeaponSystem ());
_systems.Init ();
}
void Update () {
// process all dependent systems.
_systems.Run ();
_world.EndFrame ();
}
void OnDestroy () {
// destroy systems logical group.
_systems.Destroy ();
// destroy world.
_world.Destroy ();
}
}
```
> Important: Do not forget to call `EcsWorld.EndFrame()` method when all `EcsSystems` completed.
`EcsSystems` instance can be used as nested system (any types of `IEcsInitSystem`, `IEcsRunSystem`, ecs behaviours are supported):
```csharp
// initialization.
var nestedSystems = new EcsSystems (_world).Add (new NestedSystem ());
// dont call nestedSystems.Init() here, rootSystems will do it automatically.
var rootSystems = new EcsSystems (_world).Add (nestedSystems);
rootSystems.Init ();
// update loop.
// dont call nestedSystems.Run() here, rootSystems will do it automatically.
rootSystems.Run ();
// destroying.
// dont call nestedSystems.Destroy() here, rootSystems will do it automatically.
rootSystems.Destroy ();
```
Any `IEcsRunSystem` or `EcsSystems` instance can be enabled or disabled from processing in runtime:
```csharp
class TestSystem : IEcsRunSystem {
public void Run () { }
}
var systems = new EcsSystems (_world);
systems.Add (new TestSystem (), "my special system");
systems.Init ();
var idx = systems.GetNamedRunSystem ("my special system");
// state will be true here, all systems are active by default.
var state = systems.GetRunSystemState (idx);
// disable system from execution.
systems.SetRunSystemState (idx, false);
```
# Examples
##With sources:
* [Snake game](https://github.com/Leopotam/ecs-snake)
* [Pacman game](https://github.com/SH42913/pacmanecs)
* [GTA5 custom wounds mod](https://github.com/SH42913/gunshotwound3)
* [Ecs Hybrid Unity integration](https://github.com/SH42913/leoecshybrid)
##Without sources:
* [Hattori2 game](https://www.instagram.com/hattorigame/)
* [Natives game](https://alex-kpojb.itch.io/natives-ecs)
* [PrincessRun android game](https://play.google.com/store/apps/details?id=ru.zlodey.princessrun)
* [TowerRunner Revenge android game](https://play.google.com/store/apps/details?id=ru.zlodey.towerrunner20)
* [HypnoTap android game](https://play.google.com/store/apps/details?id=com.ZlodeyStudios.HypnoTap)
* [Elves-vs-Dwarfs game](https://globalgamejam.org/2019/games/elves-vs-dwarfs)
# Extensions
* [Unity editor integration](https://github.com/Leopotam/ecs-unityintegration)
* [Unity uGui events support](https://github.com/Leopotam/ecs-ui)
* [Multi-threading support](https://github.com/Leopotam/ecs-threads)
* [Reactive systems](https://github.com/Leopotam/ecs-reactive)
* [Engine independent types](https://github.com/Leopotam/ecs-types)
# License
The software released under the terms of the [MIT license](./LICENSE.md). Enjoy.
# Donate
Its free opensource software, but you can buy me a coffee:
<a href="https://www.buymeacoffee.com/leopotam" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/yellow_img.png" alt="Buy Me A Coffee" style="height: auto !important;width: auto !important;" ></a>
# FAQ
### I want to process one system at MonoBehaviour.Update() and another - at MonoBehaviour.FixedUpdate(). How I can do it?
For splitting systems by `MonoBehaviour`-method multiple `EcsSystems` logical groups should be used:
```csharp
EcsSystems _update;
EcsSystems _fixedUpdate;
void Start () {
var world = new EcsWorld ();
_update = new EcsSystems (world).Add (new UpdateSystem ());
_update.Init ();
_fixedUpdate = new EcsSystems (world).Add (new FixedUpdateSystem ());
_fixedUpdate.Init ();
}
void Update () {
_update.Run ();
}
void FixedUpdate () {
_fixedUpdate.Run ();
}
```
### I like how dependency injection works, but i want to skip some fields from initialization. How I can do it?
You can use `[EcsIgnoreInject]` attribute on any field of system:
```csharp
...
// will be injected.
EcsFilter<C1> _filter1 = null;
// will be skipped.
[EcsIgnoreInject]
EcsFilter<C2> _filter2 = null;
```
### I do not like foreach-loops, I know that for-loops are faster. How I can use it?
Current implementation of foreach-loop fast enough (custom enumerator, no memory allocation), small performance differences can be found on 10k items and more. Current version doesnt support for-loop iterations anymore.
### I copy&paste my reset components code again and again. How I can do it in other manner?
If you want to simplify your code and keep reset-code in one place, you can use `IEcsAutoReset` interface for components:
```csharp
class MyComponent : IEcsAutoReset {
public object LinkToAnotherComponent;
public void Reset() {
// Cleanup all marshal-by-reference fields here.
LinkToAnotherComponent = null;
}
}
```
This method will be automatically called after component removing from entity and before recycling to component pool.
### I use components as events that works only one frame, then remove it at last system in execution sequence. It's boring, how I can automate it?
If you want to remove one-frame components without additional custom code, you can implement `IEcsOneFrame` interface:
```csharp
class MyComponent : IEcsOneFrame { }
```
> Important: Do not forget to call `EcsWorld.EndFrame()` method once after all `EcsSystems.Run` calls.
> Important: Do not forget that if one-frame component contains `marshal-by-reference` typed fields - this component should implements `IEcsAutoReset` interface.
### I need more than 4 components in filter, how i can do it?
Check `EcsFilter<Inc1, Inc2, Inc3, Inc4>` type source, copy&paste it to your project and add additional components support in same manner.

Some files were not shown because too many files have changed in this diff Show More