From 845c21edb271a8f0df97150a139925978a09e395 Mon Sep 17 00:00:00 2001
From: sliptrixx <37605842+sliptrixx@users.noreply.github.com>
Date: Sun, 13 Aug 2023 23:09:24 +0530
Subject: [PATCH] Adding Support for [StaticAccess] Attribute (#5)
- Created the attribute that users can add to access this system
- Added option to disable this feature using defines
- Added code for automatic code generation
- Added option to pause static access generation
- Added info about [StaticAccess] to readme
---
Editor/DefineRegistrant.cs | 23 +-
Editor/StaticAccess.cs | 306 ++++++++++++++++++
Editor/StaticAccess.cs.meta | 11 +
README.md | 23 +-
Scripts/Attributes/StaticAccessAttribute.cs | 46 +++
.../Attributes/StaticAccessAttribute.cs.meta | 11 +
package.json | 2 +-
7 files changed, 417 insertions(+), 5 deletions(-)
create mode 100644 Editor/StaticAccess.cs
create mode 100644 Editor/StaticAccess.cs.meta
create mode 100644 Scripts/Attributes/StaticAccessAttribute.cs
create mode 100644 Scripts/Attributes/StaticAccessAttribute.cs.meta
diff --git a/Editor/DefineRegistrant.cs b/Editor/DefineRegistrant.cs
index 30411ff..b93ee7d 100644
--- a/Editor/DefineRegistrant.cs
+++ b/Editor/DefineRegistrant.cs
@@ -13,6 +13,8 @@ namespace Hibzz.Singletons.Editor
///
internal class DefineRegistrant
{
+ const string Category = "Hibzz.Singletons";
+
[RegisterDefine]
static DefineRegistrationData RegisterDisableScriptableObjectCreator()
{
@@ -20,7 +22,8 @@ static DefineRegistrationData RegisterDisableScriptableObjectCreator()
data.Define = "DISABLE_SCRIPTABLE_SINGLETON_CREATOR";
data.DisplayName = "Disable Scriptable Singleton Creator";
- data.Category = "Hibzz.Singletons";
+ data.Category = Category;
+ data.EnableByDefault = false;
data.Description = "The scriptable singleton creator functionality " +
"lets users mark any ScriptableSingleton class with an attribute " +
"called `CreateScriptableSingletonAsset`. When the user presses " +
@@ -28,7 +31,25 @@ static DefineRegistrationData RegisterDisableScriptableObjectCreator()
"will create any missing assets for those singletons in the " +
"\"Resources/Singletons\" folder. \n\n" +
"Installing this define will disable this feature.";
+
+ return data;
+ }
+
+ [RegisterDefine]
+ static DefineRegistrationData RegisterDisablePublicStaticAccessor()
+ {
+ DefineRegistrationData data = new DefineRegistrationData();
+
+ data.Define = "DISABLE_SINGLETON_AUTO_PUBLIC_STATIC_ACCESSOR";
+ data.DisplayName = "Disable Public Static Accessor";
+ data.Category = Category;
data.EnableByDefault = false;
+ data.Description = "The singletons package offers a system to " +
+ "automatically generate public static accessors for items " +
+ "inside a partial singleton class when marked with a " +
+ "[StaticAccess] attribute. This is done using dynamic code " +
+ "generation.\n\n " +
+ "Installing this define will disable this feature.";
return data;
}
diff --git a/Editor/StaticAccess.cs b/Editor/StaticAccess.cs
new file mode 100644
index 0000000..0439dee
--- /dev/null
+++ b/Editor/StaticAccess.cs
@@ -0,0 +1,306 @@
+#if !DISABLE_SINGLETON_AUTO_PUBLIC_STATIC_ACCESSOR
+
+using System;
+using System.IO;
+using System.Reflection;
+using UnityEditor;
+using UnityEngine;
+
+namespace Hibzz.Singletons.Editor
+{
+ public class StaticAccess : AssetPostprocessor
+ {
+ const string STATIC_ACCESS_PAUSE_KEY = "Hibzz/Singletons/Pause Automatic Code Generation";
+
+ // gets a notification after all asset has been imported
+ private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets,
+ string[] movedAssets, string[] movedFromAssetPaths)
+ {
+ // if the user wants to pause the automatic code generation of this system, dont proceed
+ if(Menu.GetChecked(STATIC_ACCESS_PAUSE_KEY)) { return; }
+
+ // flag that indicates if code has been generated
+ bool codeGenerated = false;
+
+ // generate code for each imported asset if it's valid
+ foreach (var importedAsset in importedAssets)
+ {
+ if (TryGenerateCode(importedAsset))
+ {
+ codeGenerated = true;
+ }
+ }
+
+ // reload assembly if code was generated (or removed)
+ if (codeGenerated)
+ {
+ AssetDatabase.Refresh();
+ }
+ }
+
+ ///
+ /// Tries to generate any singleton related code for the given asset
+ ///
+ /// The path to the script asset to analyze and generate code for
+ /// Was some code generated for the given asset?
+ private static bool TryGenerateCode(string assetPath)
+ {
+ // if the changed asset is not a script file, we don't care
+ if (!assetPath.EndsWith(".cs")) { return false; }
+
+ // Issue: For now this is okay, but in the future this decision will cause issues,
+ // for example what happens the user generates a singleton script that has the static
+ // access attribute in it? The system will see that the file name contains the common
+ // convention of .generated.cs so it'll ignore it. Alternatively we generate files
+ // that end with .staticaccess.generated.cs but that's too long. We'll cross that
+ // bridge when the time comes, but just jotting this down so it's easier to understand
+ // the issue later.
+
+ // if the generated script file is a generated script file, we don't care either
+ if (assetPath.EndsWith(".generated.cs")) { return false; }
+
+ // load the asset from the given path as a monoscript
+ var monoScript = AssetDatabase.LoadAssetAtPath(assetPath);
+ if (monoScript is null) { return false; }
+
+ // make sure that they one of the variants of the singleton classes provided
+ var classType = monoScript.GetClass();
+ if (classType is null) { return false; }
+ if (!classType.IsASingletonVariant()) { return false; }
+
+ // don't generate code for abstract classes
+ if (classType.IsAbstract) { return false; }
+
+ // whether we generate some new content or not, we are going to need the path to the
+ // expected generated asset... at the end of this process we either create a new one,
+ // or check and delete an existing one
+ string generatedAssetPath = assetPath;
+ generatedAssetPath = generatedAssetPath.Substring(0, generatedAssetPath.LastIndexOf(".cs"));
+ generatedAssetPath += ".generated.cs";
+
+ // Now we need to generate the script file
+ if (GetGeneratedCode(classType, out string code))
+ {
+ File.WriteAllText(generatedAssetPath, code);
+ }
+ else
+ {
+ // no code was generated, so remove any existing generated file
+ if(!File.Exists(generatedAssetPath)) { return false; }
+ File.Delete(generatedAssetPath);
+ }
+
+ return true;
+ }
+
+ ///
+ /// Get the generated code from the given type
+ ///
+ /// The type to analyze
+ /// The generated code as a string
+ /// Was code generated as a string
+ private static bool GetGeneratedCode(Type classType, out string code)
+ {
+ bool wasCodeGenerated = false; // flag indicating whether some code was generated or not
+ string classContent = ""; // store what the contents of the class file is
+
+ // loop through all instanced members of the class and if it's members do contain the
+ // attribute [StaticAccess] then we make add a public static accessor for that member
+ var members = classType.GetMembers(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
+ foreach (var member in members)
+ {
+ // early return if no [StaticAccess] attribute found
+ var staticAccessAttribute = member.GetCustomAttribute();
+ if (staticAccessAttribute is null) { continue; }
+
+ // figure out the variable name
+ var variableName = staticAccessAttribute.VariableName;
+ if (variableName is null)
+ {
+ // since no specific variable name is given, it must be auto generated
+ variableName = member.Name;
+
+ // a naming scheme is specified in the attribute declaration, which requires an
+ // underscore for the auto generation to work
+ if (!variableName.Contains("_"))
+ {
+ Debug.LogWarning($"Skipped generating static accessor for {classType.Name}.{member.Name}: " +
+ $"The member name must contain some leading underscore for name to be autogenerated. " +
+ $"Please pass a string as a variable name to [StaticAccess(varname)] if you don't want " +
+ $"the name to be autogenerated.\n");
+
+ continue;
+ }
+
+ // remove anything before the first underscore and capitalize the first character
+ variableName = variableName.Substring(variableName.IndexOf('_') + 1);
+ variableName = char.ToUpper(variableName[0]) + variableName.Substring(1);
+ }
+
+
+ // based on the member type, the public accessor's syntax varies
+ if (member.MemberType == MemberTypes.Field)
+ {
+ var fieldInfo = member as FieldInfo;
+ classContent += $"public static {fieldInfo.FieldType.FullName} {variableName} => Instance.{member.Name};\n";
+ }
+ else if (member.MemberType == MemberTypes.Property)
+ {
+ var propertyInfo = member as PropertyInfo;
+ classContent += $"public static {propertyInfo.PropertyType.FullName} {variableName} => Instance.{member.Name};\n";
+ }
+ else if (member.MemberType == MemberTypes.Method)
+ {
+ var methodInfo = member as MethodInfo;
+
+ // stores any formal/actual parameters
+ string formalParameters = "";
+ string actualParameters = "";
+
+ // decipher what the formal and actual parameters of this function are (if any)
+ var parameters = methodInfo.GetParameters();
+ foreach(var parameter in parameters)
+ {
+ formalParameters += $"{parameter.ParameterType} {parameter.Name}, ";
+ actualParameters += $"{parameter.Name}, ";
+ }
+
+ // remove the trailing comma character from the parameters
+ if(parameters.Length > 0)
+ {
+ formalParameters = formalParameters.Substring(0, formalParameters.LastIndexOf(','));
+ actualParameters = actualParameters.Substring(0, actualParameters.LastIndexOf(','));
+ }
+
+ // string that all together
+ classContent += $"public static {methodInfo.ReturnType.FullName} {variableName}({formalParameters}) => Instance.{member.Name}({actualParameters});\n";
+ }
+
+ // update the flag letting the system know that some valid code was indeed generated
+ wasCodeGenerated = true;
+ }
+
+ // when no code was generated, either because there were no [StaticAccess] attributes
+ // or the user wanted to have an autogenerated variable name without a leading
+ // underscore, we want to perform an early exit
+ if(!wasCodeGenerated)
+ {
+ code = "";
+ return false;
+ }
+
+ // stores the generated code for just the class
+ string classCode = "";
+
+ // classes can either be public or internal, and if there's no access modifier present,
+ // it'll be considered to be internal
+ if (classType.IsPublic)
+ {
+ classCode += "public ";
+ }
+
+ // add the class defenition
+ classCode += $"partial class {classType.Name} : {classType.GetSingletonBaseName()} \n" +
+ "{ \n" +
+ $"{classContent}" +
+ "} \n" ;
+
+ // the overall code that's generated needs to be put together
+ code = "// this code is automatically generated by the Hibzz.Singletons package\n\n";
+ code += "using Hibzz.Singletons;\n\n";
+ if (!string.IsNullOrWhiteSpace(classType.Namespace))
+ {
+ code += $"namespace {classType.Namespace} \n" +
+ "{ \n" +
+ $"{classCode}" +
+ "} \n";
+ }
+ else
+ {
+ code += classCode;
+ }
+
+ return true; // code was indeed generated
+ }
+
+ [MenuItem(STATIC_ACCESS_PAUSE_KEY)]
+ private static void OnStaticAccessPauseMenuPressed()
+ {
+ // this should toggle the Automatic Code Generation
+ Menu.SetChecked(STATIC_ACCESS_PAUSE_KEY, !Menu.GetChecked(STATIC_ACCESS_PAUSE_KEY));
+ }
+ }
+
+ internal static class TypeExtensions
+ {
+ ///
+ /// Alternative version of that supports raw generic types (generic types without
+ /// any type parameters).
+ ///
+ /// The base type class for which the check is made.
+ /// To type to determine for whether it derives from .
+ ///
+ /// Credits to Denis Doomen (https://github.com/dennisdoomen)
+ ///
+ public static bool IsSubclassOfRawGeneric(this Type toCheck, Type baseType)
+ {
+ while (toCheck != typeof(object))
+ {
+ Type cur = toCheck.IsGenericType ? toCheck.GetGenericTypeDefinition() : toCheck;
+ if (baseType == cur)
+ {
+ return true;
+ }
+
+ toCheck = toCheck.BaseType;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Get the base type as a string for a singleton class
+ ///
+ /// The type of a class that inherits one of the singleton variants
+ /// A string representation of the base type of the singleton class
+ public static string GetSingletonBaseName(this Type type)
+ {
+ // we are working with the generic base type
+
+ // when we get the name of a generic base type, c# reflection systems would give us
+ // something of a compiled form... like Singleton`1 but that's not useful for code
+ // generation
+
+ // This is not a general purpose function, it's internal and is very specific to
+ // singletons package... Although it's possible to inherit a class that inherits the
+ // Singleton base class, it's not practical. None of the current class member can be
+ // accessed using the instance property
+
+ // So, we are cutting corners and creating something very specific for the singletons
+ // package that follows the pattern of base
+
+ // Issue: When in the future we decide to fix the inheritance issue (if it can be fixed),
+ // we need to revisit this function to be general purpose
+
+ string generic_base = type.BaseType.Name;
+ generic_base = generic_base.Substring(0, generic_base.IndexOf('`'));
+
+ return $"{generic_base}<{type.Name}>";
+ }
+
+ ///
+ /// check if the given type is on off the singleton variants
+ ///
+ /// The type to check
+ /// Is the given type on off the variants of the singleton
+ public static bool IsASingletonVariant(this Type type)
+ {
+ if (type.IsSubclassOfRawGeneric(typeof(Singleton<>))) { return true; }
+ if (type.IsSubclassOfRawGeneric(typeof(ScriptableSingleton<>))) { return true; }
+
+ return false;
+ }
+ }
+}
+
+#endif
diff --git a/Editor/StaticAccess.cs.meta b/Editor/StaticAccess.cs.meta
new file mode 100644
index 0000000..d4a55e6
--- /dev/null
+++ b/Editor/StaticAccess.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 00e98f7fdbde32543a16f3d72deafd68
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/README.md b/README.md
index 554c104..f76eeb6 100644
--- a/README.md
+++ b/README.md
@@ -36,9 +36,27 @@ Debug.Log($"Number of Live NPC's in scene: {_currentNPCCount}");
### Scriptable Singletons
-This package includes support for `ScriptableSingleton`s.
+This package includes support for `ScriptableSingleton`s. These are Singletons that exists as a ScriptableObject asset in the Resources folder. This allows a Scriptable Singleton to be accessed from anywhere in the project without having to be in the scene.
-Scriptable Singleton objects can be automatically created when the class is tagged with the `CreateScriptableSingletonAsset` attribute and the "Create Scriptable Singleton Asset" menu item is selected from the "Hibzz/Singletons" menu.
+Moreover, these objects can be automatically created when the class is tagged with the `[CreateScriptableSingletonAsset]` attribute and the "Create Scriptable Singleton Asset" menu item is selected from the "Hibzz/Singletons" menu.
+
+
+
+### [StaticAccess] Attribute
+The Singletons package comes with a powerful tool for accessing instanced members of a Singleton class without the `Instance` field. This is done by tagging the member with a `[StaticAccess]` attribute.
+
+```csharp
+// defining the singleton
+public partial class AIManager : Singleton
+{
+ [StaticAccess] int _liveNPCCount;
+}
+
+// Accessing the member
+var npcCount = AIManager.LiveNPCCount;
+```
+
+
Learn more about this package in the [documentation](https://docs.hibzz.games/singletons/getting-started/).
@@ -46,4 +64,3 @@ Learn more about this package in the [documentation](https://docs.hibzz.games/si
If you have any questions or want to contribute, feel free to join the [Discord server](https://discord.gg/YXdJ8cZngB) or [Twitter](https://twitter.com/hibzzgames). I'm always looking for feedback and ways to improve this tool. Thanks!
Additionally, you can support the development of these open-source projects via [GitHub Sponsors](https://github.com/sponsors/sliptrixx) and gain early access to the projects.
-
diff --git a/Scripts/Attributes/StaticAccessAttribute.cs b/Scripts/Attributes/StaticAccessAttribute.cs
new file mode 100644
index 0000000..2c3ede1
--- /dev/null
+++ b/Scripts/Attributes/StaticAccessAttribute.cs
@@ -0,0 +1,46 @@
+#if !DISABLE_SINGLETON_AUTO_PUBLIC_STATIC_ACCESSOR
+
+using System;
+
+namespace Hibzz.Singletons
+{
+ public class StaticAccessAttribute : Attribute
+ {
+ public string VariableName { get; protected set; } = null;
+
+ ///
+ /// Exposes contents inside a partial singleton class through a public
+ /// static interface using the instance property
+ ///
+ ///
+ /// Without a variable name passed to the attribute, the system will
+ /// attempt to autogenerate the variable name. Currently the naming
+ /// schemes available are as follows:
+ ///
+ /// -
+ /// Must start with an underscore: For example, _health will have a new public static accessor Health
+ ///
+ /// -
+ /// Must contain an underscore: For example, p_mana will have a new public static accessor Mana
+ ///
+ ///
+ ///
+ /// If things aren't working as expected, please file a bug report in GitHub Issues
+ ///
+ public StaticAccessAttribute() { }
+
+ ///
+ /// Exposes contents inside a partial singleton class through a public static accessor
+ ///
+ ///
+ /// The variable name to set for the new public accessor.
+ /// Make sure that the variable name is unique
+ ///
+ public StaticAccessAttribute(string variableName)
+ {
+ VariableName = variableName;
+ }
+ }
+}
+
+#endif
diff --git a/Scripts/Attributes/StaticAccessAttribute.cs.meta b/Scripts/Attributes/StaticAccessAttribute.cs.meta
new file mode 100644
index 0000000..0e224b8
--- /dev/null
+++ b/Scripts/Attributes/StaticAccessAttribute.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: d1be4f7a59f001a45aca27ab3fbdb34a
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/package.json b/package.json
index 1dfee82..34697a7 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "com.hibzz.singletons",
- "version": "1.3.1",
+ "version": "1.4.0",
"displayName": "hibzz.singletons",
"description": "A library of singletons for Unity",
"author": {