├── LICENSE.md ├── LICENSE.md.meta ├── README.md ├── README.md.meta ├── Src.meta ├── Src ├── Attributes.cs ├── Attributes.cs.meta ├── Medicine.CodeGen.meta ├── Medicine.CodeGen │ ├── CecilExtensions.cs │ ├── CecilExtensions.cs.meta │ ├── EditorGUI.meta │ ├── EditorGUI │ │ ├── SingletonObjectHeaderGUI.cs │ │ └── SingletonObjectHeaderGUI.cs.meta │ ├── ILPostProcessor.meta │ ├── ILPostProcessor │ │ ├── MedicineILPostProcessor.cs │ │ ├── MedicineILPostProcessor.cs.meta │ │ ├── PostProcessorAssemblyResolver.cs │ │ ├── PostProcessorAssemblyResolver.cs.meta │ │ ├── PostProcessorContext.cs │ │ └── PostProcessorContext.cs.meta │ ├── InjectionPostProcessor.cs │ ├── InjectionPostProcessor.cs.meta │ ├── Medicine.CodeGen.asmdef │ ├── Medicine.CodeGen.asmdef.meta │ ├── MethodInfos.cs │ └── MethodInfos.cs.meta ├── Medicine.Tests.meta ├── Medicine.Tests │ ├── Medicine.Tests.asmdef │ ├── Medicine.Tests.asmdef.meta │ ├── TestComp.cs │ ├── TestComp.cs.meta │ ├── TestExtensions.cs │ ├── TestExtensions.cs.meta │ ├── Tests.meta │ └── Tests │ │ ├── InjectAllTests.cs │ │ ├── InjectAllTests.cs.meta │ │ ├── InjectGenericTests.cs │ │ ├── InjectGenericTests.cs.meta │ │ ├── InjectLazyTests.cs │ │ ├── InjectLazyTests.cs.meta │ │ ├── InjectSingleTests.cs │ │ ├── InjectSingleTests.cs.meta │ │ ├── InjectTests.cs │ │ └── InjectTests.cs.meta ├── Medicine.asmdef ├── Medicine.asmdef.meta ├── NonAlloc.Benchmark.cs ├── NonAlloc.Benchmark.cs.meta ├── NonAlloc.RecyclableList.cs ├── NonAlloc.RecyclableList.cs.meta ├── NonAlloc.Unsafe.cs ├── NonAlloc.Unsafe.cs.meta ├── NonAlloc.cs ├── NonAlloc.cs.meta ├── RuntimeHelpers.Collection.cs ├── RuntimeHelpers.Collection.cs.meta ├── RuntimeHelpers.Singleton.cs ├── RuntimeHelpers.Singleton.cs.meta ├── RuntimeHelpers.cs └── RuntimeHelpers.cs.meta ├── package.json └── package.json.meta /LICENSE.md: -------------------------------------------------------------------------------- 1 | ## CREATIVE COMMONS 2 | 3 | # Attribution 4.0 International 4 | 5 | Creative Commons Corporation (“Creative Commons”) is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an “as-is” basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible. 6 | 7 | ### Using Creative Commons Public Licenses 8 | 9 | Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses. 10 | 11 | * __Considerations for licensors:__ Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC-licensed material, or material used under an exception or limitation to copyright. [More considerations for licensors](http://wiki.creativecommons.org/Considerations_for_licensors_and_licensees#Considerations_for_licensors). 12 | 13 | * __Considerations for the public:__ By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor’s permission is not necessary for any reason–for example, because of any applicable exception or limitation to copyright–then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. [More considerations for the public](http://wiki.creativecommons.org/Considerations_for_licensors_and_licensees#Considerations_for_licensees). 14 | 15 | ## Creative Commons Attribution 4.0 International Public License 16 | 17 | By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. 18 | 19 | ### Section 1 – Definitions. 20 | 21 | a. __Adapted Material__ means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. 22 | 23 | b. __Adapter's License__ means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. 24 | 25 | c. __Copyright and Similar Rights__ means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. 26 | 27 | d. __Effective Technological Measures__ means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. 28 | 29 | e. __Exceptions and Limitations__ means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. 30 | 31 | f. __Licensed Material__ means the artistic or literary work, database, or other material to which the Licensor applied this Public License. 32 | 33 | g. __Licensed Rights__ means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. 34 | 35 | h. __Licensor__ means the individual(s) or entity(ies) granting rights under this Public License. 36 | 37 | i. __Share__ means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. 38 | 39 | j. __Sui Generis Database Rights__ means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. 40 | 41 | k. __You__ means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. 42 | 43 | ### Section 2 – Scope. 44 | 45 | a. ___License grant.___ 46 | 47 | 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: 48 | 49 | A. reproduce and Share the Licensed Material, in whole or in part; and 50 | 51 | B. produce, reproduce, and Share Adapted Material. 52 | 53 | 2. __Exceptions and Limitations.__ For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. 54 | 55 | 3. __Term.__ The term of this Public License is specified in Section 6(a). 56 | 57 | 4. __Media and formats; technical modifications allowed.__ The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material. 58 | 59 | 5. __Downstream recipients.__ 60 | 61 | A. __Offer from the Licensor – Licensed Material.__ Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. 62 | 63 | B. __No downstream restrictions.__ You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. 64 | 65 | 6. __No endorsement.__ Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). 66 | 67 | b. ___Other rights.___ 68 | 69 | 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. 70 | 71 | 2. Patent and trademark rights are not licensed under this Public License. 72 | 73 | 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties. 74 | 75 | ### Section 3 – License Conditions. 76 | 77 | Your exercise of the Licensed Rights is expressly made subject to the following conditions. 78 | 79 | a. ___Attribution.___ 80 | 81 | 1. If You Share the Licensed Material (including in modified form), You must: 82 | 83 | A. retain the following if it is supplied by the Licensor with the Licensed Material: 84 | 85 | i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); 86 | 87 | ii. a copyright notice; 88 | 89 | iii. a notice that refers to this Public License; 90 | 91 | iv. a notice that refers to the disclaimer of warranties; 92 | 93 | v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; 94 | 95 | B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and 96 | 97 | C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. 98 | 99 | 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. 100 | 101 | 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. 102 | 103 | 4. If You Share Adapted Material You produce, the Adapter's License You apply must not prevent recipients of the Adapted Material from complying with this Public License. 104 | 105 | ### Section 4 – Sui Generis Database Rights. 106 | 107 | Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: 108 | 109 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database; 110 | 111 | b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material; and 112 | 113 | c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. 114 | 115 | For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. 116 | 117 | ### Section 5 – Disclaimer of Warranties and Limitation of Liability. 118 | 119 | a. __Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You.__ 120 | 121 | b. __To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You.__ 122 | 123 | c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. 124 | 125 | ### Section 6 – Term and Termination. 126 | 127 | a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. 128 | 129 | b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: 130 | 131 | 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or 132 | 133 | 2. upon express reinstatement by the Licensor. 134 | 135 | For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. 136 | 137 | c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. 138 | 139 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. 140 | 141 | ### Section 7 – Other Terms and Conditions. 142 | 143 | a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. 144 | 145 | b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. 146 | 147 | ### Section 8 – Interpretation. 148 | 149 | a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. 150 | 151 | b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. 152 | 153 | c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. 154 | 155 | d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. 156 | 157 | > Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at [creativecommons.org/policies](http://creativecommons.org/policies), Creative Commons does not authorize the use of the trademark “Creative Commons” or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses. 158 | > 159 | > Creative Commons may be contacted at creativecommons.org 160 | -------------------------------------------------------------------------------- /LICENSE.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b19c505a2e496a4409bcd4f8d5bd27cd 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Me*di*cine 2 | ### Code-driven component injection toolkit for [Unity](https://unity.com/). 3 | 4 | Sick and tired of assigning references between components by hand (and losing them when something goes wrong with Unity serialization)? Having a migraine from all the `GetComponent` calls sprinkled all over your codebase? 5 | 6 | ***Medicine*** is a collection of attributes that give you a code-driven, performance-oriented way to automatically hook up references between your components. Additionally, it comes with a toolbox of optimized versions of many standard component-related operations. 7 | 8 | > **Warning!** This library is experimental. Please [let me know](https://github.com/apkd/medicine/issues) about any issues you encounter. 9 | 10 | ## How to install 11 | Compatibility: ***Unity 2019.3 or newer*** 12 | 13 | Open "Add package from git URL" in the Unity Package Manager and paste the repository URL: 14 | ##### `https://github.com/apkd/Medicine.git` 15 | 16 | ## Features/examples 17 | #### Write cleaner components 18 | 19 | ```csharp 20 | class Vehicle : MonoBehaviour 21 | { 22 | // find components attached to the same GameObject 23 | [Inject] 24 | Rigidbody rigidbody { get; } 25 | 26 | // find components in child GameObjects (including self) 27 | [Inject.FromChildren] 28 | WheelCollider[] colliders { get; } 29 | 30 | // works well with interfaces and base/abstract types 31 | [Inject] 32 | IVehiclePilot pilot { get; } 33 | 34 | void OnEnable() 35 | { 36 | Debug.Log($"I have a rigidbody: {rigidbody}"); 37 | Debug.Log($"And a couple of wheels: {colliders.Length}"); 38 | Debug.Log($"And a pilot of type: {pilot.GetType()}"); 39 | } 40 | } 41 | ``` 42 | 43 | #### Create robust singleton objects, easily track active object instances 44 | ```csharp 45 | class Player : MonoBehaviour 46 | { 47 | // get reference to singleton objects (marked with the [Register.Single] attribute) 48 | [Inject.Single] 49 | LevelManager levelManager { get; } // getter always returns active instance 50 | 51 | // get array of all active instances of given script type (marked with the [Register.All] attribute) 52 | [Inject.All] 53 | Enemy[] enemies { get; } // getter always returns current set of active objects 54 | 55 | void OnEnable() 56 | { 57 | Debug.Log($"There's the level manager: {levelManager}"); 58 | Debug.Log($"I'm sensing some enemies lurking nearby: {enemies.Length}"); 59 | 60 | foreach (var enemy in enemies) 61 | Debug.Log($"Here's one: {enemy.name}"); 62 | } 63 | 64 | // cleanly access all (active and enabled) scripts implementing some interface 65 | [Inject.All] 66 | IPickup[] pickups { get; } 67 | 68 | void Update() 69 | { 70 | foreach (var pickup in pickups) 71 | if (pickup.IsInRange(transform.position)) 72 | pickup.Activate(this); 73 | } 74 | } 75 | 76 | // indicates that we want to to be able to inject all active instances of this script using [Inject.All]. 77 | // this array is always up-to-date 78 | // (even if you create/destroy registered object instances at runtime!) 79 | [Register.All] 80 | class Enemy : MonoBehaviour { /* implementation goes here */ } 81 | 82 | // indicates that there will only ever be one instance of this script active in the game at any given time. 83 | // properties injected with [Inject.Single] are always up-to-date 84 | // (even if you replace your singleton instance at runtime!) 85 | [Register.Single] 86 | class LevelManager : MonoBehaviour { /* implementation goes here */ } 87 | 88 | // allows us to obtain all (custom) scripts that implement this interface. 89 | // make sure the scripts are also registered using [Register.All]. 90 | // this also works for [Register.Single] and standard inheritance (abstract/derived classes). 91 | [Register.All] 92 | interface IPickup 93 | { 94 | bool IsInRange(Vector3 position); 95 | void Activate(Player player); 96 | } 97 | ``` 98 | 99 | #### More insight, less errors 100 | ```csharp 101 | class Gun : MonoBehaviour 102 | { 103 | // thanks to the fact that you're declaring your dependencies ahead of use, 104 | // your code automatically becomes more readable and maintainable 105 | [Inject.Single] 106 | BulletManager bulletManager { get; } 107 | 108 | // script will immediately log an error if the magazine is missing. 109 | // this indicates that the prefab is critically misconfigured and allows you to notice this early. 110 | // since the reference is immutable, we don't need to manually check this later in code. 111 | [Inject] 112 | Magazine magazine { get; } // immutable property! this is the recommended default because it 113 | // promises that we're always referring to the same object 114 | 115 | // script will immediately log an error if there are no child Renderers. 116 | // this indicates to the developer that the prefab is critically misconfigured. 117 | [Inject.FromChildren] 118 | Renderer[] renderers { get; } 119 | 120 | // this component is optional. if we don't put at least one DamageModifier component 121 | // on the GameObject, the reference will be null (you should handle that!) 122 | [Inject(Optional = true)] 123 | DamageModifier modifier { get; } 124 | 125 | // no error will be thrown if the collection is empty because of (Optional = true) 126 | // (an injected array will never be null) 127 | [Inject.FromChildren(Optional = true)] 128 | GunMod[] mods { get; } 129 | } 130 | ``` 131 | 132 | #### I still don't get it 133 | ```csharp 134 | // before 135 | class MyUglyUnreadableBugRiddenMessComponent : MonoBehaviour 136 | { 137 | Rigidbody rigidbody; 138 | MeshRenderer[] meshRenderers; 139 | 140 | void Awake() 141 | { 142 | rigidbody = GetComponent(); 143 | if (rigidbody == null) 144 | Debug.LogError($"No Rigidbody component attached to {name}!", this); 145 | 146 | meshRenderers = GetComponentsInChildren(); 147 | if (meshRenderers.Length == 0) 148 | Debug.LogError($"No child MeshRenderer components attached to {name}!", this); 149 | } 150 | } 151 | ``` 152 | ```csharp 153 | // after 154 | class MyElegantSelfDocumentingAutoDebuggingPromotionEarningGDCWorthyComponent : MonoBehaviour 155 | { 156 | [Inject] 157 | Rigidbody rigidbody { get; } 158 | 159 | [Inject.FromChildren] 160 | MeshRenderer[] meshRenderers { get; } 161 | } 162 | ``` 163 | 164 | #### Prototyping tricks 165 | ```csharp 166 | class Goose : MonoBehaviour 167 | { 168 | // easily make references public 169 | [Inject.FromChildren] 170 | public GooseNeck Neck { get; } // immutable! 171 | 172 | // simply add a setter if you're planning to pluck some 173 | [Inject.FromChildren] 174 | public GooseFeather[] Feathers { get; private set; } // not mutable from outside 175 | } 176 | ``` 177 | ```csharp 178 | class UsabilityTricks : MonoBehaviour 179 | { 180 | // easily access child transforms. 181 | // order of injected components is deterministic and depends on place in hierarchy. 182 | // (see: https://forum.unity.com/threads/getcomponentsinchildren.4582/#post-33983) 183 | [Inject.FromChildren] 184 | Transform[] childTransforms { get; } 185 | 186 | // components on inactive child GameObjects are omitted by default 187 | // (see: https://docs.unity3d.com/ScriptReference/Component.GetComponentsInChildren.html) 188 | [Inject.FromChildren(IncludeInactive = true)] 189 | Collider[] colliders { get; } 190 | 191 | // easily get a reference to a singleton ScriptableObject 192 | // (you first need to add it to preloaded assets in player settings) 193 | [Inject.Single] 194 | MyGameConfiguration gameConfig { get; } 195 | } 196 | ``` 197 | ```csharp 198 | class PerformanceTricks : MonoBehaviour 199 | { 200 | // cache transform reference to avoid extern call overhead 201 | [Inject] 202 | new Transform transform { get; } 203 | 204 | // easily refer to the main camera in your scene without additional work 205 | // (resolves Camera.main and returns a cached reference until the camera is destroyed) 206 | [Inject.Single] 207 | Camera camera { get; } 208 | } 209 | ``` 210 | ```csharp 211 | class LazyTricks : MonoBehaviour 212 | { 213 | // when you use "Lazy", the injection result is no longer stored in a local field in Awake(). 214 | // instead, a non-alloc version of GetComponents is used to obtain a temp array of *current* components. 215 | // useful, but be careful to use the getter sparingly and never store the array anywhere 216 | // because it will become invalid after next call 217 | [Inject.FromChildren.Lazy] 218 | Renderer[] currentRenderers { get; } 219 | 220 | void ShowActiveRenderers() // always up to date 221 | => Debug.Log($"Number of currently active renderers: {currentRenderers.Length}"); 222 | 223 | // because we aren't caching the components in Awake, the code won't break even when 224 | // we modify the transform hierarchy. hell, it even works in edit mode 225 | [Inject.FromParents.Lazy] 226 | Transform[] parentTransforms { get; } 227 | 228 | void WhereAmI() // always up to date 229 | => Debug.Log(string.Join(" -> ", parentTransforms.Select(x => x.name))); 230 | } 231 | ``` 232 | ```csharp 233 | // high performance update manager in <20 lines of code 234 | sealed class UpdateManager : MonoBehaviour 235 | { 236 | [Inject.All] 237 | TickableBase[] tickables { get; } 238 | 239 | void Update() 240 | { 241 | float dt = Time.deltaTime; 242 | 243 | foreach (var tickable in tickables) 244 | tickable.Tick(dt); 245 | } 246 | } 247 | [Register.All] 248 | abstract class TickableBase : MonoBehaviour 249 | { 250 | public abstract void Tick(float dt); 251 | } 252 | ``` 253 | 254 | ## API reference 255 | 256 | ### [Inject] 257 | **Automatically initializes the property in Awake() with a component (or an array of components) from the current GameObject.** 258 | 259 | This internally uses `GetComponent/GetComponents` - it finds the first component of matching type. 260 | 261 | Options: 262 | * ***Optional*** - Set this to true to allow the component to be missing. By default, injection will log an error if the component is missing (or if the array of components is empty). 263 | * ***IncludeInactive*** - not available because Unity doesn't have an equivalent `GetComponent`/`GetComponents` overload (injection always behaves as if `IncludeInactive` were true). 264 | 265 | Allowed access modifiers: 266 | * `{ get; }` 267 | * `{ get; private set; }` 268 | * `{ get; set; }` 269 | 270 | Examples: 271 | ```csharp 272 | [Inject] // single (first one found) component on current GameObject 273 | Collider collider { get; } 274 | 275 | [Inject] // all components on current GameObject 276 | Collider[] allColliders { get; } 277 | ``` 278 | 279 | ### [Inject.FromChildren] 280 | **Automatically initializes the property in Awake() with a component (or an array of components) from the current GameObject and/or child GameObjects.** 281 | 282 | This internally uses `GetComponentInChildren/GetComponentsInChildren` - it searches the GameObject's child hierarchy recursively to locate components. 283 | 284 | Options: 285 | * ***Optional*** - Set this to true to allow the component to be missing. By default, injection will log an error if the component is missing (or if the array of components is empty). 286 | * ***IncludeInactive*** - includes components that are disabled (or placed on inactive GameObjects). Keep in mind that the root GameObject is [always searched regardless](https://docs.unity3d.com/ScriptReference/Component.GetComponentsInChildren.html). 287 | 288 | Examples: 289 | ```csharp 290 | [Inject.FromChildren] // single (first one found) component on current GameObject or child GameObjects 291 | Collider collider { get; } 292 | 293 | [Inject.FromChildren] // all components on current GameObject or child GameObjects 294 | Collider[] allColliders { get; } 295 | 296 | [Inject.FromChildren(IncludeInactive = true)] // all components, including inactive child GameObjects 297 | Collider[] allCollidersIncludingInactive { get; } 298 | ``` 299 | 300 | ### [Inject.FromParents] 301 | **Automatically initializes the property in Awake() with a component (or an array of components) from the current GameObject and/or child GameObjects.** 302 | 303 | This internally uses `GetComponentInChildren/GetComponentsInChildren` - it searches the GameObject's child hierarchy recursively to locate components. 304 | 305 | Options: 306 | * ***Optional*** - Set this to true to allow the component to be missing. By default, injection will log an error if the component is missing (or if the array of components is empty). 307 | * ***IncludeInactive*** - (included for API symmetry with `GetComponentsInParent`, but won't do much since injection happens in Awake when parents are necessarily already active) 308 | 309 | Examples: 310 | ```csharp 311 | [Inject.FromParents] // single (first one found) component on current GameObject or parent GameObjects 312 | Collider collider { get; } 313 | 314 | [Inject.FromParents] // all components on current GameObject or parent GameObjects 315 | Collider[] allColliders { get; } 316 | ``` 317 | 318 | ### [Inject.Single] 319 | **Never write a singleton again! Makes the property return the currently registered singleton instance.** 320 | In order for the object to register itself as a singleton, the type needs to be marked with `[Register.Single]`. 321 | Can be used on static properties. 322 | 323 | > Protip: you can inject the main camera, too! Simply tag your camera property with `[Inject.Single]` and it will automatically return a cached reference to `Camera.Main`. 324 | 325 | Examples: 326 | ```csharp 327 | [Register.Single] // ScriptableObject-based singleton. 328 | // you need to put it in the "preloaded assets" in Project Settings 329 | class BulletConfig : ScriptableObject 330 | { 331 | public float BulletVelocity; 332 | } 333 | 334 | [Register.Single] // MonoBehaviour-based singleton. 335 | // you should put it in the scene or spawn it however you like 336 | class BulletSpawner : MonoBehaviour 337 | { 338 | // our BulletConfig instance is automatically available here 339 | [Inject.Single] 340 | BulletConfig bulletConfig { get; } 341 | 342 | public GameObject bulletPrefab; 343 | 344 | public void SpawnBullet(Vector3 position, Quaternion rotation, Vector3 direction) 345 | => Instantiate(bulletPrefab, position, rotation).GetComponent().velocity 346 | = direction.normalized * bulletConfig.BulletVelocity; 347 | } 348 | 349 | class Gun : MonoBehaviour 350 | { 351 | // our BulletSpawner instance is automatically available here 352 | [Inject.Single] 353 | BulletSpawner bulletSpawner { get; } 354 | 355 | // resolves Camera.main and returns a cached reference until the camera is destroyed 356 | [Inject.Single] 357 | Camera camera { get; } 358 | 359 | void Fire() 360 | => bulletSpawner.SpawnBullet(transform.position, transform.rotation, transform.forward); 361 | } 362 | ``` 363 | 364 | ### [Inject.All] 365 | **Never lose track of your component's instances again! Makes the property return the set of currently active objects of this type.** 366 | 367 | In order for the object to register itself in the active objects collection, the type needs to be marked with `[Register.All]`. 368 | Can be used on static properties. 369 | 370 | Examples: 371 | ```csharp 372 | class BulletUpdater : MonoBehaviour 373 | { 374 | // this property lets you access all active bullets in the game 375 | [Inject.All] 376 | public Bullet[] allBullets { get; } 377 | 378 | void Update() 379 | { 380 | foreach (var bullet in allBullets) 381 | /* do something with bullet */ 382 | } 383 | } 384 | 385 | [Register.All] // allows this class to be injected using [Inject.All] 386 | class Bullet : MonoBehaviour { } 387 | 388 | ``` 389 | 390 | ### [Register.Single] 391 | **Registers the type as a singleton that can be injected using `[Inject.Single]`.** 392 | This property will log an error if there is no registered singleton instance of this type, or if multiple objects try to register themselves as the active instance. You can change the registered instance at runtime by disabling/deleting the object 393 | 394 | **Supported types:** 395 | 396 | * ***MonoBehaviour***: 397 | The object will register/unregister itself as singleton in OnEnable/OnDisable. (If needed, you can use script execution order to make sure that the singleton registers itself before it is used) 398 | * ***ScriptableObject***: 399 | Adds a GUI to the object header that allows you to select the active ScriptableObject singleton instance. 400 | This instance is automatically added to preloaded assets (as the only instance of the type). 401 | * ***Interface***: 402 | Allows you to use `[Inject.Single]` with this interface type. Make sure the types you're injecting are also registered using `[Register.Single]`. 403 | 404 | ### [Register.All] 405 | **Tracks all active instances of the type so that they can be injected using `[Inject.All]`.** 406 | This creates a static collection of active instances of this type. The objects will automatically register/unregister themselves in OnEnable/OnDisable. This means you can assume are instances returned by the property are non-null. 407 | 408 | **Supported types:** 409 | 410 | * ***MonoBehaviour***: 411 | The object will automatically register/unregister itself in OnEnable/OnDisable. 412 | * ***ScriptableObject***: 413 | The object will automatically register/unregister itself in OnEnable/OnDisable. 414 | In practice, this returns all of the loaded instances of the ScriptableObject in your project. 415 | You might want to add them to preloaded assets (in Player Settings) to ensure they're available in build. 416 | * ***Interface***: 417 | Allows you to use `[Inject.All]` with this interface type. Make sure that the actual MonoBehaviour/ScriptableObject 418 | types you're injecting are also registered using `[Register.All]`. 419 | 420 | ### [Inject.Lazy], [Inject.FromChildren.Lazy], [Inject.FromParents.Lazy] 421 | Equivalent to regular `[Inject]`, but returns the current value every time the property is accessed (no caching, no null/empty checks). 422 | 423 | > **WARNING: The Inject.Lazy APIs use reuse the same static memory buffer to avoid allocating arrays. Improper handling of the result array *will* cause your app to crash.** Make sure you know what you're doing if you're planning to use these APIs. (Never store the temporary array reference outside of the local scope, do not use multiple temporary arrays at the same time) 424 | 425 | ### Medicine.NonAlloc 426 | **Collection of useful functions for high-performance Unity programming.** 427 | 428 | * `T[] FindObjectsOfType(bool includeInactive = false)` 429 | 430 | Faster version of `Object.FindObjectsOfType` (skips unnecessary array copying). 431 | 432 | * `T[] FindObjectsOfTypeAll()` 433 | 434 | Faster version of `Resources.FindObjectsOfTypeAll` (skips unnecessary array copying). 435 | 436 | * `T[] LoadAll(string path)` 437 | 438 | Faster version of `Resources.LoadAll` (skips unnecessary array copying). 439 | 440 | * `T[] GameObject.GetComponentsNonAlloc()` 441 | 442 | Extension method. Non-allocating version of `GameObject.GetComponents`. This re-uses the same static memory buffer to store the component array, so make sure you do not store the array reference anywhere. 443 | 444 | * `T[] GameObject.GetComponentsInChildrenNonAlloc(bool includeInactive = false)` 445 | 446 | Extension method. Non-allocating version of `GameObject.GetComponentsInChildren`. This re-uses the same static memory buffer to store the component array, so make sure you do not store the array reference anywhere. 447 | 448 | * `T[] GameObject.GetComponentsInParentNonAlloc(bool includeInactive = false)` 449 | 450 | Extension method. Non-allocating version of `GameObject.GetComponentsInParent`. This re-uses the same static memory buffer to store the component array, so make sure you do not store the array reference anywhere. 451 | 452 | * `T[] GameObject.GetComponentsInParentNonAlloc(bool includeInactive = false)` 453 | 454 | Extension method. Non-allocating version of `GameObject.GetComponentsInParent`. This re-uses the same static memory buffer to store the component array, so make sure you do not store the array reference anywhere. 455 | 456 | * `T[] GetArray(int length, bool clear = true) where T : class` 457 | 458 | Returns a temporary array of given length. This re-uses the same static memory buffer, so make sure you do not store the array reference anywhere. 459 | 460 | * `List GetList(bool clear = true) where T : class` 461 | 462 | Returns a temporary generic list of given length. This re-uses the same static memory buffer, so make sure you do not store the list reference anywhere. 463 | 464 | > **WARNING: Improper handling of the temporary array *will* cause your app to crash.** Make sure you know what you're doing if you're planning to use these APIs. (Never store the temporary array reference outside of the local scope, do not use multiple temporary arrays at the same time) 465 | 466 | ## FAQ 467 | 468 | ##### How does this work? 469 | The library uses an `ILPostProcessor` (based on [Mono.Cecil](https://github.com/Unity-Technologies/cecil)) to modify compiled assemblies. This allows it to emit additional method calls and error checks based on the attributes you placed in your code. Here's a [before and after](https://gist.github.com/apkd/406df729fa2d8ba78a50a01d4d4b4468) comparison of how the post-processing works. 470 | This approach is similar to how UNET used to work and how the `Entities.ForEach`/`Job.WithCode` ECS syntax is currently implemented. 471 | 472 | ##### Known issues 473 | - If you're hiding the `Awake` method of your base class, make sure you always call it in the child class (`base.Awake()`) for injection to work. If you have no `Awake` method in the base class, you can define an empty one, or call `this.Reinject();` instead. 474 | 475 | ##### Why plain arrays instead of IEnumerable/IReadOnlyArray/ImmutableList/CoolCustomCollection? 476 | Arrays get special treatment by the CLR that optimizes `foreach` iteration and `array[index]` accesses into basically direct memory accesses. This starts to matter on hot code paths, and means you can use `foreach` without losing out on performance. 477 | 478 | ##### Why properties instead of fields? 479 | - More visibility options (eg. `{ get; private set; }`) 480 | - Ability to easily replace the getter method with custom code (for example, case of singletons we can get the instance from `RuntimeHelpers.Singleton` which automatically includes additional error checking) 481 | -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6791e4bd8cb56df4e997ee25f1713230 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Src.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f523fadb0e924f69b15e24522a43af30 3 | timeCreated: 1592325318 -------------------------------------------------------------------------------- /Src/Attributes.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using JetBrains.Annotations; 3 | using UnityEngine; 4 | using static System.AttributeTargets; 5 | using static JetBrains.Annotations.ImplicitUseTargetFlags; 6 | 7 | // ReSharper disable MemberHidesStaticFromOuterClass 8 | namespace Medicine 9 | { 10 | /// 11 | /// Post-processed components are made to implement this interface. You can use it to access additional helper methods. 12 | /// See: . 13 | /// 14 | public interface IMedicineComponent 15 | { 16 | /// Calls the generated initialization method for given component type. 17 | void Inject(); 18 | } 19 | 20 | /// 21 | /// Automatically initializes the property in Awake() with a component (or an array of components) 22 | /// from the current GameObject using . 23 | /// 24 | /// Available options: Optional. 25 | [MeansImplicitUse] 26 | [UsedImplicitly(WithMembers)] 27 | [AttributeUsage(Property)] 28 | public sealed class Inject : Attribute 29 | { 30 | /// Set this to true to allow the component to be missing. 31 | /// By default, [Inject] will log an error if the component is missing (or if the array of components is empty). 32 | public bool Optional { get; set; } = false; 33 | 34 | /// 35 | /// Automatically calls every time the property is accessed. 36 | /// WARNING: Make sure you use the result array directly and not store any references to it, as they may become invalid on next call. 37 | /// 38 | /// A non-allocating variant of the method is used to limit GC allocation. 39 | [MeansImplicitUse] 40 | [UsedImplicitly(WithMembers)] 41 | [AttributeUsage(Property)] 42 | public sealed class Lazy : Attribute { } 43 | 44 | /// 45 | /// Automatically initializes the property in Awake() with a component (or an array of components) 46 | /// from the current GameObject and/or child GameObjects using . 47 | /// 48 | /// Available options: Optional, IncludeInactive. 49 | [MeansImplicitUse] 50 | [UsedImplicitly(WithMembers)] 51 | [AttributeUsage(Property)] 52 | public sealed class FromChildren : Attribute 53 | { 54 | /// Set this to true to allow the component to be missing. 55 | /// By default, [Inject.FromChildren] will log an error if the component is missing (or if the array of components is empty). 56 | public bool Optional { get; set; } = false; 57 | 58 | /// Set this to true to include components on inactive child GameObjects. Components on the root GameObject are always included. 59 | /// Keep in mind that this has nothing to do with whether the component is enabled - disabled components are included if the GameObject is active. 60 | public bool IncludeInactive { get; set; } = false; 61 | 62 | /// 63 | /// Automatically calls every time the property is accessed. 64 | /// WARNING: Make sure you use the result array directly and not store any references to it, as they may become invalid on next call. 65 | /// 66 | /// A non-allocating variant of the method is used to limit GC allocation. 67 | [MeansImplicitUse] 68 | [UsedImplicitly(WithMembers)] 69 | [AttributeUsage(Property)] 70 | public sealed class Lazy : Attribute 71 | { 72 | /// Set this to true to include components on inactive child GameObjects. Components on the root GameObject are always included. 73 | /// Keep in mind that this has nothing to do with whether the component is enabled - disabled components are included if the GameObject is active. 74 | public bool IncludeInactive { get; set; } = false; 75 | } 76 | } 77 | 78 | /// 79 | /// Automatically initializes the property in Awake() with a component (or an array of components) 80 | /// from the current GameObject using . 81 | /// 82 | /// Available options: Optional, IncludeInactive. 83 | [MeansImplicitUse] 84 | [UsedImplicitly(WithMembers)] 85 | [AttributeUsage(Property)] 86 | public sealed class FromParents : Attribute 87 | { 88 | /// Set this to true to allow the component to be missing. 89 | /// By default, [Inject.FromParents] will log an error if the component is missing (or if the array of components is empty). 90 | public bool Optional { get; set; } = false; 91 | 92 | /// Set this to true to include components on inactive parent GameObjects. Components on the root GameObject are always included. 93 | /// 94 | /// Note that this option is only included for API symmetry as it makes very little difference when we're injecting from parents 95 | /// (because injection is executed in Awake when parent GameObjects are pretty much guaranteed to be active). 96 | /// Keep in mind that this has nothing to do with whether the component is enabled - disabled components are included if the GameObject is active. 97 | /// 98 | #if UNITY_2020_1_OR_NEWER // this wasn't actually supported in GetComponentsInParent before 2020.1 99 | public bool IncludeInactive { get; set; } = false; 100 | #endif 101 | 102 | /// 103 | /// Automatically calls every time the property is accessed. 104 | /// WARNING: Make sure you use the result array directly and not store any references to it, as they may become invalid on next call. 105 | /// 106 | /// A non-allocating variant of the method is used to limit GC allocation. 107 | [MeansImplicitUse] 108 | [UsedImplicitly(WithMembers)] 109 | [AttributeUsage(Property)] 110 | public sealed class Lazy : Attribute 111 | { 112 | /// Set this to true to include components on inactive parent GameObjects. Components on the root GameObject are always included. 113 | /// Keep in mind that this has nothing to do with whether the component is enabled - disabled components are included if the GameObject is active. 114 | public bool IncludeInactive { get; set; } = false; 115 | } 116 | } 117 | 118 | /// 119 | /// Makes the property return the currently registered singleton instance. 120 | /// 121 | /// 122 | /// In order for the object to register itself as a singleton, the type needs to be marked with [Register.Single]. 123 | /// Can be used on static properties. 124 | /// 125 | [MeansImplicitUse] 126 | [UsedImplicitly(WithMembers)] 127 | [AttributeUsage(Property)] 128 | public sealed class Single : Attribute { } 129 | 130 | /// 131 | /// Makes the property return the set of currently active objects of this type. 132 | /// 133 | /// 134 | /// In order for the object to register itself in the active objects collection, the type needs to be marked with [Register.All]. 135 | /// Can be used on static properties. 136 | /// 137 | [MeansImplicitUse] 138 | [UsedImplicitly(WithMembers)] 139 | [AttributeUsage(Property)] 140 | public sealed class All : Attribute { } 141 | } 142 | 143 | /// 144 | /// This attribute class is a container for [Register.Single] and [Register.All] and should not be used directly. 145 | /// 146 | [UsedImplicitly] 147 | [AttributeUsage(Module)] // could have used a static class here, but that breaks IntelliSense when starting to type [Register.*] 148 | public sealed class Register : Attribute 149 | { 150 | /// 151 | /// Registers the type as a singleton that can be injected using [Inject.Single]. 152 | /// 153 | /// 154 | /// This property will log an error if there is no registered instance of this singleton, or if multiple objects 155 | /// try to register themselves as the active instance. You can change the registered instance at runtime. 156 | /// 157 | [MeansImplicitUse] 158 | [UsedImplicitly(WithMembers)] 159 | [AttributeUsage(Class | Interface)] 160 | public sealed class Single : Attribute 161 | { 162 | // Supported types: 163 | // --------------- 164 | // * MonoBehaviour: 165 | // The object will register/unregister itself as singleton in OnEnable/OnDisable. 166 | // * ScriptableObject: 167 | // Adds a GUI to the object header that allows you to select the active ScriptableObject singleton instance. 168 | // This instance is automatically added to preloaded assets (as the only instance of the type). 169 | // * Interface: 170 | // Allows you to use [Inject.Single] with this interface type. Make sure the types you're injecting are also 171 | // registered using [Register.Single]. 172 | } 173 | 174 | /// 175 | /// Tracks all active instances of the type so that they can be injected using [Inject.All]. 176 | /// 177 | /// 178 | /// This creates a static collection of active instances of this type. The objects will automatically register/unregister 179 | /// themselves in OnEnable/OnDisable. This means you can assume are instances returned by the property are non-null. 180 | /// 181 | [MeansImplicitUse] 182 | [UsedImplicitly(WithMembers)] 183 | [AttributeUsage(Class | Interface)] 184 | public sealed class All : Attribute 185 | { 186 | // Supported types: 187 | // --------------- 188 | // * MonoBehaviour: 189 | // The object will automatically register/unregister itself in OnEnable/OnDisable. 190 | // * ScriptableObject: 191 | // The object will automatically register/unregister itself in OnEnable/OnDisable. 192 | // In practice, this returns all of the loaded instances of the ScriptableObject in your project. 193 | // You might want to add them to preloaded assets (in Player Settings) to ensure they're available in build. 194 | // * Interface: 195 | // Allows you to use [Inject.All] with this interface type. Make sure that the actual MonoBehaviour/ScriptableObject 196 | // types you're injecting are also registered using [Register.All]. 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /Src/Attributes.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0d8204e1ec46f08489101ac2cb0baba3 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Src/Medicine.CodeGen.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8df8ce74ad834ab8bddc3441afcfc107 3 | timeCreated: 1592328843 -------------------------------------------------------------------------------- /Src/Medicine.CodeGen/CecilExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Reflection; 4 | using System.Threading; 5 | using Mono.Cecil; 6 | using Mono.Cecil.Cil; 7 | using Unity.Collections.LowLevel.Unsafe; 8 | using Unity.CompilationPipeline.Common.Diagnostics; 9 | using static System.StringComparison; 10 | using ICustomAttributeProvider = Mono.Cecil.ICustomAttributeProvider; 11 | 12 | namespace Medicine 13 | { 14 | static class CecilExtensions 15 | { 16 | static readonly ThreadLocal currentModule 17 | = new ThreadLocal(); 18 | 19 | static readonly int sourceDirectoryTrimLength 20 | = Environment.CurrentDirectory.Length + 1; 21 | 22 | public static ModuleDefinition CurrentModule 23 | { 24 | get => currentModule.Value; 25 | set => currentModule.Value = value; 26 | } 27 | 28 | public static string GetFilenameLineColumnString(this MethodDebugInformation methodDebugInformation) 29 | { 30 | if (methodDebugInformation.SequencePoints.FirstOrDefault() is SequencePoint seq) 31 | return $" (at {seq.Document.Url.Substring(sourceDirectoryTrimLength).Replace('\\', '/')}:{seq.StartLine})"; 32 | 33 | return ""; 34 | } 35 | 36 | public static DiagnosticMessage GetDiagnosticMessage(this MethodDefinition method, string messageData, DiagnosticType diagnosticType = DiagnosticType.Warning) 37 | { 38 | var seq = method.DebugInformation?.SequencePoints?.FirstOrDefault(); 39 | 40 | var file = seq?.Document.Url; 41 | var line = seq?.StartLine ?? -1; 42 | var column = seq?.StartColumn ?? -1; 43 | 44 | string GetMessage() 45 | { 46 | if (string.IsNullOrWhiteSpace(file)) 47 | return $"{messageData}\n\nMedicineILPostProcessor() (unknown location)"; 48 | 49 | return line < 0 50 | ? $"{messageData}\n\nMedicineILPostProcessor() (at {file})" 51 | : $"{messageData}\n\nMedicineILPostProcessor() (at {file}:{line})"; 52 | } 53 | 54 | return new DiagnosticMessage 55 | { 56 | File = file, 57 | Line = line, 58 | Column = column, 59 | DiagnosticType = diagnosticType, 60 | MessageData = GetMessage(), 61 | }; 62 | } 63 | 64 | public static string GetName(this CustomAttribute attribute) 65 | => $"[{attribute.AttributeType.FullName.Replace('/', '.')}]"; 66 | 67 | public static TypeReference Import(this TypeReference typeReference) 68 | => CurrentModule.ImportReference(typeReference); 69 | 70 | public static FieldReference Import(this FieldReference fieldReference) 71 | => CurrentModule.ImportReference(fieldReference); 72 | 73 | public static TypeReference Import(this Type type) 74 | => CurrentModule.ImportReference(type); 75 | 76 | public static MethodReference Import(this MethodReference methodReference) 77 | => CurrentModule.ImportReference(methodReference); 78 | 79 | public static MethodReference Import(this MethodBase methodInfo) 80 | => CurrentModule.ImportReference(methodInfo); 81 | 82 | public static FieldReference Import(this FieldInfo fieldInfo) 83 | => CurrentModule.ImportReference(fieldInfo); 84 | 85 | public static TypeDefinition ResolveFast(this TypeReference typeReference) 86 | => typeReference is TypeDefinition typeDefinition 87 | ? typeDefinition 88 | : typeReference.Resolve(); 89 | 90 | public static FieldDefinition ResolveFast(this FieldReference fieldReference) 91 | => fieldReference is FieldDefinition fieldDefinition 92 | ? fieldDefinition 93 | : fieldReference.Resolve(); 94 | 95 | public static bool HasAttribute(this ICustomAttributeProvider type) where T : System.Attribute 96 | { 97 | if (type.CustomAttributes is var attributes) 98 | for (int i = 0, n = attributes.Count; i < n; i++) 99 | if (attributes[i].Is()) 100 | return true; 101 | 102 | return false; 103 | } 104 | 105 | public static bool IsCompilerGenerated(this ICustomAttributeProvider type) 106 | => type.HasAttribute(); 107 | 108 | public static bool BaseTypesInclude(this TypeReference type, Func filter) 109 | { 110 | try 111 | { 112 | while ((type = type.ResolveFast().BaseType) != null) 113 | if (filter(type)) 114 | return true; 115 | } 116 | catch 117 | { 118 | /* silently ignore exceptions; we're probably unable to resolve the base type's assembly. assume false result */ 119 | } 120 | 121 | return false; 122 | } 123 | 124 | public static int CalculateDepthOfInheritanceTree(this TypeReference type) 125 | { 126 | int i = 0; 127 | 128 | try 129 | { 130 | while ((type = type.ResolveFast().BaseType) != null) 131 | i += 1; 132 | } 133 | catch 134 | { 135 | /* silently ignore exceptions; we're probably unable to resolve the base type's assembly. */ 136 | } 137 | 138 | return i; 139 | } 140 | 141 | public static bool DerivesFrom(this TypeReference type) 142 | => type.BaseTypesInclude(x => x.Is()); 143 | 144 | public static bool Is(this TypeReference @this) 145 | { 146 | var type = @this; 147 | var other = typeof(T); 148 | 149 | while (true) 150 | { 151 | if (type.Name != other.Name) 152 | return false; 153 | 154 | var declaringType = type.DeclaringType; 155 | var otherDeclaringType = other.DeclaringType; 156 | 157 | bool selfHasNoDeclaringType = declaringType == null; 158 | bool otherHasNoDeclaringType = otherDeclaringType == null; 159 | 160 | if (selfHasNoDeclaringType != otherHasNoDeclaringType) 161 | return false; 162 | 163 | if (selfHasNoDeclaringType) 164 | return (type.Namespace ?? "") == (other.Namespace ?? ""); 165 | 166 | type = declaringType; 167 | other = otherDeclaringType; 168 | } 169 | } 170 | 171 | public static bool Is(this CustomAttribute attribute) 172 | => attribute.AttributeType.Is(); 173 | 174 | public static bool HasProperty(this CustomAttribute attribute, string key, T value) 175 | { 176 | for (int i = 0, n = attribute.Properties.Count; i < n; ++i) 177 | if (attribute.Properties[i] is var property) 178 | if (property.Name == key) 179 | if (property.Argument.Value.Equals(value)) 180 | return true; 181 | 182 | return false; 183 | } 184 | 185 | public static bool HasFlagNonAlloc(this T enumValue, T enumFlag) where T : struct, Enum 186 | { 187 | int a = UnsafeUtility.EnumToInt(enumValue); 188 | int b = UnsafeUtility.EnumToInt(enumFlag); 189 | return (a & b) == b; 190 | } 191 | 192 | public static TypeReference UnwrapArrayElementType(this TypeReference type) 193 | => type is ArrayType arrayType ? arrayType.ElementType : type; 194 | 195 | public static TypeReference UnwrapGenericInstanceType(this TypeReference type) 196 | => type is GenericInstanceType genericInstanceType ? genericInstanceType.ElementType : type; 197 | 198 | public static MethodReference MakeBaseTypeHostInstanceGeneric(this MethodReference self) 199 | => self.DeclaringType.HasGenericParameters 200 | ? self.MakeHostInstanceGeneric(self.DeclaringType.GenericParameters.Cast().ToArray()) 201 | : self; 202 | 203 | public static MethodReference MakeHostInstanceGeneric(this MethodReference self, params TypeReference[] args) 204 | { 205 | var reference = new MethodReference(self.Name, self.ReturnType, self.DeclaringType.UnwrapGenericInstanceType().MakeGenericInstanceType(args)) 206 | { 207 | HasThis = self.HasThis, 208 | ExplicitThis = self.ExplicitThis, 209 | CallingConvention = self.CallingConvention, 210 | }; 211 | var referenceParameters = reference.Parameters; 212 | var referenceGenericParameters = reference.GenericParameters; 213 | 214 | var parameters = self.Parameters; 215 | for (int i = 0, n = parameters.Count; i < n; i++) 216 | referenceParameters.Add(new ParameterDefinition(parameters[i].ParameterType)); 217 | 218 | var genericParameters = self.GenericParameters; 219 | for (int i = 0, n = genericParameters.Count; i < n; i++) 220 | referenceGenericParameters.Add(new GenericParameter(genericParameters[i].Name, reference)); 221 | 222 | return reference; 223 | } 224 | 225 | public static GenericInstanceMethod MakeGenericInstanceMethod(this MethodReference method, params TypeReference[] genericArguments) 226 | { 227 | if (method.GenericParameters.Count != genericArguments.Length) 228 | throw new ArgumentException($"method.GenericParameters.Count({method.GenericParameters.Count}) != genericArguments.Length({genericArguments.Length})"); 229 | 230 | var methodGenericInstance = new GenericInstanceMethod(method); 231 | var instanceGenericArguments = methodGenericInstance.GenericArguments; 232 | 233 | for (int i = 0, n = genericArguments.Length; i < n; i++) 234 | instanceGenericArguments.Add(genericArguments[i]); 235 | 236 | return methodGenericInstance; 237 | } 238 | 239 | public static GenericInstanceType MakeGenericInstanceType(this TypeReference type, params TypeReference[] genericArguments) 240 | { 241 | if (type.GenericParameters.Count != genericArguments.Length) 242 | throw new ArgumentException($"type.GenericParameters.Count({type.GenericParameters.Count}) != genericArguments.Length({genericArguments.Length})"); 243 | 244 | var typeGenericInstance = new GenericInstanceType(type); 245 | var instanceGenericArguments = typeGenericInstance.GenericArguments; 246 | 247 | for (int i = 0, n = genericArguments.Length; i < n; i++) 248 | instanceGenericArguments.Add(genericArguments[i]); 249 | 250 | return typeGenericInstance; 251 | } 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /Src/Medicine.CodeGen/CecilExtensions.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 805c0c262b274103b2d94f8cdc4c4ded 3 | timeCreated: 1590930105 -------------------------------------------------------------------------------- /Src/Medicine.CodeGen/EditorGUI.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 95d249b7bd50448898d18d313b6b1ce5 3 | timeCreated: 1591810279 -------------------------------------------------------------------------------- /Src/Medicine.CodeGen/EditorGUI/SingletonObjectHeaderGUI.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Medicine; 3 | using UnityEditor; 4 | using UnityEngine; 5 | using static System.Reflection.BindingFlags; 6 | using Object = UnityEngine.Object; 7 | 8 | [InitializeOnLoad] 9 | static class SingletonObjectHeaderGUI 10 | { 11 | static readonly Type[] typesWithInjectSingleAttribute; 12 | 13 | static readonly GUIContent label = new GUIContent( 14 | text: "Set as current active singleton instance", 15 | tooltip: "This adds the ScriptableObject to Preloaded Assets (in Player Settings), which makes it resolvable using [Inject.Single]." 16 | ); 17 | 18 | static SingletonObjectHeaderGUI() 19 | { 20 | var types = TypeCache.GetTypesWithAttribute(); 21 | typesWithInjectSingleAttribute = new Type[types.Count]; 22 | types.CopyTo(typesWithInjectSingleAttribute, 0); 23 | 24 | Editor.finishedDefaultHeaderGUI += DrawSingletonGUI; 25 | } 26 | 27 | static void DrawSingletonGUI(Editor editor) 28 | { 29 | var target = editor.target; 30 | 31 | if (!(target is ScriptableObject)) 32 | return; 33 | 34 | var editorTargets = editor.targets; 35 | if (editorTargets.Length > 1) 36 | return; 37 | 38 | var targetObject = target; 39 | var type = targetObject.GetType(); 40 | 41 | bool hasInjectSingleAttr 42 | = Array.IndexOf(typesWithInjectSingleAttribute, type) >= 0; 43 | 44 | if (!hasInjectSingleAttr) 45 | return; 46 | 47 | var preloadedAssets = PlayerSettings.GetPreloadedAssets(); 48 | 49 | bool isAdded = false; 50 | 51 | foreach (var asset in preloadedAssets) 52 | if (asset == targetObject) 53 | isAdded = true; 54 | 55 | var controlRect = EditorGUILayout.GetControlRect( 56 | hasLabel: false, 57 | height: 14, 58 | options: Array.Empty() 59 | ); 60 | 61 | var oldColor = GUI.color; 62 | var color = oldColor; 63 | color.a = 0.8f; 64 | GUI.color = color; 65 | 66 | bool shouldBeAdded = EditorGUI.ToggleLeft( 67 | position: controlRect, 68 | label: label, 69 | value: isAdded 70 | ); 71 | 72 | GUI.color = oldColor; 73 | 74 | if (shouldBeAdded == isAdded) 75 | return; 76 | 77 | var preloadedAssetsList = NonAlloc.GetList(); 78 | 79 | // remove all objects of target object's type 80 | // (to avoid multiple instances of the same singleton type being registered) 81 | foreach (var asset in preloadedAssets) 82 | if (!asset || asset.GetType() != type) 83 | preloadedAssetsList.Add(asset); 84 | 85 | if (shouldBeAdded) 86 | preloadedAssetsList.Add(targetObject); 87 | 88 | PlayerSettings.SetPreloadedAssets(preloadedAssetsList.ToArray()); 89 | 90 | if (shouldBeAdded && Application.isPlaying) 91 | { 92 | // get generic instance of the singleton helper type 93 | var helper = typeof(RuntimeHelpers.Singleton<>).MakeGenericType(type); 94 | 95 | // get current singleton instance 96 | var currentInstance = helper.GetField("instance", Static | NonPublic).GetValue(null); 97 | 98 | // unregister current singleton instance 99 | if (currentInstance as Object) 100 | helper.GetMethod("UnregisterInstance", Static | Public).Invoke(null, new[] { currentInstance }); 101 | 102 | // replace with new singleton instance 103 | helper.GetMethod("RegisterInstance", Static | Public).Invoke(null, new object[] { targetObject }); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Src/Medicine.CodeGen/EditorGUI/SingletonObjectHeaderGUI.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8c4f75af851c4573b8793f6ff75245e1 3 | timeCreated: 1591810238 -------------------------------------------------------------------------------- /Src/Medicine.CodeGen/ILPostProcessor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f478b173628244319b2cf6ec29cf0def 3 | timeCreated: 1591810622 -------------------------------------------------------------------------------- /Src/Medicine.CodeGen/ILPostProcessor/MedicineILPostProcessor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using JetBrains.Annotations; 6 | using Mono.Cecil; 7 | using Mono.Cecil.Cil; 8 | using Unity.CompilationPipeline.Common.Diagnostics; 9 | using Unity.CompilationPipeline.Common.ILPostProcessing; 10 | using static System.StringComparison; 11 | 12 | namespace Medicine 13 | { 14 | [UsedImplicitly] 15 | sealed class MedicineILPostProcessor : ILPostProcessor 16 | { 17 | public override ILPostProcessor GetInstance() 18 | => this; 19 | 20 | public override bool WillProcess(ICompiledAssembly compiledAssembly) 21 | { 22 | var compiledAssemblyName = compiledAssembly.Name; 23 | 24 | #if MEDICINE_DISABLE 25 | return false; 26 | #endif 27 | 28 | if (compiledAssemblyName == "Unity.Medicine.CodeGen") 29 | return false; 30 | 31 | if (compiledAssemblyName == "Medicine") 32 | return false; 33 | 34 | if (compiledAssemblyName == "Assembly-CSharp") 35 | return true; 36 | 37 | if (compiledAssemblyName == "Assembly-CSharp-firstpass") 38 | return true; 39 | 40 | foreach (string reference in compiledAssembly.References) 41 | if (reference.EndsWith("Medicine.dll", Ordinal)) 42 | return true; 43 | 44 | return false; 45 | } 46 | 47 | public ILPostProcessResult PostProcessInternal(ICompiledAssembly compiledAssembly) 48 | { 49 | AssemblyDefinition assemblyDefinition; 50 | 51 | #if MEDICINE_IL_DEBUG 52 | using (NonAlloc.Benchmark.Start($"GetAssemblyDefinition ({compiledAssembly.Name})")) 53 | #endif 54 | assemblyDefinition = PostProcessorAssemblyResolver.GetAssemblyDefinitionFor(compiledAssembly); 55 | 56 | try 57 | { 58 | CecilExtensions.CurrentModule = assemblyDefinition.MainModule; 59 | PostProcessorContext context; 60 | #if MEDICINE_IL_DEBUG 61 | using (NonAlloc.Benchmark.Start($"CreatePostProcessorContext ({compiledAssembly.Name})")) 62 | #endif 63 | context = new PostProcessorContext(assemblyDefinition.MainModule); 64 | 65 | #if MEDICINE_IL_DEBUG 66 | using (NonAlloc.Benchmark.Start($"MedicineInjection ({compiledAssembly.Name})")) 67 | #endif 68 | new InjectionPostProcessor(context).ProcessAssembly(); 69 | 70 | var pe = new MemoryStream(capacity: 1024 * 64); 71 | var pdb = new MemoryStream(capacity: 1024 * 16); 72 | 73 | var writerParameters = new WriterParameters 74 | { 75 | SymbolWriterProvider = new PortablePdbWriterProvider(), 76 | SymbolStream = pdb, 77 | WriteSymbols = true, 78 | }; 79 | 80 | assemblyDefinition.Write(pe, writerParameters); 81 | var inMemoryAssembly = new InMemoryAssembly(pe.ToArray(), pdb.ToArray()); 82 | 83 | return new ILPostProcessResult(inMemoryAssembly, context.DiagnosticMessages); 84 | } 85 | catch (Exception ex) 86 | { 87 | Console.WriteLine(ex.ToString()); 88 | var error = new DiagnosticMessage 89 | { 90 | MessageData = $"Unexpected exception while post-processing assembly {compiledAssembly.Name}: {ex.Message}", 91 | DiagnosticType = DiagnosticType.Error, 92 | }; 93 | return new ILPostProcessResult(compiledAssembly.InMemoryAssembly, new List { error }); 94 | } 95 | finally 96 | { 97 | CecilExtensions.CurrentModule.Dispose(); 98 | CecilExtensions.CurrentModule = null; 99 | } 100 | } 101 | 102 | public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly) 103 | { 104 | try 105 | { 106 | if (!WillProcess(compiledAssembly)) 107 | return null; 108 | 109 | #if MEDICINE_IL_DEBUG 110 | using (NonAlloc.Benchmark.Start($"PostProcessInternal ({compiledAssembly.Name})")) 111 | #endif 112 | return PostProcessInternal(compiledAssembly); 113 | } 114 | catch (Exception ex) 115 | { 116 | Console.WriteLine(ex.ToString()); 117 | return null; 118 | } 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Src/Medicine.CodeGen/ILPostProcessor/MedicineILPostProcessor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2a157f9456b64ad9a6b1a1c98619258d 3 | timeCreated: 1590926626 -------------------------------------------------------------------------------- /Src/Medicine.CodeGen/ILPostProcessor/PostProcessorAssemblyResolver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Threading; 7 | using Mono.Cecil; 8 | using Mono.Cecil.Cil; 9 | using Unity.CompilationPipeline.Common.ILPostProcessing; 10 | using static System.StringComparison; 11 | 12 | namespace Medicine 13 | { 14 | sealed class PostProcessorReflectionImporter : DefaultReflectionImporter 15 | { 16 | const string SystemPrivateCoreLib = "System.Private.CoreLib"; 17 | readonly AssemblyNameReference correctCorlib; 18 | 19 | public PostProcessorReflectionImporter(ModuleDefinition module) : base(module) 20 | => correctCorlib = module 21 | .AssemblyReferences 22 | .FirstOrDefault( 23 | assembly => assembly.Name == "mscorlib" || assembly.Name == "netstandard" || assembly.Name == SystemPrivateCoreLib 24 | ); 25 | 26 | public override AssemblyNameReference ImportReference(AssemblyName reference) 27 | => correctCorlib == null || reference.Name != SystemPrivateCoreLib 28 | ? base.ImportReference(reference) 29 | : correctCorlib; 30 | } 31 | 32 | sealed class PostProcessorReflectionImporterProvider : IReflectionImporterProvider 33 | { 34 | public IReflectionImporter GetReflectionImporter(ModuleDefinition module) 35 | => new PostProcessorReflectionImporter(module); 36 | } 37 | 38 | sealed class PostProcessorAssemblyResolver : IAssemblyResolver 39 | { 40 | readonly Dictionary<(string filename, DateTime modified), AssemblyDefinition> cache 41 | = new Dictionary<(string, DateTime), AssemblyDefinition>(); 42 | 43 | readonly string compiledAssemblyName; 44 | readonly string[] referenceDirectoryNames; 45 | readonly string[] referenceFileNames; 46 | readonly string[] references; 47 | 48 | AssemblyDefinition selfAssembly; 49 | 50 | public PostProcessorAssemblyResolver(ICompiledAssembly compiledAssembly) 51 | { 52 | compiledAssemblyName = compiledAssembly.Name; 53 | references = compiledAssembly.References; 54 | referenceFileNames = references.Select(Path.GetFileName).ToArray(); 55 | referenceDirectoryNames = references.Select(Path.GetDirectoryName).Distinct().ToArray(); 56 | } 57 | 58 | public AssemblyDefinition Resolve(AssemblyNameReference name) 59 | => Resolve(name, new ReaderParameters(ReadingMode.Deferred)); 60 | 61 | public AssemblyDefinition Resolve(AssemblyNameReference name, ReaderParameters parameters) 62 | { 63 | if (name.Name == compiledAssemblyName) 64 | return selfAssembly; 65 | 66 | string filename = FindFile(name).Replace('\\', '/'); 67 | if (string.IsNullOrWhiteSpace(filename)) 68 | return null; 69 | 70 | var cacheKey = (fileName: filename, File.GetLastWriteTime(filename)); 71 | 72 | if (cache.TryGetValue(cacheKey, out var result)) 73 | return result; 74 | 75 | parameters.AssemblyResolver = this; 76 | 77 | var ms = MemoryStreamFor(filename); 78 | 79 | string candidate1 = filename + ".pdb"; 80 | string candidate2 = filename.Substring(filename.Length - 4) + ".pdb"; 81 | 82 | if (File.Exists(candidate1)) 83 | parameters.SymbolStream = MemoryStreamFor(candidate1); 84 | else if (File.Exists(candidate2)) 85 | parameters.SymbolStream = MemoryStreamFor(candidate2); 86 | 87 | var assemblyDefinition = AssemblyDefinition.ReadAssembly(ms, parameters); 88 | cache.Add(cacheKey, assemblyDefinition); 89 | return assemblyDefinition; 90 | } 91 | 92 | void IDisposable.Dispose() { } 93 | 94 | public static AssemblyDefinition GetAssemblyDefinitionFor(ICompiledAssembly compiledAssembly) 95 | { 96 | var resolver = new PostProcessorAssemblyResolver(compiledAssembly); 97 | 98 | var assemblyDefinition = AssemblyDefinition.ReadAssembly( 99 | stream: new MemoryStream(compiledAssembly.InMemoryAssembly.PeData), 100 | parameters: new ReaderParameters 101 | { 102 | SymbolStream = new MemoryStream(compiledAssembly.InMemoryAssembly.PdbData), 103 | SymbolReaderProvider = new PortablePdbReaderProvider(), 104 | AssemblyResolver = resolver, 105 | ReflectionImporterProvider = new PostProcessorReflectionImporterProvider(), 106 | ReadingMode = ReadingMode.Immediate, 107 | } 108 | ); 109 | 110 | resolver.selfAssembly = assemblyDefinition; 111 | 112 | return assemblyDefinition; 113 | } 114 | 115 | public string FindFile(AssemblyNameReference name) 116 | => FindFile(name.Name); 117 | 118 | public string FindFile(string name) 119 | { 120 | string nameString = name; 121 | string nameStringDll = nameString + ".dll"; 122 | string nameStringExe = nameString + ".exe"; 123 | 124 | for (var i = 0; i < referenceFileNames.Length; i++) 125 | { 126 | string referenceFileName = referenceFileNames[i]; 127 | 128 | if (referenceFileName == nameStringDll) 129 | return references[i]; 130 | if (referenceFileName == nameStringExe) 131 | return references[i]; 132 | } 133 | 134 | foreach (var parentDir in referenceDirectoryNames) 135 | { 136 | string candidate = Path.Combine(parentDir, nameStringDll); 137 | if (File.Exists(candidate)) 138 | return candidate; 139 | } 140 | 141 | return null; 142 | } 143 | 144 | static MemoryStream MemoryStreamFor(string fileName) 145 | => Retry( 146 | retryCount: 10, 147 | waitTime: TimeSpan.FromSeconds(1), 148 | func: () => 149 | { 150 | using (var fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) 151 | { 152 | var byteArray = new byte[fs.Length]; 153 | var readLength = fs.Read(byteArray, 0, (int)fs.Length); 154 | if (readLength != fs.Length) 155 | throw new InvalidOperationException("File read length is not full length of file."); 156 | 157 | return new MemoryStream(byteArray); 158 | } 159 | }); 160 | 161 | static MemoryStream Retry(int retryCount, in TimeSpan waitTime, Func func) 162 | { 163 | try 164 | { 165 | return func(); 166 | } 167 | catch (IOException) 168 | { 169 | if (retryCount == 0) 170 | throw; 171 | 172 | Console.WriteLine($"Caught IO Exception, trying {retryCount} more times"); 173 | Thread.Sleep(waitTime); 174 | return Retry(retryCount - 1, waitTime, func); 175 | } 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /Src/Medicine.CodeGen/ILPostProcessor/PostProcessorAssemblyResolver.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6ef116b9c29f444383144aa482e0fd39 3 | timeCreated: 1590927215 -------------------------------------------------------------------------------- /Src/Medicine.CodeGen/ILPostProcessor/PostProcessorContext.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Mono.Cecil; 4 | using Unity.CompilationPipeline.Common.Diagnostics; 5 | 6 | namespace Medicine 7 | { 8 | sealed class PostProcessorContext 9 | { 10 | public readonly ModuleDefinition Module; 11 | public readonly TypeDefinition[] Types; 12 | public readonly List DiagnosticMessages; 13 | 14 | public PostProcessorContext(ModuleDefinition module) 15 | { 16 | Module = module; 17 | 18 | // types are sorted based on the depth of inheritance tree 19 | // this ensures that base types have their Awake()/OnEnable() methods emitted by the time we process the derived types 20 | Types = GetAllTypeDefinitions(module).OrderBy(x => x.CalculateDepthOfInheritanceTree()).ToArray(); 21 | 22 | DiagnosticMessages = new List(); 23 | } 24 | 25 | static List GetAllTypeDefinitions(ModuleDefinition moduleDefinition) 26 | { 27 | var result = new List(capacity: moduleDefinition.Types.Count * 2); 28 | 29 | void AppendTypesRecursive(TypeDefinition parent) 30 | { 31 | result.Add(parent); 32 | var nested = parent.NestedTypes; 33 | for (int i = 0, n = nested.Count; i < n; i++) 34 | AppendTypesRecursive(nested[i]); 35 | } 36 | 37 | var topLevelTypes = moduleDefinition.Types; 38 | 39 | for (int i = 0, n = topLevelTypes.Count; i < n; i++) 40 | AppendTypesRecursive(topLevelTypes[i]); 41 | 42 | return result; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Src/Medicine.CodeGen/ILPostProcessor/PostProcessorContext.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5ae228626c0e4fb79db7501d981f53ab 3 | timeCreated: 1591871857 -------------------------------------------------------------------------------- /Src/Medicine.CodeGen/InjectionPostProcessor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics.CodeAnalysis; 4 | using System.Linq; 5 | using System.Reflection; 6 | using Mono.Cecil; 7 | using Mono.Cecil.Cil; 8 | using Mono.Collections.Generic; 9 | using Unity.CompilationPipeline.Common.Diagnostics; 10 | using UnityEngine; 11 | using static System.StringComparison; 12 | using static Mono.Cecil.Cil.Instruction; 13 | using MethodAttributes = Mono.Cecil.MethodAttributes; 14 | using TypeAttributes = Mono.Cecil.TypeAttributes; 15 | 16 | namespace Medicine 17 | { 18 | /// Exceptions that indicate compiler warnings (recoverable errors). 19 | sealed class MedicineWarning : Exception 20 | { 21 | public MethodDefinition Site { get; } 22 | 23 | public MedicineWarning(string message, PropertyDefinition property) : base(message) 24 | => Site = property.GetMethod 25 | ?? property.SetMethod 26 | ?? property.DeclaringType.Properties.FirstOrDefault(x => x.GetMethod != null)?.GetMethod 27 | ?? property.DeclaringType.Properties.FirstOrDefault(x => x.SetMethod != null)?.SetMethod 28 | ?? property.DeclaringType.Methods.FirstOrDefault(x => x.DebugInformation != null); 29 | } 30 | 31 | /// Exceptions that indicate compiler errors (unrecoverable errors, eg. ones that occur mid-weaving). 32 | sealed class MedicineError : Exception 33 | { 34 | public MethodDefinition Site { get; } 35 | 36 | public MedicineError(string message, PropertyDefinition property) 37 | : this(message, property.DeclaringType) { } 38 | 39 | public MedicineError(string message, TypeDefinition type) : base(message) 40 | => Site = type.Properties.FirstOrDefault(x => x.GetMethod != null)?.GetMethod 41 | ?? type.Properties.FirstOrDefault(x => x.SetMethod != null)?.SetMethod 42 | ?? type.Methods.FirstOrDefault(x => x.DebugInformation != null); 43 | } 44 | 45 | sealed class InjectionPostProcessor 46 | { 47 | readonly PostProcessorContext context; 48 | 49 | public InjectionPostProcessor(PostProcessorContext context) 50 | => this.context = context; 51 | 52 | public void ProcessAssembly() 53 | { 54 | var properties = new List<(TypeDefinition, FieldReference, PropertyDefinition, CustomAttribute)>(capacity: 256); 55 | 56 | foreach (var type in context.Types) 57 | { 58 | var typeProperties = type.Properties; 59 | 60 | for (int i = 0, n = type.Properties.Count; i < n; i++) 61 | { 62 | var property = typeProperties[i]; 63 | 64 | CustomAttribute GetMedicineInjectAttribute() 65 | { 66 | for (int j = 0, m = property.CustomAttributes.Count; j < m; j++) 67 | if (property.CustomAttributes[j] is var attr) 68 | if (attr.AttributeType.FullName.StartsWith($"{nameof(Medicine)}.{nameof(Inject)}", Ordinal)) 69 | return attr; 70 | 71 | return null; 72 | } 73 | 74 | var attribute = GetMedicineInjectAttribute(); 75 | if (attribute == null) 76 | continue; 77 | 78 | FieldReference GetPropertyBackingField() 79 | { 80 | // for an auto-implemented property getter, ldfld/ldsfld will be the first/second instruction 81 | int m = Math.Min(property.GetMethod.Body.Instructions.Count, 2); 82 | 83 | // look for ldfld/ldsfld instruction and return operand 84 | for (int j = 0; j < m; j++) 85 | if (property.GetMethod.Body.Instructions[j] is var instr) 86 | if (instr.OpCode == OpCodes.Ldfld || instr.OpCode == OpCodes.Ldsfld) 87 | return instr.Operand as FieldReference; 88 | 89 | return null; 90 | } 91 | 92 | if (property.GetMethod == null) 93 | { 94 | context.DiagnosticMessages.Add( 95 | property.GetMethod.GetDiagnosticMessage( 96 | $"Property {property.PropertyType.Name} {property.Name} needs to have an auto-implemented getter in order to use injection.")); 97 | continue; 98 | } 99 | 100 | var backingField = GetPropertyBackingField(); 101 | var backingFieldDef = backingField.ResolveFast(); 102 | 103 | if (backingField == null || !backingFieldDef.IsCompilerGenerated() || backingField.FieldType.FullName != property.PropertyType.FullName || backingFieldDef.IsPublic) 104 | { 105 | context.DiagnosticMessages.Add( 106 | property.GetMethod.GetDiagnosticMessage( 107 | $"Property {property.PropertyType.Name} {property.Name} needs to be an auto-implemented property to use injection. For example: " + 108 | $"{attribute.GetName()} {property.PropertyType.Name} {property.Name} {{ get; }}")); 109 | 110 | continue; 111 | } 112 | 113 | if (backingFieldDef.HasAttribute()) 114 | { 115 | context.DiagnosticMessages.Add( 116 | property.GetMethod.GetDiagnosticMessage( 117 | $"Field {backingField.FullName} cannot be serialized")); 118 | 119 | continue; 120 | } 121 | 122 | properties.Add((type, backingField, property, attribute)); 123 | } 124 | } 125 | 126 | foreach (var (type, field, property, attribute) in properties) 127 | { 128 | try 129 | { 130 | TypeDefinition propertyType; 131 | 132 | try 133 | { 134 | // try to resolve PropertyType to abort early if something's wrong 135 | propertyType = property.PropertyType.ResolveFast() ?? throw new NullReferenceException(); 136 | } 137 | catch 138 | { 139 | if (property.PropertyType is GenericParameter) 140 | throw new MedicineWarning($"Injecting generic properties is not supported ({property.PropertyType?.FullName})", property); 141 | 142 | throw new MedicineWarning($"Unknown property type: {property.PropertyType?.FullName}", property); 143 | } 144 | 145 | var isInterface = propertyType.IsInterface; 146 | var isCamera = !isInterface && propertyType.Is(); 147 | var isMonoBehaviour = !isCamera && propertyType.DerivesFrom(); 148 | var isComponent = !isCamera && (isMonoBehaviour || propertyType.DerivesFrom()); 149 | var isScriptableObject = !isCamera && propertyType.DerivesFrom(); 150 | var isArray = property.PropertyType.IsArray; 151 | 152 | if (attribute.Is()) 153 | { 154 | if (!isInterface && !isCamera && !isMonoBehaviour && !isScriptableObject) 155 | throw new MedicineError($"Type of property with [Inject.Single] needs to be a MonoBehaviour, a ScriptableObject, UnityEngine.Camera or an interface.", property); 156 | 157 | if (isArray) 158 | throw new MedicineError($"Type of property with [Inject.Single] must not be an array.", property); 159 | 160 | if (property.SetMethod != null) 161 | throw new MedicineError($"Property with [Inject.Single] must not have a setter.", property); 162 | 163 | if (isCamera) 164 | { 165 | // special handling of Camera.main injection 166 | ReplacePropertyGetterWithHelperMethod(type, property, MethodInfos.RuntimeHelpers.GetMainCamera); 167 | } 168 | else 169 | { 170 | if (!propertyType.HasAttribute()) 171 | throw new MedicineError( 172 | $"Type {propertyType.FullName} needs to be decorated with the [Register.Single] attribute in order to support singleton injection.", property); 173 | 174 | // resolve object registered using [Register.Single] 175 | ReplacePropertyGetterWithHelperMethod(type, property, MethodInfos.RuntimeHelpers.Singleton.GetInstance); 176 | } 177 | 178 | continue; 179 | } 180 | 181 | if (attribute.Is()) 182 | { 183 | if (!isInterface && !isMonoBehaviour && !isScriptableObject) 184 | throw new MedicineError($"Type of property with {attribute.GetName()} needs to be an array of MonoBehaviours, ScriptableObjects, or interfaces.", property); 185 | 186 | if (!isArray) 187 | throw new MedicineError($"Type of property with {attribute.GetName()} needs to be an array.", property); 188 | 189 | if (!propertyType.HasAttribute()) 190 | throw new MedicineError($"Type {propertyType.FullName} needs to be decorated with the [Register.All] attribute in order to support collection injection.", property); 191 | 192 | // resolve objects registered using [Register.All] 193 | ReplacePropertyGetterWithHelperMethod(type, property, MethodInfos.RuntimeHelpers.Collection.GetInstance); 194 | continue; 195 | } 196 | 197 | if (!isInterface && !isComponent && !isCamera) 198 | throw new MedicineError($"Type of property with {attribute.GetName()} needs to be a component or an interface.", property); 199 | 200 | if (type.Attributes.HasFlagNonAlloc(TypeAttributes.Abstract | TypeAttributes.Sealed)) 201 | throw new MedicineError($"Cannot use {attribute.GetName()} in a static class.", property); 202 | 203 | // get this here to make sure we generate exceptions early 204 | var initializationMethodInfo = GetInitializationMethodInfo(property, attribute); 205 | 206 | // lazy injection 207 | if (attribute.AttributeType.Name == nameof(Inject.Lazy)) 208 | { 209 | ReplacePropertyGetterWithHelperMethod( 210 | type, property, 211 | helperMethod: initializationMethodInfo, 212 | requireGameObjectArg: true 213 | ); 214 | continue; 215 | } 216 | 217 | // if none of the above matched, it means we're using component initialization in Awake() 218 | { 219 | var awakeMethod = GetOrEmitMethodWithBaseCall(type, "Awake"); 220 | var initializationMethod = GetOrEmitInitializationMethod(type, "Initialize", callSite: awakeMethod); 221 | 222 | InsertInitializationCall( 223 | initializationMethod, 224 | field, 225 | property, 226 | propertyType, 227 | attribute, 228 | initializationMethodInfo 229 | ); 230 | } 231 | } 232 | catch (MedicineWarning ex) 233 | { 234 | context.DiagnosticMessages.Add(ex.Site.GetDiagnosticMessage(ex.Message)); 235 | } 236 | catch (Exception ex) 237 | { 238 | context.DiagnosticMessages.Add(property.GetMethod.GetDiagnosticMessage(ex.ToString(), DiagnosticType.Error)); 239 | return; 240 | } 241 | } 242 | 243 | void EmitSingletonTypeRegistration(TypeReference registeredAs, TypeDefinition implementedBy) 244 | { 245 | InsertRegisteredInstanceInitializationCall( 246 | method: GetOrEmitMethodWithBaseCall(implementedBy, "OnEnable"), 247 | type: registeredAs, 248 | helperMethod: MethodInfos.RuntimeHelpers.Singleton.RegisterInstance 249 | ); 250 | InsertRegisteredInstanceInitializationCall( 251 | method: GetOrEmitMethodWithBaseCall(implementedBy, "OnDisable"), 252 | type: registeredAs, 253 | helperMethod: MethodInfos.RuntimeHelpers.Singleton.UnregisterInstance 254 | ); 255 | } 256 | 257 | void EmitCollectionTypeRegistration(TypeReference registeredAs, TypeDefinition implementedBy) 258 | { 259 | InsertRegisteredInstanceInitializationCall( 260 | method: GetOrEmitMethodWithBaseCall(implementedBy, "OnEnable"), 261 | type: registeredAs, 262 | helperMethod: MethodInfos.RuntimeHelpers.Collection.RegisterInstance 263 | ); 264 | InsertRegisteredInstanceInitializationCall( 265 | method: GetOrEmitMethodWithBaseCall(implementedBy, "OnDisable"), 266 | type: registeredAs, 267 | helperMethod: MethodInfos.RuntimeHelpers.Collection.UnregisterInstance 268 | ); 269 | } 270 | 271 | foreach (var type in context.Types) 272 | { 273 | try 274 | { 275 | // inner try to rethrow unexpected exceptions as CriticalException 276 | try 277 | { 278 | bool registerAll = type.HasAttribute(); 279 | bool registerSingle = type.HasAttribute(); 280 | 281 | if (registerAll && registerSingle) 282 | throw new MedicineError( 283 | $"Type {type.FullName} shouldn't have both [Register.Single] and [Register.All] attributes.", type); 284 | 285 | if (registerAll || registerSingle) 286 | { 287 | if (!type.IsInterface) 288 | if (!type.DerivesFrom()) 289 | if (!type.DerivesFrom()) 290 | throw new MedicineError($"Registered type needs to be a MonoBehaviour, a ScriptableObject or an interface.", type); 291 | 292 | //todo: EnsureBaseOnEnableOnDisableAreCalled(type); 293 | } 294 | 295 | if (type.IsInterface) 296 | continue; 297 | 298 | if (registerAll) 299 | { 300 | // register type as itself 301 | EmitCollectionTypeRegistration(registeredAs: type, implementedBy: type); 302 | 303 | // register type as all interfaces it implements 304 | foreach (var interfaceImplementation in type.Interfaces) 305 | if (interfaceImplementation.InterfaceType.ResolveFast() is var interfaceType) 306 | if (interfaceType.HasAttribute()) 307 | EmitCollectionTypeRegistration(registeredAs: interfaceImplementation.InterfaceType, implementedBy: type); 308 | } 309 | 310 | if (registerSingle) 311 | { 312 | // register type as itself 313 | EmitSingletonTypeRegistration(registeredAs: type, implementedBy: type); 314 | 315 | // register type as all interfaces it implements 316 | foreach (var interfaceImplementation in type.Interfaces) 317 | { 318 | if (interfaceImplementation.InterfaceType.ResolveFast() is var interfaceType) 319 | { 320 | // register singleton interfaces 321 | // this is useful if we want to have multiple implementations for the same singleton 322 | if (interfaceType.HasAttribute()) 323 | EmitSingletonTypeRegistration(registeredAs: interfaceImplementation.InterfaceType, implementedBy: type); 324 | 325 | // register collection interfaces 326 | // this is useful if we want the singleton to treat the singleton as one of the collection elements 327 | // (eg. for broadcasting messages to all implementers) 328 | if (interfaceType.HasAttribute()) 329 | EmitCollectionTypeRegistration(registeredAs: interfaceImplementation.InterfaceType, implementedBy: type); 330 | } 331 | } 332 | 333 | // add [DefaultExecutionOrder(order: -1)] attribute to the registered singleton type 334 | // this ensures that when the scene is loaded and scripts are initialized, the singleton registers itself before it is used by other scripts 335 | if (!type.IsInterface && !type.HasAttribute()) 336 | { 337 | var defaultExecutionOrderAttribute = new CustomAttribute(MethodInfos.DefaultExecutionOrderConstructor.Import()); 338 | var constructorArgument = new CustomAttributeArgument(type: context.Module.TypeSystem.Int32, value: -1); 339 | defaultExecutionOrderAttribute.ConstructorArguments.Add(constructorArgument); 340 | type.CustomAttributes.Add(defaultExecutionOrderAttribute); 341 | } 342 | } 343 | } 344 | catch (Exception ex) 345 | { 346 | throw new MedicineError(ex.ToString(), type); 347 | } 348 | } 349 | catch (MedicineWarning ex) 350 | { 351 | context.DiagnosticMessages.Add(ex.Site.GetDiagnosticMessage(ex.Message)); 352 | } 353 | catch (MedicineError ex) 354 | { 355 | context.DiagnosticMessages.Add(ex.Site.GetDiagnosticMessage(ex.Message, DiagnosticType.Error)); 356 | return; 357 | } 358 | } 359 | } 360 | 361 | MethodDefinition EmitMethod( 362 | TypeDefinition declaringType, 363 | string methodName, 364 | MethodAttributes methodAttributes = MethodAttributes.Family | MethodAttributes.HideBySig, 365 | TypeReference returnType = null) 366 | { 367 | returnType = returnType ?? context.Module.TypeSystem.Void; 368 | var method = new MethodDefinition(methodName, methodAttributes, returnType); 369 | declaringType.Methods.Add(method); 370 | return method; 371 | } 372 | 373 | MethodDefinition GetOrEmitInitializationMethod(TypeDefinition declaringType, string methodName, MethodDefinition callSite) 374 | { 375 | MethodDefinition GetMatchingMethod(Collection candidateMethods) 376 | { 377 | for (int i = 0, n = candidateMethods.Count; i < n; i++) 378 | if (candidateMethods[i] is var candidate) 379 | if (candidate.Name == methodName) 380 | return candidate; 381 | 382 | return null; 383 | } 384 | 385 | MethodDefinition Emit() 386 | { 387 | // create injection method 388 | var injectionMethod = EmitMethod(declaringType, methodName, MethodAttributes.Private | MethodAttributes.HideBySig | MethodAttributes.Virtual); 389 | 390 | // emit injection method body 391 | { 392 | injectionMethod.Body.Variables.Add(new VariableDefinition(typeof(GameObject).Import())); 393 | 394 | var il = injectionMethod.Body.GetILProcessor(); 395 | 396 | // push "this" ref on top of the stack 397 | il.Emit(OpCodes.Ldarg_0); 398 | 399 | // invoke ".gameObject" getter 400 | il.Emit(OpCodes.Call, MethodInfos.GetGameObject.Import()); 401 | 402 | // store GameObject ref in loc0 403 | // - we're doing this because it's more efficient to call GetComponent methods on the 404 | // GameObject instead of the component instance 405 | // - storing and re-using the GameObject in a local lets us save a few cycles here and 406 | // there by avoiding extern call overhead 407 | il.Emit(OpCodes.Stloc_0); 408 | 409 | /* initialization code will be inserted here by InsertInjectionCall */ 410 | 411 | il.Emit(OpCodes.Ret); 412 | } 413 | 414 | // emit the call to the injection method at target callsite 415 | { 416 | var il = callSite.Body.Instructions; 417 | int index = 0; 418 | il.Insert(index++, Create(OpCodes.Ldarg_0)); 419 | il.Insert(index++, Create(OpCodes.Call, injectionMethod.MakeBaseTypeHostInstanceGeneric())); 420 | } 421 | 422 | // implement IMedicineInjection interface 423 | // this lets us check if the component supports initialization and call it virtually 424 | var injectInterfaceMethod = MethodInfos.IMedicineInjectionInject.Import(); 425 | injectionMethod.Overrides.Add(injectInterfaceMethod); // explicit implementation 426 | declaringType.Interfaces.Add(new InterfaceImplementation(injectInterfaceMethod.DeclaringType)); 427 | 428 | return injectionMethod; 429 | } 430 | 431 | return GetMatchingMethod(declaringType.Methods) ?? Emit(); 432 | } 433 | 434 | MethodDefinition GetOrEmitMethodWithBaseCall(TypeDefinition declaringType, string methodName) 435 | { 436 | MethodDefinition GetMatchingMethod(Collection candidateMethods) 437 | { 438 | if (candidateMethods == null) 439 | return null; 440 | 441 | for (int i = 0, n = candidateMethods.Count; i < n; i++) 442 | if (candidateMethods[i] is var candidate) 443 | if (candidate.Parameters.Count == 0) 444 | if (candidate.Name == methodName) 445 | if (candidate.ReturnType.FullName == "System.Void") 446 | return candidate; 447 | 448 | return null; 449 | } 450 | 451 | MethodDefinition Emit() 452 | { 453 | MethodDefinition GetBaseMethod() 454 | => GetMatchingMethod(declaringType.BaseType?.ResolveFast()?.Methods); 455 | 456 | var method = EmitMethod(declaringType, methodName); 457 | var il = method.Body.GetILProcessor(); 458 | 459 | // emit base method call 460 | if (GetBaseMethod() is MethodDefinition baseMethod) 461 | { 462 | var attributes = baseMethod.Attributes; 463 | if ((attributes & MethodAttributes.Private) != 0) 464 | { 465 | // if the base method is private, make it protected so that we can call it 466 | // (this won't work across assembly boundary...) 467 | attributes ^= MethodAttributes.Private; 468 | attributes |= MethodAttributes.Family; 469 | baseMethod.Attributes = attributes; 470 | } 471 | 472 | // if the declaring type of the base method we're calling is a generic instance type, we need to explicitly 473 | // create a new method reference with the generic arguments included to avoid the runtime error: 474 | // BadImageFormatException: Method with open type while not compiling gshared 475 | var baseMethodCallableReference 476 | = declaringType.BaseType is GenericInstanceType genericInstanceType 477 | ? baseMethod.MakeHostInstanceGeneric(genericInstanceType.GenericArguments.ToArray()) 478 | : baseMethod; 479 | 480 | il.Emit(OpCodes.Ldarg_0); 481 | il.Emit(OpCodes.Call, baseMethodCallableReference.Import()); 482 | } 483 | 484 | il.Emit(OpCodes.Ret); 485 | return method; 486 | } 487 | 488 | return GetMatchingMethod(declaringType.Methods) ?? Emit(); 489 | } 490 | 491 | static MethodInfo GetInitializationMethodInfo(PropertyDefinition property, CustomAttribute attr) 492 | { 493 | var attrType = attr.AttributeType; 494 | bool includeInactive = attr.HasProperty(nameof(Inject.FromChildren.IncludeInactive), true); 495 | 496 | if (property.PropertyType.IsArray) 497 | { 498 | if (attrType.Is()) 499 | return MethodInfos.RuntimeHelpers.InjectArray; 500 | 501 | if (attrType.Is()) 502 | return includeInactive 503 | ? MethodInfos.RuntimeHelpers.InjectFromChildrenArrayIncludeInactive 504 | : MethodInfos.RuntimeHelpers.InjectFromChildrenArray; 505 | 506 | if (attrType.Is()) 507 | return includeInactive 508 | ? MethodInfos.RuntimeHelpers.InjectFromParentsArrayIncludeInactive 509 | : MethodInfos.RuntimeHelpers.InjectFromParentsArray; 510 | 511 | if (attrType.Is()) 512 | return MethodInfos.RuntimeHelpers.Lazy.InjectArray; 513 | 514 | if (attrType.Is()) 515 | return includeInactive 516 | ? MethodInfos.RuntimeHelpers.Lazy.InjectFromChildrenArrayIncludeInactive 517 | : MethodInfos.RuntimeHelpers.Lazy.InjectFromChildrenArray; 518 | 519 | if (attrType.Is()) 520 | return includeInactive 521 | ? MethodInfos.RuntimeHelpers.Lazy.InjectFromParentsArrayIncludeInactive 522 | : MethodInfos.RuntimeHelpers.Lazy.InjectFromParentsArray; 523 | } 524 | else 525 | { 526 | if (attrType.Is() || attrType.Is()) 527 | return MethodInfos.RuntimeHelpers.Inject; 528 | 529 | if (attrType.Is() || attrType.Is()) 530 | return includeInactive 531 | ? MethodInfos.RuntimeHelpers.InjectFromChildrenIncludeInactive 532 | : MethodInfos.RuntimeHelpers.InjectFromChildren; 533 | 534 | if (attrType.Is() || attrType.Is()) 535 | return includeInactive 536 | ? MethodInfos.RuntimeHelpers.InjectFromParentsIncludingInactive 537 | : MethodInfos.RuntimeHelpers.InjectFromParents; 538 | } 539 | 540 | throw new MedicineError($"Unknown injection attribute: {attrType.FullName.Replace('/', '.')}", property); 541 | } 542 | 543 | /// Emits instructions that initialize the property value. 544 | static void InsertInitializationCall(MethodDefinition method, FieldReference field, PropertyDefinition property, TypeDefinition propertyType, CustomAttribute attribute, MethodInfo initializationMethodInfo) 545 | { 546 | var il = method.Body.GetILProcessor(); 547 | bool isOptional = attribute.HasProperty(nameof(Inject.Optional), value: true); 548 | bool isArray = field.FieldType.IsArray; 549 | 550 | var typeOrElementType = field.FieldType.UnwrapArrayElementType(); 551 | 552 | var lastInstruction = method.Body.Instructions.Last(); 553 | 554 | // remove last instruction (ret) so we can append instructions cheaply - we'll re-insert it at the end 555 | il.Body.Instructions.RemoveAt(method.Body.Instructions.Count - 1); 556 | 557 | il.Emit(OpCodes.Ldarg_0); 558 | il.Emit(OpCodes.Ldarg_0); 559 | 560 | // load cached "gameObject" local (first arg to the initialization method) 561 | il.Emit(OpCodes.Ldloc_0); 562 | 563 | // call initialization method specific to attribute 564 | il.Emit(OpCodes.Call, initializationMethodInfo.Import().MakeGenericInstanceMethod(typeOrElementType)); 565 | 566 | // store result in field 567 | il.Emit(OpCodes.Stfld, field); 568 | 569 | // emit safety checks 570 | if (isOptional) 571 | { 572 | il.Emit(OpCodes.Pop); 573 | } 574 | else 575 | { 576 | // create target that we'll jump to if safety checks succeed 577 | // we'll append this instruction later 578 | var branchTarget = Create(OpCodes.Nop); 579 | 580 | // load value we stored in the field 581 | il.Emit(OpCodes.Ldfld, field); 582 | 583 | if (isArray) 584 | { 585 | // call helper method to determine whether the array contains at least one element 586 | il.Emit(OpCodes.Call, MethodInfos.RuntimeHelpers.ValidateArray.Import()); 587 | } 588 | else 589 | { 590 | if (propertyType.IsInterface) 591 | il.Emit(OpCodes.Castclass, typeof(UnityEngine.Object).Import()); 592 | 593 | // call UnityEngine.Object implicit bool operator to check if object is alive (exists, not destroyed, etc) 594 | il.Emit(OpCodes.Call, MethodInfos.UnityObjectBoolOpImplicit.Import()); 595 | } 596 | 597 | // branch to end if result on the stack is true 598 | il.Emit(OpCodes.Brtrue, branchTarget); 599 | 600 | // push error string to stack (generated at compile-time specifically for this property) 601 | il.Emit( 602 | OpCodes.Ldstr, 603 | $"Failed to initialize {property.PropertyType.Name} {property.Name} in component {method.DeclaringType.FullName}.\n" + 604 | $"[{attribute.AttributeType.FullName.Replace('/', '.')}] Init(){property.GetMethod.DebugInformation.GetFilenameLineColumnString()}\n" 605 | ); 606 | 607 | // push "this" to stack (last argument to UnityEngine.Debug.LogError, enables navigation to context object in the console window) 608 | il.Emit(OpCodes.Ldarg_0); 609 | 610 | // invoke UnityEngine.Debug.LogError 611 | il.Emit(OpCodes.Call, MethodInfos.LogError.Import()); 612 | 613 | // append branch target 614 | il.Append(branchTarget); 615 | } 616 | 617 | // re-insert last instruction (ret) at the end 618 | il.Append(lastInstruction); 619 | } 620 | 621 | [SuppressMessage("ReSharper", "RedundantAssignment")] 622 | static void InsertRegisteredInstanceInitializationCall(MethodDefinition method, TypeReference type, MethodInfo helperMethod) 623 | { 624 | var il = method.Body.Instructions; 625 | 626 | // create reference to the registeration method on a generic instance of the helper type 627 | var registerMethod = helperMethod.Import(); 628 | 629 | if (helperMethod.DeclaringType.ContainsGenericParameters) 630 | registerMethod = registerMethod.MakeHostInstanceGeneric(type); 631 | 632 | int index = 0; 633 | 634 | // load "this" (first/only argument to registeration methods) 635 | il.Insert(index++, Create(OpCodes.Ldarg_0)); 636 | 637 | // call registeration method 638 | il.Insert(index++, Create(OpCodes.Call, registerMethod)); 639 | } 640 | 641 | static void ReplacePropertyGetterWithHelperMethod(TypeDefinition type, PropertyDefinition property, MethodInfo helperMethod, bool requireGameObjectArg = false) 642 | { 643 | var getMethod = property.GetMethod; 644 | 645 | // get instruction that loads the field 646 | var ldfld = getMethod.Body.Instructions.Single(x => x.OpCode == OpCodes.Ldfld || x.OpCode == OpCodes.Ldsfld); 647 | 648 | // remove compiler-generated backing field 649 | type.Fields.Remove((ldfld.Operand as FieldReference).ResolveFast()); 650 | 651 | // create reference to the helper method on a generic instance of the helper type 652 | var method = helperMethod.Import(); 653 | 654 | if (helperMethod.DeclaringType.ContainsGenericParameters) 655 | method = method.MakeHostInstanceGeneric(property.PropertyType.UnwrapArrayElementType()); 656 | else if (helperMethod.ContainsGenericParameters) 657 | method = method.MakeGenericInstanceMethod(property.PropertyType.UnwrapArrayElementType()); 658 | 659 | var il = getMethod.Body.GetILProcessor(); 660 | il.Body.Instructions.Clear(); 661 | 662 | // get GameObject argument by calling .gameObject getter 663 | if (requireGameObjectArg) 664 | { 665 | // push "this" on top of the stack 666 | il.Emit(OpCodes.Ldarg_0); 667 | 668 | // invoke ".gameObject" getter 669 | il.Emit(OpCodes.Call, MethodInfos.GetGameObject.Import()); 670 | } 671 | 672 | // emit helper method call (returns the reference to the object we're interested in) 673 | il.Emit(OpCodes.Call, method); 674 | 675 | il.Emit(OpCodes.Ret); 676 | } 677 | } 678 | } 679 | -------------------------------------------------------------------------------- /Src/Medicine.CodeGen/InjectionPostProcessor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 17811328cbbc4c6691446a9967e49857 3 | timeCreated: 1590928911 -------------------------------------------------------------------------------- /Src/Medicine.CodeGen/Medicine.CodeGen.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Unity.Medicine.CodeGen", 3 | "references": [ 4 | "GUID:9d802ac6321ed6f499c8de1b6ee4ec88" 5 | ], 6 | "includePlatforms": [ 7 | "Editor" 8 | ], 9 | "excludePlatforms": [], 10 | "allowUnsafeCode": true, 11 | "overrideReferences": true, 12 | "precompiledReferences": [ 13 | "Mono.Cecil.dll", 14 | "Mono.Cecil.Rocks.dll", 15 | "Mono.Cecil.Pdb.dll", 16 | "Unity.IL2CPP", 17 | "Unity.IL2CPP.Common" 18 | ], 19 | "autoReferenced": false, 20 | "defineConstraints": [], 21 | "versionDefines": [], 22 | "noEngineReferences": false 23 | } -------------------------------------------------------------------------------- /Src/Medicine.CodeGen/Medicine.CodeGen.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 3712dd92cd03b0f438762813e3dac2f6 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Src/Medicine.CodeGen/MethodInfos.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Reflection; 4 | using UnityEngine; 5 | using static System.Reflection.BindingFlags; 6 | using Object = UnityEngine.Object; 7 | 8 | namespace Medicine 9 | { 10 | [SuppressMessage("ReSharper", "MemberHidesStaticFromOuterClass")] 11 | static class MethodInfos 12 | { 13 | internal static readonly MethodInfo LogException 14 | = typeof(Debug).GetMethod(nameof(Debug.LogException), new[] { typeof(Exception), typeof(Object) }); 15 | 16 | internal static readonly MethodInfo LogError 17 | = typeof(Debug).GetMethod(nameof(Debug.LogError), new[] { typeof(string), typeof(Object) }); 18 | 19 | internal static readonly MethodInfo GetTypeFromHandle 20 | = typeof(Type).GetMethod(nameof(Type.GetTypeFromHandle), Public | Static); 21 | 22 | internal static readonly MethodInfo GetGameObject 23 | = typeof(Component).GetProperty("gameObject", Public | Instance).GetMethod; 24 | 25 | internal static readonly ConstructorInfo DefaultExecutionOrderConstructor 26 | = typeof(DefaultExecutionOrder).GetConstructor(new[] { typeof(int) }); 27 | 28 | internal static readonly MethodInfo UnityObjectBoolOpImplicit 29 | = typeof(Object).GetMethod("op_Implicit", Public | Static); 30 | 31 | internal static readonly MethodInfo IMedicineInjectionInject 32 | = typeof(IMedicineComponent).GetMethod(nameof(IMedicineComponent.Inject)); 33 | 34 | internal static class RuntimeHelpers 35 | { 36 | internal static readonly MethodInfo ValidateArray 37 | = typeof(Medicine.RuntimeHelpers).GetMethod(nameof(Medicine.RuntimeHelpers.ValidateArray), Public | Static); 38 | 39 | internal static readonly MethodInfo GetMainCamera 40 | = typeof(Medicine.RuntimeHelpers).GetMethod(nameof(Medicine.RuntimeHelpers.GetMainCamera), Public | Static); 41 | 42 | internal static readonly MethodInfo Inject 43 | = typeof(Medicine.RuntimeHelpers).GetMethod(nameof(Medicine.RuntimeHelpers.Inject), Public | Static); 44 | 45 | internal static readonly MethodInfo InjectArray 46 | = typeof(Medicine.RuntimeHelpers).GetMethod(nameof(Medicine.RuntimeHelpers.InjectArray), Public | Static); 47 | 48 | internal static readonly MethodInfo InjectFromChildren 49 | = typeof(Medicine.RuntimeHelpers).GetMethod(nameof(Medicine.RuntimeHelpers.InjectFromChildren), Public | Static); 50 | 51 | internal static readonly MethodInfo InjectFromChildrenIncludeInactive 52 | = typeof(Medicine.RuntimeHelpers).GetMethod(nameof(Medicine.RuntimeHelpers.InjectFromChildrenIncludeInactive), Public | Static); 53 | 54 | internal static readonly MethodInfo InjectFromChildrenArray 55 | = typeof(Medicine.RuntimeHelpers).GetMethod(nameof(Medicine.RuntimeHelpers.InjectFromChildrenArray), Public | Static); 56 | 57 | internal static readonly MethodInfo InjectFromChildrenArrayIncludeInactive 58 | = typeof(Medicine.RuntimeHelpers).GetMethod(nameof(Medicine.RuntimeHelpers.InjectFromChildrenArrayIncludeInactive), Public | Static); 59 | 60 | internal static readonly MethodInfo InjectFromParents 61 | = typeof(Medicine.RuntimeHelpers).GetMethod(nameof(Medicine.RuntimeHelpers.InjectFromParents), Public | Static); 62 | 63 | internal static readonly MethodInfo InjectFromParentsIncludingInactive 64 | = typeof(Medicine.RuntimeHelpers).GetMethod(nameof(Medicine.RuntimeHelpers.InjectFromParentsIncludingInactive), Public | Static); 65 | 66 | internal static readonly MethodInfo InjectFromParentsArray 67 | = typeof(Medicine.RuntimeHelpers).GetMethod(nameof(Medicine.RuntimeHelpers.InjectFromParentsArray), Public | Static); 68 | 69 | internal static readonly MethodInfo InjectFromParentsArrayIncludeInactive 70 | = typeof(Medicine.RuntimeHelpers).GetMethod(nameof(Medicine.RuntimeHelpers.InjectFromParentsArrayIncludeInactive), Public | Static); 71 | 72 | internal static class Collection 73 | { 74 | internal static readonly MethodInfo GetInstance 75 | = typeof(Medicine.RuntimeHelpers.Collection<>).GetMethod(nameof(Medicine.RuntimeHelpers.Collection.GetInstances), Public | Static); 76 | 77 | internal static readonly MethodInfo RegisterInstance 78 | = typeof(Medicine.RuntimeHelpers.Collection<>).GetMethod(nameof(Medicine.RuntimeHelpers.Collection.RegisterInstance), Public | Static); 79 | 80 | internal static readonly MethodInfo UnregisterInstance 81 | = typeof(Medicine.RuntimeHelpers.Collection<>).GetMethod(nameof(Medicine.RuntimeHelpers.Collection.UnregisterInstance), Public | Static); 82 | } 83 | 84 | internal static class Lazy 85 | { 86 | internal static readonly MethodInfo InjectArray 87 | = typeof(Medicine.RuntimeHelpers.Lazy).GetMethod(nameof(Medicine.RuntimeHelpers.Lazy.InjectArray), Public | Static); 88 | 89 | internal static readonly MethodInfo InjectFromChildrenArray 90 | = typeof(Medicine.RuntimeHelpers.Lazy).GetMethod(nameof(Medicine.RuntimeHelpers.Lazy.InjectFromChildrenArray), Public | Static); 91 | 92 | internal static readonly MethodInfo InjectFromChildrenArrayIncludeInactive 93 | = typeof(Medicine.RuntimeHelpers.Lazy).GetMethod(nameof(Medicine.RuntimeHelpers.Lazy.InjectFromChildrenArrayIncludeInactive), Public | Static); 94 | 95 | internal static readonly MethodInfo InjectFromParentsArray 96 | = typeof(Medicine.RuntimeHelpers.Lazy).GetMethod(nameof(Medicine.RuntimeHelpers.Lazy.InjectFromParentsArray), Public | Static); 97 | 98 | internal static readonly MethodInfo InjectFromParentsArrayIncludeInactive 99 | = typeof(Medicine.RuntimeHelpers.Lazy).GetMethod(nameof(Medicine.RuntimeHelpers.Lazy.InjectFromParentsArrayIncludeInactive), Public | Static); 100 | } 101 | 102 | internal static class Singleton 103 | { 104 | internal static readonly MethodInfo GetInstance 105 | = typeof(Medicine.RuntimeHelpers.Singleton<>).GetMethod(nameof(Medicine.RuntimeHelpers.Singleton.GetInstance), Public | Static); 106 | 107 | internal static readonly MethodInfo RegisterInstance 108 | = typeof(Medicine.RuntimeHelpers.Singleton<>).GetMethod(nameof(Medicine.RuntimeHelpers.Singleton.RegisterInstance), Public | Static); 109 | 110 | internal static readonly MethodInfo UnregisterInstance 111 | = typeof(Medicine.RuntimeHelpers.Singleton<>).GetMethod(nameof(Medicine.RuntimeHelpers.Singleton.UnregisterInstance), Public | Static); 112 | } 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Src/Medicine.CodeGen/MethodInfos.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7e99ad56b6c74dc68b4f5077d05d1d53 3 | timeCreated: 1591355621 -------------------------------------------------------------------------------- /Src/Medicine.Tests.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0e5bdbe248b046f7a6553660f9c952d7 3 | timeCreated: 1592328843 -------------------------------------------------------------------------------- /Src/Medicine.Tests/Medicine.Tests.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Medicine.Tests", 3 | "references": [ 4 | "Medicine" 5 | ], 6 | "includePlatforms": [ 7 | ], 8 | "excludePlatforms": [], 9 | "allowUnsafeCode": true, 10 | "overrideReferences": false, 11 | "autoReferenced": false, 12 | "precompiledReferences": [ 13 | "nunit.framework.dll" 14 | ], 15 | "optionalUnityReferences": [ 16 | "TestAssemblies" 17 | ], 18 | "defineConstraints": [], 19 | "versionDefines": [], 20 | "noEngineReferences": false 21 | } 22 | -------------------------------------------------------------------------------- /Src/Medicine.Tests/Medicine.Tests.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1df8bd1e13fc4e42b1bf1dfc42266ad8 3 | timeCreated: 1591893338 -------------------------------------------------------------------------------- /Src/Medicine.Tests/TestComp.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace Medicine 4 | { 5 | public abstract class TestComp : MonoBehaviour { } 6 | 7 | public sealed class TestCompDerived : TestComp { } 8 | } 9 | -------------------------------------------------------------------------------- /Src/Medicine.Tests/TestComp.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6295916068d34096b78af5520f907cec 3 | timeCreated: 1591904179 -------------------------------------------------------------------------------- /Src/Medicine.Tests/TestExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using UnityEngine; 4 | 5 | namespace Medicine 6 | { 7 | public static class TestExtensions 8 | { 9 | public static T CallAwake(this T component) where T : MonoBehaviour 10 | { 11 | component 12 | .GetType() 13 | .GetMethod("Awake", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) 14 | .Invoke(component, Array.Empty()); 15 | 16 | return component; 17 | } 18 | 19 | public static GameObject WithComponent(this GameObject gameObject) where T : Component 20 | { 21 | gameObject.AddComponent(); 22 | return gameObject; 23 | } 24 | 25 | public static GameObject WithParent(this GameObject gameObject, Action configure) 26 | { 27 | var parent = new GameObject(); 28 | gameObject.transform.parent = parent.transform; 29 | configure(parent); 30 | return gameObject; 31 | } 32 | 33 | public static GameObject WithChild(this GameObject gameObject, Action configure) 34 | { 35 | var parent = new GameObject(); 36 | parent.transform.parent = gameObject.transform; 37 | configure(parent); 38 | return gameObject; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Src/Medicine.Tests/TestExtensions.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5e6cc242bd36455bbb46d7edf53de18a 3 | timeCreated: 1591898363 -------------------------------------------------------------------------------- /Src/Medicine.Tests/Tests.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8eaaa3f66015476a845224282412ee16 3 | timeCreated: 1591904698 -------------------------------------------------------------------------------- /Src/Medicine.Tests/Tests/InjectAllTests.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | using NUnit.Framework; 3 | using UnityEngine; 4 | using UnityEngine.TestTools; 5 | 6 | // ReSharper disable ClassNeverInstantiated.Global 7 | namespace Medicine 8 | { 9 | public sealed class InjectAllTests 10 | { 11 | public sealed class InjectAllMB : MonoBehaviour 12 | { 13 | [Inject.All] 14 | public SomeMB[] Instances { get; } 15 | } 16 | 17 | public sealed class InjectAllPOCO 18 | { 19 | [Inject.All] 20 | public static SomeMB[] Instances { get; } 21 | } 22 | 23 | public sealed class InjectAllStaticPOCO 24 | { 25 | [Inject.All] 26 | public static SomeMB[] Instances { get; } 27 | } 28 | 29 | public static class InjectAllStaticPOCS 30 | { 31 | [Inject.All] 32 | public static SomeMB[] Instances { get; } 33 | } 34 | 35 | [Register.All] 36 | public sealed class SomeMB : MonoBehaviour { } 37 | 38 | [Test, TestMustExpectAllLogs] 39 | public void InjectAllTest() 40 | { 41 | var component = new GameObject().AddComponent(); 42 | Assert.AreEqual(0, component.Instances.Length); 43 | 44 | var a = new GameObject().AddComponent(); 45 | Assert.AreEqual(1, component.Instances.Length); 46 | 47 | var b = new GameObject().AddComponent(); 48 | Assert.AreEqual(2, component.Instances.Length); 49 | 50 | var c = new GameObject().AddComponent(); 51 | Assert.AreEqual(3, component.Instances.Length); 52 | 53 | Object.Destroy(a.gameObject); 54 | Assert.AreEqual(2, component.Instances.Length); 55 | 56 | Object.Destroy(b.gameObject); 57 | Assert.AreEqual(1, component.Instances.Length); 58 | 59 | Object.Destroy(c.gameObject); 60 | Assert.AreEqual(0, component.Instances.Length); 61 | 62 | Object.Destroy(component.gameObject); 63 | } 64 | 65 | [Test, TestMustExpectAllLogs] 66 | public void InjectAllStaticPOCOTest() 67 | { 68 | Assert.AreEqual(0, InjectAllStaticPOCO.Instances.Length); 69 | 70 | var a = new GameObject().AddComponent(); 71 | Assert.AreEqual(1, InjectAllStaticPOCO.Instances.Length); 72 | 73 | var b = new GameObject().AddComponent(); 74 | Assert.AreEqual(2, InjectAllStaticPOCO.Instances.Length); 75 | 76 | var c = new GameObject().AddComponent(); 77 | Assert.AreEqual(3, InjectAllStaticPOCO.Instances.Length); 78 | 79 | Object.Destroy(a.gameObject); 80 | Assert.AreEqual(2, InjectAllStaticPOCO.Instances.Length); 81 | 82 | Object.Destroy(b.gameObject); 83 | Assert.AreEqual(1, InjectAllStaticPOCO.Instances.Length); 84 | 85 | Object.Destroy(c.gameObject); 86 | Assert.AreEqual(0, InjectAllStaticPOCO.Instances.Length); 87 | } 88 | 89 | [Test, TestMustExpectAllLogs] 90 | public void InjectAllStaticPOCSTest() 91 | { 92 | Assert.AreEqual(0, InjectAllStaticPOCS.Instances.Length); 93 | 94 | var a = new GameObject().AddComponent(); 95 | Assert.AreEqual(1, InjectAllStaticPOCS.Instances.Length); 96 | 97 | var b = new GameObject().AddComponent(); 98 | Assert.AreEqual(2, InjectAllStaticPOCS.Instances.Length); 99 | 100 | var c = new GameObject().AddComponent(); 101 | Assert.AreEqual(3, InjectAllStaticPOCS.Instances.Length); 102 | 103 | Object.Destroy(a.gameObject); 104 | Assert.AreEqual(2, InjectAllStaticPOCS.Instances.Length); 105 | 106 | Object.Destroy(b.gameObject); 107 | Assert.AreEqual(1, InjectAllStaticPOCS.Instances.Length); 108 | 109 | Object.Destroy(c.gameObject); 110 | Assert.AreEqual(0, InjectAllStaticPOCS.Instances.Length); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Src/Medicine.Tests/Tests/InjectAllTests.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 28900a7adbeb43e180a6bdc3495b8fd5 3 | timeCreated: 1591904742 -------------------------------------------------------------------------------- /Src/Medicine.Tests/Tests/InjectGenericTests.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | using NUnit.Framework; 3 | using UnityEngine; 4 | using UnityEngine.TestTools; 5 | 6 | namespace Medicine 7 | { 8 | public sealed class InjectGenericTests 9 | { 10 | public abstract class TestCompGenericBase : MonoBehaviour 11 | { 12 | [Inject] 13 | public TestComp TestCompInBase { get; } 14 | 15 | [Inject.FromChildren] 16 | public TestComp[] TestCompManyInBase { get; } 17 | 18 | [Inject.Single] 19 | public SingletonMB TestSingletonInBase { get; } 20 | } 21 | 22 | public abstract class TestCompGeneric : TestCompGenericBase 23 | { 24 | // ReSharper disable once NotAccessedField.Local 25 | [SerializeField] 26 | TSomeGeneric field; 27 | 28 | [Inject] 29 | public TestComp TestComp { get; } 30 | 31 | [Inject.FromChildren] 32 | public TestComp[] TestCompMany { get; } 33 | 34 | [Inject.Single] 35 | public SingletonMB TestSingleton { get; } 36 | } 37 | 38 | public sealed class TestCompGenericClosed : TestCompGeneric 39 | { 40 | [Inject] 41 | public TestComp TestCompInDerived { get; } 42 | 43 | [Inject.FromChildren] 44 | public TestComp[] TestCompManyInDerived { get; } 45 | 46 | [Inject.Single] 47 | public SingletonMB TestSingletonInDerived { get; } 48 | } 49 | 50 | [Register.Single] 51 | public sealed class SingletonMB : MonoBehaviour { } 52 | 53 | [Test, TestMustExpectAllLogs] 54 | public void InjectTest() 55 | { 56 | var instance = new GameObject() 57 | .WithComponent() 58 | .WithComponent() 59 | .AddComponent(); 60 | 61 | Assert.IsTrue(instance.TestComp); 62 | Assert.IsNotEmpty(instance.TestCompMany); 63 | 64 | Assert.IsTrue(instance.TestCompInDerived); 65 | Assert.IsNotEmpty(instance.TestCompManyInDerived); 66 | 67 | Assert.IsTrue(instance.TestCompInBase); 68 | Assert.IsNotEmpty(instance.TestCompManyInBase); 69 | 70 | Assert.IsTrue(instance.TestSingleton); 71 | Assert.IsTrue(instance.TestSingletonInDerived); 72 | Assert.IsTrue(instance.TestSingletonInBase); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Src/Medicine.Tests/Tests/InjectGenericTests.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 64743656f1359db42beb24c2eb8eace7 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Src/Medicine.Tests/Tests/InjectLazyTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using UnityEngine; 3 | using UnityEngine.TestTools; 4 | 5 | namespace Medicine 6 | { 7 | public sealed class InjectLazyTests 8 | { 9 | public sealed class InjectMB : MonoBehaviour 10 | { 11 | [Inject.Lazy] 12 | public TestComp TestComp { get; } 13 | } 14 | 15 | [Test, TestMustExpectAllLogs] 16 | public void InjectTest() 17 | { 18 | var component = new GameObject() 19 | .WithComponent() 20 | .AddComponent() 21 | .TestComp; 22 | 23 | Assert.IsTrue(component); 24 | } 25 | 26 | [Test, TestMustExpectAllLogs] 27 | public void InjectMissingTest() 28 | { 29 | var component = new GameObject() 30 | .AddComponent() 31 | .TestComp; 32 | 33 | Assert.IsFalse(component); 34 | } 35 | 36 | public sealed class InjectFromChildrenMB : MonoBehaviour 37 | { 38 | [Inject.FromChildren.Lazy] 39 | public TestComp TestComp { get; } 40 | } 41 | 42 | [Test, TestMustExpectAllLogs] 43 | public void InjectFromChildrenTest() 44 | { 45 | var component = new GameObject() 46 | .WithChild(x => x.WithComponent()) 47 | .AddComponent() 48 | .TestComp; 49 | 50 | Assert.IsTrue(component); 51 | } 52 | 53 | [Test, TestMustExpectAllLogs] 54 | public void InjectFromChildrenMissingTest() 55 | { 56 | var component = new GameObject() 57 | .AddComponent() 58 | .TestComp; 59 | 60 | Assert.IsFalse(component); 61 | } 62 | 63 | public sealed class InjectFromParentsMB : MonoBehaviour 64 | { 65 | [Inject.FromParents.Lazy] 66 | public TestComp TestComp { get; } 67 | } 68 | 69 | [Test, TestMustExpectAllLogs] 70 | public void InjectFromParentsTest() 71 | { 72 | var component = new GameObject() 73 | .WithParent(x => x.WithComponent()) 74 | .AddComponent() 75 | .TestComp; 76 | 77 | Assert.IsTrue(component); 78 | } 79 | 80 | [Test, TestMustExpectAllLogs] 81 | public void InjectFromParentsMissingTest() 82 | { 83 | var component = new GameObject() 84 | .AddComponent() 85 | .TestComp; 86 | 87 | Assert.IsFalse(component); 88 | } 89 | 90 | public sealed class InjectArrayMB : MonoBehaviour 91 | { 92 | [Inject.Lazy] 93 | public TestComp[] Colliders { get; } 94 | } 95 | 96 | [Test, TestMustExpectAllLogs] 97 | public void InjectArrayTest() 98 | { 99 | var components = new GameObject() 100 | .WithComponent() 101 | .WithComponent() 102 | .WithComponent() 103 | .AddComponent() 104 | .Colliders; 105 | 106 | Assert.AreEqual(3, components.Length); 107 | } 108 | 109 | [Test, TestMustExpectAllLogs] 110 | public void InjectArrayMissingTest() 111 | { 112 | var components = new GameObject() 113 | .AddComponent() 114 | .Colliders; 115 | 116 | Assert.AreEqual(0, components.Length); 117 | } 118 | 119 | public sealed class InjectFromChildrenArrayMB : MonoBehaviour 120 | { 121 | [Inject.FromChildren.Lazy] 122 | public TestComp[] Colliders { get; } 123 | } 124 | 125 | [Test, TestMustExpectAllLogs] 126 | public void InjectFromChildrenArrayTest() 127 | { 128 | var components = new GameObject() 129 | .WithComponent() 130 | .WithChild( 131 | x => x 132 | .WithComponent() 133 | .WithComponent() 134 | .WithComponent()) 135 | .AddComponent() 136 | .Colliders; 137 | 138 | Assert.AreEqual(4, components.Length); 139 | } 140 | 141 | [Test, TestMustExpectAllLogs] 142 | public void InjectFromChildrenArrayMissingTest() 143 | { 144 | var components = new GameObject() 145 | .AddComponent() 146 | .Colliders; 147 | 148 | Assert.AreEqual(0, components.Length); 149 | } 150 | 151 | public sealed class InjectFromParentsArrayMB : MonoBehaviour 152 | { 153 | [Inject.FromParents.Lazy] 154 | public TestComp[] Colliders { get; } 155 | } 156 | 157 | [Test, TestMustExpectAllLogs] 158 | public void InjectFromParentsArrayTest() 159 | { 160 | var components = new GameObject() 161 | .WithComponent() 162 | .WithParent( 163 | x => x 164 | .WithComponent() 165 | .WithComponent() 166 | .WithComponent()) 167 | .AddComponent() 168 | .Colliders; 169 | 170 | Assert.AreEqual(4, components.Length); 171 | } 172 | 173 | [Test, TestMustExpectAllLogs] 174 | public void InjectFromParentsArrayMissingTest() 175 | { 176 | var components = new GameObject() 177 | .AddComponent() 178 | .Colliders; 179 | 180 | Assert.AreEqual(0, components.Length); 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /Src/Medicine.Tests/Tests/InjectLazyTests.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 48404b5cfe8f437d8bc17e76376786a6 3 | timeCreated: 1591899628 -------------------------------------------------------------------------------- /Src/Medicine.Tests/Tests/InjectSingleTests.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | using NUnit.Framework; 3 | using UnityEngine; 4 | using UnityEngine.TestTools; 5 | 6 | // ReSharper disable ClassNeverInstantiated.Global 7 | namespace Medicine 8 | { 9 | public sealed class InjectSingleTests 10 | { 11 | public sealed class InjectSingleMB : MonoBehaviour 12 | { 13 | [Inject.Single] 14 | public SingletonMB Singleton { get; } 15 | } 16 | 17 | public sealed class InjectSinglePOCO 18 | { 19 | [Inject.Single] 20 | public static SingletonMB Singleton { get; } 21 | } 22 | 23 | public sealed class InjectSingleStaticPOCO 24 | { 25 | [Inject.Single] 26 | public static SingletonMB Singleton { get; } 27 | } 28 | 29 | public static class InjectSingleStaticPOCS 30 | { 31 | [Inject.Single] 32 | public static SingletonMB Singleton { get; } 33 | } 34 | 35 | [Register.Single] 36 | public sealed class SingletonMB : MonoBehaviour { } 37 | 38 | [Test, TestMustExpectAllLogs] 39 | public void InjectSingleTest() 40 | { 41 | var singleton = new GameObject() 42 | .AddComponent(); 43 | 44 | var component = new GameObject() 45 | .AddComponent(); 46 | 47 | Assert.IsTrue(component.Singleton); 48 | Assert.AreSame(singleton, component.Singleton); 49 | 50 | Object.Destroy(component.gameObject); 51 | Object.Destroy(singleton.gameObject); 52 | } 53 | 54 | [Test, TestMustExpectAllLogs] 55 | public void InjectSingleDuplicateTest() 56 | { 57 | var singleton1 = new GameObject() 58 | .AddComponent(); 59 | var singleton2 = new GameObject() 60 | .AddComponent(); 61 | 62 | var component = new GameObject() 63 | .AddComponent(); 64 | 65 | LogAssert.Expect(LogType.Error, new Regex("Failed to register singleton instance .*: a registered instance already exists.*")); 66 | Assert.AreSame(singleton1, component.Singleton); 67 | 68 | Object.Destroy(component.gameObject); 69 | Object.Destroy(singleton1.gameObject); 70 | Object.Destroy(singleton2.gameObject); 71 | } 72 | 73 | [Test, TestMustExpectAllLogs] 74 | public void InjectSingleMissingTest() 75 | { 76 | var component = new GameObject() 77 | .AddComponent(); 78 | 79 | LogAssert.Expect(LogType.Error, new Regex("No registered singleton instance:.*")); 80 | Assert.IsFalse(component.Singleton); 81 | 82 | Object.Destroy(component.gameObject); 83 | } 84 | 85 | [Test, TestMustExpectAllLogs] 86 | public void InjectSingleStaticPOCOTest() 87 | { 88 | var singleton = new GameObject() 89 | .AddComponent(); 90 | 91 | Assert.IsTrue(InjectSingleStaticPOCO.Singleton); 92 | 93 | Object.Destroy(singleton.gameObject); 94 | } 95 | 96 | [Test, TestMustExpectAllLogs] 97 | public void InjectSingleStaticPOCOMissingTest() 98 | { 99 | Assert.IsFalse(InjectSingleStaticPOCO.Singleton); 100 | LogAssert.Expect(LogType.Error, new Regex("No registered singleton instance:.*")); 101 | } 102 | 103 | [Test, TestMustExpectAllLogs] 104 | public void InjectSingleStaticPOCSTest() 105 | { 106 | var singleton = new GameObject() 107 | .AddComponent(); 108 | 109 | Assert.IsTrue(InjectSingleStaticPOCS.Singleton); 110 | 111 | Object.Destroy(singleton.gameObject); 112 | } 113 | 114 | [Test, TestMustExpectAllLogs] 115 | public void InjectSingleStaticPOCSMissingTest() 116 | { 117 | Assert.IsFalse(InjectSingleStaticPOCS.Singleton); 118 | LogAssert.Expect(LogType.Error, new Regex("No registered singleton instance:.*")); 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Src/Medicine.Tests/Tests/InjectSingleTests.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 77036e0ee3034e8b901a094275124300 3 | timeCreated: 1591903371 -------------------------------------------------------------------------------- /Src/Medicine.Tests/Tests/InjectTests.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | using NUnit.Framework; 3 | using UnityEngine; 4 | using UnityEngine.TestTools; 5 | 6 | namespace Medicine 7 | { 8 | public sealed class InjectTests 9 | { 10 | public sealed class InjectMB : MonoBehaviour 11 | { 12 | [Inject] 13 | public TestComp TestComp { get; } 14 | } 15 | 16 | public sealed class InjectOptionalMB : MonoBehaviour 17 | { 18 | [Inject(Optional = true)] 19 | public TestComp TestComp { get; } 20 | } 21 | 22 | [Test, TestMustExpectAllLogs] 23 | public void InjectTest() 24 | { 25 | var component = new GameObject() 26 | .WithComponent() 27 | .AddComponent() 28 | .TestComp; 29 | 30 | Assert.IsTrue(component); 31 | } 32 | 33 | [Test, TestMustExpectAllLogs] 34 | public void InjectMissingTest() 35 | { 36 | var component = new GameObject() 37 | .AddComponent() 38 | .TestComp; 39 | 40 | Assert.IsFalse(component); 41 | LogAssert.Expect(LogType.Error, new Regex("Failed to initialize.*")); 42 | } 43 | 44 | [Test, TestMustExpectAllLogs] 45 | public void InjectMissingOptionalTest() 46 | { 47 | var component = new GameObject() 48 | .AddComponent() 49 | .TestComp; 50 | 51 | Assert.IsFalse(component); 52 | } 53 | 54 | public sealed class InjectFromChildrenMB : MonoBehaviour 55 | { 56 | [Inject.FromChildren] 57 | public TestComp TestComp { get; } 58 | 59 | [Inject.FromChildren(IncludeInactive = true)] 60 | public TestComp TestCompIncludeInactive { get; } 61 | } 62 | 63 | public sealed class InjectFromChildrenOptionalMB : MonoBehaviour 64 | { 65 | [Inject.FromChildren(Optional = true)] 66 | public TestComp TestComp { get; } 67 | 68 | [Inject.FromChildren(Optional = true, IncludeInactive = true)] 69 | public TestComp TestCompIncludingInactive { get; } 70 | } 71 | 72 | [Test, TestMustExpectAllLogs] 73 | public void InjectFromChildrenTest() 74 | { 75 | var component = new GameObject() 76 | .WithChild(x => x.WithComponent()) 77 | .AddComponent() 78 | .TestComp; 79 | 80 | Assert.IsTrue(component); 81 | } 82 | 83 | [Test, TestMustExpectAllLogs] 84 | public void InjectFromChildrenIncludingInactiveTest() 85 | { 86 | var gameObject = new GameObject() 87 | .WithChild(x => x.WithComponent().SetActive(false)) 88 | .AddComponent(); 89 | 90 | Assert.IsFalse(gameObject.TestComp); 91 | Assert.IsTrue(gameObject.TestCompIncludeInactive); 92 | LogAssert.Expect(LogType.Error, new Regex("Failed to initialize.*")); 93 | } 94 | 95 | [Test, TestMustExpectAllLogs] 96 | public void InjectFromChildrenMissingTest() 97 | { 98 | var component = new GameObject() 99 | .AddComponent() 100 | .TestComp; 101 | 102 | Assert.IsFalse(component); 103 | LogAssert.Expect(LogType.Error, new Regex("Failed to initialize.*")); 104 | LogAssert.Expect(LogType.Error, new Regex("Failed to initialize.*")); 105 | } 106 | 107 | [Test, TestMustExpectAllLogs] 108 | public void InjectFromChildrenMissingOptionalTest() 109 | { 110 | var component = new GameObject() 111 | .AddComponent() 112 | .TestComp; 113 | 114 | Assert.IsFalse(component); 115 | } 116 | 117 | public sealed class InjectFromParentsMB : MonoBehaviour 118 | { 119 | [Inject.FromParents] 120 | public TestComp TestComp { get; } 121 | } 122 | 123 | public sealed class InjectFromParentsOptionalMB : MonoBehaviour 124 | { 125 | [Inject.FromParents(Optional = true)] 126 | public TestComp TestComp { get; } 127 | } 128 | 129 | [Test, TestMustExpectAllLogs] 130 | public void InjectFromParentsTest() 131 | { 132 | var component = new GameObject() 133 | .WithParent(x => x.WithComponent()) 134 | .AddComponent() 135 | .TestComp; 136 | 137 | Assert.IsTrue(component); 138 | } 139 | 140 | [Test, TestMustExpectAllLogs] 141 | public void InjectFromParentsMissingTest() 142 | { 143 | var component = new GameObject() 144 | .AddComponent() 145 | .TestComp; 146 | 147 | Assert.IsFalse(component); 148 | LogAssert.Expect(LogType.Error, new Regex("Failed to initialize.*")); 149 | } 150 | 151 | [Test, TestMustExpectAllLogs] 152 | public void InjectFromParentsMissingOptionalTest() 153 | { 154 | var component = new GameObject() 155 | .AddComponent() 156 | .TestComp; 157 | 158 | Assert.IsFalse(component); 159 | } 160 | 161 | public sealed class InjectArrayMB : MonoBehaviour 162 | { 163 | [Inject] 164 | public TestComp[] Colliders { get; } 165 | } 166 | 167 | public sealed class InjectArrayOptionalMB : MonoBehaviour 168 | { 169 | [Inject(Optional = true)] 170 | public TestComp[] Colliders { get; } 171 | } 172 | 173 | [Test, TestMustExpectAllLogs] 174 | public void InjectArrayTest() 175 | { 176 | var components = new GameObject() 177 | .WithComponent() 178 | .WithComponent() 179 | .WithComponent() 180 | .AddComponent() 181 | .Colliders; 182 | 183 | Assert.AreEqual(3, components.Length); 184 | } 185 | 186 | [Test, TestMustExpectAllLogs] 187 | public void InjectArrayMissingTest() 188 | { 189 | var components = new GameObject() 190 | .AddComponent() 191 | .Colliders; 192 | 193 | Assert.AreEqual(0, components.Length); 194 | LogAssert.Expect(LogType.Error, new Regex("Failed to initialize.*")); 195 | } 196 | 197 | [Test, TestMustExpectAllLogs] 198 | public void InjectArrayMissingOptionalTest() 199 | { 200 | var components = new GameObject() 201 | .AddComponent() 202 | .Colliders; 203 | 204 | Assert.AreEqual(0, components.Length); 205 | } 206 | 207 | public sealed class InjectFromChildrenArrayMB : MonoBehaviour 208 | { 209 | [Inject.FromChildren] 210 | public TestComp[] Colliders { get; } 211 | 212 | [Inject.FromChildren(IncludeInactive = true)] 213 | public TestComp[] CollidersIncludingInactive { get; } 214 | 215 | } 216 | 217 | public sealed class InjectFromChildrenArrayOptionalMB : MonoBehaviour 218 | { 219 | [Inject.FromChildren(Optional = true)] 220 | public TestComp[] Colliders { get; } 221 | } 222 | 223 | [Test, TestMustExpectAllLogs] 224 | public void InjectFromChildrenArrayTest() 225 | { 226 | var components = new GameObject() 227 | .WithComponent() 228 | .WithChild( 229 | x => x 230 | .WithComponent() 231 | .WithComponent() 232 | .WithComponent()) 233 | .AddComponent() 234 | .Colliders; 235 | 236 | Assert.AreEqual(4, components.Length); 237 | } 238 | 239 | [Test, TestMustExpectAllLogs] 240 | public void InjectFromChildrenArrayIncludingInactiveTest() 241 | { 242 | var gameObject = new GameObject() 243 | .WithComponent() 244 | .WithChild( 245 | x => x 246 | .WithComponent() 247 | .WithComponent() 248 | .WithComponent() 249 | .SetActive(false)) 250 | .AddComponent(); 251 | 252 | Assert.AreEqual(1, gameObject.Colliders.Length); 253 | Assert.AreEqual(4, gameObject.CollidersIncludingInactive.Length); 254 | } 255 | 256 | [Test, TestMustExpectAllLogs] 257 | public void InjectFromChildrenArrayMissingTest() 258 | { 259 | var components = new GameObject() 260 | .AddComponent() 261 | .Colliders; 262 | 263 | Assert.AreEqual(0, components.Length); 264 | LogAssert.Expect(LogType.Error, new Regex("Failed to initialize.*")); 265 | LogAssert.Expect(LogType.Error, new Regex("Failed to initialize.*")); 266 | } 267 | 268 | [Test, TestMustExpectAllLogs] 269 | public void InjectFromChildrenArrayMissingOptionalTest() 270 | { 271 | var components = new GameObject() 272 | .AddComponent() 273 | .Colliders; 274 | 275 | Assert.AreEqual(0, components.Length); 276 | } 277 | 278 | public sealed class InjectFromParentsArrayMB : MonoBehaviour 279 | { 280 | [Inject.FromParents] 281 | public TestComp[] Colliders { get; } 282 | } 283 | 284 | public sealed class InjectFromParentsArrayOptionalMB : MonoBehaviour 285 | { 286 | [Inject.FromParents(Optional = true)] 287 | public TestComp[] Colliders { get; } 288 | } 289 | 290 | [Test, TestMustExpectAllLogs] 291 | public void InjectFromParentsArrayTest() 292 | { 293 | var components = new GameObject() 294 | .WithComponent() 295 | .WithParent( 296 | x => x 297 | .WithComponent() 298 | .WithComponent() 299 | .WithComponent()) 300 | .AddComponent() 301 | .Colliders; 302 | 303 | Assert.AreEqual(4, components.Length); 304 | } 305 | 306 | [Test, TestMustExpectAllLogs] 307 | public void InjectFromParentsArrayMissingTest() 308 | { 309 | var components = new GameObject() 310 | .AddComponent() 311 | .Colliders; 312 | 313 | Assert.AreEqual(0, components.Length); 314 | LogAssert.Expect(LogType.Error, new Regex("Failed to initialize.*")); 315 | } 316 | 317 | [Test, TestMustExpectAllLogs] 318 | public void InjectFromParentsArrayMissingOptionalTest() 319 | { 320 | var components = new GameObject() 321 | .AddComponent() 322 | .Colliders; 323 | 324 | Assert.AreEqual(0, components.Length); 325 | } 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /Src/Medicine.Tests/Tests/InjectTests.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1baf62725bb645e8bc51e1baef04b030 3 | timeCreated: 1591896519 -------------------------------------------------------------------------------- /Src/Medicine.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Medicine", 3 | "references": [], 4 | "includePlatforms": [], 5 | "excludePlatforms": [], 6 | "allowUnsafeCode": true, 7 | "overrideReferences": true, 8 | "precompiledReferences": [], 9 | "autoReferenced": true, 10 | "defineConstraints": [], 11 | "versionDefines": [], 12 | "noEngineReferences": false 13 | } 14 | -------------------------------------------------------------------------------- /Src/Medicine.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9d802ac6321ed6f499c8de1b6ee4ec88 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Src/NonAlloc.Benchmark.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using System.Runtime.CompilerServices; 5 | using JetBrains.Annotations; 6 | using UnityEngine.Profiling; 7 | using Debug = UnityEngine.Debug; 8 | 9 | // ReSharper disable MethodOverloadWithOptionalParameter 10 | namespace Medicine 11 | { 12 | public static partial class NonAlloc 13 | { 14 | /// 15 | /// Utility struct for conveniently easily measuring the duration of execution of a 16 | /// code fragment. 17 | /// 18 | /// 19 | /// using (NonAlloc.Benchmark.Start("GetComponentsGeneric")) 20 | /// { 21 | /// MyExpensiveOperations(); 22 | /// // duration is automatically logged to console when we go out of scope 23 | /// } 24 | /// 25 | [UsedImplicitly] 26 | public readonly struct Benchmark : IDisposable 27 | { 28 | readonly string name; 29 | readonly long ticks; 30 | 31 | Benchmark(string name, long ticks) 32 | => (this.name, this.ticks) = (name, ticks); 33 | 34 | static readonly double tickFrequency = Stopwatch.IsHighResolution 35 | ? 10000000.0 / Stopwatch.Frequency 36 | : 1.0; 37 | 38 | /// Start a new benchmark with default name (based on call location). 39 | public static Benchmark Start([CallerMemberName] string name = "", [CallerFilePath] string path = "", [CallerLineNumber] int cln = 0) 40 | { 41 | name = $"{name}() ({Path.GetFileName(path)}:{cln.ToString()})"; 42 | #if DEBUG 43 | Profiler.BeginSample($"[Benchmark] {name}"); 44 | #endif 45 | return new Benchmark(name, Stopwatch.GetTimestamp()); 46 | } 47 | 48 | /// Start a new benchmark with given name. 49 | public static Benchmark Start(string name) 50 | { 51 | #if DEBUG 52 | Profiler.BeginSample("[Benchmark] " + name); 53 | #endif 54 | return new Benchmark(name, Stopwatch.GetTimestamp()); 55 | } 56 | 57 | long ElapsedTicks 58 | => Stopwatch.GetTimestamp() - ticks; 59 | 60 | long GetElapsedDateTimeTicks() 61 | => Stopwatch.IsHighResolution 62 | ? (long)(ElapsedTicks * tickFrequency) 63 | : ElapsedTicks; 64 | 65 | TimeSpan Elapsed 66 | => new TimeSpan(GetElapsedDateTimeTicks()); 67 | 68 | public void Dispose() 69 | { 70 | var elapsed = Elapsed.TotalMilliseconds; 71 | #if DEBUG 72 | Profiler.EndSample(); 73 | #endif 74 | #if UNITY_EDITOR 75 | Debug.Log($"[Benchmark] {name}: {elapsed:0.00}ms"); 76 | #else 77 | Debug.Log($"[Benchmark] {name}: {elapsed:0.00}ms"); 78 | #endif 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Src/NonAlloc.Benchmark.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5fc47857114b4d6ba35a1d6df0861947 3 | timeCreated: 1591475808 -------------------------------------------------------------------------------- /Src/NonAlloc.RecyclableList.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Runtime.CompilerServices; 4 | using System.Runtime.InteropServices; 5 | using System.Runtime.Serialization; 6 | using Unity.Collections.LowLevel.Unsafe; 7 | using static System.Runtime.CompilerServices.MethodImplOptions; 8 | 9 | // ReSharper disable StaticMemberInGenericType 10 | namespace Medicine 11 | { 12 | public static partial class NonAlloc 13 | { 14 | // number of recyclable lists to switch between 15 | const int RecyclableListCount = 4; 16 | 17 | // pool of RecyclableLists. 18 | // we're switching between multiple lists for each GetRecyclableList to avoid issues with nested GetComponentsNonAlloc usage. 19 | // this is still a non-deal solution because it will cause crashes when we're using 4+ temporary RecyclableLists at the 20 | // same time, but it should cover vast majority of use cases 21 | static readonly RecyclableList[] recyclableLists = InitializeRecyclableLists(); 22 | 23 | // this method lets us avoid a static ctor to ensure beforefieldinit: 24 | // https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1810 25 | static RecyclableList[] InitializeRecyclableLists() 26 | { 27 | var lists = new RecyclableList[RecyclableListCount]; 28 | for (int i = 0; i < lists.Length; ++i) 29 | lists[i] = new RecyclableList(initialCapacity: 1024); 30 | return lists; 31 | } 32 | 33 | // index of the last returned list 34 | static int currentRecyclableList; 35 | 36 | #if UNITY_EDITOR 37 | [UnityEditor.InitializeOnEnterPlayMode] 38 | static void ClearRecyclableListsOnEnterPlayMode() 39 | { 40 | for (int i = 0; i < recyclableLists.Length; ++i) 41 | recyclableLists[i] = new RecyclableList(initialCapacity: 1024); 42 | } 43 | #endif 44 | 45 | /// 46 | /// A wrapper type that stores a together with its backing array, and implements utilities that allow you to 47 | /// re-use allocated memory by mutating the list's/array's managed type at runtime. 48 | /// 49 | public sealed class RecyclableList 50 | { 51 | /// The recyclable list of objects. 52 | public readonly List InternalList = new List(); 53 | 54 | /// The actual backing array for the . 55 | public Array InternalBackingArray { get; private set; } 56 | 57 | Type currentType; 58 | int actualCapacity; 59 | 60 | /// The actual allocated length of the . 61 | public int ActualCapacity 62 | => actualCapacity; 63 | 64 | /// The current managed type of the elements stored in the /. 65 | public Type CurrentType 66 | => currentType; 67 | 68 | /// 69 | /// Main constructor. 70 | /// This allows you to create your own RecyclableLists to use in worker threads, to avoid nested iteration issues, etc. 71 | /// 72 | /// Initial capacity of the RecyclableList's . 73 | public RecyclableList(int initialCapacity = 256) 74 | { 75 | actualCapacity = initialCapacity; 76 | InternalBackingArray = new object[initialCapacity]; 77 | Unsafe.SetListBackingArray(InternalList, InternalBackingArray); 78 | } 79 | 80 | /// 81 | /// Prepares the RecyclableList to be used as a , restoring original capacity and setting element type. 82 | /// (This is the method you're probably looking for if you're trying to use the RecyclableList API manually and you need a ). 83 | /// 84 | /// 85 | /// Setting this to false allows you to skip clearing the list before you use it. 86 | /// This is useful if you're going to pass it to a method that clears the list anyway, such as 87 | /// 88 | /// Type of elements that will be stored in the list. 89 | /// Temporary that should be used and discarded in current scope. 90 | [MethodImpl(AggressiveInlining)] 91 | public List AsList(bool clear = true) where T : class 92 | { 93 | #if MEDICINE_DEBUG 94 | return new List(); 95 | #endif 96 | EnsureListSyncedWithArray(); 97 | ExpandArrayToActualLength(); 98 | 99 | if (clear) 100 | InternalList.Clear(); 101 | 102 | SetType(); 103 | return InternalList as List; 104 | } 105 | 106 | /// 107 | /// Prepares the RecyclableList to be used as a generic array, setting array length and element type. 108 | /// (This is the method you're probably looking for if you're trying to use the RecyclableList API manually and you need an array). 109 | /// 110 | /// Requested length of the array. 111 | /// Setting this to false allows you to skip clearing the array before you use it. 112 | /// Type of elements that will be stored in the array. 113 | /// Temporary array that should be used and discarded in current scope. 114 | [MethodImpl(AggressiveInlining)] 115 | public T[] AsArray(int length, bool clear = true) where T : class 116 | { 117 | #if MEDICINE_DEBUG 118 | return new T[length]; 119 | #endif 120 | void ThrowArgumentOutOfRange() 121 | => throw new ArgumentOutOfRangeException(nameof(length)); 122 | 123 | if (length <= 0) 124 | ThrowArgumentOutOfRange(); 125 | 126 | if (clear) 127 | { 128 | ExpandArrayToActualLength(); 129 | InternalList.Clear(); 130 | } 131 | 132 | SetType(); 133 | SetCapacity(length); 134 | return InternalBackingArray as T[]; 135 | } 136 | 137 | /// 138 | /// Ensures that the reference correctly points to the 's backing array. 139 | /// This can no longer be true if the list's capacity has changed, for example by adding elements to it or by a GetComponents call. 140 | /// 141 | [MethodImpl(AggressiveInlining)] 142 | public unsafe void EnsureListSyncedWithArray() 143 | { 144 | var capacity = InternalList.Capacity; 145 | 146 | // detect if the list's capacity has changed 147 | // this indicates that the internal array has been replaced with a new one 148 | // (in that case, we want to start using its internal array as the new staticArray) 149 | if (capacity == InternalBackingArray.Length) 150 | return; 151 | 152 | void* ptr = UnsafeUtility.PinGCObjectAndGetAddress(InternalList, out ulong gcHandle); 153 | var array = UnsafeUtility.AsRef(ptr).Array; 154 | UnsafeUtility.ReleaseGCObject(gcHandle); 155 | InternalBackingArray = array; 156 | actualCapacity = capacity; 157 | } 158 | 159 | /// 160 | /// Trims the list's backing array to the number of elements stored in the list. 161 | /// 162 | [MethodImpl(AggressiveInlining)] 163 | public void TrimArrayToListLength() 164 | => Unsafe.OverwriteArrayLength(InternalBackingArray, InternalList.Count); 165 | 166 | /// 167 | /// Expands the 's length to the actual memory-allocated capacity. 168 | /// 169 | [MethodImpl(AggressiveInlining)] 170 | public void ExpandArrayToActualLength() 171 | => Unsafe.OverwriteArrayLength(InternalBackingArray, actualCapacity); 172 | 173 | /// 174 | /// Sets the array length (== list capacity), reallocating or trimming the array if necessary. 175 | /// 176 | [MethodImpl(AggressiveInlining)] 177 | public void SetCapacity(int length) 178 | { 179 | if (InternalBackingArray.Length == length) 180 | return; 181 | 182 | if (length <= actualCapacity) 183 | { 184 | Unsafe.OverwriteArrayLength(InternalBackingArray, length); 185 | } 186 | else 187 | { 188 | InternalList.Clear(); 189 | InternalBackingArray = new object[length]; 190 | Unsafe.SetListBackingArray(InternalList, InternalBackingArray); 191 | } 192 | } 193 | 194 | /// 195 | /// Sets the element type of the recyclable array and list. 196 | /// 197 | /// Type of elements stored in the array/list. 198 | [MethodImpl(AggressiveInlining)] 199 | public unsafe void SetType() 200 | { 201 | var type = typeof(T); 202 | if (currentType == typeof(T)) 203 | return; 204 | 205 | currentType = type; 206 | void* arrayPtr = UnsafeUtility.PinGCObjectAndGetAddress(InternalBackingArray, out ulong arrayGcHandle); 207 | void* listPtr = UnsafeUtility.PinGCObjectAndGetAddress(InternalList, out ulong listGcHandle); 208 | UnsafeUtility.AsRef(arrayPtr) = TypeHeaders.Header; 209 | UnsafeUtility.AsRef(listPtr) = TypeHeaders>.Header; 210 | UnsafeUtility.ReleaseGCObject(arrayGcHandle); 211 | UnsafeUtility.ReleaseGCObject(listGcHandle); 212 | } 213 | } 214 | 215 | // generic "dictionary-like" static class that is used to obtain the header of a given managed type. 216 | // by overwriting this header with a header of another type, we can effectively mutate the object's managed type. 217 | static class TypeHeaders 218 | { 219 | /// Managed object header data for type . 220 | public static readonly ObjectHeader Header; 221 | 222 | static unsafe TypeHeaders() 223 | { 224 | // create temporary instance of managed type in order to read the type header 225 | // this is done once per type in the lifetime of the program 226 | 227 | // ReSharper disable once AssignNullToNotNullAttribute 228 | var tempInstance = typeof(T).IsArray 229 | // create array of 0 length 230 | ? Array.CreateInstance(typeof(T).GetElementType(), 0) 231 | // create object instance without calling the ctor 232 | : FormatterServices.GetUninitializedObject(typeof(T)); 233 | 234 | void* ptr = UnsafeUtility.PinGCObjectAndGetAddress(tempInstance, out ulong gcHandle); 235 | UnsafeUtility.CopyPtrToStructure(ptr, out Header); 236 | UnsafeUtility.ReleaseGCObject(gcHandle); 237 | } 238 | } 239 | 240 | [StructLayout(LayoutKind.Sequential)] 241 | readonly struct ObjectHeader 242 | { 243 | readonly IntPtr data; 244 | } 245 | 246 | [StructLayout(LayoutKind.Sequential)] 247 | struct ListHeader 248 | { 249 | readonly IntPtr data0, data1; 250 | public Array Array; 251 | } 252 | 253 | [StructLayout(LayoutKind.Sequential)] 254 | struct ArrayHeader 255 | { 256 | readonly IntPtr data0, data1, data2; 257 | public int ManagedArrayLength; 258 | } 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /Src/NonAlloc.RecyclableList.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2d3b7920772d495782bd976136fad026 3 | timeCreated: 1591454732 -------------------------------------------------------------------------------- /Src/NonAlloc.Unsafe.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Runtime.CompilerServices; 4 | using JetBrains.Annotations; 5 | using Unity.Collections.LowLevel.Unsafe; 6 | using static System.Runtime.CompilerServices.MethodImplOptions; 7 | 8 | namespace Medicine 9 | { 10 | public static partial class NonAlloc 11 | { 12 | [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] 13 | public static class Unsafe 14 | { 15 | [MethodImpl(AggressiveInlining)] 16 | public static unsafe T[] GetInternalArray(List list) 17 | { 18 | void* ptr = UnsafeUtility.PinGCObjectAndGetAddress(list, out ulong gcHandle); 19 | var array = UnsafeUtility.AsRef(ptr).Array; 20 | UnsafeUtility.ReleaseGCObject(gcHandle); 21 | return array as T[]; 22 | } 23 | 24 | [MethodImpl(AggressiveInlining)] 25 | public static unsafe void OverwriteArrayLength(Array array, int length) 26 | { 27 | if (array.Length == length) 28 | return; 29 | 30 | void* ptr = UnsafeUtility.PinGCObjectAndGetAddress(array, out ulong gcHandle); 31 | UnsafeUtility.AsRef(ptr).ManagedArrayLength = length; 32 | UnsafeUtility.ReleaseGCObject(gcHandle); 33 | } 34 | 35 | [MethodImpl(AggressiveInlining)] 36 | public static unsafe void SetListBackingArray(object InternalList, Array array) 37 | { 38 | void* ptr = UnsafeUtility.PinGCObjectAndGetAddress(InternalList, out ulong gcHandle); 39 | UnsafeUtility.AsRef(ptr).Array = array; 40 | UnsafeUtility.ReleaseGCObject(gcHandle); 41 | } 42 | 43 | [MethodImpl(AggressiveInlining)] 44 | public static unsafe void SetManagedObjectType(object source) 45 | { 46 | void* ptr = UnsafeUtility.PinGCObjectAndGetAddress(source, out ulong gcHandle); 47 | UnsafeUtility.AsRef(ptr) = TypeHeaders.Header; 48 | UnsafeUtility.ReleaseGCObject(gcHandle); 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Src/NonAlloc.Unsafe.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: dd223d45c8984d66a7c143c793af71c7 3 | timeCreated: 1591454721 -------------------------------------------------------------------------------- /Src/NonAlloc.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Runtime.CompilerServices; 4 | using JetBrains.Annotations; 5 | using Obj = UnityEngine.Object; 6 | using UnityEngine; 7 | using static System.Runtime.CompilerServices.MethodImplOptions; 8 | 9 | namespace Medicine 10 | { 11 | public static partial class NonAlloc 12 | { 13 | /// 14 | /// Returns a list of all active loaded objects of given type. 15 | /// This does not return assets (such as meshes, textures or prefabs), or objects with HideFlags.DontSave set. 16 | /// Objects attached to inactive GameObjects are only included if inactiveObjects is set to true. 17 | /// Use FindObjectsOfTypeAll to avoid these limitations. 18 | /// In Editor, this searches the Scene view by default. 19 | /// If you want to find an object in the Prefab stage, see the StageUtility APIs. 20 | /// 21 | /// 22 | /// This is an optimized version of the Object.FindObjectsOfType method that reduces unnecessary array copying. 23 | /// It can be used as a direct replacement. 24 | /// 25 | [UsedImplicitly] 26 | [MethodImpl(AggressiveInlining)] 27 | #if UNITY_2020_1_OR_NEWER // includeInactive argument only supported in Unity 2020.1+ 28 | public static T[] FindObjectsOfType(bool includeInactive = false) where T : Object 29 | { 30 | #if MEDICINE_DEBUG 31 | return Object.FindObjectsOfType(includeInactive); 32 | #endif 33 | var array = Object.FindObjectsOfType(typeof(T), includeInactive); 34 | Unsafe.SetManagedObjectType(array); 35 | return array as T[]; 36 | } 37 | #else 38 | public static T[] FindObjectsOfType() where T : Object 39 | { 40 | #if MEDICINE_DEBUG 41 | return Object.FindObjectsOfType(); 42 | #endif 43 | var array = Object.FindObjectsOfType(typeof(T)); 44 | Unsafe.SetManagedObjectType(array); 45 | return array as T[]; 46 | } 47 | #endif 48 | 49 | /// 50 | /// Returns an array of all objects of type T. 51 | /// This function can return any type of Unity object that is loaded, including game objects, prefabs, 52 | /// materials, meshes, textures, etc. 53 | /// It will also list internal objects, therefore be careful with the way you handle the returned objects. 54 | /// 55 | /// 56 | /// This is an optimized version of the Resources.FindObjectsOfTypeAll method that reduces unnecessary array copying. 57 | /// It can be used as a direct replacement. 58 | /// 59 | [UsedImplicitly] 60 | [MethodImpl(AggressiveInlining)] 61 | public static T[] FindObjectsOfTypeAll() where T : Object 62 | { 63 | #if MEDICINE_DEBUG 64 | return Resources.FindObjectsOfTypeAll(); 65 | #endif 66 | var array = Resources.FindObjectsOfTypeAll(typeof(T)); 67 | Unsafe.SetManagedObjectType(array); 68 | return array as T[]; 69 | } 70 | 71 | /// 72 | /// Loads all assets of given type in a folder or file at path in a Resources folder. 73 | /// 74 | /// 75 | /// Path of the target folder. When using the empty string (i.e., ""), 76 | /// the function will load the entire contents of the Resources folder. 77 | /// 78 | /// 79 | /// This is an optimized version of the Resources.LoadAll method that reduces unnecessary array copying. 80 | /// It can be used as a direct replacement. 81 | /// 82 | [UsedImplicitly] 83 | [MethodImpl(AggressiveInlining)] 84 | public static T[] LoadAll(string path) where T : Object 85 | { 86 | #if MEDICINE_DEBUG 87 | return Resources.LoadAll(path); 88 | #endif 89 | var array = Resources.LoadAll(path, typeof(T)); 90 | Unsafe.SetManagedObjectType(array); 91 | return array as T[]; 92 | } 93 | 94 | /// 95 | /// Equivalent to , but re-uses a recyclable buffer to minimize memory allocations 96 | /// and improve performance. 97 | /// WARNING: Make sure you use the result array directly and not store any references to it, as they may become invalid on next call. 98 | /// 99 | /// Type of components to find. 100 | /// Temporary array of components. 101 | [MethodImpl(AggressiveInlining)] 102 | public static T[] GetComponentsNonAlloc(this GameObject gameObject) where T : class 103 | { 104 | #if MEDICINE_DEBUG 105 | return gameObject.GetComponents(); 106 | #endif 107 | var recyclableList = GetRecyclableList(); 108 | var list = recyclableList.AsList(clear: false); 109 | gameObject.GetComponents(list); 110 | recyclableList.TrimArrayToListLength(); 111 | return recyclableList.InternalBackingArray as T[]; 112 | } 113 | 114 | /// 115 | /// Equivalent to , but re-uses a recyclable buffer to minimize memory allocations. 116 | /// WARNING: Make sure you use the result array directly and not store any references to it, as they may become invalid on next call. 117 | /// 118 | /// Type of components to find. 119 | /// Temporary array of components. 120 | [MethodImpl(AggressiveInlining)] 121 | public static T[] GetComponentsInChildrenNonAlloc(this GameObject gameObject, bool includeInactive = false) where T : class 122 | { 123 | #if MEDICINE_DEBUG 124 | return gameObject.GetComponentsInChildren(includeInactive); 125 | #endif 126 | var recyclableList = GetRecyclableList(); 127 | var list = recyclableList.AsList(clear: false); 128 | gameObject.GetComponentsInChildren(includeInactive, list); 129 | recyclableList.TrimArrayToListLength(); 130 | return recyclableList.InternalBackingArray as T[]; 131 | } 132 | 133 | /// 134 | /// Equivalent to , but re-uses a recyclable buffer to minimize memory allocations. 135 | /// WARNING: Make sure you use the result array directly and not store any references to it, as they may become invalid on next call. 136 | /// 137 | /// Type of components to find. 138 | /// Temporary array of components. 139 | [MethodImpl(AggressiveInlining)] 140 | public static T[] GetComponentsInParentNonAlloc(this GameObject gameObject, bool includeInactive = false) where T : class 141 | { 142 | #if MEDICINE_DEBUG 143 | return gameObject.GetComponentsInParent(includeInactive); 144 | #endif 145 | var recyclableList = GetRecyclableList(); 146 | var list = recyclableList.AsList(clear: false); 147 | gameObject.GetComponentsInParent(includeInactive, list); 148 | recyclableList.TrimArrayToListLength(); 149 | return recyclableList.InternalBackingArray as T[]; 150 | } 151 | 152 | /// 153 | /// Equivalent to , but re-uses a recyclable buffer to minimize memory allocations 154 | /// and improve performance. 155 | /// WARNING: Make sure you use the result array directly and not store any references to it, as they may become invalid on next call. 156 | /// 157 | /// Type of components to find. 158 | /// Temporary array of components. 159 | [MethodImpl(AggressiveInlining)] 160 | public static T[] GetComponentsNonAlloc(this Component component) where T : class 161 | => GetComponentsNonAlloc(component.gameObject); 162 | 163 | 164 | /// 165 | /// Equivalent to , but re-uses a recyclable buffer to minimize memory allocations. 166 | /// WARNING: Make sure you use the result array directly and not store any references to it, as they may become invalid on next call. 167 | /// 168 | /// Type of components to find. 169 | /// Temporary array of components. 170 | [MethodImpl(AggressiveInlining)] 171 | public static T[] GetComponentsInChildrenNonAlloc(this Component component, bool includeInactive = false) where T : class 172 | => GetComponentsInChildrenNonAlloc(component.gameObject, includeInactive); 173 | 174 | 175 | /// 176 | /// Equivalent to , but re-uses a recyclable buffer to minimize memory allocations. 177 | /// WARNING: Make sure you use the result array directly and not store any references to it, as they may become invalid on next call. 178 | /// 179 | /// Type of components to find. 180 | /// Temporary array of components. 181 | [MethodImpl(AggressiveInlining)] 182 | public static T[] GetComponentsInParentNonAlloc(this Component component, bool includeInactive = false) where T : class 183 | => GetComponentsInParentNonAlloc(component.gameObject, includeInactive); 184 | 185 | /// 186 | /// Gets a RecyclableList that is based on a recycled buffer. 187 | /// 188 | [MethodImpl(AggressiveInlining)] 189 | public static RecyclableList GetRecyclableList() 190 | => recyclableLists[currentRecyclableList = (currentRecyclableList + 1) % RecyclableListCount]; 191 | 192 | /// 193 | /// Gets an array of given length that is based on a recycled buffer. 194 | /// 195 | /// Requested length of the array. 196 | /// Settings this to false allows you to skip clearing the array before you use it. 197 | /// Type of elements that will be stored in the list. 198 | /// Temporary that should be used and discarded in current scope. 199 | [MethodImpl(AggressiveInlining)] 200 | public static T[] GetArray(int length, bool clear = true) where T : class 201 | => GetRecyclableList().AsArray(length, clear); 202 | 203 | /// 204 | /// Prepares the RecyclableList to be used as a , restoring original capacity and setting element type. 205 | /// (This is the method you're probably looking for if you're trying to use the RecyclableList API manually and you need a ). 206 | /// 207 | /// 208 | /// Setting this to false allows you to skip clearing the list before you use it. 209 | /// This is useful if you're going to pass it to a method that clears the list anyway, such as 210 | /// 211 | /// Type of elements that will be stored in the list. 212 | /// Temporary that should be used and discarded in current scope. 213 | [MethodImpl(AggressiveInlining)] 214 | public static List GetList(bool clear = true) where T : class 215 | => GetRecyclableList().AsList(clear); 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /Src/NonAlloc.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: bacc4c78c51949f4888c09b1408dff76 3 | timeCreated: 1591116513 -------------------------------------------------------------------------------- /Src/RuntimeHelpers.Collection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Runtime.CompilerServices; 4 | using UnityEngine; 5 | using static System.Runtime.CompilerServices.MethodImplOptions; 6 | #pragma warning disable CS0162 // ReSharper disable HeuristicUnreachableCode 7 | // ReSharper disable StaticMemberInGenericType 8 | 9 | namespace Medicine 10 | { 11 | public static partial class RuntimeHelpers 12 | { 13 | /// 14 | /// Helper methods related to the [Inject.All] implementation. 15 | /// 16 | public static class Collection where TRegistered : class 17 | { 18 | const int InitialCapacity = 32; 19 | 20 | static TRegistered[] instances = CreateInstanceArray(); 21 | static int capacity = InitialCapacity; 22 | static int count = 0; 23 | 24 | #if UNITY_EDITOR 25 | static bool checkedAttribute; 26 | #endif 27 | 28 | // this method lets us avoid a static ctor to ensure beforefieldinit: 29 | // https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1810 30 | static TRegistered[] CreateInstanceArray() 31 | { 32 | #if UNITY_EDITOR 33 | reinitializeAction += () => 34 | { 35 | // if (MedicineDebug) 36 | Debug.Log($"Clearing collection: {typeof(TRegistered).Name}"); 37 | instances = new TRegistered[capacity = InitialCapacity]; 38 | count = 0; 39 | }; 40 | 41 | debugAction += () => Debug.Log($"Collection<{typeof(TRegistered).Name}> = {count}/{capacity}"); 42 | #endif 43 | return new TRegistered[InitialCapacity]; 44 | } 45 | 46 | /// 47 | /// Get an array of active registered instances of type . 48 | /// 49 | /// 50 | /// This is a helper method. Useful for some edge cases, but you don't usually need to use it directly. 51 | /// See and to learn more. 52 | /// 53 | [MethodImpl(AggressiveInlining)] 54 | public static TRegistered[] GetInstances() 55 | { 56 | if (!ApplicationIsPlaying) 57 | return ErrorEditMode(); 58 | 59 | #if UNITY_EDITOR 60 | if (!checkedAttribute && typeof(TRegistered).CustomAttributes.All(x => x.AttributeType != typeof(Register.All))) 61 | Debug.LogError($"Tried to obtain all instances of {typeof(TRegistered).Name}, but it isn't marked with [Medicine.Register.All]."); 62 | 63 | checkedAttribute = true; 64 | #endif 65 | 66 | if (count == 0) 67 | return Array.Empty(); 68 | 69 | #if MEDICINE_DEBUG 70 | var instancesForEnumeration = new TRegistered[count]; 71 | 72 | Array.Copy( 73 | sourceArray: instances, 74 | destinationArray: instancesForEnumeration, 75 | length: count 76 | ); 77 | 78 | return instancesForEnumeration; 79 | #elif MEDICINE_FUNSAFE_COLLECTIONS 80 | // copyless implementation - trim the array and return it directly without copying 81 | NonAlloc.Unsafe.OverwriteArrayLength(instances, count); 82 | return instances; 83 | #else 84 | // copy instances to temporary buffer 85 | // this avoids issues with instances being disabled during enumeration 86 | var instancesForEnumeration = NonAlloc.GetArray(length: count, clear: false); 87 | 88 | Array.Copy( 89 | sourceArray: instances, 90 | destinationArray: instancesForEnumeration, 91 | length: count 92 | ); 93 | 94 | return instancesForEnumeration; 95 | #endif 96 | } 97 | 98 | /// 99 | /// Add an instance of to the registered instances. 100 | /// 101 | /// 102 | /// This is a helper method. Useful for some edge cases, but you don't usually need to use it directly. 103 | /// See and to learn more. 104 | /// 105 | [MethodImpl(AggressiveInlining)] 106 | public static void RegisterInstance(TRegistered instance) 107 | { 108 | if (MedicineDebug && (instance == null || instance is UnityEngine.Object obj && !obj)) 109 | { 110 | Debug.LogError($"Tried to register null {typeof(TRegistered).Name} instance."); 111 | return; 112 | } 113 | 114 | if (MedicineDebug) 115 | Debug.Log($"Registering {instance} as {typeof(TRegistered).Name}"); 116 | 117 | if (count == capacity) 118 | Resize(); 119 | 120 | #if MEDICINE_FUNSAFE_COLLECTIONS && !MEDICINE_DEBUG 121 | // copyless implementation - array was (possibly) trimmed during enumeration. 122 | // ensure array length is reset to capacity before registering new instances 123 | NonAlloc.Unsafe.OverwriteArrayLength(instances, capacity); 124 | #endif 125 | 126 | instances[count++] = instance; 127 | 128 | static void Resize() 129 | { 130 | capacity *= 2; 131 | Array.Resize(ref instances, capacity); 132 | } 133 | } 134 | 135 | /// 136 | /// Remove an instance of from the registered instances. 137 | /// 138 | /// 139 | /// This is a helper method. Useful for some edge cases, but you don't usually need to use it directly. 140 | /// See and to learn more. 141 | /// 142 | [MethodImpl(AggressiveInlining)] 143 | public static void UnregisterInstance(TRegistered instance) 144 | { 145 | if (MedicineDebug && (instance == null || instance is UnityEngine.Object obj && !obj)) 146 | { 147 | Debug.LogError($"Tried to unregister null {typeof(TRegistered).Name} instance."); 148 | return; 149 | } 150 | 151 | // search from end - assume the oldest instances are the most likely to be long-lived 152 | for (int i = count - 1; i >= 0; --i) 153 | { 154 | if (!ReferenceEquals(instances[i], instance)) 155 | continue; 156 | 157 | // remove by swapping with last array element 158 | instances[i] = instances[count - 1]; 159 | instances[count - 1] = null; // clear reference to allow gc 160 | count -= 1; 161 | return; 162 | } 163 | } 164 | 165 | static TRegistered[] ErrorEditMode() 166 | { 167 | Debug.LogError($"Cannot acquire registered object array in edit mode: {typeof(TRegistered).Name}"); 168 | return null; 169 | } 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /Src/RuntimeHelpers.Collection.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f0d325010c6a4eecb5003af8df5ceff2 3 | timeCreated: 1591027593 -------------------------------------------------------------------------------- /Src/RuntimeHelpers.Singleton.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.Serialization; 4 | using static System.Runtime.CompilerServices.MethodImplOptions; 5 | using UnityEngine; 6 | using Obj = UnityEngine.Object; 7 | 8 | #pragma warning disable 162 9 | // ReSharper disable StaticMemberInGenericType 10 | namespace Medicine 11 | { 12 | public static partial class RuntimeHelpers 13 | { 14 | /// 15 | /// Helper methods related to the [Inject.Single] implementation. 16 | /// 17 | public static class Singleton where TSingleton : Obj 18 | { 19 | static TSingleton instance = SetupReinitialization(); 20 | 21 | static TSingleton uninitializedInstance; 22 | 23 | #if UNITY_EDITOR 24 | static bool checkedAttribute; 25 | #endif 26 | 27 | static TSingleton SetupReinitialization() 28 | { 29 | #if UNITY_EDITOR 30 | reinitializeAction += () => 31 | { 32 | // if (MedicineDebug) 33 | Debug.Log($"Clearing singleton: {typeof(TSingleton).Name}"); 34 | instance = null; 35 | }; 36 | 37 | debugAction += () => Debug.Log($"Singleton<{typeof(TSingleton).Name}> = {(instance ? instance.ToString() : "null")}", instance); 38 | #endif 39 | return null; 40 | } 41 | 42 | [MethodImpl(AggressiveInlining)] 43 | public static void ReplaceCurrentInstance(TSingleton obj) 44 | { 45 | if (MedicineDebug) 46 | Debug.Log($"Replacing singleton instance: {obj.name} as {typeof(TSingleton).Name}", obj); 47 | 48 | instance = obj; 49 | } 50 | 51 | /// 52 | /// Register the object as the active instance. 53 | /// 54 | /// 55 | /// This is a helper method. Useful for some edge cases, but you don't usually need to use it directly. 56 | /// See and to learn more. 57 | /// 58 | [MethodImpl(AggressiveInlining)] 59 | public static void RegisterInstance(TSingleton obj) 60 | { 61 | #if UNITY_EDITOR 62 | // only allow the registration of ScriptableObject singleton instances from Preloaded Assets. 63 | // ------------------------------------------------------------------------------------------ 64 | // in editor, we can have multiple loaded instances of the SO, which will result in all of them 65 | // trying to register themselves as the active instance. 66 | // in build, the only loaded instance will be the one in preloaded assets, which means we don't have that problem 67 | // (as long as the developer doesn't reference other instances from other loaded objects - not supported for now). 68 | if (obj is ScriptableObject) 69 | if (System.Array.IndexOf(UnityEditor.PlayerSettings.GetPreloadedAssets(), obj) < 0) 70 | return; 71 | #endif 72 | if (!obj) 73 | { 74 | Debug.LogError($"Failed to register singleton instance {typeof(TSingleton).Name}: {(ReferenceEquals(obj, null) ? "Null" : "Destroyed")} object reference"); 75 | return; 76 | } 77 | 78 | if (!instance) 79 | { 80 | if (MedicineDebug) 81 | Debug.Log($"Registering singleton instance: {obj.name} as {typeof(TSingleton).Name}", obj); 82 | 83 | instance = obj; 84 | } 85 | else if (instance != obj) 86 | { 87 | Debug.LogError($"Failed to register singleton instance {obj.name} as {typeof(TSingleton).Name}: a registered instance already exists: {instance.name}", obj); 88 | } 89 | } 90 | 91 | /// 92 | /// Unregister the object from being the active instance. 93 | /// 94 | /// 95 | /// This is a helper method. Useful for some edge cases, but you don't usually need to use it directly. 96 | /// See and to learn more. 97 | /// 98 | [MethodImpl(AggressiveInlining)] 99 | public static void UnregisterInstance(TSingleton obj) 100 | { 101 | if (ReferenceEquals(instance, obj)) 102 | { 103 | if (MedicineDebug) 104 | Debug.Log($"Unregistering singleton instance: {obj.name} as {typeof(TSingleton).Name}", obj); 105 | 106 | instance = null; 107 | } 108 | else 109 | { 110 | if (MedicineDebug) 111 | Debug.LogError($"Failed to unregister singleton instance: {obj.name} as {typeof(TSingleton).Name}", obj); 112 | } 113 | } 114 | 115 | /// 116 | /// Get the active registered instance. 117 | /// Logs an error and returns null when there is no registered instance. 118 | /// 119 | /// 120 | /// This is a helper method. Useful for some edge cases, but you don't usually need to use it directly. 121 | /// See and to learn more. 122 | /// 123 | [MethodImpl(AggressiveInlining)] 124 | public static TSingleton GetInstance() 125 | { 126 | #if UNITY_EDITOR 127 | if (!checkedAttribute && typeof(TSingleton).CustomAttributes.All(x => x.AttributeType != typeof(Register.Single))) 128 | Debug.LogError($"Tried to obtain singleton instance of {typeof(TSingleton).Name}, but it isn't marked with [Medicine.Register.Single]."); 129 | 130 | checkedAttribute = true; 131 | 132 | if (!ApplicationIsPlaying) 133 | { 134 | if (instance) 135 | return instance; 136 | 137 | return instance = TryFindObjectByType() ?? TryFindScriptableObject() ?? ErrorNoSingletonInstance(); 138 | } 139 | #endif 140 | // ReSharper disable once Unity.NoNullCoalescing 141 | // we can safely use reference comparison assuming objects always unregister themselves in OnDestroy 142 | return instance ?? (instance = TryFindScriptableObject() ?? ErrorNoSingletonInstance()); 143 | } 144 | 145 | /// 146 | /// Get the active registered instance. 147 | /// Returns null when there is no registered instance. 148 | /// 149 | /// 150 | /// This is a helper method. Useful for some edge cases, but you don't usually need to use it directly. 151 | /// See and to learn more. 152 | /// 153 | [MethodImpl(AggressiveInlining)] 154 | public static TSingleton TryGetInstance() 155 | { 156 | #if UNITY_EDITOR 157 | if (!ApplicationIsPlaying) 158 | { 159 | if (instance) 160 | return instance; 161 | 162 | return instance = TryFindObjectByType() ?? TryFindScriptableObject(); 163 | } 164 | #endif 165 | // ReSharper disable once Unity.NoNullCoalescing 166 | // we can safely use reference comparison assuming objects always unregister themselves in OnDestroy 167 | return instance ?? (instance = TryFindScriptableObject()); 168 | } 169 | 170 | static TSingleton TryFindObjectByType() 171 | { 172 | var objectsOfType = NonAlloc.FindObjectsOfType(); 173 | 174 | return objectsOfType.Length > 0 175 | ? objectsOfType[0] 176 | : null; 177 | } 178 | 179 | static TSingleton TryFindScriptableObject() 180 | { 181 | #if UNITY_EDITOR 182 | // create static noninitialized instance for fast inheritance checks 183 | if (ReferenceEquals(uninitializedInstance, null)) 184 | uninitializedInstance = FormatterServices.GetUninitializedObject(typeof(TSingleton)) as TSingleton; 185 | 186 | // give up unless derived from ScriptableObject 187 | if (!(uninitializedInstance is ScriptableObject)) 188 | return null; 189 | 190 | // try find ScriptableObject instance in preloaded assets 191 | var preloadedAssets = UnityEditor.PlayerSettings.GetPreloadedAssets(); 192 | 193 | foreach (var asset in preloadedAssets) 194 | if (asset is TSingleton singleton) 195 | return singleton; 196 | #endif 197 | 198 | return null; 199 | } 200 | 201 | static TSingleton ErrorNoSingletonInstance() 202 | { 203 | Debug.LogError( 204 | ReferenceEquals(instance, null) 205 | ? $"No registered singleton instance: {typeof(TSingleton).Name}" 206 | : $"Singleton instance has been destroyed: {typeof(TSingleton).Name}" 207 | ); 208 | 209 | return null; 210 | } 211 | } 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /Src/RuntimeHelpers.Singleton.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f87e6ec65be6424fbdaefc27268cad78 3 | timeCreated: 1591027130 -------------------------------------------------------------------------------- /Src/RuntimeHelpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Runtime.CompilerServices; 4 | using JetBrains.Annotations; 5 | using UnityEngine; 6 | using static System.Runtime.CompilerServices.MethodImplOptions; 7 | using Obj = UnityEngine.Object; 8 | 9 | #pragma warning disable 162 10 | 11 | // ReSharper disable UnusedParameter.Global 12 | // ReSharper disable MemberHidesStaticFromOuterClass 13 | namespace Medicine 14 | { 15 | /// 16 | /// Runtime helpers that are internally used to implement the [Inject] attribute functionality. 17 | /// You probably don't need to access these methods directly. 18 | /// 19 | public static partial class RuntimeHelpers 20 | { 21 | /// This is a helper method. You don't usually need to use it directly. See to learn more. 22 | [MethodImpl(AggressiveInlining)] 23 | public static bool ValidateArray(Array array) 24 | => array.Length > 0; 25 | 26 | /// This is a helper method. You don't usually need to use it directly. See to learn more. 27 | [MethodImpl(AggressiveInlining)] 28 | public static Camera GetMainCamera() 29 | #if UNITY_2020_2_OR_NEWER 30 | // this is now fast enough that there's no point in caching (better to have implementation parity with Camera.main) 31 | // see: https://blogs.unity3d.com/2020/09/21/new-performance-improvements-in-unity-2020-2/ 32 | => Camera.main; 33 | #else 34 | => currentMainCamera && currentMainCamera.isActiveAndEnabled 35 | ? currentMainCamera 36 | : currentMainCamera = Camera.main; 37 | 38 | static Camera currentMainCamera; 39 | #endif 40 | 41 | /// This is a helper method. You don't usually need to use it directly. See to learn more. 42 | [MethodImpl(AggressiveInlining)] 43 | public static T Inject(GameObject context) 44 | => context.GetComponent(); 45 | 46 | /// This is a helper method. You don't usually need to use it directly. See to learn more. 47 | [MethodImpl(AggressiveInlining)] 48 | public static T[] InjectArray(GameObject context) 49 | => context.GetComponents(); 50 | 51 | /// This is a helper method. You don't usually need to use it directly. See to learn more. 52 | [MethodImpl(AggressiveInlining)] 53 | public static T InjectFromChildren(GameObject context) where T : class 54 | => context.GetComponentInChildren(typeof(T), includeInactive: false) as T; 55 | 56 | /// This is a helper method. You don't usually need to use it directly. See to learn more. 57 | [MethodImpl(AggressiveInlining)] 58 | public static T InjectFromChildrenIncludeInactive(GameObject context) where T : class 59 | => context.GetComponentInChildren(typeof(T), includeInactive: true) as T; 60 | 61 | /// This is a helper method. You don't usually need to use it directly. See to learn more. 62 | [MethodImpl(AggressiveInlining)] 63 | public static T[] InjectFromChildrenArray(GameObject context) 64 | => context.GetComponentsInChildren(includeInactive: false); 65 | 66 | /// This is a helper method. You don't usually need to use it directly. See to learn more. 67 | [MethodImpl(AggressiveInlining)] 68 | public static T[] InjectFromChildrenArrayIncludeInactive(GameObject context) 69 | => context.GetComponentsInChildren(includeInactive: true); 70 | 71 | #if UNITY_2020_1_OR_NEWER 72 | /// This is a helper method. You don't usually need to use it directly. See to learn more. 73 | [MethodImpl(AggressiveInlining)] 74 | public static T InjectFromParents(GameObject context) where T : class 75 | => context.GetComponentInParent(includeInactive: false, type: typeof(T)) as T; 76 | 77 | /// This is a helper method. You don't usually need to use it directly. See to learn more. 78 | [MethodImpl(AggressiveInlining)] 79 | public static T InjectFromParentsIncludingInactive(GameObject context) where T : class 80 | => context.GetComponentInParent(includeInactive: true, type: typeof(T)) as T; 81 | #else 82 | [MethodImpl(AggressiveInlining)] 83 | public static T InjectFromParents(GameObject context) where T : class 84 | => context.GetComponentInParent(type: typeof(T)) as T; 85 | 86 | [MethodImpl(AggressiveInlining)] 87 | public static T InjectFromParentsIncludingInactive(GameObject context) where T : class 88 | => context.GetComponentInParent(type: typeof(T)) as T; 89 | #endif 90 | 91 | /// This is a helper method. You don't usually need to use it directly. See to learn more. 92 | [MethodImpl(AggressiveInlining)] 93 | public static T[] InjectFromParentsArray(GameObject context) 94 | => context.GetComponentsInParent(includeInactive: false); 95 | 96 | /// This is a helper method. You don't usually need to use it directly. See to learn more. 97 | [MethodImpl(AggressiveInlining)] 98 | public static T[] InjectFromParentsArrayIncludeInactive(GameObject context) 99 | => context.GetComponentsInParent(includeInactive: true); 100 | 101 | public static class Lazy 102 | { 103 | /// This is a helper method. You don't usually need to use it directly. See to learn more. 104 | [MethodImpl(AggressiveInlining)] 105 | public static T[] InjectArray(GameObject context) where T : class 106 | => context.GetComponentsNonAlloc(); 107 | 108 | /// This is a helper method. You don't usually need to use it directly. See to learn more. 109 | [MethodImpl(AggressiveInlining)] 110 | public static T[] InjectFromChildrenArray(GameObject context) where T : class 111 | => context.GetComponentsInChildrenNonAlloc(includeInactive: false); 112 | 113 | /// This is a helper method. You don't usually need to use it directly. See to learn more. 114 | [MethodImpl(AggressiveInlining)] 115 | public static T[] InjectFromChildrenArrayIncludeInactive(GameObject context) where T : class 116 | => context.GetComponentsInChildrenNonAlloc(includeInactive: true); 117 | 118 | /// This is a helper method. You don't usually need to use it directly. See to learn more. 119 | [MethodImpl(AggressiveInlining)] 120 | public static T[] InjectFromParentsArray(GameObject context) where T : class 121 | => context.GetComponentsInParentNonAlloc(includeInactive: false); 122 | 123 | /// This is a helper method. You don't usually need to use it directly. See to learn more. 124 | [MethodImpl(AggressiveInlining)] 125 | public static T[] InjectFromParentsArrayIncludeInactive(GameObject context) where T : class 126 | => context.GetComponentsInParentNonAlloc(includeInactive: true); 127 | } 128 | 129 | #if UNITY_EDITOR 130 | [UsedImplicitly] 131 | static Action reinitializeAction; 132 | 133 | [UsedImplicitly] 134 | static Action debugAction; 135 | 136 | /// 137 | /// forces preloaded assets initialization in editor (eg. to make singletons register themselves) 138 | /// inexplicably, Unity doesn't do this by default 139 | /// 140 | [UnityEditor.InitializeOnLoadMethod] 141 | static void EditorInitializeOnLoad() 142 | => UnityEditor.PlayerSettings.GetPreloadedAssets(); 143 | 144 | // static RuntimeHelpers() 145 | // => UnityEditor.EditorApplication.playModeStateChanged += (x) => 146 | // { 147 | // if (x is UnityEditor.PlayModeStateChange.ExitingEditMode) 148 | // reinitializeAction?.Invoke(); 149 | // }; 150 | 151 | [UnityEditor.MenuItem("Tools/Medicine/List registered objects")] 152 | static void MenuCommandDebug() 153 | => debugAction?.Invoke(); 154 | 155 | [UnityEditor.MenuItem("Tools/Medicine/Clear registered objects")] 156 | static void MenuCommandReinitialize() 157 | => reinitializeAction?.Invoke(); 158 | #endif 159 | 160 | /// 161 | /// Re-initializes the properties injected using the [Inject] attribute family. 162 | /// This is useful when you've made changes to the GameObject's hierarchy or removed/added 163 | /// components, but should be used sparingly for performance reasons. 164 | /// 165 | [UsedImplicitly, SuppressMessage("ReSharper", "SuspiciousTypeConversion.Global")] 166 | public static void Reinject(this MonoBehaviour monoBehaviour) 167 | => (monoBehaviour as IMedicineComponent)?.Inject(); 168 | 169 | #if MEDICINE_DEBUG 170 | internal const bool MedicineDebug = true; 171 | #else 172 | internal const bool MedicineDebug = false; 173 | #endif 174 | 175 | #if UNITY_EDITOR 176 | internal static bool ApplicationIsPlaying 177 | => Application.isPlaying; 178 | #else 179 | // constant outside the editor 180 | internal const bool ApplicationIsPlaying = true; 181 | #endif 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /Src/RuntimeHelpers.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: fc3d0cc1c02e448f89077d9636abe982 3 | timeCreated: 1590924375 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pl.apkd.medicine", 3 | "displayName": "Medicine", 4 | "version": "1.0.0-preview", 5 | "unity": "2020.1", 6 | "unityRelease": "0b11", 7 | "dependencies": { 8 | "nuget.mono-cecil": "0.1.6-preview" 9 | }, 10 | "type": "tool", 11 | "keywords": [ 12 | "medicine", 13 | "di", 14 | "dependency injection", 15 | "inject" 16 | ], 17 | "hideInEditor": false, 18 | "description": "Medicine is a code-driven, performance-oriented way to automatically hook up references between your components. Additionally, it comes with a toolbox of optimized versions of many component-related operations." 19 | } -------------------------------------------------------------------------------- /package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 31900a7d1996a22498a13f8aed17c4ee 3 | PackageManifestImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | --------------------------------------------------------------------------------