├── .github └── workflows │ └── docfx-pages.yaml ├── .gitignore ├── CHANGELOG.md ├── CHANGELOG.md.meta ├── Editor.meta ├── Editor ├── EditorEvent.cs ├── EditorEvent.cs.meta ├── EditorExtension.cs ├── EditorExtension.cs.meta ├── ProjectSettingsData.cs ├── ProjectSettingsData.cs.meta ├── ProjectSettingsPanel.cs ├── ProjectSettingsPanel.cs.meta ├── ProjectSettingsSingleton.cs ├── ProjectSettingsSingleton.cs.meta ├── USGEngine.cs ├── USGEngine.cs.meta ├── USGUtility.cs └── USGUtility.cs.meta ├── LICENSE.md ├── LICENSE.md.meta ├── README.md ├── README.md.meta ├── Runtime.meta ├── Runtime ├── Attributes.cs ├── Attributes.cs.meta ├── Interfaces.cs ├── Interfaces.cs.meta ├── IsExternalInit.cs ├── IsExternalInit.cs.meta ├── StringBuilderExtension.cs ├── StringBuilderExtension.cs.meta ├── USGContext.cs ├── USGContext.cs.meta ├── USGFullNameOf.cs ├── USGFullNameOf.cs.meta ├── USGReflection.cs └── USGReflection.cs.meta ├── Sample.meta ├── Sample ├── SceneBuildIndex.cs ├── SceneBuildIndex.cs.meta ├── SceneBuildIndexGenerator.cs └── SceneBuildIndexGenerator.cs.meta ├── SatorImaging.UnitySourceGenerator.asmdef ├── SatorImaging.UnitySourceGenerator.asmdef.meta ├── Template.meta ├── Template ├── Template_MethodGenerator.txt ├── Template_MethodGenerator.txt.meta ├── Template_SelfEmitGenerator.txt └── Template_SelfEmitGenerator.txt.meta ├── package.json └── package.json.meta /.github/workflows/docfx-pages.yaml: -------------------------------------------------------------------------------- 1 | name: docfx for GitHub Pages 2 | 3 | on: 4 | # Runs on pushes targeting the default branch 5 | push: 6 | branches: 7 | - 'main' 8 | #- 'releases/**' 9 | #pull_request: 10 | # branches: 11 | # - 'main' 12 | 13 | # Allows you to run this workflow manually from the Actions tab 14 | workflow_dispatch: 15 | 16 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 17 | permissions: 18 | contents: read 19 | pages: write 20 | id-token: write 21 | 22 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 23 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 24 | concurrency: 25 | group: "pages" 26 | cancel-in-progress: false 27 | 28 | jobs: 29 | # Single deploy job since we're just deploying 30 | deploy: 31 | environment: 32 | name: github-pages 33 | url: ${{ steps.deployment.outputs.page_url }} 34 | runs-on: ubuntu-latest 35 | 36 | 37 | ######################################### 38 | ###### sator-imaging/docfx-pages ###### 39 | ######################################### 40 | steps: 41 | 42 | # for extensibility, docfx-pages does NOT checkout your repository. 43 | # so you need to checkout manually prior to sator-imaging/docfx-pages. 44 | - name: Checkout 45 | uses: actions/checkout@v3 46 | 47 | 48 | # after checkout, you can checkout another repo, copy logo image or 49 | # other assets into '.docfx' folder to setup own docfx environment. 50 | # note that images must be copied into '.docfx/images' folder. 51 | 52 | 53 | ###### main ###### 54 | 55 | - uses: sator-imaging/docfx-pages@v1 56 | id: deployment # required to show url in actions result page. 57 | with: 58 | # required options - NOTE: double-quote (") cannot be used 59 | app_name: 'USG' 60 | site_title: 'Source Generator for Unity' 61 | site_footer: '© 2024 Sator Imaging' 62 | 63 | # optional 64 | class_members: 'separatePages' # 'separatePages' or 'samePage' 65 | google_analytics: '' # empty to disable 66 | 67 | # paths must be relative from .docfx folder. 68 | # note that url works but only in top page. see sample website for detail. 69 | ## site_logo: 'https://avatars.githubusercontent.com/u/16752340' 70 | ## site_favicon: 'https://avatars.githubusercontent.com/u/45192683' 71 | 72 | # advanced options 73 | # --> https://dotnet.github.io/docfx/docs/template.html?tabs=modern#custom-template 74 | # main.js 75 | # NOTE: double-quote (") cannot be used 76 | main_js: | 77 | export default { 78 | defaultTheme: 'light', 79 | showLightbox: (img) => true, 80 | iconLinks: [ 81 | { 82 | icon: 'github', 83 | href: 'https://github.com/sator-imaging', 84 | title: 'GitHub' 85 | }, 86 | { 87 | icon: 'twitter', 88 | href: 'https://twitter.com/sator_imaging', 89 | title: 'Twitter' 90 | }, 91 | { 92 | icon: 'unity', 93 | href: 'https://prf.hn/l/0eoDOON', 94 | title: 'AssetStore' 95 | }, 96 | { 97 | icon: 'youtube', 98 | href: 'https://www.youtube.com/@SatorImaging', 99 | title: 'YouTube' 100 | }, 101 | { 102 | icon: 'chat-quote-fill', 103 | href: 'https://www.sator-imaging.com/', 104 | title: 'Contact' 105 | }, 106 | ], 107 | } 108 | 109 | # main.css 110 | # NOTE: double-quote (") cannot be used 111 | main_css: | 112 | 113 | 114 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # This .gitignore file should be placed at the root of your Unity project directory 2 | # 3 | # Get latest from https://github.com/github/gitignore/blob/master/Unity.gitignore 4 | # 5 | /[Ll]ibrary/ 6 | /[Tt]emp/ 7 | /[Oo]bj/ 8 | /[Bb]uild/ 9 | /[Bb]uilds/ 10 | /[Ll]ogs/ 11 | /[Mm]emoryCaptures/ 12 | 13 | # Asset meta data should only be ignored when the corresponding asset is also ignored 14 | !/[Aa]ssets/**/*.meta 15 | 16 | # Uncomment this line if you wish to ignore the asset store tools plugin 17 | # /[Aa]ssets/AssetStoreTools* 18 | 19 | # Autogenerated Jetbrains Rider plugin 20 | [Aa]ssets/Plugins/Editor/JetBrains* 21 | 22 | # Visual Studio cache directory 23 | .vs/ 24 | 25 | # Gradle cache directory 26 | .gradle/ 27 | 28 | # Autogenerated VS/MD/Consulo solution and project files 29 | ExportedObj/ 30 | .consulo/ 31 | *.csproj 32 | *.unityproj 33 | *.sln 34 | *.suo 35 | *.tmp 36 | *.user 37 | *.userprefs 38 | *.pidb 39 | *.booproj 40 | *.svd 41 | *.pdb 42 | *.mdb 43 | *.opendb 44 | *.VC.db 45 | 46 | # Unity3D generated meta files 47 | *.pidb.meta 48 | *.pdb.meta 49 | *.mdb.meta 50 | 51 | # Unity3D generated file on crash reports 52 | sysinfo.txt 53 | 54 | # Builds 55 | *.apk 56 | *.unitypackage 57 | 58 | # Crashlytics generated file 59 | crashlytics-build.properties 60 | 61 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | 9 | ## [Unreleased](https://github.com/sator-imaging/Unity-AltSourceGenerator) 10 | 11 | ### TODO 12 | 13 | - nothing yet 14 | 15 | 16 | 17 | ## [3.0.0](https://github.com/sator-imaging/Unity-AltSourceGenerator/releases/tag/v3.0.0) 18 | 19 | ### API Changes 😉 20 | 21 | - Features works only on Unity Editor 22 | - classes moved to `Editor` namespace. 23 | 24 | - `USGEngine.ProcessFile()` will be removed 25 | - methods still exist but obsolete. 26 | - use `USGUtility.ForceGenerateByType(typeof(...))` instead. 27 | 28 | - `USGUtility.**ByName()` will be removed 29 | - methods still exist but obsolete. 30 | - use `USGUtility.**ByType()` instead. 31 | 32 | 33 | 34 | ## [2.0.0](https://github.com/sator-imaging/Unity-AltSourceGenerator/releases/tag/v2.0.0) 35 | 36 | ### Breaking Changes ;-) 37 | 38 | - USGEngine.ProcessFile(string assetsRelPath) 39 | - signature changed 40 | - `ProcessFile(string assetsRelPath, bool ignoreOverwriteSettingOnAttribute, bool autoRunReferencingEmittersNow = false)` 41 | 42 | - ~~public~~ static bool USGEngine.IgnoreOverwriteSettingByAttribute 43 | - now private. use `ProcessFile(path, *true*)` instead. 44 | 45 | - USGUtility.ForceGenerateByName(string clsName, bool showInProjectPanel = *false*) 46 | - `showInProjectPanel` now false by default. 47 | 48 | - usg\(params string[] memberNames) 49 | - `global::` namespace will be added. 50 | 51 | - usg(Type cls, params string[] memberNames) 52 | - signature changed 53 | - `usg(object valueOrType, bool isFullName = true)` 54 | -------------------------------------------------------------------------------- /CHANGELOG.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e2fe57dc49d168b43820de01346f7ae9 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7ddb20ab15127f64d93dae52720bd53f 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/EditorEvent.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_EDITOR 2 | 3 | using System; 4 | using System.Reflection; 5 | using UnityEditor; 6 | 7 | namespace SatorImaging.UnitySourceGenerator.Editor 8 | { 9 | public static class EditorEvent 10 | { 11 | // NOTE: Unity doesn't invoke asset import event correctly when load script in background. 12 | // This will prevent Unity to reload updated scripts in background. 13 | [InitializeOnLoadMethod] 14 | static void InitializeOnLoad() 15 | { 16 | ProjectSettingsData.instance.SuspendAutoReloadWhileEditorInBackground 17 | = ProjectSettingsData.instance.SuspendAutoReloadWhileEditorInBackground; 18 | } 19 | 20 | 21 | static bool _eventWasRegistered = false; // no way to determine other classes use focusChanged event? 22 | internal static void RegisterFocusChangedEvent(bool registerOrRemove) 23 | { 24 | //https://github.com/Unity-Technologies/UnityCsReference/blob/master/Editor/Mono/EditorApplication.cs#L275 25 | var focusChanged = typeof(EditorApplication).GetField("focusChanged", 26 | BindingFlags.Static | BindingFlags.NonPublic); 27 | if (focusChanged == null) 28 | return; 29 | 30 | // TODO: better cleanup. 31 | // currently, event can be unregistered but it seems empty action runs on focus changed event...? 32 | if (!_eventWasRegistered) 33 | { 34 | if (!registerOrRemove) 35 | return; 36 | EditorApplication.quitting += () => OnEditorApplicationFocus(true); 37 | } 38 | 39 | var currentAction = focusChanged.GetValue(null) as Action; 40 | if (registerOrRemove) 41 | { 42 | currentAction -= OnEditorApplicationFocus; 43 | currentAction += OnEditorApplicationFocus; 44 | _eventWasRegistered = true; 45 | } 46 | else 47 | { 48 | currentAction -= OnEditorApplicationFocus; 49 | } 50 | focusChanged.SetValue(null, currentAction); 51 | //Debug.Log($"[USG] Null? {currentAction == null} Method:{currentAction.Method} Target:{currentAction.Target}"); 52 | 53 | _restoreAutoRefresh = EditorPrefs.GetInt(PREF_AUTO_REFRESH, EditorPrefs.GetInt(PREF_AUTO_REFRESH_OLD, DEFAULT_AUTO_REFRESH)); 54 | //_restoreDirMonitoring = EditorPrefs.GetBool(PREF_DIR_MONITORING, DEFAULT_DIR_MONITORING); 55 | } 56 | 57 | const bool DEFAULT_DIR_MONITORING = true; 58 | const int DEFAULT_AUTO_REFRESH = 1; 59 | const string PREF_AUTO_REFRESH = "kAutoRefreshMode"; 60 | const string PREF_AUTO_REFRESH_OLD = "kAutoRefresh"; 61 | //const string PREF_DIR_MONITORING = "DirectoryMonitoring"; 62 | //static bool _restoreDirMonitoring = DEFAULT_DIR_MONITORING; 63 | static int _restoreAutoRefresh = DEFAULT_AUTO_REFRESH; 64 | static void OnEditorApplicationFocus(bool focus) 65 | { 66 | //https://github.com/Unity-Technologies/UnityCsReference/blob/master/Editor/Mono/PreferencesWindow/AssetPipelinePreferences.cs#L94 67 | if (focus == false) 68 | { 69 | _restoreAutoRefresh = EditorPrefs.GetInt(PREF_AUTO_REFRESH, EditorPrefs.GetInt(PREF_AUTO_REFRESH_OLD, DEFAULT_AUTO_REFRESH)); 70 | //_restoreDirMonitoring = EditorPrefs.GetBool(PREF_DIR_MONITORING, DEFAULT_DIR_MONITORING); 71 | 72 | //AssetDatabase.DisallowAutoRefresh(); 73 | EditorApplication.LockReloadAssemblies(); 74 | //EditorPrefs.SetBool(PREF_DIR_MONITORING, false); 75 | EditorPrefs.SetInt(PREF_AUTO_REFRESH, 0); 76 | EditorPrefs.SetInt(PREF_AUTO_REFRESH_OLD, 0); 77 | } 78 | else 79 | { 80 | //EditorPrefs.SetBool(PREF_DIR_MONITORING, _restoreDirMonitoring); 81 | EditorPrefs.SetInt(PREF_AUTO_REFRESH, _restoreAutoRefresh); 82 | EditorPrefs.SetInt(PREF_AUTO_REFRESH_OLD, _restoreAutoRefresh); 83 | 84 | //AssetDatabase.AllowAutoRefresh(); 85 | EditorApplication.UnlockReloadAssemblies(); 86 | AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate); // option is required...? 87 | } 88 | //Debug.Log($"[USG] Focus:{focus} Restore:{_restoreAutoRefresh}/{_restoreDirMonitoring}"); 89 | } 90 | 91 | 92 | } 93 | } 94 | 95 | #endif 96 | -------------------------------------------------------------------------------- /Editor/EditorEvent.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 57308a90fe3c5a944aa97e222da9408a 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/EditorExtension.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_EDITOR 2 | 3 | using System.Collections.Generic; 4 | using UnityEditor; 5 | 6 | namespace SatorImaging.UnitySourceGenerator.Editor 7 | { 8 | public static class EditorExtension 9 | { 10 | const string ROOT_MENU = @"Assets/Unity Source Generator/"; 11 | const string TEMPLATE_PATH = @"Packages/com.sator-imaging.alt-source-generator/Template/Template_"; 12 | const string CS_EXT = @".cs"; 13 | const string TXT_EXT = @".txt"; 14 | 15 | 16 | [MenuItem(ROOT_MENU + "Force Generate while Overwriting Disabled")] 17 | static void ForceGenerateSelectedScripts() 18 | { 19 | if (Selection.assetGUIDs == null || Selection.assetGUIDs.Length == 0) 20 | return; 21 | 22 | // NOTE: when multiple files selected, first import event initialize C# environment. 23 | // --> https://docs.unity3d.com/2021.3/Documentation/Manual/DomainReloading.html 24 | // so that need to process files at once. 25 | var filePathList = new List(Selection.assetGUIDs.Length); 26 | 27 | foreach (var guid in Selection.assetGUIDs) 28 | { 29 | var path = AssetDatabase.GUIDToAssetPath(guid); 30 | if (AssetDatabase.LoadAssetAtPath(path) is not MonoScript) 31 | continue; 32 | 33 | filePathList.Add(path); 34 | } 35 | USGEngine.Process(filePathList.ToArray(), true); 36 | } 37 | 38 | 39 | [MenuItem(ROOT_MENU + "Method Generator Template", priority = 100)] 40 | static void MethodGenerator() 41 | { 42 | ProjectWindowUtil.CreateScriptAssetFromTemplateFile( 43 | TEMPLATE_PATH + nameof(MethodGenerator) + TXT_EXT, 44 | nameof(MethodGenerator) + CS_EXT); 45 | } 46 | 47 | 48 | [MenuItem(ROOT_MENU + "Self-Emit Generator Template", priority = 100)] 49 | static void SelfEmitGenerator() 50 | { 51 | ProjectWindowUtil.CreateScriptAssetFromTemplateFile( 52 | TEMPLATE_PATH + nameof(SelfEmitGenerator) + TXT_EXT, 53 | nameof(SelfEmitGenerator) + CS_EXT); 54 | } 55 | 56 | 57 | } 58 | } 59 | 60 | #endif 61 | -------------------------------------------------------------------------------- /Editor/EditorExtension.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: cb4c93a860c058840b2662753fb7af1a 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/ProjectSettingsData.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_EDITOR 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using UnityEngine; 6 | 7 | 8 | namespace SatorImaging.UnitySourceGenerator.Editor 9 | { 10 | public class ProjectSettingsData : ProjectSettingsSingleton 11 | { 12 | public void Save() => base.Save(true); 13 | 14 | [SerializeField] public bool AutoEmitOnScriptUpdate = true; 15 | [SerializeField] public List AutoEmitDisabledPaths = new(); 16 | [Range(0, 1920)] 17 | [SerializeField] public int DenseViewWidthThreshold = 512; 18 | 19 | //properties 20 | [HideInInspector][SerializeField] bool _disableAutoReloadInBackground = false; 21 | public bool SuspendAutoReloadWhileEditorInBackground 22 | { 23 | get => _disableAutoReloadInBackground; 24 | set 25 | { 26 | _disableAutoReloadInBackground = value; 27 | EditorEvent.RegisterFocusChangedEvent(value); 28 | if (value) 29 | Debug.Log($"[{nameof(UnitySourceGenerator)}] Auto Reload completely disabled while Unity Editor in Background."); 30 | //else 31 | // Debug.Log($"[{nameof(UnitySourceGenerator)}] Unity Editor event was unregistered."); 32 | } 33 | } 34 | 35 | 36 | // temporary storage between domain reloading. 37 | [HideInInspector][SerializeField] internal List ImportedScriptPaths = new(); 38 | [HideInInspector][SerializeField] internal List PathsToSkipImportEvent = new(); 39 | [HideInInspector][SerializeField] internal List PathsToIgnoreOverwriteSettingOnAttribute = new(); 40 | 41 | } 42 | } 43 | 44 | #endif 45 | -------------------------------------------------------------------------------- /Editor/ProjectSettingsData.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c40623a0469766b4095282517245602d 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/ProjectSettingsPanel.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_EDITOR 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Linq; 7 | using UnityEditor; 8 | using UnityEngine; 9 | using UnityEngine.UIElements; 10 | 11 | 12 | namespace SatorImaging.UnitySourceGenerator.Editor 13 | { 14 | public class ProjectSettingsPanel : SettingsProvider 15 | { 16 | const float PADDING_WIDTH = 4; 17 | const string DISPLAY_NAME = "Alternative Source Generator for Unity"; 18 | 19 | 20 | #region //////// SETTINGS PROVIDER //////// 21 | 22 | [SettingsProvider] 23 | public static SettingsProvider CreateProvider() 24 | { 25 | return new ProjectSettingsPanel("Project/" + DISPLAY_NAME, SettingsScope.Project, null); 26 | } 27 | 28 | public ProjectSettingsPanel(string path, SettingsScope scopes, IEnumerable keywords) 29 | : base(path, scopes, keywords) 30 | { 31 | } 32 | 33 | 34 | Vector2 _scroll; 35 | UnityEditor.Editor _cachedEditor; 36 | 37 | public override void OnActivate(string searchContext, VisualElement rootElement) 38 | { 39 | Wakeup(ref _cachedEditor); 40 | } 41 | 42 | public override void OnGUI(string searchContext) 43 | { 44 | DrawEditor(_cachedEditor, ref _scroll); 45 | } 46 | 47 | public override void OnDeactivate() 48 | { 49 | _settings.Save(); 50 | } 51 | 52 | #endregion 53 | 54 | 55 | #region //////// EDITOR WINDOW //////// 56 | 57 | [MenuItem("Tools/" + DISPLAY_NAME)] 58 | static void ShowWindow() 59 | { 60 | var wnd = EditorWindow.GetWindow("USG"); 61 | wnd.Show(); 62 | } 63 | 64 | public class USGWindow : EditorWindow 65 | { 66 | Vector2 _scroll; 67 | UnityEditor.Editor _cachedEditor; 68 | 69 | void OnEnable() 70 | { 71 | Wakeup(ref _cachedEditor); 72 | } 73 | 74 | void OnGUI() 75 | { 76 | DrawEditor(_cachedEditor, ref _scroll); 77 | } 78 | 79 | void OnLostFocus() => _settings.Save(); 80 | void OnDisable() => _settings.Save(); 81 | } 82 | 83 | 84 | #endregion 85 | 86 | 87 | static ProjectSettingsData _settings = ProjectSettingsData.instance; 88 | static Type _generatorTypeToShowEmittersInGUI = null; 89 | static Type[] _referencingEmittersToShowInGUI = Array.Empty(); 90 | static Type[] _generatorTypes = Array.Empty(); 91 | static bool[] _isGeneratorHasEmitters = Array.Empty(); 92 | static Dictionary _targetClassToScriptFilePath = new(); 93 | static Dictionary _targetClassToOutputFilePaths = new(); 94 | static Dictionary _targetClassToOutputFileNames = new(); 95 | // some GUI classes cannot be accessed on field definition. 96 | static GUIContent gui_emittersBtn; 97 | static GUIContent gui_deleteBtn; 98 | static GUIContent gui_runBtn; 99 | static GUIContent gui_unveilBtn; 100 | static GUIStyle gui_noBGButtonStyle; 101 | static GUIStyle gui_deleteMiniLabel; 102 | static GUIContent gui_suspendAutoReloadLabel = new GUIContent(" Suspend Auto Reload while Unity Editor in Background *experimental"); 103 | static GUIContent gui_autoRunLabel = new GUIContent(" Auto Run Generators on Script Update / Reimport"); 104 | static GUIContent gui_buttonColumnLabel = new GUIContent("On Run"); 105 | static GUIContent gui_refEmittersLabel = new GUIContent("Referencing Emitters"); 106 | static GUIContent gui_multiGeneratorsLabel = new GUIContent("MULTIPLE GENERATORS"); 107 | static GUIContent gui_noSourceGenLabel = new GUIContent("NO SOURCE GENERATORS IN PROJECT"); 108 | static GUIContent gui_debugLabel = new GUIContent("DEBUG"); 109 | static GUILayoutOption gui_toggleWidth = GUILayout.Width(16); 110 | static GUILayoutOption gui_buttonWidth = GUILayout.Width(32); 111 | 112 | // NOTE: class is reference type and reference type variable is "passed by value". 113 | // to take reference to newly created object, need `ref` chain. 114 | static void Wakeup(ref UnityEditor.Editor cachedEditor) 115 | { 116 | gui_emittersBtn ??= new(EditorGUIUtility.IconContent("d_icon dropdown")); 117 | gui_deleteBtn ??= new(EditorGUIUtility.IconContent("d_TreeEditor.Trash")); 118 | gui_runBtn ??= new(EditorGUIUtility.IconContent("PlayButton On")); 119 | gui_unveilBtn ??= new(EditorGUIUtility.IconContent("d_Linked")); 120 | if (gui_noBGButtonStyle == null) 121 | { 122 | gui_noBGButtonStyle = new(EditorStyles.iconButton); 123 | gui_noBGButtonStyle.alignment = TextAnchor.LowerCenter; 124 | gui_noBGButtonStyle.imagePosition = ImagePosition.ImageOnly; 125 | gui_noBGButtonStyle.margin.top = EditorStyles.inspectorDefaultMargins.padding.top; 126 | } 127 | if (gui_deleteMiniLabel == null) 128 | { 129 | gui_deleteMiniLabel = new(EditorStyles.centeredGreyMiniLabel); 130 | gui_deleteMiniLabel.alignment = TextAnchor.MiddleRight; 131 | gui_deleteMiniLabel.fixedHeight = EditorGUIUtility.singleLineHeight; 132 | gui_deleteMiniLabel.padding.top = 1; 133 | } 134 | 135 | _settings = ProjectSettingsData.instance; 136 | _settings.hideFlags = HideFlags.HideAndDontSave & ~HideFlags.NotEditable; 137 | 138 | // caching heavy ops 139 | _targetClassToOutputFilePaths = USGEngine.GeneratorInfoList 140 | .ToLookup(static x => x.TargetClass) 141 | .ToDictionary( 142 | static x => x.Key, 143 | static x => x.Select(static x => USGEngine.GetGeneratorOutputPath(x)).ToArray()); 144 | _targetClassToOutputFileNames = _targetClassToOutputFilePaths 145 | .ToDictionary( 146 | static x => x.Key, 147 | static x => x.Value.Select(static x => Path.GetFileName(x)).ToArray()); 148 | 149 | _generatorTypes = USGEngine.GeneratorInfoList 150 | .Select(static x => x.Attribute.GeneratorClass) 151 | .Union(_targetClassToOutputFileNames 152 | .Where(static x => x.Value.Length > 1) 153 | .Select(static x => x.Key)) 154 | .Distinct() 155 | .OrderBy(static x => x.FullName) 156 | .ToArray(); 157 | _isGeneratorHasEmitters = _generatorTypes 158 | .Select(static x => GetReferencingEmitters(x).Length > 0) 159 | .ToArray(); 160 | _referencingEmittersToShowInGUI = GetReferencingEmitters(null); // not only set variable, but clear ref emitters panel settings. 161 | 162 | _targetClassToScriptFilePath = USGEngine.GeneratorInfoList 163 | .Select(static x => x.TargetClass) 164 | .Union(_generatorTypes) 165 | .ToDictionary( 166 | static x => x, 167 | static x => USGUtility.GetAssetPathByType(x) ?? throw new Exception()); 168 | 169 | UnityEditor.Editor.CreateCachedEditor(_settings, null, ref cachedEditor); 170 | } 171 | 172 | 173 | static bool _debugFoldout; 174 | static void DrawEditor(UnityEditor.Editor cachedEditor, ref Vector2 currentScroll) 175 | { 176 | var restoreLabelWidth = EditorGUIUtility.labelWidth; 177 | EditorGUIUtility.labelWidth = EditorGUIUtility.currentViewWidth * 0.2f; 178 | 179 | currentScroll = EditorGUILayout.BeginScrollView(currentScroll); 180 | { 181 | EditorGUILayout.BeginHorizontal(); 182 | EditorGUILayout.LabelField(GUIContent.none, GUILayout.MaxWidth(PADDING_WIDTH)); 183 | EditorGUILayout.BeginVertical(); 184 | { 185 | EditorGUI.BeginChangeCheck(); 186 | var suspendAutoReload = EditorGUILayout.ToggleLeft(gui_suspendAutoReloadLabel, _settings.SuspendAutoReloadWhileEditorInBackground); 187 | var autoEmit = EditorGUILayout.ToggleLeft(gui_autoRunLabel, _settings.AutoEmitOnScriptUpdate); 188 | if (EditorGUI.EndChangeCheck()) 189 | { 190 | _settings.SuspendAutoReloadWhileEditorInBackground = suspendAutoReload; 191 | _settings.AutoEmitOnScriptUpdate = autoEmit; 192 | _settings.Save(); 193 | } 194 | //EditorGUILayout.Space(); 195 | 196 | EditorGUILayout.LabelField(gui_buttonColumnLabel, EditorStyles.miniLabel); 197 | if (_generatorTypes.Length == 0) 198 | EditorGUILayout.LabelField(gui_noSourceGenLabel); 199 | else 200 | for (int i = 0; i < _generatorTypes.Length; i++) 201 | { 202 | DrawGenerator(_generatorTypes[i], _isGeneratorHasEmitters[i]); 203 | } 204 | EditorGUILayout.Space(); 205 | 206 | if (_generatorTypeToShowEmittersInGUI != null) 207 | { 208 | EditorGUILayout.LabelField(gui_refEmittersLabel, EditorStyles.largeLabel); 209 | for (int i = 0; i < _referencingEmittersToShowInGUI.Length; i++) 210 | { 211 | DrawGenerator(_referencingEmittersToShowInGUI[i], false); 212 | } 213 | } 214 | EditorGUILayout.Space(); 215 | 216 | GUILayout.FlexibleSpace(); 217 | _debugFoldout = EditorGUILayout.Foldout(_debugFoldout, gui_debugLabel, true); 218 | if (_debugFoldout) 219 | { 220 | cachedEditor.OnInspectorGUI(); 221 | } 222 | 223 | 224 | EditorGUILayout.Space(); 225 | } 226 | EditorGUILayout.EndVertical(); 227 | EditorGUILayout.EndHorizontal(); 228 | } 229 | EditorGUILayout.EndScrollView(); 230 | 231 | EditorGUIUtility.labelWidth = restoreLabelWidth; 232 | } 233 | 234 | 235 | static void DrawGenerator(Type t, bool showEmitterBtn)//string filePath, bool showEmitterBtn) 236 | { 237 | EditorGUILayout.BeginHorizontal(); 238 | { 239 | // NOTE: USGUtility functions are heavy for use in GUI loops, cache it!! 240 | var filePath = _targetClassToScriptFilePath[t]; 241 | 242 | EditorGUI.BeginChangeCheck(); 243 | var isOn = EditorGUILayout.Toggle(!_settings.AutoEmitDisabledPaths.Contains(filePath), gui_toggleWidth); 244 | if (EditorGUI.EndChangeCheck()) 245 | { 246 | if (isOn) 247 | _settings.AutoEmitDisabledPaths.TryRemove(filePath); 248 | else 249 | _settings.AutoEmitDisabledPaths.TryAddUnique(filePath); 250 | 251 | _settings.Save(); 252 | } 253 | 254 | //run 255 | if (GUILayout.Button(gui_runBtn, gui_buttonWidth)) 256 | { 257 | Debug.Log($"[{nameof(UnitySourceGenerator)}] Generator running: {t.FullName}"); 258 | USGUtility.ForceGenerateByType(t, false); 259 | } 260 | 261 | //label 262 | if (EditorGUILayout.LinkButton(t.Name)) 263 | { 264 | EditorGUIUtility.PingObject(AssetDatabase.LoadAssetAtPath(filePath)); 265 | } 266 | 267 | //emitters?? 268 | if (showEmitterBtn) 269 | { 270 | if (GUILayout.Button(gui_emittersBtn, gui_noBGButtonStyle)) 271 | { 272 | _referencingEmittersToShowInGUI = GetReferencingEmitters(t); 273 | } 274 | } 275 | //unveil?? 276 | else 277 | { 278 | if (_targetClassToOutputFilePaths.ContainsKey(t)) 279 | for (int i = 0; i < _targetClassToOutputFilePaths[t].Length; i++) 280 | { 281 | if (GUILayout.Button(gui_unveilBtn, gui_noBGButtonStyle)) 282 | { 283 | EditorGUIUtility.PingObject( 284 | AssetDatabase.LoadAssetAtPath(_targetClassToOutputFilePaths[t][i])); 285 | } 286 | } 287 | } 288 | 289 | //deleteBtn 290 | if (_targetClassToOutputFilePaths.ContainsKey(t)) 291 | { 292 | if (_targetClassToOutputFilePaths[t].Length == 1) 293 | { 294 | GUILayout.FlexibleSpace(); 295 | if (EditorGUIUtility.currentViewWidth > _settings.DenseViewWidthThreshold) 296 | GUILayout.Label(_targetClassToOutputFileNames[t][0], gui_deleteMiniLabel); 297 | } 298 | else 299 | { 300 | GUILayout.FlexibleSpace(); 301 | //if (EditorGUIUtility.currentViewWidth > _settings.DenseViewWidthThreshold) 302 | // GUILayout.Label(gui_multiGeneratorsLabel, gui_deleteMiniLabel); 303 | } 304 | 305 | for (int i = 0; i < _targetClassToOutputFilePaths[t].Length; i++) 306 | { 307 | if (GUILayout.Button(gui_deleteBtn)) 308 | { 309 | if (File.Exists(_targetClassToOutputFilePaths[t][i]) 310 | && EditorUtility.DisplayDialog( 311 | nameof(UnitySourceGenerator), 312 | $"Would you like to delete emitted file?\n" + 313 | $"- {_targetClassToOutputFileNames[t][i]}\n" + 314 | $"\n" + 315 | $"File Path: {_targetClassToOutputFilePaths[t][i]}", 316 | "Yes", "cancel")) 317 | { 318 | File.Delete(_targetClassToOutputFilePaths[t][i]); 319 | Debug.Log($"[{nameof(UnitySourceGenerator)}] File is deleted: {_targetClassToOutputFilePaths[t][i]}"); 320 | AssetDatabase.Refresh(); 321 | } 322 | } 323 | } 324 | } 325 | } 326 | EditorGUILayout.EndHorizontal(); 327 | } 328 | 329 | 330 | static Type[] GetReferencingEmitters(Type t) 331 | { 332 | _generatorTypeToShowEmittersInGUI = t; 333 | if (t == null) 334 | return Array.Empty(); 335 | 336 | // NOTE: self-emit generator can have other target. 337 | var ret = USGEngine.GeneratorInfoList 338 | .Where(x => x.Attribute.GeneratorClass == t) 339 | .Select(static x => x.TargetClass) 340 | .ToArray(); 341 | 342 | if (ret.Length == 1 && ret[0] == t) 343 | return Array.Empty(); 344 | 345 | return ret; 346 | } 347 | 348 | 349 | } 350 | } 351 | 352 | #endif 353 | -------------------------------------------------------------------------------- /Editor/ProjectSettingsPanel.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6d51516eac38574478f621d9e326d8d0 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/ProjectSettingsSingleton.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_EDITOR 2 | 3 | using System.IO; 4 | using UnityEditorInternal; 5 | using UnityEngine; 6 | 7 | 8 | namespace SatorImaging.UnitySourceGenerator.Editor 9 | { 10 | ///Polyfill for use in Unity 2019. 11 | ///Output path is hard-coded. 12 | public abstract class ProjectSettingsSingleton : ScriptableObject 13 | where T : ScriptableObject 14 | { 15 | private static string _path = null; 16 | protected static string GetFilePath() 17 | => _path ?? (_path = Application.dataPath + "/../ProjectSettings/" + typeof(T).FullName + ".asset"); 18 | 19 | 20 | #region https://github.com/Unity-Technologies/UnityCsReference/blob/master/Editor/Mono/ScriptableSingleton.cs 21 | 22 | static T s_Instance; 23 | 24 | public static T instance 25 | { 26 | get 27 | { 28 | if (s_Instance == null) 29 | CreateAndLoad(); 30 | 31 | return s_Instance; 32 | } 33 | } 34 | 35 | // On domain reload ScriptableObject objects gets reconstructed from a backup. We therefore set the s_Instance here 36 | protected ProjectSettingsSingleton() 37 | { 38 | if (s_Instance != null) 39 | { 40 | Debug.LogError("ScriptableSingleton already exists. Did you query the singleton in a constructor?"); 41 | } 42 | else 43 | { 44 | object casted = this; 45 | s_Instance = casted as T; 46 | System.Diagnostics.Debug.Assert(s_Instance != null); 47 | } 48 | } 49 | 50 | private static void CreateAndLoad() 51 | { 52 | System.Diagnostics.Debug.Assert(s_Instance == null); 53 | 54 | // Load 55 | string filePath = GetFilePath(); 56 | if (!string.IsNullOrEmpty(filePath)) 57 | { 58 | // If a file exists then load it and deserialize it. 59 | // This creates an instance of T which will set s_Instance in the constructor. Then it will deserialize it and call relevant serialization callbacks. 60 | InternalEditorUtility.LoadSerializedFileAndForget(filePath); 61 | } 62 | 63 | if (s_Instance == null) 64 | { 65 | // Create 66 | T t = CreateInstance(); 67 | t.hideFlags = HideFlags.HideAndDontSave; 68 | } 69 | 70 | System.Diagnostics.Debug.Assert(s_Instance != null); 71 | } 72 | 73 | protected virtual void Save(bool saveAsText) 74 | { 75 | if (s_Instance == null) 76 | { 77 | Debug.LogError("Cannot save ScriptableSingleton: no instance!"); 78 | return; 79 | } 80 | 81 | string filePath = GetFilePath(); 82 | if (!string.IsNullOrEmpty(filePath)) 83 | { 84 | string folderPath = Path.GetDirectoryName(filePath); 85 | if (!Directory.Exists(folderPath)) 86 | Directory.CreateDirectory(folderPath); 87 | 88 | InternalEditorUtility.SaveToSerializedFileAndForget(new[] { s_Instance }, filePath, saveAsText); 89 | } 90 | else 91 | { 92 | Debug.LogWarning($"Saving has no effect. Your class '{GetType()}' is missing the FilePathAttribute. Use this attribute to specify where to save your ScriptableSingleton.\nOnly call Save() and use this attribute if you want your state to survive between sessions of Unity."); 93 | } 94 | } 95 | 96 | #endregion 97 | 98 | 99 | } 100 | 101 | 102 | } 103 | 104 | #endif 105 | -------------------------------------------------------------------------------- /Editor/ProjectSettingsSingleton.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 584776981c0006045b1bbb060608a851 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/USGEngine.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_EDITOR 2 | 3 | using System; 4 | using System.Buffers; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Reflection; 9 | using System.Text; 10 | using UnityEditor; 11 | using UnityEditor.Compilation; 12 | using UnityEngine; 13 | 14 | namespace SatorImaging.UnitySourceGenerator.Editor 15 | { 16 | /// 17 | /// > [!WARNING] 18 | /// > Works only on Unity Editor 19 | /// 20 | public sealed class USGEngine : AssetPostprocessor 21 | { 22 | const int BUFFER_LENGTH = 1024 * 64; 23 | const int BUFFER_MAX_CHAR_LENGTH = BUFFER_LENGTH / 3; // worst case of UTF-8 24 | const string GENERATOR_PREFIX = "."; 25 | const string GENERATOR_EXT = ".g"; 26 | const string GENERATOR_DIR = @"/USG.g"; // don't append last slash. used to determine file is generated one or not. 27 | const string ASSETS_DIR_NAME = "Assets"; 28 | const string ASSETS_DIR_SLASH = ASSETS_DIR_NAME + "/"; 29 | const string TARGET_FILE_EXT = @".cs"; 30 | const string PATH_PREFIX_TO_IGNORE = @"Packages/"; 31 | private const string METHOD_NAME_OUTPUT_FILE_NAME = "OutputFileName"; 32 | private const string METHOD_NAME_EMIT = "Emit"; 33 | readonly static char[] DIR_SEPARATORS = new char[] { '\\', '/' }; 34 | 35 | 36 | static bool IsAppropriateTarget(string filePath) 37 | { 38 | if (!filePath.EndsWith(TARGET_FILE_EXT, StringComparison.OrdinalIgnoreCase) || 39 | !filePath.StartsWith(ASSETS_DIR_SLASH, StringComparison.OrdinalIgnoreCase)) 40 | { 41 | return false; 42 | } 43 | return true; 44 | } 45 | 46 | 47 | static readonly ProjectSettingsData _settings = ProjectSettingsData.instance; 48 | 49 | static void OnPostprocessAllAssets( 50 | string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths) 51 | { 52 | // NOTE: Do NOT handle deleted assets because Unity tracking changes perfectly. 53 | // Even if delete file while Unity shutted down, asset deletion event happens on next Unity launch. 54 | // As a result, delete/import event loops infinitely and file cannot be deleted. 55 | // NOTE: [DidReloadScripts] is executed before AssetPostprocessor, cannot be used. 56 | 57 | // TODO: Unity sometimes reloads scripts in background automatically. 58 | // (it happens when Save All command was done in Visual Studio, for example.) 59 | // In this situation, code generation will be done with script data right before saving. 60 | // so that generated code is not what expected, and this behaviour cannot be solved on C#. 61 | // Using [DidReloadScripts] or EditorApplication.delayCall, It works fine with Reimport 62 | // menu command but OnPostprocessAllAssets event doesn't work as expected. 63 | // (script runs with static field cleared even though .Clear() is only in ProcessingFiles(). 64 | // it's weird that event happens and asset paths retrieved but hashset items gone.) 65 | // --> https://docs.unity3d.com/2021.3/Documentation/Manual/DomainReloading.html 66 | 67 | if (!_settings.AutoEmitOnScriptUpdate) 68 | return; 69 | 70 | // NOTE: Use project-wide ScriptableObject as a temporary storage. 71 | for (int i = 0; i < importedAssets.Length; i++) 72 | { 73 | if (_settings.PathsToSkipImportEvent.TryRemove(importedAssets[i])) 74 | continue; 75 | if (_settings.AutoEmitDisabledPaths.Contains(importedAssets[i], StringComparer.Ordinal)) 76 | continue; 77 | if (!IsAppropriateTarget(importedAssets[i])) 78 | continue; 79 | 80 | _settings.ImportedScriptPaths.TryAddUnique(importedAssets[i]); 81 | } 82 | _settings.PathsToSkipImportEvent.Clear(); 83 | _settings.Save(); 84 | 85 | // NOTE: processing files are done in CompilationPipeline callback. 86 | } 87 | 88 | 89 | // NOTE: event registration is done in InitializeOnLoad 90 | static void OnCompilationFinished(object context) 91 | { 92 | if (!_settings.AutoEmitOnScriptUpdate) 93 | return; 94 | 95 | try 96 | { 97 | RunGenerators(_settings.ImportedScriptPaths.ToArray());//, false); 98 | } 99 | catch 100 | { 101 | _settings.ImportedScriptPaths.Clear(); 102 | _settings.Save(); 103 | throw; 104 | } 105 | } 106 | 107 | 108 | static bool RunGenerators(string[] targetPaths) 109 | { 110 | bool somethingUpdated = false; 111 | try 112 | { 113 | var pathsToImportSet = new HashSet(); 114 | 115 | for (int i = 0; i < targetPaths.Length; i++) 116 | { 117 | if (!TryGetTargetOrGeneratorClassByPath(targetPaths[i], out var targetOrGeneratorCls)) 118 | continue; 119 | 120 | // NOTE: need to search both target and generator 121 | foreach (var info in _generatorInfoList 122 | .Where(x => x.TargetClass == targetOrGeneratorCls || x.Attribute.GeneratorClass == targetOrGeneratorCls)) 123 | { 124 | if (TryEmit(info)) 125 | { 126 | somethingUpdated = true; 127 | pathsToImportSet.Add(GetGeneratorOutputPath(info)); 128 | } 129 | } 130 | } 131 | 132 | //import 133 | if (!BuildPipeline.isBuildingPlayer) 134 | { 135 | foreach (var path in pathsToImportSet) 136 | { 137 | _settings.PathsToSkipImportEvent.TryAddUnique(path); 138 | AssetDatabase.ImportAsset(path); 139 | somethingUpdated = true; 140 | } 141 | 142 | if (somethingUpdated) 143 | //// need a delay? 144 | //EditorApplication.delayCall += () => 145 | AssetDatabase.Refresh(); 146 | } 147 | } 148 | finally 149 | { 150 | for (int i = 0; i < targetPaths.Length; i++) 151 | { 152 | _settings.PathsToIgnoreOverwriteSettingOnAttribute.TryRemove(targetPaths[i]); 153 | } 154 | } 155 | 156 | return somethingUpdated; 157 | } 158 | 159 | 160 | static bool TryGetTargetOrGeneratorClassByPath(string assetsRelPath, out Type targetOrGeneratorCls) 161 | { 162 | if (!File.Exists(assetsRelPath)) 163 | throw new FileNotFoundException(assetsRelPath); 164 | 165 | targetOrGeneratorCls = null; 166 | 167 | var generatorClsName = Path.GetFileNameWithoutExtension(assetsRelPath); 168 | 169 | // NOTE: File naming convention 170 | // Emitter: ...g.cs 171 | // SelfGen: ..g.cs 172 | if (!generatorClsName.EndsWith(GENERATOR_EXT, StringComparison.OrdinalIgnoreCase)) 173 | { 174 | if (AssetDatabase.LoadAssetAtPath(assetsRelPath) is not MonoScript mono) 175 | throw new NotSupportedException("path is not script file: " + assetsRelPath); 176 | 177 | targetOrGeneratorCls = mono.GetClass(); 178 | } 179 | else // try find generator for .g.cs file 180 | { 181 | // NOTE: When generated code has error, fix it and save will invoke Unity 182 | // import event and then same error code will be generated again. 183 | // Emit from generated file should only work when forced. 184 | // (delaying code generation won't solve this behaviour...? return anyway) 185 | if (!_settings.PathsToIgnoreOverwriteSettingOnAttribute.Contains(assetsRelPath, StringComparer.Ordinal)) 186 | return false; 187 | 188 | generatorClsName = Path.GetFileNameWithoutExtension(generatorClsName); 189 | generatorClsName = Path.GetExtension(generatorClsName); 190 | if (generatorClsName.Length == 0) 191 | return false; 192 | generatorClsName = generatorClsName.Substring(1); 193 | 194 | var found = _generatorInfoList.FirstOrDefault(x => x.Attribute.GeneratorClass.Name == generatorClsName); 195 | if (found == default) 196 | return false; 197 | 198 | targetOrGeneratorCls = found.Attribute.GeneratorClass; 199 | } 200 | 201 | return true; 202 | } 203 | 204 | 205 | static bool TryEmit(CachedGeneratorInfo info) 206 | { 207 | var assetsRelPath = USGUtility.GetAssetPathByType(info.TargetClass); 208 | if (assetsRelPath == null) 209 | throw new FileNotFoundException("target class not found."); 210 | 211 | var generatorCls = info.Attribute.GeneratorClass; 212 | string outputPath = GetGeneratorOutputPath(info); 213 | 214 | var context = new USGContext 215 | { 216 | TargetClass = info.TargetClass, 217 | AssetPath = assetsRelPath.Replace('\\', '/'), 218 | OutputPath = outputPath.Replace('\\', '/'), 219 | }; 220 | 221 | var sb = new StringBuilder(); 222 | sb.AppendLine($"// {generatorCls.Name}"); 223 | 224 | var isSaveFile = false; 225 | try 226 | { 227 | isSaveFile = (bool)info.EmitMethod.Invoke(null, new object[] { context, sb }); 228 | } 229 | catch 230 | { 231 | //Debug.LogError($"[{nameof(UnitySourceGenerator)}] Unhandled Error on Emit(): {generatorCls}"); 232 | throw; 233 | } 234 | 235 | //save?? 236 | if (!isSaveFile || sb == null || string.IsNullOrWhiteSpace(context.OutputPath)) 237 | return false; 238 | 239 | // NOTE: overwrite check must be done after Emit() due to allowing output path modification. 240 | // TODO: code generation happens but file is not written when overwrite is disabled. 241 | // any way to skip code generation? 242 | if (File.Exists(context.OutputPath)) 243 | { 244 | if (!info.Attribute.OverwriteIfFileExists && 245 | !_settings.PathsToIgnoreOverwriteSettingOnAttribute.Contains(assetsRelPath, StringComparer.Ordinal)) 246 | return false; 247 | } 248 | 249 | 250 | var outputDir = Path.GetDirectoryName(context.OutputPath); 251 | if (!Directory.Exists(outputDir)) 252 | Directory.CreateDirectory(outputDir); 253 | 254 | #if UNITY_2021_3_OR_NEWER 255 | 256 | // OPTIMIZE: use sb.GetChunks() in future release of Unity. 2021 LTS doesn't support it. 257 | using (var fs = new FileStream(context.OutputPath, FileMode.Create, FileAccess.Write)) 258 | { 259 | //Span buffer = stackalloc byte[BUFFER_LENGTH]; 260 | var rentalBuffer = ArrayPool.Shared.Rent(BUFFER_LENGTH); 261 | try 262 | { 263 | Span buffer = rentalBuffer; 264 | var span = sb.ToString().AsSpan(); 265 | int len, written; 266 | for (int start = 0; start < span.Length; start += BUFFER_MAX_CHAR_LENGTH) 267 | { 268 | len = BUFFER_MAX_CHAR_LENGTH; 269 | if (len + start > span.Length) 270 | len = span.Length - start; 271 | 272 | written = info.Attribute.OutputFileEncoding.GetBytes(span.Slice(start, len), buffer); 273 | fs.Write(buffer.Slice(0, written)); 274 | } 275 | fs.Flush(); 276 | } 277 | finally 278 | { 279 | ArrayPool.Shared.Return(rentalBuffer); 280 | } 281 | } 282 | 283 | #else 284 | File.WriteAllText(context.OutputPath, sb.ToString(), info.Attribute.OutputFileEncoding); 285 | #endif 286 | 287 | Debug.Log($"[{nameof(UnitySourceGenerator)}] Generated: {context.OutputPath}", 288 | AssetDatabase.LoadAssetAtPath(context.OutputPath)); 289 | return true; 290 | } 291 | 292 | 293 | /* entry ================================================================ */ 294 | 295 | ///Run specified generator upon request. Designed for use in Unity build event. 296 | ///Path need to be started with "Assets/" 297 | ///Set true to run referencing emitters immediately. For use in build event. 298 | ///true if file is written 299 | [Obsolete("use USGUtility.ForceGenerateByType() instead.")] 300 | public static bool ProcessFile(string assetsRelPath, bool ignoreOverwriteSettingOnAttribute 301 | /* TODO: remove for future release */ 302 | , bool autoRunReferencingEmittersNow = false) 303 | => Process(new string[] { assetsRelPath }, ignoreOverwriteSettingOnAttribute); 304 | 305 | 306 | internal static bool Process(string[] assetsRelPaths, bool ignoreOverwriteSettingOnAttribute) 307 | { 308 | if (ignoreOverwriteSettingOnAttribute) 309 | { 310 | for (int i = 0; i < assetsRelPaths.Length; i++) 311 | _settings.PathsToIgnoreOverwriteSettingOnAttribute.TryAddUnique(assetsRelPaths[i]); 312 | } 313 | return RunGenerators(assetsRelPaths); 314 | } 315 | 316 | 317 | /* utility ================================================================ */ 318 | 319 | ///throw if failed. 320 | internal static string GetGeneratorOutputPath(CachedGeneratorInfo info) 321 | { 322 | var fileName = info.OutputFileName; 323 | if (fileName == null || fileName.Length == 0) 324 | throw new Exception("cannot retrieve output path."); 325 | 326 | string dirPath = USGUtility.GetAssetPathByType(info.TargetClass); 327 | if (dirPath == null) 328 | throw new FileNotFoundException("generator script file is not found."); 329 | 330 | dirPath = Path.GetDirectoryName(dirPath).Replace('\\', '/'); 331 | if (!dirPath.EndsWith(GENERATOR_DIR, StringComparison.OrdinalIgnoreCase)) 332 | dirPath += GENERATOR_DIR; 333 | 334 | return dirPath + '/' + fileName; 335 | } 336 | 337 | 338 | /* typedef ================================================================ */ 339 | 340 | internal sealed class CachedGeneratorInfo 341 | { 342 | public Type TargetClass { get; internal init; } 343 | public UnitySourceGeneratorAttribute Attribute { get; internal init; } 344 | 345 | public string OutputFileName { get; internal set; } 346 | public MethodInfo EmitMethod { get; internal set; } 347 | public MethodInfo OutputFileNameMethod { get; internal set; } 348 | } 349 | 350 | 351 | /* initialize ================================================================ */ 352 | 353 | static readonly BindingFlags METHOD_FLAGS 354 | = BindingFlags.NonPublic 355 | | BindingFlags.Public 356 | | BindingFlags.Static 357 | ; 358 | 359 | 360 | readonly static List _generatorInfoList = new(); 361 | internal static IReadOnlyList GeneratorInfoList => _generatorInfoList; 362 | 363 | 364 | [InitializeOnLoadMethod] 365 | static void InitializeOnLoad() 366 | { 367 | CompilationPipeline.compilationFinished -= OnCompilationFinished; 368 | CompilationPipeline.compilationFinished += OnCompilationFinished; 369 | 370 | 371 | // fantastic UnityEditor.TypeCache system!! 372 | var generatorInfos = TypeCache.GetTypesWithAttribute() 373 | .SelectMany(static t => 374 | { 375 | var attrs = t.GetCustomAttributes(false); 376 | var ret = new CachedGeneratorInfo[attrs.Count()]; 377 | for (int i = 0; i < ret.Length; i++) 378 | { 379 | ret[i] = new CachedGeneratorInfo 380 | { 381 | TargetClass = t, 382 | Attribute = attrs.ElementAt(i), 383 | }; 384 | } 385 | return ret; 386 | }) 387 | ; 388 | 389 | 390 | foreach (var generatorInfo in generatorInfos) 391 | { 392 | // NOTE: self-emit generators which initialized without generator type parameter, 393 | // need to fill it correctly. 394 | generatorInfo.Attribute._generatorClass ??= generatorInfo.TargetClass; 395 | 396 | var generatorCls = generatorInfo.Attribute.GeneratorClass; 397 | var outputMethod = generatorCls.GetMethod(METHOD_NAME_OUTPUT_FILE_NAME, METHOD_FLAGS, null, Type.EmptyTypes, null); 398 | var emitMethod = generatorCls.GetMethod(METHOD_NAME_EMIT, METHOD_FLAGS, null, new Type[] { typeof(USGContext), typeof(StringBuilder) }, null); 399 | 400 | if (outputMethod == null || emitMethod == null) 401 | { 402 | Debug.LogError($"[{nameof(UnitySourceGenerator)}] Required static method(s) not found: {generatorCls}"); 403 | continue; 404 | } 405 | 406 | generatorInfo.EmitMethod = emitMethod; 407 | generatorInfo.OutputFileNameMethod = outputMethod; 408 | 409 | //filename?? 410 | if (!TryBuildOutputFileName(generatorInfo)) 411 | { 412 | Debug.LogError($"[{nameof(UnitySourceGenerator)}] Output file name is invalid: {generatorInfo.OutputFileName}"); 413 | continue; 414 | } 415 | 416 | //register!! 417 | _generatorInfoList.Add(generatorInfo); 418 | } 419 | } 420 | 421 | 422 | static bool TryBuildOutputFileName(CachedGeneratorInfo info) 423 | { 424 | info.OutputFileName = (string)info.OutputFileNameMethod?.Invoke(null, null); 425 | if (string.IsNullOrWhiteSpace(info.OutputFileName)) 426 | return false; 427 | 428 | string fileName = Path.GetFileNameWithoutExtension(info.OutputFileName); 429 | string fileExt = Path.GetExtension(info.OutputFileName); 430 | info.OutputFileName = fileName + GENERATOR_PREFIX + info.TargetClass.Name; 431 | if (info.Attribute.GeneratorClass != info.TargetClass) 432 | info.OutputFileName += GENERATOR_PREFIX + info.Attribute.GeneratorClass.Name; 433 | info.OutputFileName += GENERATOR_EXT + fileExt; 434 | 435 | return true; 436 | } 437 | 438 | 439 | } 440 | } 441 | #endif 442 | -------------------------------------------------------------------------------- /Editor/USGEngine.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7c75e7a7663a43f42ac5302de2272f2d 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/USGUtility.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_EDITOR 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Runtime.CompilerServices; 8 | using UnityEditor; 9 | 10 | namespace SatorImaging.UnitySourceGenerator.Editor 11 | { 12 | /// 13 | /// > [!WARNING] 14 | /// > Works only on Unity Editor 15 | /// 16 | public static class USGUtility 17 | { 18 | const string SEARCH_FILTER = "t:" + nameof(MonoScript) + " "; 19 | 20 | 21 | // NOTE: AssetDatabase.FindAssets() returns many of partial name matches and deep type checking on them 22 | // is so slow. as a result, ProjectSettingsPanel initialization takes a while to launch even if 23 | // only a dozen generators and tagets in the project. 24 | // caching will significantly gain the query performance. 25 | readonly static Dictionary _typeToAssetPath = new(); 26 | 27 | ///Returns "Assets/" or "Packages/" starting path to the script. (relative path from Unity project directory) 28 | ///null if not found 29 | public static string GetAssetPathByType(Type t) 30 | { 31 | if (_typeToAssetPath.ContainsKey(t)) 32 | return _typeToAssetPath[t]; 33 | 34 | var GUIDs = AssetDatabase.FindAssets(SEARCH_FILTER + t.Name); 35 | foreach (var GUID in GUIDs) 36 | { 37 | var path = AssetDatabase.GUIDToAssetPath(GUID); 38 | if (AssetDatabase.LoadAssetAtPath(path) is not MonoScript mono 39 | || mono.GetClass() != t) 40 | continue; 41 | 42 | _typeToAssetPath.Add(t, path); 43 | return path; 44 | } 45 | return null; 46 | } 47 | 48 | ///Force perform source code generation by class name. 49 | ///works only when Unity is not building app. 50 | public static void ForceGenerateByType(Type t, bool showInProjectPanel = false) 51 | { 52 | var path = GetAssetPathByType(t); 53 | if (path == null) 54 | return; 55 | 56 | ForceGenerate(path, t, showInProjectPanel); 57 | } 58 | 59 | 60 | static void ForceGenerate(string path, Type t, bool showInProjectPanel) 61 | { 62 | USGEngine.Process(new string[] { path }, true); 63 | 64 | if (BuildPipeline.isBuildingPlayer) 65 | return; 66 | 67 | AssetDatabase.Refresh(); 68 | 69 | if (showInProjectPanel) 70 | { 71 | if (t == null && AssetDatabase.LoadAssetAtPath(path) is MonoScript mono) 72 | t = mono.GetClass(); 73 | 74 | if (t != null) 75 | { 76 | var infos = USGEngine.GeneratorInfoList.Where(x => x.TargetClass == t); 77 | 78 | if (infos.Count() > 1) 79 | { 80 | UnityEngine.Debug.Log($"[{nameof(UnitySourceGenerator)}] there are multiple output files: " 81 | + string.Join(", ", infos.Select(static x => x.OutputFileName))); 82 | } 83 | 84 | if (infos.Count() > 0) 85 | path = USGEngine.GetGeneratorOutputPath(infos.ElementAt(0)); 86 | } 87 | 88 | EditorGUIUtility.PingObject(AssetDatabase.LoadAssetAtPath(path)); 89 | } 90 | } 91 | 92 | 93 | /* internal ---------------------------------------------------------------------- */ 94 | 95 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 96 | internal static bool TryRemove(this List list, T val) 97 | where T : class 98 | { 99 | if (val is null || !list.Contains(val)) 100 | return false; 101 | 102 | do 103 | { 104 | list.Remove(val); 105 | } 106 | while (list.Contains(val)); 107 | 108 | return true; 109 | } 110 | 111 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 112 | internal static bool TryAddUnique(this List list, T val) 113 | where T : class 114 | { 115 | if (val is null || list.Contains(val)) 116 | return false; 117 | 118 | list.Add(val); 119 | return true; 120 | } 121 | 122 | 123 | /* obsolete ================================================================ */ 124 | 125 | ///Force perform source code generation by class name. 126 | ///works only when Unity is not building app. 127 | [Obsolete("use ForceGenerateByType() instead.")] 128 | public static void ForceGenerateByName(string clsName, bool showInProjectPanel = false) 129 | { 130 | var path = GetAssetPathByName(clsName); 131 | if (path == null) 132 | return; 133 | 134 | ForceGenerate(path, null, showInProjectPanel); 135 | } 136 | 137 | ///Returns "Assets/" or "Packages/" starting path to the script. (relative path from Unity project directory) 138 | ///null if not found 139 | [Obsolete("use GetAssetPathByType() instead.")] 140 | public static string GetAssetPathByName(string clsName) 141 | { 142 | var GUIDs = AssetDatabase.FindAssets(SEARCH_FILTER + clsName); 143 | foreach (var GUID in GUIDs) 144 | { 145 | var path = AssetDatabase.GUIDToAssetPath(GUID); 146 | if (Path.GetFileNameWithoutExtension(path) != clsName) 147 | continue; 148 | 149 | return path; 150 | } 151 | return null; 152 | } 153 | 154 | } 155 | } 156 | #endif 157 | -------------------------------------------------------------------------------- /Editor/USGUtility.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 485a07dee7de93a439f0427e7d87b328 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 sator-imaging 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSE.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d16aabd7ed1eee045b50effaf45c0d39 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Alternative Source Generator for Unity 3 | ====================================== 4 | 5 | Alternative Source Generator is built on the Unity native functions. 6 | 7 | - ✅ Unity Package Manager Support 8 | - ✅ No Complicated IDE Environment Setup 9 | - ✅ No Additional Package Installation 10 | 11 | 12 | As you already know, Roslyn's source generator is too sophisticated. This framework provides more simple, ease of use and good enough functions for source code generation. 13 | 14 | 15 |

日本語 / JA 16 | 17 | 超簡単に使える Unity 向けソースジェネレーターです。 18 | 19 |

20 | 21 | 22 | - [🎉 USG Control Centre *New*](#usg-control-panel--window) 23 | ![](https://dl.dropbox.com/scl/fi/jijclnarrruxdt590vss1/USG_Panel.png?rlkey=k44lc9swk0mmui849ck7tappk&dl=0) 24 | - [🛠 Breaking Changes](CHANGELOG.md) 25 | - [✅ TODO](#todo) 26 | 27 | 28 |

📃 Table of Contents 29 | 30 | - [How to Use](#how-to-use) 31 | - [Method Generator](#method-generator) 32 | - [How to Generate Source Code](#how-to-generate-source-code) 33 | - [Result](#result) 34 | - [Self-Emit Generator](#self-emit-generator) 35 | - [Result](#result-1) 36 | - [Output Directory and File Name](#output-directory-and-file-name) 37 | - [Coding Goodies](#coding-goodies) 38 | - [Samples](#samples) 39 | - [SceneBuildIndexGenerator](#scenebuildindexgenerator) 40 | - [Utility Functions for Build Event](#utility-functions-for-build-event) 41 | - [Technical Notes](#technical-notes) 42 | - [Naming Convention](#naming-convention) 43 | - [`` Tag](#auto-generated-tag) 44 | - [Unity Editor Integration](#unity-editor-integration) 45 | - [Installation](#installation) 46 | - [USG Control Panel \& Window](#usg-control-panel--window) 47 | - [Context Menu](#context-menu) 48 | - [Troubleshooting](#troubleshooting) 49 | - [Copyright](#copyright) 50 | - [License](#license) 51 | - [Devnote](#devnote) 52 | - [TODO](#todo) 53 | 54 |

55 | 56 | 57 | 58 | 59 | 60 | How to Use 61 | ========== 62 | 63 | Here is minimal implementation of source generator. 64 | 65 | See [API Reference](https://sator-imaging.github.io/Unity-AltSourceGenerator/) for further details. 66 | 67 | 68 |

日本語 / JA 69 | 70 | 最小限のソースジェネレーターの構成はこちら。`StringBuilder` が渡されるので書き込んで `true` を返せば `context.OutputPath` に内容を書き込みます。`false` を返せば書き込みを中止できます。 71 | 72 |

73 | 74 | 75 | 76 | ## Method Generator 77 | 78 | This example will add `Panic()` method to target class. 79 | 80 |

日本語 / JA 81 | 82 | ターゲットのクラスに `Panic()` メソッドを追加するサンプル。 83 | 84 |

85 | 86 | 87 | ```csharp 88 | public class PanicMethodGenerator 89 | { 90 | static string OutputFileName() => "PanicMethod.cs"; // -> PanicMethod...g.cs 91 | 92 | static bool Emit(USGContext context, StringBuilder sb) 93 | { 94 | if (!context.TargetClass.IsClass || context.TargetClass.IsAbstract) 95 | return false; // return false to tell USG doesn't write file. 96 | 97 | // code generation 98 | sb.Append($@" 99 | namespace {context.TargetClass.Namespace} 100 | {{ 101 | internal partial class {context.TargetClass.Name} 102 | {{ 103 | public void Panic() => throw new System.Exception(); 104 | }} 105 | }} 106 | "); 107 | return true; 108 | } 109 | } 110 | ``` 111 | 112 | 113 | 114 | ### How to Generate Source Code 115 | 116 | 117 | ```csharp 118 | using SatorImaging.UnitySourceGenerator; 119 | 120 | namespace Sample 121 | { 122 | // Add attribute to target class to use method generator. 123 | // Note that class must be defined as partial class. 124 | [UnitySourceGenerator(typeof(PanicMethodGenerator), OverwriteIfFileExists = false)] 125 | internal partial class MethodGeneratorSample 126 | { 127 | } 128 | 129 | } 130 | ``` 131 | 132 | ### Result 133 | 134 | Generated code looks like this. 135 | 136 | ```csharp 137 | // PanicMethodGenerator 138 | 139 | namespace Sample 140 | { 141 | internal partial class MethodGeneratorSample 142 | { 143 | public void Panic() => throw new System.Exception(); 144 | } 145 | } 146 | ``` 147 | 148 | 149 | 150 | ## Self-Emit Generator 151 | 152 | Here is target-less, self-emitting generator example. 153 | 154 | It is useful to generate static database that cannot be generated on Unity runtime. For example, build asset GUIDs database using `UnityEditor.AssetDatabase`, resource integrity tables, etc. 155 | 156 | 157 | 158 | 159 | ```csharp 160 | using System.Text; 161 | using SatorImaging.UnitySourceGenerator; 162 | 163 | [UnitySourceGenerator(OverwriteIfFileExists = false)] 164 | class MinimalGenerator 165 | { 166 | static string OutputFileName() => "Test.cs"; // -> Test..g.cs 167 | 168 | static bool Emit(USGContext context, StringBuilder sb) 169 | { 170 | // write content into passed StringBuilder. 171 | sb.AppendLine($"Asset Path: {context.AssetPath}"); 172 | sb.AppendLine($"Hello World from {typeof(MinimalGenerator)}"); 173 | 174 | // you can modify output path. initial file name is that USG updated. 175 | // NOTE: USG doesn't care the modified path is valid or not. 176 | context.OutputPath += "_MyFirstTest.txt"; 177 | 178 | // return true to tell USG to write content into OutputPath. false to do nothing. 179 | return true; 180 | } 181 | } 182 | ``` 183 | 184 | 185 | 186 | ### Result 187 | 188 | 189 | ```csharp 190 | // MinimalGenerator 191 | Asset Path: Assets/Scripts/MinimalGenerator.cs 192 | Hello World from Sample.MinimalGenerator 193 | ``` 194 | 195 | 196 | Output Directory and File Name 197 | ============================== 198 | 199 | Source Generator creates `USG.g` folder next to target script and append class names to file name. 200 | 201 | Resulting file path will be: 202 | 203 | - Assets/Scripts/USG.g/Test.MinimalGenerator.g.cs 204 | - Assets/Scripts/USG.g/PanicMethod.MethodGeneratorSample.PanicMethodGenerator.g.cs 205 | 206 |

日本語 / JA 207 | 208 | 書き出し先は上記の通り。フォルダーとターゲット・ジェネレータークラス名が付与されます。 209 | 210 |

211 | 212 | > NOTE: In above example, output path is modified so that resulting file name is `Test.MinimalGenerator.g.cs_MyFirstTest.txt` 213 | 214 | 215 | 216 | Coding Goodies 217 | ============== 218 | 219 | There are utility methods for coding source generator more efficient and readable. 220 | 221 | - `StringBuilder` Extentions 222 | - `IndentLine` / `IndentAppend` 223 | - `IndentBegin` / `IndentEnd` 224 | - `IndentLevel` / `IndentChar` / `IndentSize` 225 | - `USGFullNameOf` 226 | - `usg` 227 | - `USGReflection` * can be used on Unity runtime 228 | - `GetAllPublicInstanceFieldAndProperty` 229 | - `TryGetFieldOrPropertyType` 230 | - `GetEnumNamesAndValuesAsDictionary` 231 | - `GetEnumNamesAndValuesAsTuple` 232 | 233 | 234 | ```csharp 235 | // indent utility 236 | sb.IndentChar(' '); // default 237 | sb.IndentSize(4); // default 238 | sb.IndentLevel(3); // template default 239 | sb.IndentBegin("void MethodName() {"); 240 | { 241 | // cast int value to enum 242 | sb.IndentLine($"MyObject.EnumValue = ({usg()})intValue"); 243 | // --- or --- 244 | string CAST_MY_ENUM = "(" + usg(targetTypeVar) + ")"; 245 | sb.IndentLine($"MyObject.EnumValue = {CAST_MY_ENUM}intValue"); 246 | } 247 | sb.IndentEnd("}"); 248 | ``` 249 | 250 | 251 | `usg` is a special utility that is designed for refactoring-ready source generator more readable, script template import it as a static library by default. 252 | 253 | 254 |

日本語 / JA 255 | 256 | `System.Reflection` 系のユーティリティーと `StringBuilder` の拡張メソッド群。 257 | 258 | `usg` は特殊で、クラス名やら何やらのリファクタリングに強いジェネレーターにすると読みづらくなってしまうのを緩和するためのモノ。 259 | 260 | `{typeof(MyClass).FullName}.{nameof(MyClass.Property)}` どんどん長くなるだけなら良いけどクラス内クラスとか構造体の名前が + 付きの不正な状態で出てくる。その他にもジェネリッククラスへの対応とかなんとか、結局何かが必要になる。それならばと可能な限り短く書けるようにした。 261 | 262 | インデント系はトリッキーだけど開発機での実行なのでまあ良し。 263 | 264 |

265 | 266 | 267 | ```csharp 268 | using static SatorImaging.UnitySourceGenerator.USGFullNameOf; // usg() to work 269 | 270 | // usg() allows to write refactoring-ready code with auto completion 271 | sb.Append($"public static {usg>()} MyDict = new() {{ init... }};"); 272 | 273 | // usg{params string[]) to generate full name with specified member name. 274 | usg("NestedStruct.MyField"); // -> global::Full.Namespace.To.MyClass.MyStruct.MyField 275 | // most strict refactoring-ready code 276 | usg(nameof(MyClass.NestedStruct), nameof(MyClass.NestedStruct.MyField)); 277 | 278 | // usg(object valueOrType, bool isFullName) to retrieve full type definition literal 279 | static class MyClass { 280 | Dictionary[]>> Complex = new(0); // usg() throws when null 281 | } 282 | usg(MyClass.Complex); // -> global::...Dictionary[]>> 283 | ``` 284 | 285 | 286 | 287 | Samples 288 | ======= 289 | 290 | `SceneBuildIndexGenerator` 291 | -------------------------- 292 | 293 | This sample allows you to handle scene index more efficiently. 294 | 295 | To use this sample, add `[UnitySourceGenerator(typeof(SceneBuildIndexGenerator))]` attribute to your class. after that, you can use the following enum and helper methods. 296 | 297 | 298 | - enum `SceneBuildIndex` 299 | 300 | - enum consists of scene file names which registered in build settings. 301 | - easy to use with Unity inspector. note that class field doesn't track build index changes. 302 | 303 | - static class `SceneBuildIndexResolver` 304 | 305 | - `GetByName(string sceneFileNameWithoutExtension)` 306 | - get build index by scene name or throws if scene is not found. 307 | - this method ensures the scene must be included in build and also track build index changes. 308 | ```csharp 309 | // use like this in entry point to validate scene existence 310 | SceneBuildIndex ImportantSceneIndex = SceneBuildIndexResolver.GetByName("Scene_Must_Be_Included_in_Build"); 311 | ``` 312 | 313 | - `GetListByPrefix(string fileNamePrefix)` 314 | - Get list of index which starts with prefix. 315 | 316 | - `GetListByPath(string assetsPath)` 317 | - Path must be started with **"Assets/"**. 318 | 319 | 320 | 321 | Utility Functions for Build Event 322 | ================================= 323 | 324 | There are utility functions to perform source code generation on build event. 325 | 326 |

日本語 / JA 327 | 328 | `IPostprocessBuildWithReport` も実装しようかと思ったものの、ビルドイベントに勝手に処理追加するのはなんか訳わからんが動かんの原因だし、BuildReport として渡される全てのファイル名を走査する処理は効率も良くない。ということで。 329 | 330 |

331 | 332 | 333 | ```csharp 334 | // generate code for specified generator type 335 | USGUtility.ForceGenerateByType(typeof(MinimalGenerator)); 336 | 337 | // or for the emitter type 338 | USGUtility.ForceGenerateByType(typeof(ClassHasUSGAttribute)); 339 | ``` 340 | 341 | 342 | 343 | Technical Notes 344 | =============== 345 | 346 | As of C# 9.0, it doesn't allow to define `abstract static` methods in interface, USG reports error when source generator class doesn't implement required static methods. 347 | 348 |

日本語 / JA 349 | 350 | 理想はアトリビュートとインターフェイスによるフィルタリングですが、Unity 2021 は C# 9.0 で `abstract static` を含んだインターフェイスが使えません。 351 | 352 | しょうがないのでメソッドのシグネチャを確認して存在しなければエラーをコンソールに出します。 353 | 354 |

355 | 356 | 357 | ![](https://dl.dropbox.com/s/kstbafbnyc54k2l/USG_IntefaceError.jpg) 358 | 359 | 360 | 361 | ## Naming Convention 362 | 363 | - Generator/target class name and filename must be matched. 364 | - ~~Class name must be unique in whole project.~~ 365 | - ~~Classes are ignored if defined in assembly which name starts with:~~ 366 | - ~~`Unity` (no trailing dot)~~ 367 | - ~~`System.`~~ 368 | - ~~`Mono.`~~ 369 | 370 | 371 |

日本語 / JA 372 | 373 | - ジェネレーター・対象クラスの名前はファイル名と一致 374 | - ~~ジェネレータクラスの名前はプロジェクト内で一意~~ 375 | - ~~クラスが以下で始まる名前のアセンブリで宣言されている場合は対象としない~~ 376 | - ~~`Unity` (末尾ドット無し)~~ 377 | - ~~`System.`~~ 378 | - ~~`Mono.`~~ 379 | 380 |

381 | 382 | 383 | 384 | ## `` Tag 385 | 386 | USG automatically adds document tag at the beginning of generated file. You can remove this document tag by `sb.Clear()` in `Emit()` method. 387 | 388 | 389 |

日本語 / JA 390 | 391 | 渡される `StringBuilder` の冒頭にはドキュメントタグが入ってます。不要なら `sb.Clear()` してください。 392 | 393 |

394 | 395 | 396 | 397 | Unity Editor Integration 398 | ======================== 399 | 400 | ## Installation 401 | 402 | Use the following git URL in Unity Package Manager (UPM). 403 | 404 | - Latest: https://github.com/sator-imaging/Unity-AltSourceGenerator.git 405 | - v3.0.0: https://github.com/sator-imaging/Unity-AltSourceGenerator.git#v3.0.0 406 | - v2.0.1: https://github.com/sator-imaging/Unity-AltSourceGenerator.git#v2.0.1 407 | 408 | 409 | ## USG Control Panel & Window 410 | 411 | - `Main Menu > Edit > Project Settings > Alternative Source Generator` 412 | 413 | ![](https://dl.dropbox.com/scl/fi/jijclnarrruxdt590vss1/USG_Panel.png?rlkey=k44lc9swk0mmui849ck7tappk&dl=0) 414 | 415 | 416 | - **On** 417 | - Include generator in auto run sequence. 418 | 419 | - **Run** 420 | - Run generator on demand. Note that `OverwriteIfFileExists` setting on attribute is ignored. 421 | 422 | - **Link** 423 | - Click name to unveil generator script file in project window. 424 | - Down arrow icon (▼) will show referencing emitters below. 425 | - Linked chain icon (🔗) to unveil emitted file in `USG.g` folder. 426 | - 🗑️ 427 | - Delete *emitted* file from `USG.g` folder. 428 | 429 | - `Main Menu > Tools > Alternative Source Generator` 430 | - open as a window. 431 | 432 | ![](https://dl.dropbox.com/scl/fi/dedb30699adhss8zqwft5/USG_Window.png?rlkey=13gq24ypciw00o9tkhicdpxpe&dl=0) 433 | 434 | 435 | 436 | ## Context Menu 437 | 438 |

日本語 / JA 439 | 440 | 手動でソースコード生成イベントの発火も可能です。「ジェネレーターのスクリプトファイル」か「生成されたファイル」を選択して、Project ウインドウで `Reimport` か `Unity Source Generator` 以下のメニューを実行します。 441 | 442 | ジェネレーターとして参照されているファイルを Reimport した場合は、関連するクラスすべてが再生成されます。`Force Generate...` はクラスアトリビュートの設定に関わらず強制的に上書き生成します。 443 | 444 |

445 | 446 | There is an ability to invoke source code generation by hand. With generator script file or generated file selected in Project window: 447 | 448 | 449 | - `Reimport` 450 | - This command respects `OverwriteIfFileExists` setting by generator class attribute. 451 | - Classes referencing selected generator will also be re-generated. 452 | 453 | - `Unity Source Generator > Force Generate` 454 | - This command will force re-generate source code even if overwrite setting is disabled. 455 | 456 | 457 | ![](https://dl.dropbox.com/s/skqa6mwh932lsrg/USG_FireEvent.jpg) 458 | 459 | 460 | 461 | Troubleshooting 462 | =============== 463 | 464 | #### Generator script update is not applied to generated file. 465 | 466 | Usually, this problem happens when Unity automatically reloads updated scripts WITHOUT Editor window getting focus. To solve the problem: 467 | 468 | 1. Close Unity and Visual Studio. 469 | 1. Restart Unity. 470 | 1. Launch Visual Studio by double clicking `.cs` script in Unity Editor. 471 | 472 | > *NOTE*: There is experimental feature to suspend auto reloading while Unity Editor in background (doesn't get focused). Open `Edit > Project Settings > Alternative Source Generator` to enable it. 473 | 474 | 475 | 476 | 477 | 478 | Copyright 479 | ========= 480 | 481 | Copyright © 2023-2024 Sator Imaging, all rights reserved. 482 | 483 | 484 | 485 | License 486 | ======= 487 | 488 |

489 |

490 | The above copyright notice and this permission notice shall be included in all 491 | copies or substantial portions of the Software. 492 | 493 | ```text 494 | MIT License 495 | 496 | Copyright (c) 2023-2024 Sator Imaging 497 | 498 | Permission is hereby granted, free of charge, to any person obtaining a copy 499 | of this software and associated documentation files (the "Software"), to deal 500 | in the Software without restriction, including without limitation the rights 501 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 502 | copies of the Software, and to permit persons to whom the Software is 503 | furnished to do so, subject to the following conditions: 504 | 505 | The above copyright notice and this permission notice shall be included in all 506 | copies or substantial portions of the Software. 507 | 508 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 509 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 510 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 511 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 512 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 513 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 514 | SOFTWARE. 515 | ``` 516 | 517 |
518 |

519 | 520 | 521 | 522 | 523 | 524 |   525 |   526 | 527 | # Devnote 528 | 529 | ## TODO 530 | 531 | - `GenerateOnce` attribute parameter 532 | - currently USG generates same class/enum multiple times when multiple classes refer enum/singleton generator. 533 | - ex. `SceneBuildIndex` must be referred only once in project to avoid conflict 534 | - v4: remove obsolete functions 535 | - support C# 11 language features (waiting for Unity 6 update!!) 536 | - see also: [CHANGELOG](CHANGELOG.md#unreleased) 537 | 538 | 539 | 545 | 546 | 547 | 562 | -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 62e489ccfe1378d4ca5d9dbeb58a320e 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Runtime.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: acb48a7bfcc2c6145b31b8b676f62040 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Runtime/Attributes.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | 4 | namespace SatorImaging.UnitySourceGenerator 5 | { 6 | /// 7 | /// Implement the following methods on generator class.
8 | /// - static string OutputFileName()
9 | /// - static bool Emit(USGContext, StringBuilder) 10 | ///
11 | // TODO: Implement "IUnitySourceGenerator" (C# 11.0) 12 | [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)] 13 | public sealed class UnitySourceGeneratorAttribute : Attribute 14 | { 15 | // NOTE: not follows C# guideline but need read/write access on this field 16 | // to support self-emit generator which can be initialized without generator type parameter. 17 | internal /*readonly*/ Type _generatorClass; 18 | 19 | public UnitySourceGeneratorAttribute(Type generatorClass = null) 20 | { 21 | this._generatorClass = generatorClass; 22 | } 23 | 24 | public Type GeneratorClass => _generatorClass; 25 | 26 | public bool OverwriteIfFileExists { get; set; } = true; 27 | public Encoding OutputFileEncoding { get; set; } = Encoding.UTF8; 28 | 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Runtime/Attributes.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: aad4ed242a3fbbc4a9bdc6a93309f7a8 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/Interfaces.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Pending Update for C# 11.0+ 3 | * 4 | * NOTE: `abstract static` definition in interface is allowed in C# 11.0 or later. 5 | * the following code is just for reference for future enhancement. 6 | * currently, USG engine checks implementation using `System.Reflection`. 7 | * 8 | */ 9 | 10 | #if UNITY_2025_1_OR_NEWER 11 | 12 | using System.Text; 13 | 14 | namespace SatorImaging.UnitySourceGenerator 15 | { 16 | public interface IUnitySourceGenerator 17 | { 18 | ///Return true if write StringBuilder content to file. 19 | abstract static bool Emit(in USGContext context, in StringBuilder sb); 20 | 21 | ///Return just only filename with extension. SourceGenerator will automatically arrange output path. 22 | abstract static string OutputFileName(); 23 | 24 | } 25 | } 26 | 27 | #endif 28 | -------------------------------------------------------------------------------- /Runtime/Interfaces.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e897e6eee6e12b04088c75dbd191424a 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/IsExternalInit.cs: -------------------------------------------------------------------------------- 1 | // NOTE: to use record & init accessor in Unity 2021 2 | // https://stackoverflow.com/questions/62648189/testing-c-sharp-9-0-in-vs2019-cs0518-isexternalinit-is-not-defined-or-imported 3 | 4 | namespace System.Runtime.CompilerServices 5 | { 6 | [ComponentModel.EditorBrowsable(ComponentModel.EditorBrowsableState.Never)] 7 | class IsExternalInit { } 8 | } 9 | -------------------------------------------------------------------------------- /Runtime/IsExternalInit.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1bf78de4adb778e43a9fb31fcd71a444 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/StringBuilderExtension.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | using System.Text; 4 | 5 | 6 | namespace SatorImaging.UnitySourceGenerator 7 | { 8 | public static class StringBuilderExtension 9 | { 10 | public static char CurrentIndentChar = ' '; 11 | public static int CurrentIndentSize = 4; 12 | public static int CurrentIndentLevel = 0; 13 | 14 | 15 | private static int s_lastIndentLevel = int.MinValue; // must be different from CurrentLevel to initialize indentString 16 | 17 | private static string s_indentString = string.Empty; 18 | public static string IndentString 19 | { 20 | get 21 | { 22 | if (s_lastIndentLevel == CurrentIndentLevel) 23 | return s_indentString; 24 | 25 | s_lastIndentLevel = CurrentIndentLevel; 26 | s_indentString = new string(CurrentIndentChar, CurrentIndentLevel * CurrentIndentSize); 27 | return s_indentString; 28 | } 29 | } 30 | 31 | 32 | 33 | [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void IndentChar(this StringBuilder sb, char value) => CurrentIndentChar = value; 34 | [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void IndentSize(this StringBuilder sb, int size) => CurrentIndentSize = Math.Max(0, size); 35 | [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void IndentLevel(this StringBuilder sb, int level) => CurrentIndentLevel = Math.Max(0, level); 36 | 37 | 38 | ///Shorthand for IndentLine(); IndentLevel++; 39 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 40 | public static void IndentBegin(this StringBuilder sb, string value = null) 41 | { 42 | sb.IndentLine(value); 43 | CurrentIndentLevel = Math.Max(0, CurrentIndentLevel + 1); 44 | } 45 | 46 | ///Shorthand for IndentLevel--; IndentLine(); 47 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 48 | public static void IndentEnd(this StringBuilder sb, string value = null) 49 | { 50 | CurrentIndentLevel = Math.Max(0, CurrentIndentLevel - 1); 51 | sb.IndentLine(value); 52 | } 53 | 54 | 55 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 56 | public static void IndentLine(this StringBuilder sb, string value = null) 57 | { 58 | sb.IndentAppend(value); 59 | sb.AppendLine(); 60 | } 61 | 62 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 63 | public static void IndentAppend(this StringBuilder sb, string value) 64 | { 65 | if (string.IsNullOrEmpty(value)) 66 | return; 67 | 68 | sb.Append(IndentString); 69 | sb.Append(value); 70 | } 71 | 72 | 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Runtime/StringBuilderExtension.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b263c90f3b732e244a99b8b08d39510c 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/USGContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | 4 | namespace SatorImaging.UnitySourceGenerator 5 | { 6 | public sealed class USGContext 7 | { 8 | public Type TargetClass { get; init; } 9 | public string AssetPath { get; init; } 10 | public string OutputPath { get; set; } 11 | 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Runtime/USGContext.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 341ef0b8604f1fe4784b81f55bd4dda3 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/USGFullNameOf.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SatorImaging.UnitySourceGenerator 4 | { 5 | public static class USGFullNameOf 6 | { 7 | ///Refactor-ready full name generator. 8 | ///Ex: usg<MyClass.InnerClass>(nameof(Something), "Generated") 9 | ///global::Full.Namespace.To.MyClass.InnerClass.Something.Generated 10 | public static string usg(params string[] memberNames) 11 | { 12 | var ret = GetTypeDef(typeof(T), true); 13 | if (memberNames == null) 14 | return ret; 15 | 16 | for (int i = 0; i < memberNames.Length; i++) 17 | { 18 | if (memberNames[i] == null || memberNames[i].Length == 0) 19 | continue; 20 | ret += '.' + memberNames[i]; 21 | } 22 | return ret; 23 | } 24 | 25 | ///Get type definition literal of supplied object. 26 | ///Throw when valueOrType is null. 27 | ///Ex: Dictionary<int, List<Dictionary<string, float[][]>[]>>[] 28 | public static string usg(object valueOrType, bool isFullName = true) 29 | { 30 | if (valueOrType == null) 31 | throw new ArgumentNullException(); 32 | 33 | if (valueOrType is Type t) 34 | return GetTypeDef(t, isFullName); 35 | 36 | return GetTypeDef(valueOrType.GetType(), isFullName); 37 | } 38 | 39 | 40 | //internals 41 | static string GetTypeDef(Type t, bool isFullName)// Immutable collections cannot use new(){...}, bool isNew) 42 | { 43 | var ns = t.Namespace; 44 | ns = string.IsNullOrEmpty(ns) ? string.Empty : ns + '.'; 45 | // NOTE: FullName sometimes returns AssemblyQualifiedName. 46 | var ret = t.FullName; 47 | if (ns.Length > 0) 48 | ret = ret.Substring(ns.Length, ret.Length - ns.Length); 49 | // remove assembly info 50 | int idx = ret.IndexOf(','); 51 | if (idx > -1) 52 | ret = ret.Substring(0, idx); 53 | 54 | // NOTE: class or struct defined in other class is separated with + sign. 55 | // ex: Namespace.MyClass+InnerClass 56 | ret = ret.Replace('+', '.'); 57 | 58 | int arrayDim = 0; 59 | while (t.IsArray) 60 | { 61 | arrayDim++; 62 | ret = ret.Substring(0, ret.Length - 2); //[] 63 | t = t.GetElementType(); 64 | } 65 | 66 | bool isBuiltinType = TryGetBuiltinDef(ref ret); 67 | if (!isBuiltinType && isFullName && ns.Length > 0) 68 | ret = ns + ret; 69 | 70 | if (t.IsGenericType) 71 | { 72 | idx = ret.IndexOf('`'); 73 | ret = ret.Substring(0, idx) + '<'; 74 | bool comma = false; 75 | foreach (var nested in t.GetGenericArguments()) 76 | { 77 | ret += (comma ? ", " : string.Empty) + GetTypeDef(nested, isFullName); 78 | comma = true; 79 | } 80 | ret += '>'; 81 | } 82 | 83 | while (arrayDim > 0) 84 | { 85 | ret += "[]"; 86 | arrayDim--; 87 | } 88 | 89 | if (!isBuiltinType && isFullName) 90 | ret = "global::" + ret; 91 | 92 | return ret; 93 | } 94 | 95 | static bool TryGetBuiltinDef(ref string name) 96 | { 97 | string ret = name switch 98 | { 99 | "SByte" => "sbyte", 100 | "Byte" => "byte", 101 | "Int16" => "short", 102 | "UInt16" => "ushort", 103 | "Int32" => "int", 104 | "UInt32" => "uint", 105 | "Int64" => "long", 106 | "UInt64" => "ulong", 107 | 108 | "Single" => "float", 109 | "Double" => "double", 110 | "Decimal" => "decimal", 111 | 112 | "Boolean" => "bool", 113 | "Char" => "char", 114 | "String" => "string", 115 | "Object" => "object", 116 | 117 | _ => null, 118 | }; 119 | 120 | if (ret == null) 121 | return false; 122 | 123 | name = ret; 124 | return true; 125 | } 126 | 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /Runtime/USGFullNameOf.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9b2e6dde16b4bae4998755bfcc5f8f4a 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/USGReflection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | 6 | 7 | namespace SatorImaging.UnitySourceGenerator 8 | { 9 | public static class USGReflection 10 | { 11 | public readonly static BindingFlags PUBLIC_INSTANCE_FIELD_OR_PROPERTY 12 | = BindingFlags.Public | BindingFlags.Instance 13 | | BindingFlags.DeclaredOnly 14 | | BindingFlags.GetProperty | BindingFlags.SetProperty 15 | | BindingFlags.GetField | BindingFlags.SetField 16 | ; 17 | 18 | 19 | ///Note that constructor (`.ctor`) is always ignored. 20 | public static MemberInfo[] GetAllPublicInstanceFieldAndProperty(Type cls, params string[] namesToIgnore) 21 | => GetMembers(cls, PUBLIC_INSTANCE_FIELD_OR_PROPERTY, namesToIgnore ?? new string[] { }); 22 | 23 | static MemberInfo[] GetMembers(Type cls, BindingFlags flags, string[] namesToIgnore) 24 | { 25 | if (cls == null) 26 | throw new ArgumentNullException(nameof(cls)); 27 | 28 | 29 | var members = cls.GetMembers(flags) 30 | .Where(x => 31 | { 32 | if (namesToIgnore.Contains(x.Name) || x.Name == ".ctor") 33 | return false; 34 | return true; 35 | }) 36 | .ToArray(); 37 | ; 38 | 39 | return members; 40 | } 41 | 42 | 43 | ///Try get value type of field or property. 44 | public static bool TryGetFieldOrPropertyType(MemberInfo info, out Type outType) 45 | { 46 | if (info == null) 47 | throw new ArgumentNullException(nameof(info)); 48 | 49 | 50 | outType = null; 51 | if (info is FieldInfo field) 52 | { 53 | outType = field.FieldType; 54 | } 55 | else if (info is PropertyInfo property) 56 | { 57 | outType = property.PropertyType; 58 | } 59 | 60 | 61 | if (outType == null) 62 | return false; 63 | 64 | return true; 65 | } 66 | 67 | 68 | ///Enum names and values as a dictionary. 69 | ///int, uint, long or something 70 | public static Dictionary GetEnumNamesAndValuesAsDictionary(Type enumType) 71 | where TValue : struct 72 | { 73 | if (enumType?.IsEnum != true) 74 | throw new ArgumentException(nameof(enumType)); 75 | 76 | 77 | var names = enumType.GetEnumNames(); 78 | var dict = new Dictionary(capacity: names.Length); 79 | 80 | int iter = -1; 81 | foreach (TValue val in enumType.GetEnumValues()) 82 | { 83 | iter++; 84 | dict.Add(names[iter], val); 85 | } 86 | 87 | return dict; 88 | } 89 | 90 | 91 | #if UNITY_2021_3_OR_NEWER 92 | ///Enum names and values as a tuple. 93 | ///int, uint, long or something 94 | public static (string[], TValue[]) GetEnumNamesAndValuesAsTuple(Type enumType) 95 | where TValue : struct 96 | { 97 | if (enumType?.IsEnum != true) 98 | throw new ArgumentException(nameof(enumType)); 99 | 100 | return (enumType.GetEnumNames(), enumType.GetEnumValues().Cast().ToArray()); 101 | } 102 | #endif 103 | 104 | 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Runtime/USGReflection.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7ae21c13d56276a42b73955cdfa079c1 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Sample.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 45de77c87b18b4945a662c971ee98df1 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Sample/SceneBuildIndex.cs: -------------------------------------------------------------------------------- 1 | // NOTE: to avoid error until first source generation happens. 2 | public enum SceneBuildIndex 3 | { 4 | NotInitialized = -9999, 5 | _, 6 | ToInitializeEnumValues, 7 | SeeAlternativeSourceGeneratorSampleFolder, 8 | 9 | } 10 | -------------------------------------------------------------------------------- /Sample/SceneBuildIndex.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c3247011e7877194891fd55d49877516 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Sample/SceneBuildIndexGenerator.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Text; 4 | using System.Text.RegularExpressions; 5 | 6 | #if UNITY_EDITOR 7 | using UnityEditor; 8 | using UnityEditor.Build; 9 | using UnityEditor.Build.Reporting; 10 | #endif 11 | 12 | namespace SatorImaging.UnitySourceGenerator 13 | { 14 | // HOW TO USE: Add the following attribute to *target* class. 15 | // enum, ScenesInBuild will be generated in the target class namespace. 16 | //[UnitySourceGenerator(typeof(SceneBuildIndexEnumGenerator))] 17 | public class SceneBuildIndexGenerator 18 | 19 | #if UNITY_EDITOR 20 | // NOTE: class definition is required to avoid build error due to referencing from USG attributes. 21 | // (or exclude [UnitySourceGenerator(typeof(...))] attributes from build) 22 | : IPreprocessBuildWithReport 23 | #endif 24 | 25 | { 26 | 27 | #if UNITY_EDITOR // USG: class definition is required to avoid build error but methods are not. 28 | #pragma warning disable IDE0051 29 | 30 | static string OutputFileName() => "Enum.cs"; // -> Test..g.cs 31 | 32 | 33 | const string ENUM_NAME = nameof(SceneBuildIndex); 34 | readonly static Regex RE_REMOVE_INVALID = new Regex(@"[^A-Za-z_0-9]+", RegexOptions.Compiled); 35 | 36 | static bool Emit(USGContext context, StringBuilder sb) 37 | { 38 | // code generation 39 | sb.Append($@" 40 | namespace {context.TargetClass.Namespace} 41 | {{ 42 | public enum {ENUM_NAME} 43 | {{ 44 | "); 45 | /* enum ================================================================ */ 46 | sb.IndentLevel(2); 47 | 48 | var scenePathList = new List(); 49 | var sceneNameList = new List(); 50 | for (int i = 0; i < UnityEditor.EditorBuildSettings.scenes.Length; i++) 51 | { 52 | var scene = UnityEditor.EditorBuildSettings.scenes[i]; //SceneManager.GetSceneAt(i); 53 | scenePathList.Add(scene.path); 54 | sceneNameList.Add(Path.GetFileNameWithoutExtension(scene.path)); 55 | 56 | var name = RE_REMOVE_INVALID.Replace(Path.GetFileNameWithoutExtension(scene.path), "_"); 57 | sb.IndentLine($"{name} = {i},"); 58 | } 59 | 60 | //---------------------------------------------------------------------- 61 | sb.Append($@" 62 | }} 63 | 64 | public static class {ENUM_NAME}Resolver 65 | {{ 66 | readonly static string[] Paths = new string[] 67 | {{ 68 | "); 69 | /* paths ================================================================ */ 70 | sb.IndentLevel(3); 71 | foreach (var path in scenePathList) 72 | { 73 | sb.IndentLine($"\"{path}\","); 74 | } 75 | //---------------------------------------------------------------------- 76 | sb.Append($@" 77 | }}; 78 | 79 | readonly static string[] Names = new string[] 80 | {{ 81 | "); 82 | /* names ================================================================ */ 83 | sb.IndentLevel(3); 84 | foreach (var name in sceneNameList) 85 | { 86 | sb.IndentLine($"\"{name}\","); 87 | } 88 | //---------------------------------------------------------------------- 89 | sb.Append($@" 90 | }}; 91 | 92 | public static {ENUM_NAME} GetByName(string name) 93 | {{ 94 | int found = -1; 95 | for (int i = 0; i < Names.Length; i++) 96 | {{ 97 | if (Names[i] == name) 98 | {{ 99 | if (found < 0) 100 | found = i; 101 | else 102 | throw new System.Exception($""multiple scenes are found: '{{name}}'""); 103 | }} 104 | }} 105 | if (found < 0) 106 | throw new System.Exception($""scene file '{{name}}' is not registered in build settings.""); 107 | return ({ENUM_NAME})found; 108 | }} 109 | 110 | public static System.Collections.Generic.List<{ENUM_NAME}> GetListByPrefix(string fileNamePrefix) 111 | {{ 112 | var ret = new System.Collections.Generic.List<{ENUM_NAME}>(capacity: Names.Length); 113 | for (int i = 0; i < Names.Length; i++) 114 | {{ 115 | if (Names[i].StartsWith(fileNamePrefix, System.StringComparison.Ordinal)) 116 | {{ 117 | ret.Add(({ENUM_NAME})i); 118 | }} 119 | }} 120 | return ret; 121 | }} 122 | 123 | ///Path must be started with 'Assets/'. 124 | public static System.Collections.Generic.List<{ENUM_NAME}> GetListByPath(string assetsPath) 125 | {{ 126 | var ret = new System.Collections.Generic.List<{ENUM_NAME}>(capacity: Paths.Length); 127 | for (int i = 0; i < Paths.Length; i++) 128 | {{ 129 | if (Paths[i].StartsWith(assetsPath, System.StringComparison.Ordinal)) 130 | {{ 131 | ret.Add(({ENUM_NAME})i); 132 | }} 133 | }} 134 | return ret; 135 | }} 136 | }} 137 | }} 138 | "); 139 | return true; 140 | } 141 | 142 | 143 | /* events ================================================================ */ 144 | 145 | public int callbackOrder => 0; 146 | public void OnPreprocessBuild(BuildReport report) => ForceUpdate(); 147 | 148 | static void ForceUpdate() => Editor.USGUtility.ForceGenerateByType(typeof(SceneBuildIndexGenerator), false); 149 | 150 | [InitializeOnLoadMethod] 151 | static void RegisterEvent() 152 | { 153 | EditorBuildSettings.sceneListChanged -= ForceUpdate; 154 | EditorBuildSettings.sceneListChanged += ForceUpdate; 155 | } 156 | 157 | 158 | #pragma warning restore IDE0051 159 | #endif 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /Sample/SceneBuildIndexGenerator.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1adcf30d8bc4c004f8068338fff0eb4c 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /SatorImaging.UnitySourceGenerator.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SatorImaging.UnitySourceGenerator", 3 | "rootNamespace": "SatorImaging.UnitySourceGenerator", 4 | "references": [], 5 | "includePlatforms": [], 6 | "excludePlatforms": [], 7 | "allowUnsafeCode": false, 8 | "overrideReferences": false, 9 | "precompiledReferences": [], 10 | "autoReferenced": true, 11 | "defineConstraints": [], 12 | "versionDefines": [], 13 | "noEngineReferences": false 14 | } -------------------------------------------------------------------------------- /SatorImaging.UnitySourceGenerator.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0db6c7834d8a24e41a1e99b80f8c2b68 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Template.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 98ca00585880763449532774466f7002 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Template/Template_MethodGenerator.txt: -------------------------------------------------------------------------------- 1 | using static SatorImaging.UnitySourceGenerator.USGFullNameOf; 2 | using SatorImaging.UnitySourceGenerator; 3 | using System.Text; 4 | using Debug = UnityEngine.Debug; 5 | using Object = UnityEngine.Object; 6 | #if UNITY_EDITOR 7 | using UnityEditor; 8 | #endif 9 | 10 | #ROOTNAMESPACEBEGIN# 11 | // HOW TO USE: Add the following attribute to *target* class. 12 | // Note that target class must be defined as partial. 13 | //[UnitySourceGenerator(typeof(#SCRIPTNAME#), OverwriteIfFileExists = false)] 14 | public partial class #SCRIPTNAME# 15 | { 16 | #if UNITY_EDITOR // USG: class definition is required to avoid build error but methods are not. 17 | #pragma warning disable IDE0051 18 | 19 | readonly static string MEMBER_ACCESS = "internal"; 20 | readonly static string MAIN_MEMBER_NAME = __________________________; 21 | static string OutputFileName() => MAIN_MEMBER_NAME + ".cs"; // -> Name...g.cs 22 | 23 | static bool Emit(USGContext context, StringBuilder sb) 24 | { 25 | // USG: static classes are IsAbstract is set. 26 | if (!context.TargetClass.IsClass) 27 | return false; // return false to tell USG doesn't write file. 28 | 29 | // USG: you can modify output path. default file name is that USG generated. 30 | // note that USG doesn't care the modified path is valid or not. 31 | //context.OutputPath += "_MyFirstTest.txt"; 32 | 33 | // USG: EditorUtility.DisplayDialog() or others don't work in batch mode. 34 | // throw if method depending on GUI based functions. 35 | //if (UnityEngine.Application.isBatchMode) 36 | // throw new System.NotSupportedException("GUI based functions do nothing in batch mode."); 37 | 38 | // USG: write content into passed StringBuilder. 39 | sb.Append($@" 40 | using System; 41 | using System.Collections.Generic; 42 | using UnityEngine; 43 | using Debug = UnityEngine.Debug; 44 | using Object = UnityEngine.Object; 45 | 46 | namespace {context.TargetClass.Namespace} 47 | {{ 48 | partial class {context.TargetClass.Name} 49 | {{ 50 | "); 51 | // class open ---------------------------------------------------------------------- 52 | 53 | 54 | #region // USG: MainMember 55 | sb.Append($@" 56 | {MEMBER_ACCESS} void {MAIN_MEMBER_NAME}() 57 | {{ 58 | "); 59 | sb.IndentLevel(3); 60 | 61 | 62 | 63 | // USG: semicolon? 64 | sb.Append($@" 65 | }} 66 | "); 67 | #endregion 68 | 69 | 70 | // class close ---------------------------------------------------------------------- 71 | sb.Append($@" 72 | }} 73 | }} 74 | "); 75 | 76 | // USG: return true to tell USG to write content into OutputPath. false to do nothing. 77 | return true; 78 | } 79 | 80 | #pragma warning restore IDE0051 81 | #endif 82 | } 83 | #ROOTNAMESPACEEND# 84 | -------------------------------------------------------------------------------- /Template/Template_MethodGenerator.txt.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2b79255e1e2d4fc478db7efe8a0d4ba8 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Template/Template_SelfEmitGenerator.txt: -------------------------------------------------------------------------------- 1 | using static SatorImaging.UnitySourceGenerator.USGFullNameOf; 2 | using SatorImaging.UnitySourceGenerator; 3 | using System.Text; 4 | using Debug = UnityEngine.Debug; 5 | using Object = UnityEngine.Object; 6 | #if UNITY_EDITOR 7 | using UnityEditor; 8 | #endif 9 | 10 | #ROOTNAMESPACEBEGIN# 11 | // HOW TO USE: Place this file in Assets/ folder to enable source generator. 12 | // USG is processing files only in Assets/ folder. 13 | [UnitySourceGenerator(OverwriteIfFileExists = false)] 14 | partial class #SCRIPTNAME# 15 | { 16 | #if UNITY_EDITOR // USG: class definition is required to avoid build error but methods are not. 17 | #pragma warning disable IDE0051 18 | 19 | readonly static string MEMBER_ACCESS = "internal"; 20 | readonly static string MAIN_MEMBER_NAME = __________________________; 21 | static string OutputFileName() => MAIN_MEMBER_NAME + ".cs"; // -> Name..g.cs 22 | 23 | static bool Emit(USGContext context, StringBuilder sb) 24 | { 25 | // USG: static classes are IsAbstract is set. 26 | if (!context.TargetClass.IsClass) 27 | return false; // return false to tell USG doesn't write file. 28 | 29 | // USG: you can modify output path. default file name is that USG generated. 30 | // note that USG doesn't care the modified path is valid or not. 31 | //context.OutputPath += "_MyFirstTest.txt"; 32 | 33 | // USG: EditorUtility.DisplayDialog() or others don't work in batch mode. 34 | // throw if method depending on GUI based functions. 35 | //if (UnityEngine.Application.isBatchMode) 36 | // throw new System.NotSupportedException("GUI based functions do nothing in batch mode."); 37 | 38 | // USG: write content into passed StringBuilder. 39 | sb.Append($@" 40 | using System; 41 | using System.Collections.Generic; 42 | using UnityEngine; 43 | using Debug = UnityEngine.Debug; 44 | using Object = UnityEngine.Object; 45 | 46 | namespace {context.TargetClass.Namespace} 47 | {{ 48 | partial class {context.TargetClass.Name} 49 | {{ 50 | "); 51 | // class open ---------------------------------------------------------------------- 52 | 53 | 54 | #region // USG: MainMember 55 | sb.Append($@" 56 | {MEMBER_ACCESS} void {MAIN_MEMBER_NAME}() 57 | {{ 58 | "); 59 | sb.IndentLevel(3); 60 | 61 | 62 | 63 | // USG: semicolon? 64 | sb.Append($@" 65 | }} 66 | "); 67 | #endregion 68 | 69 | 70 | // class close ---------------------------------------------------------------------- 71 | sb.Append($@" 72 | }} 73 | }} 74 | "); 75 | 76 | // USG: return true to tell USG to write content into OutputPath. false to do nothing. 77 | return true; 78 | } 79 | 80 | #pragma warning restore IDE0051 81 | #endif 82 | } 83 | #ROOTNAMESPACEEND# 84 | -------------------------------------------------------------------------------- /Template/Template_SelfEmitGenerator.txt.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7dc80e72dfc763d4f83d36fddae5d795 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.sator-imaging.alt-source-generator", 3 | "displayName": "Alternative Source Generator for Unity", 4 | "version": "3.0.1", 5 | "unity": "2021.3", 6 | "description": "Ease-of-Use Source Generator Alternative for Unity.", 7 | "author": { 8 | "name": "Sator Imaging", 9 | "url": "https://twitter.com/sator_imaging" 10 | }, 11 | "category": "Source Generator", 12 | "keywords": [ 13 | "Unity", 14 | "Source", 15 | "Generator", 16 | "Alternative", 17 | "Scripting", 18 | "Asset", 19 | "Postprocessor" 20 | ], 21 | "dependencies": { 22 | "com.unity.modules.uielements": "1.0.0" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/sator-imaging/Unity-AltSourceGenerator.git" 27 | }, 28 | "url": "https://github.com/sator-imaging/Unity-AltSourceGenerator.git", 29 | "documentationUrl": "https://github.com/sator-imaging/Unity-AltSourceGenerator", 30 | "changelogUrl": "https://github.com/sator-imaging/Unity-AltSourceGenerator", 31 | "licensesUrl": "https://www.sator-imaging.com/" 32 | } 33 | -------------------------------------------------------------------------------- /package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f6b48c267ef64174c9ee3ebacec6716d 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | --------------------------------------------------------------------------------