Compare commits
22 Commits
b87465d80f
...
codex/pre-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b17b6237f3 | ||
|
|
185d4830d4 | ||
|
|
b91fc5484a | ||
|
|
d42c8f9d1d | ||
|
|
e92fed2d4f | ||
|
|
e6d1f00e23 | ||
|
|
55fda1e0cd | ||
|
|
8f3982c165 | ||
|
|
dfa25aaf15 | ||
|
|
7b2243a40b | ||
|
|
0930e7c173 | ||
|
|
ac8fcc3c24 | ||
|
|
3b3957fb74 | ||
|
|
31641b2c35 | ||
|
|
f5887da9fb | ||
|
|
37e6d4ca57 | ||
|
|
7cde2cfaec | ||
|
|
2a88dad6f4 | ||
|
|
e211a7169d | ||
|
|
c363785adb | ||
|
|
4c9eaff7d8 | ||
|
|
558bc919ef |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,6 +6,7 @@ Client/ServerData
|
||||
/cfgPack/encryptcfg
|
||||
.gemini/
|
||||
.worktrees/
|
||||
TestFlowScopePackage/
|
||||
GEMINI.md
|
||||
CLAUDE.md
|
||||
AGENTS.md
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c35227ceb2c96da44873807b1908facf
|
||||
fileFormatVersion: 2
|
||||
guid: f38893bbcb86bbb4cb6705a205682113
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
@@ -0,0 +1,109 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 468a46d0ae32c3544b7d98094e6448a9, type: 3}
|
||||
m_Name: AddressableAssetSettings
|
||||
m_EditorClassIdentifier:
|
||||
m_DefaultGroup: 7a5b4d76501c82c408509719f87e537b
|
||||
m_currentHash:
|
||||
serializedVersion: 2
|
||||
Hash: 00000000000000000000000000000000
|
||||
m_OptimizeCatalogSize: 0
|
||||
m_BuildRemoteCatalog: 0
|
||||
m_BundleLocalCatalog: 0
|
||||
m_CatalogRequestsTimeout: 0
|
||||
m_DisableCatalogUpdateOnStart: 0
|
||||
m_IgnoreUnsupportedFilesInBuild: 0
|
||||
m_UniqueBundleIds: 0
|
||||
m_NonRecursiveBuilding: 1
|
||||
m_CCDEnabled: 0
|
||||
m_maxConcurrentWebRequests: 3
|
||||
m_ContiguousBundles: 1
|
||||
m_StripUnityVersionFromBundleBuild: 0
|
||||
m_DisableVisibleSubAssetRepresentations: 0
|
||||
m_ShaderBundleNaming: 0
|
||||
m_ShaderBundleCustomNaming:
|
||||
m_MonoScriptBundleNaming: 0
|
||||
m_CheckForContentUpdateRestrictionsOption: 0
|
||||
m_MonoScriptBundleCustomNaming:
|
||||
m_RemoteCatalogBuildPath:
|
||||
m_Id: 07b49c5050b466b429ffe37d3fea9381
|
||||
m_RemoteCatalogLoadPath:
|
||||
m_Id: 9acaa2031bf7fa240af17e019ab4a7ff
|
||||
m_ContentStateBuildPathProfileVariableName: <default settings path>
|
||||
m_CustomContentStateBuildPath:
|
||||
m_ContentStateBuildPath:
|
||||
m_BuildAddressablesWithPlayerBuild: 0
|
||||
m_overridePlayerVersion: '[UnityEditor.PlayerSettings.bundleVersion]'
|
||||
m_GroupAssets:
|
||||
- {fileID: 11400000, guid: cb21c9bd6efb15940aa0cc136e03ec0f, type: 2}
|
||||
- {fileID: 11400000, guid: ad50471a1bc339946b9dd02f9fd1b735, type: 2}
|
||||
m_BuildSettings:
|
||||
m_CompileScriptsInVirtualMode: 0
|
||||
m_CleanupStreamingAssetsAfterBuilds: 1
|
||||
m_LogResourceManagerExceptions: 1
|
||||
m_BundleBuildPath: Temp/com.unity.addressables/AssetBundles
|
||||
m_ProfileSettings:
|
||||
m_Profiles:
|
||||
- m_InheritedParent:
|
||||
m_Id: 7dc81bb5e6404c446a8e47e2b54e4880
|
||||
m_ProfileName: Default
|
||||
m_Values:
|
||||
- m_Id: 88540d677a2c08c42b7902abc4da490b
|
||||
m_Value: '[UnityEditor.EditorUserBuildSettings.activeBuildTarget]'
|
||||
- m_Id: 4be60150ce46e224586b89230ab67d48
|
||||
m_Value: '[UnityEngine.AddressableAssets.Addressables.BuildPath]/[BuildTarget]'
|
||||
- m_Id: 8dd5e21a04741aa4ab24bb52c0e3c5a3
|
||||
m_Value: '{UnityEngine.AddressableAssets.Addressables.RuntimePath}/[BuildTarget]'
|
||||
- m_Id: 07b49c5050b466b429ffe37d3fea9381
|
||||
m_Value: ServerData/[BuildTarget]
|
||||
- m_Id: 9acaa2031bf7fa240af17e019ab4a7ff
|
||||
m_Value: http://[PrivateIpAddress]:[HostingServicePort]
|
||||
m_ProfileEntryNames:
|
||||
- m_Id: 88540d677a2c08c42b7902abc4da490b
|
||||
m_Name: BuildTarget
|
||||
m_InlineUsage: 0
|
||||
- m_Id: 4be60150ce46e224586b89230ab67d48
|
||||
m_Name: Local.BuildPath
|
||||
m_InlineUsage: 0
|
||||
- m_Id: 8dd5e21a04741aa4ab24bb52c0e3c5a3
|
||||
m_Name: Local.LoadPath
|
||||
m_InlineUsage: 0
|
||||
- m_Id: 07b49c5050b466b429ffe37d3fea9381
|
||||
m_Name: Remote.BuildPath
|
||||
m_InlineUsage: 0
|
||||
- m_Id: 9acaa2031bf7fa240af17e019ab4a7ff
|
||||
m_Name: Remote.LoadPath
|
||||
m_InlineUsage: 0
|
||||
m_ProfileVersion: 1
|
||||
m_LabelTable:
|
||||
m_LabelNames:
|
||||
- default
|
||||
m_SchemaTemplates: []
|
||||
m_GroupTemplateObjects:
|
||||
- {fileID: 11400000, guid: b8b58afcc1b4e244db44625e28a47b36, type: 2}
|
||||
m_InitializationObjects: []
|
||||
m_CertificateHandlerType:
|
||||
m_AssemblyName:
|
||||
m_ClassName:
|
||||
m_ActivePlayerDataBuilderIndex: 3
|
||||
m_DataBuilders:
|
||||
- {fileID: 11400000, guid: ca567c17e92783c4d9869e540320d76f, type: 2}
|
||||
- {fileID: 11400000, guid: 9c179b50ddf0f4a49ba95a61fce40d9d, type: 2}
|
||||
- {fileID: 11400000, guid: e268c485b53ab6547bfc8919ae0f900d, type: 2}
|
||||
- {fileID: 11400000, guid: 7f9d8997bb36a7f44aabfe404cd182f8, type: 2}
|
||||
m_ActiveProfileId: 7dc81bb5e6404c446a8e47e2b54e4880
|
||||
m_HostingServicesManager:
|
||||
m_HostingServiceInfos: []
|
||||
m_Settings: {fileID: 11400000}
|
||||
m_NextInstanceId: 0
|
||||
m_RegisteredServiceTypeRefs: []
|
||||
m_PingTimeoutInMilliseconds: 5000
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c67335a4d75941d4881a64ebedebbce8
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1f19d44b51ad9214693ce4ebaa68a5fa
|
||||
fileFormatVersion: 2
|
||||
guid: f9bc8a0d61c2e9e4cb5557b7586ac071
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
@@ -0,0 +1,76 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &-4998770171994096319
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: e5d17a21594effb4e9591490b009e7aa, type: 3}
|
||||
m_Name: BundledAssetGroupSchema
|
||||
m_EditorClassIdentifier:
|
||||
m_Group: {fileID: 0}
|
||||
m_InternalBundleIdMode: 1
|
||||
m_Compression: 1
|
||||
m_IncludeAddressInCatalog: 1
|
||||
m_IncludeGUIDInCatalog: 1
|
||||
m_IncludeLabelsInCatalog: 1
|
||||
m_InternalIdNamingMode: 0
|
||||
m_CacheClearBehavior: 0
|
||||
m_IncludeInBuild: 1
|
||||
m_BundledAssetProviderType:
|
||||
m_AssemblyName:
|
||||
m_ClassName:
|
||||
m_ForceUniqueProvider: 0
|
||||
m_UseAssetBundleCache: 1
|
||||
m_UseAssetBundleCrc: 1
|
||||
m_UseAssetBundleCrcForCachedBundles: 1
|
||||
m_UseUWRForLocalBundles: 0
|
||||
m_Timeout: 0
|
||||
m_ChunkedTransfer: 0
|
||||
m_RedirectLimit: -1
|
||||
m_RetryCount: 0
|
||||
m_BuildPath:
|
||||
m_Id:
|
||||
m_LoadPath:
|
||||
m_Id:
|
||||
m_BundleMode: 0
|
||||
m_AssetBundleProviderType:
|
||||
m_AssemblyName:
|
||||
m_ClassName:
|
||||
m_BundleNaming: 0
|
||||
m_AssetLoadMode: 0
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 1a3c5d64ac83548c09dd1678b9f6f1cd, type: 3}
|
||||
m_Name: Packed Assets
|
||||
m_EditorClassIdentifier:
|
||||
m_SchemaObjects:
|
||||
- {fileID: -4998770171994096319}
|
||||
- {fileID: 438921025847764287}
|
||||
m_Description: Pack assets into asset bundles.
|
||||
m_Settings: {fileID: 11400000, guid: c67335a4d75941d4881a64ebedebbce8, type: 2}
|
||||
--- !u!114 &438921025847764287
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 1
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 5834b5087d578d24c926ce20cd31e6d6, type: 3}
|
||||
m_Name: ContentUpdateGroupSchema
|
||||
m_EditorClassIdentifier:
|
||||
m_Group: {fileID: 0}
|
||||
m_StaticContent: 0
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b8b58afcc1b4e244db44625e28a47b36
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
My project/Assets/AddressableAssetsData/AssetGroups.meta
Normal file
8
My project/Assets/AddressableAssetsData/AssetGroups.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0ee6e7ad669b89342a6dd8bd0dd3bf84
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,34 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: bbb281ee3bf0b054c82ac2347e9e782c, type: 3}
|
||||
m_Name: Built In Data
|
||||
m_EditorClassIdentifier:
|
||||
m_GroupName: Built In Data
|
||||
m_Data:
|
||||
m_SerializedData: []
|
||||
m_GUID: 1989d512082b7394d9f03cd0ea5fd1b5
|
||||
m_SerializeEntries:
|
||||
- m_GUID: Resources
|
||||
m_Address: Resources
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: EditorSceneList
|
||||
m_Address: EditorSceneList
|
||||
m_ReadOnly: 1
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
m_ReadOnly: 1
|
||||
m_Settings: {fileID: 11400000, guid: c67335a4d75941d4881a64ebedebbce8, type: 2}
|
||||
m_SchemaSet:
|
||||
m_Schemas:
|
||||
- {fileID: 11400000, guid: a7177bba2e94b2a40b5302657833b7d8, type: 2}
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cb21c9bd6efb15940aa0cc136e03ec0f
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,30 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: bbb281ee3bf0b054c82ac2347e9e782c, type: 3}
|
||||
m_Name: Default Local Group
|
||||
m_EditorClassIdentifier:
|
||||
m_GroupName: Default Local Group
|
||||
m_Data:
|
||||
m_SerializedData: []
|
||||
m_GUID: 7a5b4d76501c82c408509719f87e537b
|
||||
m_SerializeEntries:
|
||||
- m_GUID: 7e04733ce9dab74459592d3bd99fcafd
|
||||
m_Address: FlowScope.Tests.Addressables.Text
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels: []
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
m_ReadOnly: 0
|
||||
m_Settings: {fileID: 11400000, guid: c67335a4d75941d4881a64ebedebbce8, type: 2}
|
||||
m_SchemaSet:
|
||||
m_Schemas:
|
||||
- {fileID: 11400000, guid: 4a894c5b933ed1040b799de00906a668, type: 2}
|
||||
- {fileID: 11400000, guid: ee832a18fbe81564d9e5cb06031890c9, type: 2}
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ad50471a1bc339946b9dd02f9fd1b735
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e16dab92ed201134eb1bdc0159aa569c
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,17 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: b1487f5d688e4f94f828f879d599dbdc, type: 3}
|
||||
m_Name: Built In Data_PlayerDataGroupSchema
|
||||
m_EditorClassIdentifier:
|
||||
m_Group: {fileID: 11400000, guid: cb21c9bd6efb15940aa0cc136e03ec0f, type: 2}
|
||||
m_IncludeResourcesFolders: 1
|
||||
m_IncludeBuildSettingsScenes: 1
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a7177bba2e94b2a40b5302657833b7d8
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,45 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: e5d17a21594effb4e9591490b009e7aa, type: 3}
|
||||
m_Name: Default Local Group_BundledAssetGroupSchema
|
||||
m_EditorClassIdentifier:
|
||||
m_Group: {fileID: 11400000, guid: ad50471a1bc339946b9dd02f9fd1b735, type: 2}
|
||||
m_InternalBundleIdMode: 1
|
||||
m_Compression: 1
|
||||
m_IncludeAddressInCatalog: 1
|
||||
m_IncludeGUIDInCatalog: 1
|
||||
m_IncludeLabelsInCatalog: 1
|
||||
m_InternalIdNamingMode: 0
|
||||
m_CacheClearBehavior: 0
|
||||
m_IncludeInBuild: 1
|
||||
m_BundledAssetProviderType:
|
||||
m_AssemblyName: Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
|
||||
m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.BundledAssetProvider
|
||||
m_ForceUniqueProvider: 0
|
||||
m_UseAssetBundleCache: 1
|
||||
m_UseAssetBundleCrc: 1
|
||||
m_UseAssetBundleCrcForCachedBundles: 1
|
||||
m_UseUWRForLocalBundles: 0
|
||||
m_Timeout: 0
|
||||
m_ChunkedTransfer: 0
|
||||
m_RedirectLimit: -1
|
||||
m_RetryCount: 0
|
||||
m_BuildPath:
|
||||
m_Id: 4be60150ce46e224586b89230ab67d48
|
||||
m_LoadPath:
|
||||
m_Id: 8dd5e21a04741aa4ab24bb52c0e3c5a3
|
||||
m_BundleMode: 0
|
||||
m_AssetBundleProviderType:
|
||||
m_AssemblyName: Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
|
||||
m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider
|
||||
m_BundleNaming: 0
|
||||
m_AssetLoadMode: 0
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ee832a18fbe81564d9e5cb06031890c9
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,16 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 5834b5087d578d24c926ce20cd31e6d6, type: 3}
|
||||
m_Name: Default Local Group_ContentUpdateGroupSchema
|
||||
m_EditorClassIdentifier:
|
||||
m_Group: {fileID: 11400000, guid: ad50471a1bc339946b9dd02f9fd1b735, type: 2}
|
||||
m_StaticContent: 0
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4a894c5b933ed1040b799de00906a668
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cfb9fefe94d254d44a8f7d5f6b6d22ef
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,20 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 88d21199f5d473f4db36845f2318f180, type: 3}
|
||||
m_Name: BuildScriptFastMode
|
||||
m_EditorClassIdentifier:
|
||||
instanceProviderType:
|
||||
m_AssemblyName: Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
|
||||
m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.InstanceProvider
|
||||
sceneProviderType:
|
||||
m_AssemblyName: Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
|
||||
m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.SceneProvider
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ca567c17e92783c4d9869e540320d76f
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,20 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 3e2e0ffa088c91d41a086d0b8cb16bdc, type: 3}
|
||||
m_Name: BuildScriptPackedMode
|
||||
m_EditorClassIdentifier:
|
||||
instanceProviderType:
|
||||
m_AssemblyName: Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
|
||||
m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.InstanceProvider
|
||||
sceneProviderType:
|
||||
m_AssemblyName: Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
|
||||
m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.SceneProvider
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7f9d8997bb36a7f44aabfe404cd182f8
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,20 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: ad8c280d42ee0ed41a27db23b43dd2bf, type: 3}
|
||||
m_Name: BuildScriptPackedPlayMode
|
||||
m_EditorClassIdentifier:
|
||||
instanceProviderType:
|
||||
m_AssemblyName: Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
|
||||
m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.InstanceProvider
|
||||
sceneProviderType:
|
||||
m_AssemblyName: Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
|
||||
m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.SceneProvider
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e268c485b53ab6547bfc8919ae0f900d
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,20 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: bb0e4994b34add1409fd8ccaf4a82de5, type: 3}
|
||||
m_Name: BuildScriptVirtualMode
|
||||
m_EditorClassIdentifier:
|
||||
instanceProviderType:
|
||||
m_AssemblyName: Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
|
||||
m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.InstanceProvider
|
||||
sceneProviderType:
|
||||
m_AssemblyName: Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
|
||||
m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.SceneProvider
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9c179b50ddf0f4a49ba95a61fce40d9d
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
15
My project/Assets/AddressableAssetsData/DefaultObject.asset
Normal file
15
My project/Assets/AddressableAssetsData/DefaultObject.asset
Normal file
@@ -0,0 +1,15 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 3a189bb168d8d90478a09ea08c2f3d72, type: 3}
|
||||
m_Name: DefaultObject
|
||||
m_EditorClassIdentifier:
|
||||
m_AddressableAssetSettingsGuid: c67335a4d75941d4881a64ebedebbce8
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f9fd621d22433a64aa0741deda630f70
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4bd6a11cc58df434eb82db6a3fe8362f
|
||||
guid: e74ee61e34d742b9b44057032c6431bb
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
@@ -0,0 +1,70 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FlowScope.Resources;
|
||||
using UnityEngine.ResourceManagement.AsyncOperations;
|
||||
using UnityAddressables = UnityEngine.AddressableAssets.Addressables;
|
||||
|
||||
namespace FlowScope.Addressables
|
||||
{
|
||||
public sealed class AddressablesResourceBackend : IResourceBackend
|
||||
{
|
||||
public string Name => "Addressables";
|
||||
|
||||
public async Task<T> LoadAsync<T>(string key, CancellationToken cancellationToken)
|
||||
where T : class
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var handle = UnityAddressables.LoadAssetAsync<T>(key);
|
||||
try
|
||||
{
|
||||
await WaitWithCancellation(handle.Task, cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
UnityAddressables.Release(handle);
|
||||
throw;
|
||||
}
|
||||
|
||||
if (handle.OperationException != null ||
|
||||
handle.Status != AsyncOperationStatus.Succeeded ||
|
||||
handle.Result == null)
|
||||
{
|
||||
var exception = new InvalidOperationException(
|
||||
$"Failed to load Addressables resource '{key}' as {typeof(T).Name} from backend '{Name}'.",
|
||||
handle.OperationException);
|
||||
UnityAddressables.Release(handle);
|
||||
throw exception;
|
||||
}
|
||||
|
||||
return handle.Result;
|
||||
}
|
||||
|
||||
public void Release<T>(string key, T asset)
|
||||
where T : class
|
||||
{
|
||||
UnityAddressables.Release(asset);
|
||||
}
|
||||
|
||||
private static async Task WaitWithCancellation(Task task, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!cancellationToken.CanBeCanceled)
|
||||
{
|
||||
await task;
|
||||
return;
|
||||
}
|
||||
|
||||
var cancellation = new TaskCompletionSource<bool>();
|
||||
using (cancellationToken.Register(() => cancellation.TrySetResult(true)))
|
||||
{
|
||||
if (await Task.WhenAny(task, cancellation.Task) == cancellation.Task)
|
||||
{
|
||||
throw new OperationCanceledException(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
await task;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f65b4f61d3a34358bf48957eb3a14dd7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,38 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FlowScope.Addressables;
|
||||
|
||||
namespace FlowScope.Resources
|
||||
{
|
||||
public sealed class AddressablesResourceService : IResourceService, ICompletedResourceService
|
||||
{
|
||||
private readonly ResourceService _inner;
|
||||
|
||||
public AddressablesResourceService()
|
||||
: this(new AddressablesResourceBackend())
|
||||
{
|
||||
}
|
||||
|
||||
public AddressablesResourceService(IResourceBackend backend)
|
||||
{
|
||||
_inner = new ResourceService(backend);
|
||||
}
|
||||
|
||||
public Task<IResourceHandle<T>> LoadAsync<T>(string key, CancellationToken cancellationToken)
|
||||
where T : class
|
||||
{
|
||||
return _inner.LoadAsync<T>(key, cancellationToken);
|
||||
}
|
||||
|
||||
public IResourceGroup CreateGroup()
|
||||
{
|
||||
return _inner.CreateGroup();
|
||||
}
|
||||
|
||||
public bool TryLoadCompleted<T>(string key, out IResourceHandle<T> handle)
|
||||
where T : class
|
||||
{
|
||||
return _inner.TryLoadCompleted(key, out handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 62c87792aac14414b46ac0eb3e716f25
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "FlowScope.Addressables",
|
||||
"rootNamespace": "FlowScope.Addressables",
|
||||
"references": [
|
||||
"FlowScope.Runtime",
|
||||
"Unity.Addressables",
|
||||
"Unity.ResourceManager"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 917e69d4247b4f548a5951d84e7d35db
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
My project/Assets/FlowScope/Editor.meta
Normal file
8
My project/Assets/FlowScope/Editor.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5b5d64c7b9f94d1db437f60ce8944a8f
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
16
My project/Assets/FlowScope/Editor/FlowScope.Editor.asmdef
Normal file
16
My project/Assets/FlowScope/Editor/FlowScope.Editor.asmdef
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "FlowScope.Editor",
|
||||
"rootNamespace": "FlowScope.Editor",
|
||||
"references": [],
|
||||
"includePlatforms": [
|
||||
"Editor"
|
||||
],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f3ee591f10b84cf7a39d50219698312e
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
268
My project/Assets/FlowScope/Editor/FlowScopeDiagnosticsWindow.cs
Normal file
268
My project/Assets/FlowScope/Editor/FlowScopeDiagnosticsWindow.cs
Normal file
@@ -0,0 +1,268 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEditor.PackageManager;
|
||||
using UnityEditor.PackageManager.Requests;
|
||||
using UnityEngine;
|
||||
|
||||
namespace FlowScope.Editor
|
||||
{
|
||||
public sealed class FlowScopeDiagnosticsWindow : EditorWindow
|
||||
{
|
||||
private const string RuntimeAsmdefPath = "Assets/FlowScope/Runtime/FlowScope.Runtime.asmdef";
|
||||
private const string AddressablesAsmdefPath = "Assets/FlowScope/Addressables/FlowScope.Addressables.asmdef";
|
||||
private const string MainMenuScenePath = "Assets/FlowScope/Samples/MainMenuP0/Scenes/MainMenuP0.unity";
|
||||
private const string MainMenuPanelPrefabPath = "Assets/FlowScope/Samples/MainMenuP0/Prefabs/MainMenuPanel.prefab";
|
||||
private const string AddressablesPackageName = "com.unity.addressables";
|
||||
|
||||
private static readonly IReadOnlyList<AssetCheck> AssetChecks = new[]
|
||||
{
|
||||
new AssetCheck("Runtime asmdef", RuntimeAsmdefPath),
|
||||
new AssetCheck("Addressables asmdef", AddressablesAsmdefPath),
|
||||
new AssetCheck("MainMenuP0 scene", MainMenuScenePath),
|
||||
new AssetCheck("MainMenuPanel prefab", MainMenuPanelPrefabPath),
|
||||
};
|
||||
|
||||
private readonly List<DiagnosticResult> _assetResults = new List<DiagnosticResult>();
|
||||
|
||||
private ListRequest _packageListRequest;
|
||||
private DiagnosticResult _addressablesPackageResult = DiagnosticResult.Pending(
|
||||
"Addressables package",
|
||||
"Package status has not been refreshed.");
|
||||
|
||||
private Vector2 _scrollPosition;
|
||||
|
||||
[MenuItem("Tools/FlowScope/Diagnostics")]
|
||||
public static void Open()
|
||||
{
|
||||
FlowScopeDiagnosticsWindow window = GetWindow<FlowScopeDiagnosticsWindow>("FlowScope Diagnostics");
|
||||
window.minSize = new Vector2(520f, 320f);
|
||||
window.RefreshAll();
|
||||
window.Show();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
RefreshAssetChecks();
|
||||
RefreshPackageStatus();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
EditorApplication.update -= PollPackageListRequest;
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
using (new EditorGUILayout.VerticalScope())
|
||||
{
|
||||
EditorGUILayout.LabelField("FlowScope Diagnostics", EditorStyles.boldLabel);
|
||||
EditorGUILayout.HelpBox("Read-only diagnostics. This window checks project state and does not create, move, delete, or repair user assets.", MessageType.Info);
|
||||
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
if (GUILayout.Button("Refresh", GUILayout.Width(96f)))
|
||||
{
|
||||
RefreshAll();
|
||||
}
|
||||
|
||||
GUILayout.Label(
|
||||
_packageListRequest != null && !_packageListRequest.IsCompleted
|
||||
? "Refreshing package status..."
|
||||
: "Package status is complete or has not been requested.",
|
||||
EditorStyles.miniLabel);
|
||||
}
|
||||
|
||||
EditorGUILayout.Space();
|
||||
|
||||
_scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition);
|
||||
DrawResult(_addressablesPackageResult);
|
||||
|
||||
foreach (DiagnosticResult result in _assetResults)
|
||||
{
|
||||
DrawResult(result);
|
||||
}
|
||||
|
||||
EditorGUILayout.EndScrollView();
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshAll()
|
||||
{
|
||||
RefreshAssetChecks();
|
||||
RefreshPackageStatus();
|
||||
Repaint();
|
||||
}
|
||||
|
||||
private void RefreshAssetChecks()
|
||||
{
|
||||
_assetResults.Clear();
|
||||
|
||||
foreach (AssetCheck check in AssetChecks)
|
||||
{
|
||||
bool exists = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(check.Path) != null;
|
||||
_assetResults.Add(exists
|
||||
? DiagnosticResult.Pass(check.Label, check.Path)
|
||||
: DiagnosticResult.Fail(check.Label, check.Path));
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshPackageStatus()
|
||||
{
|
||||
EditorApplication.update -= PollPackageListRequest;
|
||||
|
||||
_addressablesPackageResult = DiagnosticResult.Pending(
|
||||
"Addressables package",
|
||||
"Querying com.unity.addressables through Unity Package Manager.");
|
||||
|
||||
_packageListRequest = Client.List(true);
|
||||
EditorApplication.update += PollPackageListRequest;
|
||||
}
|
||||
|
||||
private void PollPackageListRequest()
|
||||
{
|
||||
if (_packageListRequest == null || !_packageListRequest.IsCompleted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EditorApplication.update -= PollPackageListRequest;
|
||||
|
||||
if (_packageListRequest.Status == StatusCode.Success)
|
||||
{
|
||||
bool found = false;
|
||||
foreach (UnityEditor.PackageManager.PackageInfo packageInfo in _packageListRequest.Result)
|
||||
{
|
||||
if (string.Equals(packageInfo.name, AddressablesPackageName, StringComparison.Ordinal))
|
||||
{
|
||||
found = true;
|
||||
_addressablesPackageResult = DiagnosticResult.Pass(
|
||||
"Addressables package",
|
||||
packageInfo.name + " " + packageInfo.version);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found)
|
||||
{
|
||||
_addressablesPackageResult = DiagnosticResult.Fail(
|
||||
"Addressables package",
|
||||
"com.unity.addressables was not found in the current Package Manager list.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
string errorMessage = _packageListRequest.Error != null
|
||||
? _packageListRequest.Error.message
|
||||
: "Package Manager returned an unknown error.";
|
||||
_addressablesPackageResult = DiagnosticResult.Warning("Addressables package", errorMessage);
|
||||
}
|
||||
|
||||
_packageListRequest = null;
|
||||
Repaint();
|
||||
}
|
||||
|
||||
private static void DrawResult(DiagnosticResult result)
|
||||
{
|
||||
MessageType messageType = ToMessageType(result.Status);
|
||||
|
||||
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
|
||||
{
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
GUILayout.Label(ToStatusText(result.Status), GUILayout.Width(72f));
|
||||
EditorGUILayout.LabelField(result.Label, EditorStyles.boldLabel);
|
||||
}
|
||||
|
||||
EditorGUILayout.HelpBox(result.Detail, messageType);
|
||||
}
|
||||
}
|
||||
|
||||
private static MessageType ToMessageType(DiagnosticStatus status)
|
||||
{
|
||||
switch (status)
|
||||
{
|
||||
case DiagnosticStatus.Pass:
|
||||
return MessageType.Info;
|
||||
case DiagnosticStatus.Fail:
|
||||
return MessageType.Error;
|
||||
case DiagnosticStatus.Warning:
|
||||
return MessageType.Warning;
|
||||
default:
|
||||
return MessageType.None;
|
||||
}
|
||||
}
|
||||
|
||||
private static string ToStatusText(DiagnosticStatus status)
|
||||
{
|
||||
switch (status)
|
||||
{
|
||||
case DiagnosticStatus.Pass:
|
||||
return "OK";
|
||||
case DiagnosticStatus.Fail:
|
||||
return "Missing";
|
||||
case DiagnosticStatus.Warning:
|
||||
return "Warning";
|
||||
default:
|
||||
return "Pending";
|
||||
}
|
||||
}
|
||||
|
||||
private readonly struct AssetCheck
|
||||
{
|
||||
public AssetCheck(string label, string path)
|
||||
{
|
||||
Label = label;
|
||||
Path = path;
|
||||
}
|
||||
|
||||
public string Label { get; }
|
||||
|
||||
public string Path { get; }
|
||||
}
|
||||
|
||||
private readonly struct DiagnosticResult
|
||||
{
|
||||
private DiagnosticResult(DiagnosticStatus status, string label, string detail)
|
||||
{
|
||||
Status = status;
|
||||
Label = label;
|
||||
Detail = detail;
|
||||
}
|
||||
|
||||
public DiagnosticStatus Status { get; }
|
||||
|
||||
public string Label { get; }
|
||||
|
||||
public string Detail { get; }
|
||||
|
||||
public static DiagnosticResult Pass(string label, string detail)
|
||||
{
|
||||
return new DiagnosticResult(DiagnosticStatus.Pass, label, detail);
|
||||
}
|
||||
|
||||
public static DiagnosticResult Fail(string label, string detail)
|
||||
{
|
||||
return new DiagnosticResult(DiagnosticStatus.Fail, label, detail);
|
||||
}
|
||||
|
||||
public static DiagnosticResult Warning(string label, string detail)
|
||||
{
|
||||
return new DiagnosticResult(DiagnosticStatus.Warning, label, detail);
|
||||
}
|
||||
|
||||
public static DiagnosticResult Pending(string label, string detail)
|
||||
{
|
||||
return new DiagnosticResult(DiagnosticStatus.Pending, label, detail);
|
||||
}
|
||||
}
|
||||
|
||||
private enum DiagnosticStatus
|
||||
{
|
||||
Pending,
|
||||
Pass,
|
||||
Warning,
|
||||
Fail,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4aac2cbe9a0b55b4a847cc7eb58e21e5
|
||||
fileFormatVersion: 2
|
||||
guid: 599ef528ec0c4331a77311f6ef7ef56b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
8
My project/Assets/FlowScope/Runtime/Audio.meta
Normal file
8
My project/Assets/FlowScope/Runtime/Audio.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: faa0e59581ea46ed8994f743336decdc
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
25
My project/Assets/FlowScope/Runtime/Audio/AudioHandle.cs
Normal file
25
My project/Assets/FlowScope/Runtime/Audio/AudioHandle.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
|
||||
namespace FlowScope.Audio
|
||||
{
|
||||
public sealed class AudioHandle : IAudioHandle
|
||||
{
|
||||
private readonly Func<bool> _isPlaying;
|
||||
private readonly Action<float> _stop;
|
||||
|
||||
internal AudioHandle(Func<bool> isPlaying, Action<float> stop)
|
||||
{
|
||||
_isPlaying = isPlaying ?? throw new ArgumentNullException(nameof(isPlaying));
|
||||
_stop = stop ?? throw new ArgumentNullException(nameof(stop));
|
||||
}
|
||||
|
||||
public bool IsPlaying => _isPlaying();
|
||||
|
||||
public void Stop(float fadeOut = 0f)
|
||||
{
|
||||
_stop(fadeOut);
|
||||
}
|
||||
|
||||
internal static AudioHandle Stopped { get; } = new AudioHandle(() => false, _ => { });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9e7da42c17284465ab9a68bec5387f19
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
392
My project/Assets/FlowScope/Runtime/Audio/AudioService.cs
Normal file
392
My project/Assets/FlowScope/Runtime/Audio/AudioService.cs
Normal file
@@ -0,0 +1,392 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FlowScope.Resources;
|
||||
using UnityEngine;
|
||||
|
||||
namespace FlowScope.Audio
|
||||
{
|
||||
public sealed class AudioService : MonoBehaviour, IAudioService
|
||||
{
|
||||
private readonly List<SfxSlot> _sfxSlots = new();
|
||||
private IResourceService _resources;
|
||||
private AudioServiceConfig _config;
|
||||
private AudioSource _bgmSource;
|
||||
private IResourceHandle<AudioClip> _bgmResource;
|
||||
private Coroutine _bgmFade;
|
||||
private float _bgmVolume = 1f;
|
||||
private float _sfxVolume = 1f;
|
||||
private bool _mute;
|
||||
private int _playSequence;
|
||||
|
||||
public AudioService Initialize(IResourceService resources, AudioServiceConfig config = null)
|
||||
{
|
||||
_resources = resources ?? throw new ArgumentNullException(nameof(resources));
|
||||
_config = config ?? new AudioServiceConfig();
|
||||
if (_config.SfxPoolSize < 0)
|
||||
{
|
||||
_config.SfxPoolSize = 0;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public IAudioHandle PlayBgm(string key, bool loop = true, float fadeIn = 0f)
|
||||
{
|
||||
EnsureInitialized();
|
||||
var clipHandle = LoadClipIfCompleted(key);
|
||||
return PlayBgm(clipHandle, loop, fadeIn);
|
||||
}
|
||||
|
||||
public async Task<IAudioHandle> PlayBgmAsync(
|
||||
string key,
|
||||
bool loop = true,
|
||||
float fadeIn = 0f,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
EnsureInitialized();
|
||||
var clipHandle = await LoadClipAsync(key, cancellationToken);
|
||||
return PlayBgm(clipHandle, loop, fadeIn);
|
||||
}
|
||||
|
||||
public IAudioHandle PlaySfx(string key)
|
||||
{
|
||||
EnsureInitialized();
|
||||
var clipHandle = LoadClipIfCompleted(key);
|
||||
var slot = GetSfxSlot(key);
|
||||
if (slot == null)
|
||||
{
|
||||
clipHandle.Dispose();
|
||||
return AudioHandle.Stopped;
|
||||
}
|
||||
|
||||
return PlaySfx(slot, clipHandle);
|
||||
}
|
||||
|
||||
public async Task<IAudioHandle> PlaySfxAsync(
|
||||
string key,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
EnsureInitialized();
|
||||
var clipHandle = await LoadClipAsync(key, cancellationToken);
|
||||
var slot = GetSfxSlot(key);
|
||||
if (slot == null)
|
||||
{
|
||||
clipHandle.Dispose();
|
||||
return AudioHandle.Stopped;
|
||||
}
|
||||
|
||||
return PlaySfx(slot, clipHandle);
|
||||
}
|
||||
|
||||
public void StopBgm(float fadeOut = 0f)
|
||||
{
|
||||
StopBgmSource(_bgmSource, fadeOut);
|
||||
}
|
||||
|
||||
public void SetBgmVolume(float volume)
|
||||
{
|
||||
_bgmVolume = Mathf.Clamp01(volume);
|
||||
if (_bgmSource != null)
|
||||
{
|
||||
_bgmSource.volume = EffectiveBgmVolume;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetSfxVolume(float volume)
|
||||
{
|
||||
_sfxVolume = Mathf.Clamp01(volume);
|
||||
foreach (var slot in _sfxSlots)
|
||||
{
|
||||
if (slot.Source != null)
|
||||
{
|
||||
slot.Source.volume = EffectiveSfxVolume;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SetMute(bool mute)
|
||||
{
|
||||
_mute = mute;
|
||||
if (_bgmSource != null)
|
||||
{
|
||||
_bgmSource.volume = EffectiveBgmVolume;
|
||||
}
|
||||
|
||||
foreach (var slot in _sfxSlots)
|
||||
{
|
||||
if (slot.Source != null)
|
||||
{
|
||||
slot.Source.volume = EffectiveSfxVolume;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IAudioHandle PlayBgm(IResourceHandle<AudioClip> clipHandle, bool loop, float fadeIn)
|
||||
{
|
||||
StopBgm();
|
||||
_bgmSource = gameObject.AddComponent<AudioSource>();
|
||||
_bgmResource = clipHandle;
|
||||
_bgmSource.clip = clipHandle.Asset;
|
||||
_bgmSource.loop = loop;
|
||||
_bgmSource.volume = fadeIn > 0f ? 0f : EffectiveBgmVolume;
|
||||
_bgmSource.Play();
|
||||
|
||||
if (fadeIn > 0f)
|
||||
{
|
||||
_bgmFade = StartCoroutine(FadeVolume(_bgmSource, 0f, EffectiveBgmVolume, fadeIn, null));
|
||||
}
|
||||
|
||||
var source = _bgmSource;
|
||||
return new AudioHandle(
|
||||
() => source != null && source.isPlaying,
|
||||
fadeOut => StopBgmSource(source, fadeOut));
|
||||
}
|
||||
|
||||
private IAudioHandle PlaySfx(SfxSlot slot, IResourceHandle<AudioClip> clipHandle)
|
||||
{
|
||||
slot.Release();
|
||||
slot.Resource = clipHandle;
|
||||
slot.Source.clip = clipHandle.Asset;
|
||||
slot.Source.loop = false;
|
||||
slot.Source.volume = EffectiveSfxVolume;
|
||||
slot.Sequence = ++_playSequence;
|
||||
slot.Source.Play();
|
||||
|
||||
return new AudioHandle(
|
||||
() => slot.Source != null && slot.Source.isPlaying,
|
||||
fadeOut => StopSfxSlot(slot, fadeOut));
|
||||
}
|
||||
|
||||
private float EffectiveBgmVolume => _mute ? 0f : _bgmVolume;
|
||||
private float EffectiveSfxVolume => _mute ? 0f : _sfxVolume;
|
||||
|
||||
private void Update()
|
||||
{
|
||||
foreach (var slot in _sfxSlots)
|
||||
{
|
||||
if (slot.Source != null && !slot.Source.isPlaying)
|
||||
{
|
||||
slot.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
StopBgm();
|
||||
foreach (var slot in _sfxSlots)
|
||||
{
|
||||
slot.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private IResourceHandle<AudioClip> LoadClipIfCompleted(string key)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!(_resources is ICompletedResourceService completedResources) ||
|
||||
!completedResources.TryLoadCompleted(key, out IResourceHandle<AudioClip> handle))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Audio clip is not loaded synchronously: {key}. Use PlayBgmAsync or PlaySfxAsync.");
|
||||
}
|
||||
|
||||
return ValidateLoadedClip(key, handle);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to load audio clip '{key}'.", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IResourceHandle<AudioClip>> LoadClipAsync(
|
||||
string key,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var handle = await _resources.LoadAsync<AudioClip>(key, cancellationToken);
|
||||
return ValidateLoadedClip(key, handle);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to load audio clip '{key}'.", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static IResourceHandle<AudioClip> ValidateLoadedClip(
|
||||
string key,
|
||||
IResourceHandle<AudioClip> handle)
|
||||
{
|
||||
if (handle == null || handle.Asset == null)
|
||||
{
|
||||
handle?.Dispose();
|
||||
throw new InvalidOperationException($"Audio clip is missing: {key}");
|
||||
}
|
||||
|
||||
return handle;
|
||||
}
|
||||
|
||||
private SfxSlot GetSfxSlot(string key)
|
||||
{
|
||||
foreach (var slot in _sfxSlots)
|
||||
{
|
||||
if (slot.Source != null && !slot.Source.isPlaying)
|
||||
{
|
||||
return slot;
|
||||
}
|
||||
}
|
||||
|
||||
if (_sfxSlots.Count < _config.SfxPoolSize)
|
||||
{
|
||||
var source = gameObject.AddComponent<AudioSource>();
|
||||
var slot = new SfxSlot(source);
|
||||
_sfxSlots.Add(slot);
|
||||
return slot;
|
||||
}
|
||||
|
||||
if (_config.ReuseOldestWhenExhausted && _sfxSlots.Count > 0)
|
||||
{
|
||||
var oldest = _sfxSlots[0];
|
||||
foreach (var slot in _sfxSlots)
|
||||
{
|
||||
if (slot.Sequence < oldest.Sequence)
|
||||
{
|
||||
oldest = slot;
|
||||
}
|
||||
}
|
||||
|
||||
oldest.Source.Stop();
|
||||
return oldest;
|
||||
}
|
||||
|
||||
Debug.LogWarning($"SFX pool exhausted, skipped key: {key}");
|
||||
return null;
|
||||
}
|
||||
|
||||
private void StopBgmSource(AudioSource source, float fadeOut)
|
||||
{
|
||||
if (source == null || source != _bgmSource)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_bgmFade != null)
|
||||
{
|
||||
StopCoroutine(_bgmFade);
|
||||
_bgmFade = null;
|
||||
}
|
||||
|
||||
if (fadeOut > 0f && source.isPlaying)
|
||||
{
|
||||
_bgmFade = StartCoroutine(FadeVolume(
|
||||
source,
|
||||
source.volume,
|
||||
0f,
|
||||
fadeOut,
|
||||
() => CompleteBgmStop(source)));
|
||||
return;
|
||||
}
|
||||
|
||||
CompleteBgmStop(source);
|
||||
}
|
||||
|
||||
private void CompleteBgmStop(AudioSource source)
|
||||
{
|
||||
if (source == null || source != _bgmSource)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
source.Stop();
|
||||
source.volume = 0f;
|
||||
Destroy(source);
|
||||
_bgmSource = null;
|
||||
_bgmFade = null;
|
||||
_bgmResource?.Dispose();
|
||||
_bgmResource = null;
|
||||
}
|
||||
|
||||
private void StopSfxSlot(SfxSlot slot, float fadeOut)
|
||||
{
|
||||
if (slot?.Source == null || !slot.Source.isPlaying)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (fadeOut > 0f)
|
||||
{
|
||||
StartCoroutine(FadeVolume(
|
||||
slot.Source,
|
||||
slot.Source.volume,
|
||||
0f,
|
||||
fadeOut,
|
||||
() =>
|
||||
{
|
||||
slot.Source.Stop();
|
||||
slot.Release();
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
slot.Source.Stop();
|
||||
slot.Release();
|
||||
}
|
||||
|
||||
private IEnumerator FadeVolume(
|
||||
AudioSource source,
|
||||
float from,
|
||||
float to,
|
||||
float duration,
|
||||
Action complete)
|
||||
{
|
||||
var elapsed = 0f;
|
||||
while (elapsed < duration && source != null)
|
||||
{
|
||||
elapsed += Time.deltaTime;
|
||||
source.volume = Mathf.Lerp(from, to, Mathf.Clamp01(elapsed / duration));
|
||||
yield return null;
|
||||
}
|
||||
|
||||
if (source != null)
|
||||
{
|
||||
source.volume = to;
|
||||
}
|
||||
|
||||
complete?.Invoke();
|
||||
}
|
||||
|
||||
private void EnsureInitialized()
|
||||
{
|
||||
if (_resources == null)
|
||||
{
|
||||
throw new InvalidOperationException("AudioService must be initialized before use.");
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class SfxSlot
|
||||
{
|
||||
public SfxSlot(AudioSource source)
|
||||
{
|
||||
Source = source;
|
||||
}
|
||||
|
||||
public AudioSource Source { get; }
|
||||
public IResourceHandle<AudioClip> Resource { get; set; }
|
||||
public int Sequence { get; set; }
|
||||
|
||||
public void Release()
|
||||
{
|
||||
Resource?.Dispose();
|
||||
Resource = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9e81e8f0ce1a42e6a3b09bf74893b6b6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace FlowScope.Audio
|
||||
{
|
||||
public sealed class AudioServiceConfig
|
||||
{
|
||||
public int SfxPoolSize = 10;
|
||||
public bool ReuseOldestWhenExhausted = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 276b2d62e26a4edcaaeba1a96a3c2917
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace FlowScope.Audio
|
||||
{
|
||||
public interface IAudioHandle
|
||||
{
|
||||
void Stop(float fadeOut = 0f);
|
||||
bool IsPlaying { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 780afa085183438eaa9b372219113d62
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
25
My project/Assets/FlowScope/Runtime/Audio/IAudioService.cs
Normal file
25
My project/Assets/FlowScope/Runtime/Audio/IAudioService.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FlowScope.Audio
|
||||
{
|
||||
public interface IAudioService
|
||||
{
|
||||
IAudioHandle PlayBgm(string key, bool loop = true, float fadeIn = 0f);
|
||||
Task<IAudioHandle> PlayBgmAsync(
|
||||
string key,
|
||||
bool loop = true,
|
||||
float fadeIn = 0f,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
IAudioHandle PlaySfx(string key);
|
||||
Task<IAudioHandle> PlaySfxAsync(
|
||||
string key,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
void StopBgm(float fadeOut = 0f);
|
||||
void SetBgmVolume(float volume);
|
||||
void SetSfxVolume(float volume);
|
||||
void SetMute(bool mute);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2763bff6b4ca4ac2ae5b9e5f764ad984
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
My project/Assets/FlowScope/Runtime/Common.meta
Normal file
8
My project/Assets/FlowScope/Runtime/Common.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 76e811c4a8bd4f6497926d5befb64b83
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
22
My project/Assets/FlowScope/Runtime/Common/ResultLog.cs
Normal file
22
My project/Assets/FlowScope/Runtime/Common/ResultLog.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace FlowScope.Common
|
||||
{
|
||||
public sealed class ResultLog
|
||||
{
|
||||
private readonly List<string> _warnings = new();
|
||||
private readonly List<string> _errors = new();
|
||||
|
||||
public IReadOnlyList<string> Warnings => _warnings;
|
||||
public IReadOnlyList<string> Errors => _errors;
|
||||
public bool HasErrors => _errors.Count > 0;
|
||||
|
||||
public void AddWarning(string message) => _warnings.Add(message);
|
||||
public void AddError(string message) => _errors.Add(message);
|
||||
public void Clear()
|
||||
{
|
||||
_warnings.Clear();
|
||||
_errors.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
11
My project/Assets/FlowScope/Runtime/Common/ResultLog.cs.meta
Normal file
11
My project/Assets/FlowScope/Runtime/Common/ResultLog.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a4063b7b41014f9399532dd3cc83e68f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
My project/Assets/FlowScope/Runtime/Config.meta
Normal file
8
My project/Assets/FlowScope/Runtime/Config.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2f421e0a585f4a279d8db9410eb82711
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FlowScope.Config
|
||||
{
|
||||
public sealed class FileConfigSource : IConfigSource
|
||||
{
|
||||
private readonly string _rootPath;
|
||||
|
||||
public FileConfigSource(string rootPath, string name = "File")
|
||||
{
|
||||
_rootPath = string.IsNullOrWhiteSpace(rootPath)
|
||||
? throw new ArgumentException("Config root path is required.", nameof(rootPath))
|
||||
: rootPath;
|
||||
Name = string.IsNullOrWhiteSpace(name)
|
||||
? throw new ArgumentException("Config source name is required.", nameof(name))
|
||||
: name;
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public Task<string> LoadTextAsync(string fileName, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var path = Path.Combine(_rootPath, fileName);
|
||||
return Task.Run(() => File.ReadAllText(path), cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8d50cd860ba849a1aa7647deba4e71db
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
14
My project/Assets/FlowScope/Runtime/Config/IConfigParser.cs
Normal file
14
My project/Assets/FlowScope/Runtime/Config/IConfigParser.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace FlowScope.Config
|
||||
{
|
||||
public interface IConfigParser
|
||||
{
|
||||
string Format { get; }
|
||||
|
||||
IReadOnlyList<T> ParseRows<T>(
|
||||
string fileName,
|
||||
string text)
|
||||
where T : class, IConfigRow;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0d22229e25964ea29b3c6d153507b1db
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,13 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FlowScope.Config
|
||||
{
|
||||
public interface IConfigProvider
|
||||
{
|
||||
Task LoadAllAsync(CancellationToken cancellationToken);
|
||||
T Get<T>(int id) where T : class, IConfigRow;
|
||||
IReadOnlyList<T> GetAll<T>() where T : class, IConfigRow;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: af3d9a24ab0e4c6ebe3bf906cfd9e2bb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
7
My project/Assets/FlowScope/Runtime/Config/IConfigRow.cs
Normal file
7
My project/Assets/FlowScope/Runtime/Config/IConfigRow.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace FlowScope.Config
|
||||
{
|
||||
public interface IConfigRow
|
||||
{
|
||||
int Id { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 20d7e0e8230e4df1b5afa8000ef26fea
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
14
My project/Assets/FlowScope/Runtime/Config/IConfigSource.cs
Normal file
14
My project/Assets/FlowScope/Runtime/Config/IConfigSource.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FlowScope.Config
|
||||
{
|
||||
public interface IConfigSource
|
||||
{
|
||||
string Name { get; }
|
||||
|
||||
Task<string> LoadTextAsync(
|
||||
string fileName,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 708db2a1c26743f3994b5d8beb1a03f8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FlowScope.Config
|
||||
{
|
||||
public sealed class InMemoryConfigSource : IConfigSource
|
||||
{
|
||||
private readonly Dictionary<string, string> _files;
|
||||
|
||||
public InMemoryConfigSource(
|
||||
IReadOnlyDictionary<string, string> files,
|
||||
string name = "InMemory")
|
||||
{
|
||||
_files = files != null
|
||||
? new Dictionary<string, string>(files)
|
||||
: throw new ArgumentNullException(nameof(files));
|
||||
Name = string.IsNullOrWhiteSpace(name)
|
||||
? throw new ArgumentException("Config source name is required.", nameof(name))
|
||||
: name;
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public Task<string> LoadTextAsync(string fileName, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (!_files.TryGetValue(fileName, out var text))
|
||||
{
|
||||
throw new InvalidOperationException($"Config file is missing: {fileName}");
|
||||
}
|
||||
|
||||
return Task.FromResult(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 607d9b20e7f04565929ea5876e5c25e1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,16 @@
|
||||
using System.Collections.Generic;
|
||||
using FlowScope.Data;
|
||||
|
||||
namespace FlowScope.Config
|
||||
{
|
||||
public sealed class JsonConfigParser : IConfigParser
|
||||
{
|
||||
public string Format => "json";
|
||||
|
||||
public IReadOnlyList<T> ParseRows<T>(string fileName, string text)
|
||||
where T : class, IConfigRow
|
||||
{
|
||||
return ReactivePropertyJsonConverter.DeserializeArray<T>(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f60f8a8e66394f26a20ed6f5ef314c63
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
166
My project/Assets/FlowScope/Runtime/Config/JsonConfigProvider.cs
Normal file
166
My project/Assets/FlowScope/Runtime/Config/JsonConfigProvider.cs
Normal file
@@ -0,0 +1,166 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FlowScope.Config
|
||||
{
|
||||
public sealed class JsonConfigProvider : IConfigProvider
|
||||
{
|
||||
private readonly IReadOnlyList<ConfigMapping> _mappings;
|
||||
private readonly Dictionary<Type, object> _rowsByType = new();
|
||||
|
||||
public JsonConfigProvider(IReadOnlyList<ConfigMapping> mappings)
|
||||
{
|
||||
_mappings = mappings ?? throw new ArgumentNullException(nameof(mappings));
|
||||
}
|
||||
|
||||
public static ConfigMapping Mapping<T>(string fileName, Func<CancellationToken, Task<string>> loadTextAsync)
|
||||
where T : class, IConfigRow
|
||||
{
|
||||
return Mapping<T>(
|
||||
fileName,
|
||||
new DelegateConfigSource(fileName, loadTextAsync),
|
||||
new JsonConfigParser());
|
||||
}
|
||||
|
||||
public static ConfigMapping Mapping<T>(
|
||||
string fileName,
|
||||
IConfigSource source,
|
||||
IConfigParser parser)
|
||||
where T : class, IConfigRow
|
||||
{
|
||||
return new ConfigMapping(typeof(T), fileName, source, parser);
|
||||
}
|
||||
|
||||
public async Task LoadAllAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var loadedRows = new Dictionary<Type, object>();
|
||||
|
||||
foreach (var mapping in _mappings)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
string json;
|
||||
try
|
||||
{
|
||||
json = await mapping.Source.LoadTextAsync(mapping.FileName, cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Failed to load config file '{mapping.FileName}' from source '{mapping.Source.Name}'.",
|
||||
exception);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
loadedRows[mapping.RowType] = mapping.Parse(json);
|
||||
}
|
||||
catch (Exception exception) when (exception is not OperationCanceledException)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Failed to parse config file '{mapping.FileName}': {exception.GetBaseException().Message}",
|
||||
exception);
|
||||
}
|
||||
}
|
||||
|
||||
_rowsByType.Clear();
|
||||
foreach (var pair in loadedRows)
|
||||
{
|
||||
_rowsByType[pair.Key] = pair.Value;
|
||||
}
|
||||
}
|
||||
|
||||
public T Get<T>(int id) where T : class, IConfigRow
|
||||
{
|
||||
return _rowsByType.TryGetValue(typeof(T), out var table) &&
|
||||
table is Dictionary<int, T> rows &&
|
||||
rows.TryGetValue(id, out var row)
|
||||
? row
|
||||
: null;
|
||||
}
|
||||
|
||||
public IReadOnlyList<T> GetAll<T>() where T : class, IConfigRow
|
||||
{
|
||||
if (!_rowsByType.TryGetValue(typeof(T), out var table) || table is not Dictionary<int, T> rows)
|
||||
{
|
||||
return Array.Empty<T>();
|
||||
}
|
||||
|
||||
return new List<T>(rows.Values);
|
||||
}
|
||||
|
||||
public sealed class ConfigMapping
|
||||
{
|
||||
private readonly MethodInfo _parseRowsMethod;
|
||||
|
||||
internal ConfigMapping(
|
||||
Type rowType,
|
||||
string fileName,
|
||||
IConfigSource source,
|
||||
IConfigParser parser)
|
||||
{
|
||||
RowType = rowType ?? throw new ArgumentNullException(nameof(rowType));
|
||||
FileName = string.IsNullOrWhiteSpace(fileName)
|
||||
? throw new ArgumentException("Config file name is required.", nameof(fileName))
|
||||
: fileName;
|
||||
Source = source ?? throw new ArgumentNullException(nameof(source));
|
||||
Parser = parser ?? throw new ArgumentNullException(nameof(parser));
|
||||
_parseRowsMethod = typeof(IConfigParser)
|
||||
.GetMethod(nameof(IConfigParser.ParseRows))
|
||||
.MakeGenericMethod(rowType);
|
||||
}
|
||||
|
||||
public Type RowType { get; }
|
||||
public string FileName { get; }
|
||||
public IConfigSource Source { get; }
|
||||
public IConfigParser Parser { get; }
|
||||
|
||||
internal object Parse(string json)
|
||||
{
|
||||
var rows = (System.Collections.IEnumerable)_parseRowsMethod.Invoke(
|
||||
Parser,
|
||||
new object[] { FileName, json });
|
||||
var byIdType = typeof(Dictionary<,>).MakeGenericType(typeof(int), RowType);
|
||||
var byId = (System.Collections.IDictionary)Activator.CreateInstance(byIdType);
|
||||
foreach (IConfigRow row in rows)
|
||||
{
|
||||
if (byId.Contains(row.Id))
|
||||
{
|
||||
throw new InvalidOperationException($"Duplicate config id {row.Id} for {RowType.Name}.");
|
||||
}
|
||||
|
||||
byId.Add(row.Id, row);
|
||||
}
|
||||
|
||||
return byId;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class DelegateConfigSource : IConfigSource
|
||||
{
|
||||
private readonly Func<CancellationToken, Task<string>> _loadTextAsync;
|
||||
|
||||
public DelegateConfigSource(
|
||||
string fileName,
|
||||
Func<CancellationToken, Task<string>> loadTextAsync)
|
||||
{
|
||||
Name = fileName;
|
||||
_loadTextAsync = loadTextAsync ?? throw new ArgumentNullException(nameof(loadTextAsync));
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public Task<string> LoadTextAsync(string fileName, CancellationToken cancellationToken)
|
||||
{
|
||||
return _loadTextAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c3102de921ecf4a40acc99c9bd099adc
|
||||
fileFormatVersion: 2
|
||||
guid: 0b6c05d7a1d74798901d3efcc51cb7c4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
8
My project/Assets/FlowScope/Runtime/Container.meta
Normal file
8
My project/Assets/FlowScope/Runtime/Container.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8d24db89928e4010a3c1af79ffb15709
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
301
My project/Assets/FlowScope/Runtime/Container/Container.cs
Normal file
301
My project/Assets/FlowScope/Runtime/Container/Container.cs
Normal file
@@ -0,0 +1,301 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace FlowScope.Container
|
||||
{
|
||||
public sealed class Container : IDisposable
|
||||
{
|
||||
[ThreadStatic]
|
||||
private static Stack<Type> _resolutionStack;
|
||||
|
||||
private readonly Container _parent;
|
||||
private readonly Dictionary<Type, ContainerRegistration> _registrations = new Dictionary<Type, ContainerRegistration>();
|
||||
private readonly List<Container> _children = new List<Container>();
|
||||
private readonly List<IDisposable> _ownedDisposables = new List<IDisposable>();
|
||||
private bool _disposed;
|
||||
|
||||
public Container()
|
||||
{
|
||||
}
|
||||
|
||||
private Container(Container parent)
|
||||
{
|
||||
_parent = parent;
|
||||
}
|
||||
|
||||
public void RegisterInstance<T>(T instance)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
if (instance == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(instance));
|
||||
}
|
||||
|
||||
RegisterLocal(typeof(T), ContainerRegistration.ForInstance(instance));
|
||||
}
|
||||
|
||||
public void RegisterFactory<T>(Func<Container, T> factory)
|
||||
{
|
||||
RegisterSingletonFactory(factory);
|
||||
}
|
||||
|
||||
public void RegisterSingletonFactory<T>(Func<Container, T> factory)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
if (factory == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(factory));
|
||||
}
|
||||
|
||||
RegisterLocal(typeof(T), ContainerRegistration.ForSingletonFactory(this, c => factory(c)));
|
||||
}
|
||||
|
||||
public void RegisterScoped<T>(Func<Container, T> factory)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
if (factory == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(factory));
|
||||
}
|
||||
|
||||
RegisterLocal(typeof(T), ContainerRegistration.ForScopedFactory(this, c => factory(c)));
|
||||
}
|
||||
|
||||
public void RegisterTransient<T>(Func<Container, T> factory)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
if (factory == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(factory));
|
||||
}
|
||||
|
||||
RegisterLocal(typeof(T), ContainerRegistration.ForTransientFactory(this, c => factory(c)));
|
||||
}
|
||||
|
||||
public void RegisterType<TInterface, TImplementation>() where TImplementation : TInterface
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
RegisterType(typeof(TInterface), typeof(TImplementation));
|
||||
}
|
||||
|
||||
public void RegisterType<TImplementation>()
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
RegisterType(typeof(TImplementation), typeof(TImplementation));
|
||||
}
|
||||
|
||||
public void RegisterAssembly()
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
GeneratedFactories.RegisterAssembly(this);
|
||||
}
|
||||
|
||||
public T Resolve<T>()
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
return (T)Resolve(typeof(T));
|
||||
}
|
||||
|
||||
public bool TryResolve<T>(out T value)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
if (!TryFindRegistration(typeof(T), out var registration))
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = (T)ResolveRegistration(typeof(T), registration);
|
||||
return true;
|
||||
}
|
||||
|
||||
public Container CreateScope()
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
var child = new Container(this);
|
||||
_children.Add(child);
|
||||
return child;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
var exceptions = new List<Exception>();
|
||||
|
||||
for (var i = _children.Count - 1; i >= 0; i--)
|
||||
{
|
||||
TryDispose(_children[i], exceptions);
|
||||
}
|
||||
|
||||
for (var i = _ownedDisposables.Count - 1; i >= 0; i--)
|
||||
{
|
||||
TryDispose(_ownedDisposables[i], exceptions);
|
||||
}
|
||||
|
||||
_children.Clear();
|
||||
_ownedDisposables.Clear();
|
||||
ForgetScopedInstancesFor(this);
|
||||
_registrations.Clear();
|
||||
|
||||
if (_parent != null)
|
||||
{
|
||||
_parent._children.Remove(this);
|
||||
}
|
||||
|
||||
if (exceptions.Count > 0)
|
||||
{
|
||||
throw new AggregateException("One or more container-owned instances failed to dispose.", exceptions);
|
||||
}
|
||||
}
|
||||
|
||||
internal object Resolve(Type serviceType)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
if (!TryFindRegistration(serviceType, out var registration))
|
||||
{
|
||||
throw new InvalidOperationException($"No registration found for {FormatType(serviceType)}.");
|
||||
}
|
||||
|
||||
return ResolveRegistration(serviceType, registration);
|
||||
}
|
||||
|
||||
internal void RegisterFactory(Type serviceType, Func<Container, object> factory)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
if (serviceType == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(serviceType));
|
||||
}
|
||||
|
||||
if (factory == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(factory));
|
||||
}
|
||||
|
||||
RegisterLocal(serviceType, ContainerRegistration.ForTransientFactory(this, factory));
|
||||
}
|
||||
|
||||
internal void TrackOwnedDisposable(IDisposable disposable)
|
||||
{
|
||||
_ownedDisposables.Add(disposable);
|
||||
}
|
||||
|
||||
private void RegisterType(Type serviceType, Type implementationType)
|
||||
{
|
||||
if (!serviceType.IsAssignableFrom(implementationType))
|
||||
{
|
||||
throw new InvalidOperationException($"{FormatType(implementationType)} cannot be assigned to {FormatType(serviceType)}.");
|
||||
}
|
||||
|
||||
if (!GeneratedFactories.TryGetFactory(implementationType, out var factory))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"No generated factory registered for {FormatType(implementationType)}. Use GeneratedFactories.Register, ReflectionFactoryBuilder.RegisterType, or RegisterFactory before resolving this type.");
|
||||
}
|
||||
|
||||
RegisterFactory(serviceType, factory);
|
||||
}
|
||||
|
||||
private void RegisterLocal(Type serviceType, ContainerRegistration registration)
|
||||
{
|
||||
if (_registrations.ContainsKey(serviceType))
|
||||
{
|
||||
throw new InvalidOperationException($"{FormatType(serviceType)} is already registered in this container scope.");
|
||||
}
|
||||
|
||||
_registrations.Add(serviceType, registration);
|
||||
}
|
||||
|
||||
private object ResolveRegistration(Type serviceType, ContainerRegistration registration)
|
||||
{
|
||||
_resolutionStack = _resolutionStack ?? new Stack<Type>();
|
||||
|
||||
if (_resolutionStack.Contains(serviceType))
|
||||
{
|
||||
var chain = _resolutionStack.Reverse()
|
||||
.Concat(new[] { serviceType })
|
||||
.Select(t => t.Name);
|
||||
throw new InvalidOperationException($"Circular dependency detected: {string.Join(" -> ", chain)}");
|
||||
}
|
||||
|
||||
_resolutionStack.Push(serviceType);
|
||||
try
|
||||
{
|
||||
return registration.Resolve(this);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_resolutionStack.Pop();
|
||||
if (_resolutionStack.Count == 0)
|
||||
{
|
||||
_resolutionStack = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryFindRegistration(Type serviceType, out ContainerRegistration registration)
|
||||
{
|
||||
if (_registrations.TryGetValue(serviceType, out registration))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_parent != null)
|
||||
{
|
||||
return _parent.TryFindRegistration(serviceType, out registration);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void ForgetScopedInstancesFor(Container scope)
|
||||
{
|
||||
foreach (var registration in _registrations.Values)
|
||||
{
|
||||
registration.ForgetScope(scope);
|
||||
}
|
||||
|
||||
_parent?.ForgetScopedInstancesFor(scope);
|
||||
}
|
||||
|
||||
private void ThrowIfDisposed()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(Container));
|
||||
}
|
||||
}
|
||||
|
||||
private static void TryDispose(IDisposable disposable, List<Exception> exceptions)
|
||||
{
|
||||
try
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exceptions.Add(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatType(Type type) => type.FullName ?? type.Name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace FlowScope.Container
|
||||
{
|
||||
internal sealed class ContainerRegistration
|
||||
{
|
||||
private enum Lifetime
|
||||
{
|
||||
Instance,
|
||||
Singleton,
|
||||
Scoped,
|
||||
Transient
|
||||
}
|
||||
|
||||
private readonly Container _owner;
|
||||
private readonly Func<Container, object> _factory;
|
||||
private readonly Lifetime _lifetime;
|
||||
private readonly Dictionary<Container, object> _scopedInstances;
|
||||
private object _instance;
|
||||
private bool _created;
|
||||
|
||||
private ContainerRegistration(
|
||||
Container owner,
|
||||
Func<Container, object> factory,
|
||||
object instance,
|
||||
bool created,
|
||||
Lifetime lifetime)
|
||||
{
|
||||
_owner = owner;
|
||||
_factory = factory;
|
||||
_instance = instance;
|
||||
_created = created;
|
||||
_lifetime = lifetime;
|
||||
_scopedInstances = lifetime == Lifetime.Scoped
|
||||
? new Dictionary<Container, object>()
|
||||
: null;
|
||||
}
|
||||
|
||||
public static ContainerRegistration ForInstance(object instance)
|
||||
{
|
||||
return new ContainerRegistration(null, null, instance, true, Lifetime.Instance);
|
||||
}
|
||||
|
||||
public static ContainerRegistration ForSingletonFactory(Container owner, Func<Container, object> factory)
|
||||
{
|
||||
return new ContainerRegistration(owner, factory, null, false, Lifetime.Singleton);
|
||||
}
|
||||
|
||||
public static ContainerRegistration ForScopedFactory(Container owner, Func<Container, object> factory)
|
||||
{
|
||||
return new ContainerRegistration(owner, factory, null, false, Lifetime.Scoped);
|
||||
}
|
||||
|
||||
public static ContainerRegistration ForTransientFactory(Container owner, Func<Container, object> factory)
|
||||
{
|
||||
return new ContainerRegistration(owner, factory, null, false, Lifetime.Transient);
|
||||
}
|
||||
|
||||
public object Resolve(Container requestScope)
|
||||
{
|
||||
switch (_lifetime)
|
||||
{
|
||||
case Lifetime.Instance:
|
||||
return _instance;
|
||||
case Lifetime.Singleton:
|
||||
return ResolveSingleton();
|
||||
case Lifetime.Scoped:
|
||||
return ResolveScoped(requestScope);
|
||||
case Lifetime.Transient:
|
||||
return CreateOwnedInstance(requestScope);
|
||||
default:
|
||||
throw new InvalidOperationException($"Unsupported container lifetime {_lifetime}.");
|
||||
}
|
||||
}
|
||||
|
||||
public void ForgetScope(Container scope)
|
||||
{
|
||||
_scopedInstances?.Remove(scope);
|
||||
}
|
||||
|
||||
private object ResolveSingleton()
|
||||
{
|
||||
if (!_created)
|
||||
{
|
||||
_instance = CreateOwnedInstance(_owner);
|
||||
_created = true;
|
||||
}
|
||||
|
||||
return _instance;
|
||||
}
|
||||
|
||||
private object ResolveScoped(Container requestScope)
|
||||
{
|
||||
if (_scopedInstances.TryGetValue(requestScope, out var instance))
|
||||
{
|
||||
return instance;
|
||||
}
|
||||
|
||||
instance = CreateOwnedInstance(requestScope);
|
||||
_scopedInstances.Add(requestScope, instance);
|
||||
return instance;
|
||||
}
|
||||
|
||||
private object CreateOwnedInstance(Container owner)
|
||||
{
|
||||
var instance = _factory(owner);
|
||||
|
||||
if (instance is IDisposable disposable)
|
||||
{
|
||||
owner.TrackOwnedDisposable(disposable);
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace FlowScope.Container
|
||||
{
|
||||
public static class GeneratedFactories
|
||||
{
|
||||
private static readonly Dictionary<Type, Func<Container, object>> Factories = new Dictionary<Type, Func<Container, object>>();
|
||||
|
||||
public static void Register<TImplementation>(Func<Container, TImplementation> factory)
|
||||
{
|
||||
if (factory == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(factory));
|
||||
}
|
||||
|
||||
Factories[typeof(TImplementation)] = c => factory(c);
|
||||
}
|
||||
|
||||
public static bool TryGetFactory(Type implementationType, out Func<Container, object> factory)
|
||||
{
|
||||
if (implementationType == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(implementationType));
|
||||
}
|
||||
|
||||
return Factories.TryGetValue(implementationType, out factory);
|
||||
}
|
||||
|
||||
public static void RegisterAssembly(Container container)
|
||||
{
|
||||
if (container == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(container));
|
||||
}
|
||||
|
||||
foreach (var pair in Factories)
|
||||
{
|
||||
container.RegisterFactory(pair.Key, pair.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fc50e3c76ee147b59d2663e872ced1a0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,9 @@
|
||||
using System;
|
||||
|
||||
namespace FlowScope.Container
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class InjectableAttribute : Attribute
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 039257eaaf6c4ac198a3504795b52df0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace FlowScope.Container
|
||||
{
|
||||
public static class ReflectionFactoryBuilder
|
||||
{
|
||||
public static void RegisterType<TImplementation>(Container container)
|
||||
{
|
||||
if (container == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(container));
|
||||
}
|
||||
|
||||
container.RegisterFactory(typeof(TImplementation), BuildFactory(typeof(TImplementation)));
|
||||
}
|
||||
|
||||
public static void RegisterType<TInterface, TImplementation>(Container container)
|
||||
where TImplementation : TInterface
|
||||
{
|
||||
if (container == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(container));
|
||||
}
|
||||
|
||||
container.RegisterFactory(typeof(TInterface), BuildFactory(typeof(TImplementation)));
|
||||
}
|
||||
|
||||
public static Func<Container, object> BuildFactory(Type implementationType)
|
||||
{
|
||||
if (implementationType == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(implementationType));
|
||||
}
|
||||
|
||||
if (!Attribute.IsDefined(implementationType, typeof(InjectableAttribute)))
|
||||
{
|
||||
throw new InvalidOperationException($"{FormatType(implementationType)} must be marked with InjectableAttribute for reflection registration.");
|
||||
}
|
||||
|
||||
var constructors = implementationType.GetConstructors(BindingFlags.Public | BindingFlags.Instance);
|
||||
if (constructors.Length != 1)
|
||||
{
|
||||
throw new InvalidOperationException($"{FormatType(implementationType)} must declare exactly one public constructor.");
|
||||
}
|
||||
|
||||
var constructor = constructors[0];
|
||||
var parameters = constructor.GetParameters();
|
||||
|
||||
return container =>
|
||||
{
|
||||
var arguments = parameters
|
||||
.Select(parameter => container.Resolve(parameter.ParameterType))
|
||||
.ToArray();
|
||||
return constructor.Invoke(arguments);
|
||||
};
|
||||
}
|
||||
|
||||
private static string FormatType(Type type) => type.FullName ?? type.Name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1d2827e4c16e4b4ea60a84f709d54665
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,167 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace FlowScope.Core.Container
|
||||
{
|
||||
internal sealed class ArchitectureScope : IArchitectureScope
|
||||
{
|
||||
private readonly ArchitectureScope _parent;
|
||||
private readonly global::FlowScope.Container.Container _container;
|
||||
private readonly List<ArchitectureScope> _children = new List<ArchitectureScope>();
|
||||
|
||||
public ArchitectureScope(string name)
|
||||
: this(name, null, new global::FlowScope.Container.Container())
|
||||
{
|
||||
}
|
||||
|
||||
private ArchitectureScope(
|
||||
string name,
|
||||
ArchitectureScope parent,
|
||||
global::FlowScope.Container.Container container)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
throw new ArgumentException("Scope name cannot be empty.", nameof(name));
|
||||
}
|
||||
|
||||
Name = name;
|
||||
_parent = parent;
|
||||
_container = container;
|
||||
Path = parent == null ? name : $"{parent.Path}/{name}";
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
public IScopeInfo Parent => _parent;
|
||||
public string Path { get; }
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
public IArchitectureScope CreateScope(string name)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
throw new ArgumentException("Scope name cannot be empty.", nameof(name));
|
||||
}
|
||||
|
||||
if (_children.Exists(child => !child.IsDisposed && child.Name == name))
|
||||
{
|
||||
throw new InvalidOperationException($"Scope '{name}' already exists under '{Path}'.");
|
||||
}
|
||||
|
||||
var child = new ArchitectureScope(name, this, _container.CreateScope(name));
|
||||
_children.Add(child);
|
||||
return child;
|
||||
}
|
||||
|
||||
public IArchitectureScope CreateFeature<TFeature>()
|
||||
where TFeature : IFeature, new()
|
||||
{
|
||||
return CreateFeature<TFeature>(typeof(TFeature).Name);
|
||||
}
|
||||
|
||||
public IArchitectureScope CreateFeature<TFeature>(string name)
|
||||
where TFeature : IFeature, new()
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
var featureScope = (ArchitectureScope)CreateScope(name);
|
||||
try
|
||||
{
|
||||
var feature = new TFeature();
|
||||
feature.Install(featureScope);
|
||||
return featureScope;
|
||||
}
|
||||
catch
|
||||
{
|
||||
featureScope.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public IRegistrationBuilder Register<TImplementation>()
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
return new RegistrationBuilderAdapter(_container.Register<TImplementation>());
|
||||
}
|
||||
|
||||
public IRegistrationBuilder Register<TImplementation>(string name)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
return new RegistrationBuilderAdapter(_container.Register<TImplementation>(name));
|
||||
}
|
||||
|
||||
public IRegistrationBuilder Register<TService, TImplementation>()
|
||||
where TImplementation : TService
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
return new RegistrationBuilderAdapter(_container.Register<TService, TImplementation>());
|
||||
}
|
||||
|
||||
public IRegistrationBuilder Register<TService, TImplementation>(string name)
|
||||
where TImplementation : TService
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
return new RegistrationBuilderAdapter(_container.Register<TService, TImplementation>(name));
|
||||
}
|
||||
|
||||
public void RegisterInstance<TService>(TService instance)
|
||||
{
|
||||
RegisterInstance(instance, null);
|
||||
}
|
||||
|
||||
public void RegisterInstance<TService>(TService instance, string name)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
_container.RegisterInstance(instance, name);
|
||||
}
|
||||
|
||||
public TService Resolve<TService>()
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
return _container.Resolve<TService>();
|
||||
}
|
||||
|
||||
public TService Resolve<TService>(string name)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
return _container.Resolve<TService>(name);
|
||||
}
|
||||
|
||||
public bool TryResolve<TService>(out TService value)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
return _container.TryResolve(out value);
|
||||
}
|
||||
|
||||
public bool TryResolve<TService>(out TService value, string name)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
return _container.TryResolve(out value, name);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (IsDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IsDisposed = true;
|
||||
for (var i = _children.Count - 1; i >= 0; i--)
|
||||
{
|
||||
_children[i].Dispose();
|
||||
}
|
||||
|
||||
_children.Clear();
|
||||
_container.Dispose();
|
||||
_parent?._children.Remove(this);
|
||||
}
|
||||
|
||||
private void ThrowIfDisposed()
|
||||
{
|
||||
if (IsDisposed)
|
||||
{
|
||||
throw new ObjectDisposedException(Path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,330 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace FlowScope.Container
|
||||
{
|
||||
public sealed class Container : IDisposable
|
||||
{
|
||||
[ThreadStatic]
|
||||
private static Stack<Type> _resolutionStack;
|
||||
|
||||
private readonly Container _parent;
|
||||
private readonly Dictionary<ServiceKey, ContainerRegistration> _registrations =
|
||||
new Dictionary<ServiceKey, ContainerRegistration>();
|
||||
private readonly List<Container> _children = new List<Container>();
|
||||
private readonly List<IDisposable> _ownedDisposables = new List<IDisposable>();
|
||||
private bool _disposed;
|
||||
|
||||
public Container()
|
||||
{
|
||||
}
|
||||
|
||||
private Container(Container parent, string name)
|
||||
{
|
||||
_parent = parent;
|
||||
Name = string.IsNullOrWhiteSpace(name) ? null : name;
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public void RegisterInstance<T>(T instance)
|
||||
{
|
||||
RegisterInstance(instance, null);
|
||||
}
|
||||
|
||||
public void RegisterInstance<T>(T instance, string name)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
if (instance == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(instance));
|
||||
}
|
||||
|
||||
RegisterLocal(new ServiceKey(typeof(T), name), ContainerRegistration.ForInstance(instance));
|
||||
}
|
||||
|
||||
public void RegisterFactory<T>(Func<Container, T> factory)
|
||||
{
|
||||
RegisterFactory(null, factory);
|
||||
}
|
||||
|
||||
public void RegisterFactory<T>(string name, Func<Container, T> factory)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
if (factory == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(factory));
|
||||
}
|
||||
|
||||
RegisterLocal(
|
||||
new ServiceKey(typeof(T), name),
|
||||
ContainerRegistration.ForFactory(this, c => factory(c), ServiceLifetime.Singleton));
|
||||
}
|
||||
|
||||
public ContainerRegistrationBuilder Register<TImplementation>()
|
||||
{
|
||||
return Register<TImplementation>(null);
|
||||
}
|
||||
|
||||
public ContainerRegistrationBuilder Register<TImplementation>(string name)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
return RegisterBuiltFactory(
|
||||
new ServiceKey(typeof(TImplementation), name),
|
||||
typeof(TImplementation));
|
||||
}
|
||||
|
||||
public ContainerRegistrationBuilder Register<TInterface, TImplementation>()
|
||||
where TImplementation : TInterface
|
||||
{
|
||||
return Register<TInterface, TImplementation>(null);
|
||||
}
|
||||
|
||||
public ContainerRegistrationBuilder Register<TInterface, TImplementation>(string name)
|
||||
where TImplementation : TInterface
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
var serviceType = typeof(TInterface);
|
||||
var implementationType = typeof(TImplementation);
|
||||
if (!serviceType.IsAssignableFrom(implementationType))
|
||||
{
|
||||
throw new InvalidOperationException($"{FormatType(implementationType)} cannot be assigned to {FormatType(serviceType)}.");
|
||||
}
|
||||
|
||||
return RegisterBuiltFactory(new ServiceKey(serviceType, name), implementationType);
|
||||
}
|
||||
|
||||
public T Resolve<T>()
|
||||
{
|
||||
return Resolve<T>(null);
|
||||
}
|
||||
|
||||
public T Resolve<T>(string name)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
return (T)Resolve(typeof(T), name);
|
||||
}
|
||||
|
||||
public bool TryResolve<T>(out T value)
|
||||
{
|
||||
return TryResolve(out value, null);
|
||||
}
|
||||
|
||||
public bool TryResolve<T>(out T value, string name)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
var key = new ServiceKey(typeof(T), name);
|
||||
if (!TryFindRegistration(key, out var registration))
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = (T)ResolveRegistration(key, registration);
|
||||
return true;
|
||||
}
|
||||
|
||||
public Container CreateScope()
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
var child = new Container(this, null);
|
||||
_children.Add(child);
|
||||
return child;
|
||||
}
|
||||
|
||||
public Container CreateScope(string name)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
throw new ArgumentException("Scope name cannot be empty.", nameof(name));
|
||||
}
|
||||
|
||||
if (_children.Exists(child => !child._disposed && child.Name == name))
|
||||
{
|
||||
throw new InvalidOperationException($"Scope '{name}' already exists in this container.");
|
||||
}
|
||||
|
||||
var child = new Container(this, name);
|
||||
_children.Add(child);
|
||||
return child;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
var exceptions = new List<Exception>();
|
||||
|
||||
for (var i = _children.Count - 1; i >= 0; i--)
|
||||
{
|
||||
TryDispose(_children[i], exceptions);
|
||||
}
|
||||
|
||||
for (var i = _ownedDisposables.Count - 1; i >= 0; i--)
|
||||
{
|
||||
TryDispose(_ownedDisposables[i], exceptions);
|
||||
}
|
||||
|
||||
_children.Clear();
|
||||
_ownedDisposables.Clear();
|
||||
_registrations.Clear();
|
||||
|
||||
if (_parent != null)
|
||||
{
|
||||
_parent._children.Remove(this);
|
||||
}
|
||||
|
||||
if (exceptions.Count > 0)
|
||||
{
|
||||
throw new AggregateException("One or more container-owned instances failed to dispose.", exceptions);
|
||||
}
|
||||
}
|
||||
|
||||
internal object Resolve(Type serviceType)
|
||||
{
|
||||
return Resolve(serviceType, null);
|
||||
}
|
||||
|
||||
internal object Resolve(Type serviceType, string name)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
var key = new ServiceKey(serviceType, name);
|
||||
if (!TryFindRegistration(key, out var registration))
|
||||
{
|
||||
throw new InvalidOperationException($"No registration found for {key}.");
|
||||
}
|
||||
|
||||
return ResolveRegistration(key, registration);
|
||||
}
|
||||
|
||||
internal void RegisterBuilt(ServiceKey serviceKey, ContainerRegistration registration)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
RegisterLocal(serviceKey, registration);
|
||||
}
|
||||
|
||||
internal void TrackOwnedDisposable(IDisposable disposable)
|
||||
{
|
||||
_ownedDisposables.Add(disposable);
|
||||
}
|
||||
|
||||
private ContainerRegistrationBuilder RegisterBuiltFactory(
|
||||
ServiceKey serviceKey,
|
||||
Type implementationType)
|
||||
{
|
||||
var factory = BuildFactory(implementationType);
|
||||
return new ContainerRegistrationBuilder(this, serviceKey, factory);
|
||||
}
|
||||
|
||||
private static Func<Container, object> BuildFactory(Type implementationType)
|
||||
{
|
||||
var constructors = implementationType.GetConstructors(BindingFlags.Public | BindingFlags.Instance);
|
||||
if (constructors.Length != 1)
|
||||
{
|
||||
throw new InvalidOperationException($"{FormatType(implementationType)} must declare exactly one public constructor.");
|
||||
}
|
||||
|
||||
var constructor = constructors[0];
|
||||
var parameters = constructor.GetParameters();
|
||||
|
||||
return container =>
|
||||
{
|
||||
var arguments = parameters
|
||||
.Select(parameter => container.Resolve(parameter.ParameterType))
|
||||
.ToArray();
|
||||
return constructor.Invoke(arguments);
|
||||
};
|
||||
}
|
||||
|
||||
private void RegisterLocal(ServiceKey serviceKey, ContainerRegistration registration)
|
||||
{
|
||||
if (_registrations.ContainsKey(serviceKey))
|
||||
{
|
||||
throw new InvalidOperationException($"{serviceKey} is already registered in this container scope.");
|
||||
}
|
||||
|
||||
_registrations.Add(serviceKey, registration);
|
||||
}
|
||||
|
||||
private object ResolveRegistration(ServiceKey serviceKey, ContainerRegistration registration)
|
||||
{
|
||||
_resolutionStack = _resolutionStack ?? new Stack<Type>();
|
||||
|
||||
if (_resolutionStack.Contains(serviceKey.ServiceType))
|
||||
{
|
||||
var chain = _resolutionStack.Reverse()
|
||||
.Concat(new[] { serviceKey.ServiceType })
|
||||
.Select(t => t.Name);
|
||||
throw new InvalidOperationException($"Circular dependency detected: {string.Join(" -> ", chain)}");
|
||||
}
|
||||
|
||||
_resolutionStack.Push(serviceKey.ServiceType);
|
||||
try
|
||||
{
|
||||
return registration.Resolve(this);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_resolutionStack.Pop();
|
||||
if (_resolutionStack.Count == 0)
|
||||
{
|
||||
_resolutionStack = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryFindRegistration(ServiceKey serviceKey, out ContainerRegistration registration)
|
||||
{
|
||||
if (_registrations.TryGetValue(serviceKey, out registration))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_parent != null)
|
||||
{
|
||||
return _parent.TryFindRegistration(serviceKey, out registration);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void ThrowIfDisposed()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(Container));
|
||||
}
|
||||
}
|
||||
|
||||
private static void TryDispose(IDisposable disposable, List<Exception> exceptions)
|
||||
{
|
||||
try
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exceptions.Add(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatType(Type type)
|
||||
{
|
||||
return type.FullName ?? type.Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace FlowScope.Container
|
||||
{
|
||||
public enum ServiceLifetime
|
||||
{
|
||||
Singleton,
|
||||
Scoped,
|
||||
Transient
|
||||
}
|
||||
|
||||
public readonly struct ServiceKey : IEquatable<ServiceKey>
|
||||
{
|
||||
public ServiceKey(Type serviceType, string name)
|
||||
{
|
||||
ServiceType = serviceType ?? throw new ArgumentNullException(nameof(serviceType));
|
||||
Name = string.IsNullOrWhiteSpace(name) ? null : name;
|
||||
}
|
||||
|
||||
public Type ServiceType { get; }
|
||||
public string Name { get; }
|
||||
|
||||
public bool Equals(ServiceKey other)
|
||||
{
|
||||
return ServiceType == other.ServiceType && string.Equals(Name, other.Name, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj is ServiceKey other && Equals(other);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
return ((ServiceType != null ? ServiceType.GetHashCode() : 0) * 397) ^
|
||||
(Name != null ? StringComparer.Ordinal.GetHashCode(Name) : 0);
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var typeName = ServiceType.FullName ?? ServiceType.Name;
|
||||
return Name == null ? typeName : $"{typeName} named '{Name}'";
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ContainerRegistrationBuilder
|
||||
{
|
||||
private readonly Container _owner;
|
||||
private readonly ServiceKey _serviceKey;
|
||||
private readonly Func<Container, object> _factory;
|
||||
private bool _committed;
|
||||
|
||||
internal ContainerRegistrationBuilder(
|
||||
Container owner,
|
||||
ServiceKey serviceKey,
|
||||
Func<Container, object> factory)
|
||||
{
|
||||
_owner = owner ?? throw new ArgumentNullException(nameof(owner));
|
||||
_serviceKey = serviceKey;
|
||||
_factory = factory ?? throw new ArgumentNullException(nameof(factory));
|
||||
}
|
||||
|
||||
public void AsSingleton()
|
||||
{
|
||||
Commit(ServiceLifetime.Singleton);
|
||||
}
|
||||
|
||||
public void PerScope()
|
||||
{
|
||||
Commit(ServiceLifetime.Scoped);
|
||||
}
|
||||
|
||||
public void AsTransient()
|
||||
{
|
||||
Commit(ServiceLifetime.Transient);
|
||||
}
|
||||
|
||||
private void Commit(ServiceLifetime lifetime)
|
||||
{
|
||||
if (_committed)
|
||||
{
|
||||
throw new InvalidOperationException($"{_serviceKey} registration has already been committed.");
|
||||
}
|
||||
|
||||
_committed = true;
|
||||
_owner.RegisterBuilt(_serviceKey, ContainerRegistration.ForFactory(_owner, _factory, lifetime));
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class ContainerRegistration
|
||||
{
|
||||
private readonly Container _owner;
|
||||
private readonly Func<Container, object> _factory;
|
||||
private readonly ServiceLifetime _lifetime;
|
||||
private readonly Dictionary<Container, object> _scopedInstances;
|
||||
private object _singletonInstance;
|
||||
private bool _singletonCreated;
|
||||
|
||||
private ContainerRegistration(
|
||||
Container owner,
|
||||
Func<Container, object> factory,
|
||||
object instance,
|
||||
bool created,
|
||||
ServiceLifetime lifetime)
|
||||
{
|
||||
_owner = owner;
|
||||
_factory = factory;
|
||||
_singletonInstance = instance;
|
||||
_singletonCreated = created;
|
||||
_lifetime = lifetime;
|
||||
_scopedInstances = lifetime == ServiceLifetime.Scoped
|
||||
? new Dictionary<Container, object>()
|
||||
: null;
|
||||
}
|
||||
|
||||
public static ContainerRegistration ForInstance(object instance)
|
||||
{
|
||||
return new ContainerRegistration(null, null, instance, true, ServiceLifetime.Singleton);
|
||||
}
|
||||
|
||||
public static ContainerRegistration ForFactory(Container owner, Func<Container, object> factory)
|
||||
{
|
||||
return ForFactory(owner, factory, ServiceLifetime.Singleton);
|
||||
}
|
||||
|
||||
public static ContainerRegistration ForFactory(
|
||||
Container owner,
|
||||
Func<Container, object> factory,
|
||||
ServiceLifetime lifetime)
|
||||
{
|
||||
return new ContainerRegistration(owner, factory, null, false, lifetime);
|
||||
}
|
||||
|
||||
public object Resolve(Container requester)
|
||||
{
|
||||
switch (_lifetime)
|
||||
{
|
||||
case ServiceLifetime.Singleton:
|
||||
return ResolveSingleton();
|
||||
case ServiceLifetime.Scoped:
|
||||
return ResolveScoped(requester);
|
||||
case ServiceLifetime.Transient:
|
||||
return CreateAndTrack(requester);
|
||||
default:
|
||||
throw new InvalidOperationException($"Unknown service lifetime {_lifetime}.");
|
||||
}
|
||||
}
|
||||
|
||||
private object ResolveSingleton()
|
||||
{
|
||||
if (_singletonCreated)
|
||||
{
|
||||
return _singletonInstance;
|
||||
}
|
||||
|
||||
_singletonInstance = CreateAndTrack(_owner);
|
||||
_singletonCreated = true;
|
||||
return _singletonInstance;
|
||||
}
|
||||
|
||||
private object ResolveScoped(Container requester)
|
||||
{
|
||||
if (requester == null)
|
||||
{
|
||||
requester = _owner;
|
||||
}
|
||||
|
||||
if (_scopedInstances.TryGetValue(requester, out var instance))
|
||||
{
|
||||
return instance;
|
||||
}
|
||||
|
||||
instance = CreateAndTrack(requester);
|
||||
_scopedInstances.Add(requester, instance);
|
||||
return instance;
|
||||
}
|
||||
|
||||
private object CreateAndTrack(Container owner)
|
||||
{
|
||||
var instance = _factory(owner);
|
||||
if (instance is IDisposable disposable)
|
||||
{
|
||||
owner.TrackOwnedDisposable(disposable);
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
using FlowScope.Container;
|
||||
|
||||
namespace FlowScope.Core.Container
|
||||
{
|
||||
internal sealed class RegistrationBuilderAdapter : IRegistrationBuilder
|
||||
{
|
||||
private readonly ContainerRegistrationBuilder _inner;
|
||||
|
||||
public RegistrationBuilderAdapter(ContainerRegistrationBuilder inner)
|
||||
{
|
||||
_inner = inner;
|
||||
}
|
||||
|
||||
public void AsSingleton() => _inner.AsSingleton();
|
||||
public void PerScope() => _inner.PerScope();
|
||||
public void AsTransient() => _inner.AsTransient();
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
using FlowScope.Core.Container;
|
||||
|
||||
namespace FlowScope
|
||||
{
|
||||
public static class FlowScope
|
||||
{
|
||||
public static IArchitectureScope Create()
|
||||
{
|
||||
return Create("Root");
|
||||
}
|
||||
|
||||
public static IArchitectureScope Create(string rootName)
|
||||
{
|
||||
return new ArchitectureScope(rootName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace FlowScope
|
||||
{
|
||||
public interface IArchitectureScope : IScopeInfo, IDisposable
|
||||
{
|
||||
IArchitectureScope CreateScope(string name);
|
||||
IArchitectureScope CreateFeature<TFeature>() where TFeature : IFeature, new();
|
||||
IArchitectureScope CreateFeature<TFeature>(string name) where TFeature : IFeature, new();
|
||||
|
||||
IRegistrationBuilder Register<TImplementation>();
|
||||
IRegistrationBuilder Register<TImplementation>(string name);
|
||||
IRegistrationBuilder Register<TService, TImplementation>() where TImplementation : TService;
|
||||
IRegistrationBuilder Register<TService, TImplementation>(string name) where TImplementation : TService;
|
||||
|
||||
void RegisterInstance<TService>(TService instance);
|
||||
void RegisterInstance<TService>(TService instance, string name);
|
||||
|
||||
TService Resolve<TService>();
|
||||
TService Resolve<TService>(string name);
|
||||
bool TryResolve<TService>(out TService value);
|
||||
bool TryResolve<TService>(out TService value, string name);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace FlowScope
|
||||
{
|
||||
public interface IFeature
|
||||
{
|
||||
void Install(IArchitectureScope scope);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
namespace FlowScope
|
||||
{
|
||||
public interface IRegistrationBuilder
|
||||
{
|
||||
void AsSingleton();
|
||||
void PerScope();
|
||||
void AsTransient();
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6e26efad6333ec6469df54dbf85637e0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,10 +0,0 @@
|
||||
namespace FlowScope
|
||||
{
|
||||
public interface IScopeInfo
|
||||
{
|
||||
string Name { get; }
|
||||
IScopeInfo Parent { get; }
|
||||
string Path { get; }
|
||||
bool IsDisposed { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a005e79c7b865c4458eb72135769a42f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
My project/Assets/FlowScope/Runtime/Data.meta
Normal file
8
My project/Assets/FlowScope/Runtime/Data.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 67eb0ac45b8b4cf8bafcb604dd6e0e11
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,731 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
namespace FlowScope.Data
|
||||
{
|
||||
public static class ReactivePropertyJsonConverter
|
||||
{
|
||||
public static string SerializeObject(object value)
|
||||
{
|
||||
return SimpleJson.Write(ToJsonValue(value));
|
||||
}
|
||||
|
||||
public static T DeserializeObject<T>(string json)
|
||||
{
|
||||
var instance = (T)Activator.CreateInstance(typeof(T), true);
|
||||
PopulateObject(json, instance);
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static void PopulateObject(string json, object target)
|
||||
{
|
||||
if (target == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(target));
|
||||
}
|
||||
|
||||
if (SimpleJson.Parse(json) is not Dictionary<string, object> values)
|
||||
{
|
||||
throw new InvalidOperationException($"JSON root must be an object for {target.GetType().Name}.");
|
||||
}
|
||||
|
||||
PopulateFromDictionary(values, target);
|
||||
}
|
||||
|
||||
public static IReadOnlyList<T> DeserializeArray<T>(string json) where T : class
|
||||
{
|
||||
if (SimpleJson.Parse(json) is not List<object> rows)
|
||||
{
|
||||
throw new InvalidOperationException("JSON root must be an array.");
|
||||
}
|
||||
|
||||
var results = new List<T>(rows.Count);
|
||||
foreach (var row in rows)
|
||||
{
|
||||
if (row is not Dictionary<string, object> rowValues)
|
||||
{
|
||||
throw new InvalidOperationException($"JSON row must be an object for {typeof(T).Name}.");
|
||||
}
|
||||
|
||||
var instance = (T)Activator.CreateInstance(typeof(T), true);
|
||||
PopulateFromDictionary(rowValues, instance);
|
||||
results.Add(instance);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
internal static object ToJsonValue(object value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var type = value.GetType();
|
||||
if (IsSimple(type))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
if (TryGetReactivePropertyValue(value, out var reactiveValue))
|
||||
{
|
||||
return ToJsonValue(reactiveValue);
|
||||
}
|
||||
|
||||
if (IsReactiveCollection(type) || value is IEnumerable && value is not string)
|
||||
{
|
||||
var array = new List<object>();
|
||||
foreach (var item in (IEnumerable)value)
|
||||
{
|
||||
array.Add(ToJsonValue(item));
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
var result = new Dictionary<string, object>();
|
||||
foreach (var property in GetSerializableProperties(type))
|
||||
{
|
||||
result[property.Name] = ToJsonValue(property.GetValue(value));
|
||||
}
|
||||
|
||||
foreach (var field in GetSerializableFields(type))
|
||||
{
|
||||
result[field.Name] = ToJsonValue(field.GetValue(value));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
internal static object ConvertValue(object value, Type targetType, string fieldName)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
if (targetType.IsValueType && Nullable.GetUnderlyingType(targetType) == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Field '{fieldName}' cannot be null.");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
var nullableType = Nullable.GetUnderlyingType(targetType);
|
||||
if (nullableType != null)
|
||||
{
|
||||
return ConvertValue(value, nullableType, fieldName);
|
||||
}
|
||||
|
||||
if (targetType == typeof(string))
|
||||
{
|
||||
if (value is string text)
|
||||
{
|
||||
return text;
|
||||
}
|
||||
|
||||
throw TypeMismatch(fieldName, targetType, value);
|
||||
}
|
||||
|
||||
if (targetType == typeof(bool))
|
||||
{
|
||||
if (value is bool boolValue)
|
||||
{
|
||||
return boolValue;
|
||||
}
|
||||
|
||||
throw TypeMismatch(fieldName, targetType, value);
|
||||
}
|
||||
|
||||
if (targetType.IsEnum)
|
||||
{
|
||||
if (value is string enumName)
|
||||
{
|
||||
return Enum.Parse(targetType, enumName);
|
||||
}
|
||||
|
||||
return Enum.ToObject(targetType, Convert.ToInt32(value, CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
if (IsNumeric(targetType))
|
||||
{
|
||||
if (value is string || value is bool)
|
||||
{
|
||||
throw TypeMismatch(fieldName, targetType, value);
|
||||
}
|
||||
|
||||
return Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
if (value is Dictionary<string, object> nested)
|
||||
{
|
||||
var instance = Activator.CreateInstance(targetType, true);
|
||||
PopulateFromDictionary(nested, instance);
|
||||
return instance;
|
||||
}
|
||||
|
||||
throw TypeMismatch(fieldName, targetType, value);
|
||||
}
|
||||
|
||||
private static void PopulateFromDictionary(IReadOnlyDictionary<string, object> values, object target)
|
||||
{
|
||||
var members = GetWritableMembers(target.GetType());
|
||||
foreach (var pair in values)
|
||||
{
|
||||
if (!members.TryGetValue(pair.Key, out var member))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
PopulateMember(target, member, pair.Value, pair.Key);
|
||||
}
|
||||
}
|
||||
|
||||
private static void PopulateMember(object target, MemberInfo member, object value, string fieldName)
|
||||
{
|
||||
var memberType = GetMemberType(member);
|
||||
var currentValue = GetMemberValue(member, target);
|
||||
|
||||
if (IsReactiveProperty(memberType))
|
||||
{
|
||||
var valueProperty = memberType.GetProperty("Value", BindingFlags.Instance | BindingFlags.Public);
|
||||
var converted = ConvertValue(value, valueProperty.PropertyType, fieldName);
|
||||
valueProperty.SetValue(currentValue, converted);
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsReactiveCollection(memberType) || IsMutableCollection(memberType, currentValue))
|
||||
{
|
||||
PopulateCollection(currentValue, memberType, value, fieldName);
|
||||
return;
|
||||
}
|
||||
|
||||
if (CanSet(member))
|
||||
{
|
||||
SetMemberValue(member, target, ConvertValue(value, memberType, fieldName));
|
||||
}
|
||||
}
|
||||
|
||||
private static void PopulateCollection(object collection, Type collectionType, object value, string fieldName)
|
||||
{
|
||||
if (value is not List<object> rows)
|
||||
{
|
||||
throw TypeMismatch(fieldName, collectionType, value);
|
||||
}
|
||||
|
||||
var itemType = GetCollectionItemType(collectionType);
|
||||
collectionType.GetMethod("Clear", Type.EmptyTypes)?.Invoke(collection, Array.Empty<object>());
|
||||
var addMethod = collectionType.GetMethod("Add", new[] { itemType });
|
||||
if (addMethod == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Field '{fieldName}' collection does not expose Add.");
|
||||
}
|
||||
|
||||
foreach (var row in rows)
|
||||
{
|
||||
addMethod.Invoke(collection, new[] { ConvertValue(row, itemType, fieldName) });
|
||||
}
|
||||
}
|
||||
|
||||
private static Dictionary<string, MemberInfo> GetWritableMembers(Type type)
|
||||
{
|
||||
var members = new Dictionary<string, MemberInfo>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var property in type.GetProperties(BindingFlags.Instance | BindingFlags.Public))
|
||||
{
|
||||
if (property.GetIndexParameters().Length == 0)
|
||||
{
|
||||
members[property.Name] = property;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.Public))
|
||||
{
|
||||
members[field.Name] = field;
|
||||
}
|
||||
|
||||
return members;
|
||||
}
|
||||
|
||||
private static IEnumerable<PropertyInfo> GetSerializableProperties(Type type)
|
||||
{
|
||||
foreach (var property in type.GetProperties(BindingFlags.Instance | BindingFlags.Public))
|
||||
{
|
||||
if (property.GetIndexParameters().Length == 0 && property.CanRead)
|
||||
{
|
||||
yield return property;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<FieldInfo> GetSerializableFields(Type type)
|
||||
{
|
||||
return type.GetFields(BindingFlags.Instance | BindingFlags.Public);
|
||||
}
|
||||
|
||||
private static Type GetMemberType(MemberInfo member)
|
||||
{
|
||||
return member switch
|
||||
{
|
||||
PropertyInfo property => property.PropertyType,
|
||||
FieldInfo field => field.FieldType,
|
||||
_ => throw new NotSupportedException(member.MemberType.ToString())
|
||||
};
|
||||
}
|
||||
|
||||
private static object GetMemberValue(MemberInfo member, object target)
|
||||
{
|
||||
return member switch
|
||||
{
|
||||
PropertyInfo property => property.GetValue(target),
|
||||
FieldInfo field => field.GetValue(target),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
private static void SetMemberValue(MemberInfo member, object target, object value)
|
||||
{
|
||||
switch (member)
|
||||
{
|
||||
case PropertyInfo property:
|
||||
property.SetValue(target, value);
|
||||
break;
|
||||
case FieldInfo field:
|
||||
field.SetValue(target, value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool CanSet(MemberInfo member)
|
||||
{
|
||||
return member switch
|
||||
{
|
||||
PropertyInfo property => property.CanWrite,
|
||||
FieldInfo field => !field.IsInitOnly,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
private static bool TryGetReactivePropertyValue(object value, out object reactiveValue)
|
||||
{
|
||||
reactiveValue = null;
|
||||
if (!IsReactiveProperty(value.GetType()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
reactiveValue = value.GetType().GetProperty("Value", BindingFlags.Instance | BindingFlags.Public)?.GetValue(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsReactiveProperty(Type type)
|
||||
{
|
||||
return type.IsGenericType && type.GetGenericTypeDefinition().FullName == "R3.ReactiveProperty`1";
|
||||
}
|
||||
|
||||
private static bool IsReactiveCollection(Type type)
|
||||
{
|
||||
return type.IsGenericType && type.GetGenericTypeDefinition().FullName == "R3.ReactiveCollection`1";
|
||||
}
|
||||
|
||||
private static bool IsMutableCollection(Type type, object value)
|
||||
{
|
||||
return value != null &&
|
||||
type != typeof(string) &&
|
||||
GetCollectionItemType(type) != null &&
|
||||
type.GetMethod("Clear", Type.EmptyTypes) != null;
|
||||
}
|
||||
|
||||
private static Type GetCollectionItemType(Type type)
|
||||
{
|
||||
if (type.IsArray)
|
||||
{
|
||||
return type.GetElementType();
|
||||
}
|
||||
|
||||
if (type.IsGenericType && type.GetGenericArguments().Length == 1)
|
||||
{
|
||||
return type.GetGenericArguments()[0];
|
||||
}
|
||||
|
||||
foreach (var interfaceType in type.GetInterfaces())
|
||||
{
|
||||
if (interfaceType.IsGenericType &&
|
||||
interfaceType.GetGenericTypeDefinition() == typeof(ICollection<>))
|
||||
{
|
||||
return interfaceType.GetGenericArguments()[0];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool IsSimple(Type type)
|
||||
{
|
||||
type = Nullable.GetUnderlyingType(type) ?? type;
|
||||
return type.IsPrimitive || type.IsEnum || type == typeof(string) || type == typeof(decimal);
|
||||
}
|
||||
|
||||
private static bool IsNumeric(Type type)
|
||||
{
|
||||
type = Nullable.GetUnderlyingType(type) ?? type;
|
||||
return type == typeof(byte) || type == typeof(sbyte) ||
|
||||
type == typeof(short) || type == typeof(ushort) ||
|
||||
type == typeof(int) || type == typeof(uint) ||
|
||||
type == typeof(long) || type == typeof(ulong) ||
|
||||
type == typeof(float) || type == typeof(double) ||
|
||||
type == typeof(decimal);
|
||||
}
|
||||
|
||||
private static InvalidOperationException TypeMismatch(string fieldName, Type targetType, object value)
|
||||
{
|
||||
return new InvalidOperationException(
|
||||
$"Field '{fieldName}' expected {targetType.Name}, but JSON value was {value.GetType().Name}.");
|
||||
}
|
||||
|
||||
internal static class SimpleJson
|
||||
{
|
||||
public static object Parse(string json)
|
||||
{
|
||||
var parser = new Parser(json);
|
||||
var value = parser.ParseValue();
|
||||
parser.SkipWhitespace();
|
||||
if (!parser.IsEnd)
|
||||
{
|
||||
throw new FormatException("Unexpected trailing JSON content.");
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public static string Write(object value)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
WriteValue(builder, value);
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static void WriteValue(StringBuilder builder, object value)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case null:
|
||||
builder.Append("null");
|
||||
break;
|
||||
case string text:
|
||||
WriteString(builder, text);
|
||||
break;
|
||||
case bool boolValue:
|
||||
builder.Append(boolValue ? "true" : "false");
|
||||
break;
|
||||
case IDictionary<string, object> map:
|
||||
WriteObject(builder, map);
|
||||
break;
|
||||
case IEnumerable<object> array:
|
||||
WriteArray(builder, array);
|
||||
break;
|
||||
case float or double or decimal:
|
||||
builder.Append(Convert.ToString(value, CultureInfo.InvariantCulture));
|
||||
break;
|
||||
default:
|
||||
if (value.GetType().IsEnum)
|
||||
{
|
||||
WriteString(builder, value.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append(Convert.ToString(value, CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteObject(StringBuilder builder, IDictionary<string, object> map)
|
||||
{
|
||||
builder.Append('{');
|
||||
var first = true;
|
||||
foreach (var pair in map)
|
||||
{
|
||||
if (!first)
|
||||
{
|
||||
builder.Append(',');
|
||||
}
|
||||
|
||||
first = false;
|
||||
WriteString(builder, pair.Key);
|
||||
builder.Append(':');
|
||||
WriteValue(builder, pair.Value);
|
||||
}
|
||||
|
||||
builder.Append('}');
|
||||
}
|
||||
|
||||
private static void WriteArray(StringBuilder builder, IEnumerable<object> values)
|
||||
{
|
||||
builder.Append('[');
|
||||
var first = true;
|
||||
foreach (var value in values)
|
||||
{
|
||||
if (!first)
|
||||
{
|
||||
builder.Append(',');
|
||||
}
|
||||
|
||||
first = false;
|
||||
WriteValue(builder, value);
|
||||
}
|
||||
|
||||
builder.Append(']');
|
||||
}
|
||||
|
||||
private static void WriteString(StringBuilder builder, string value)
|
||||
{
|
||||
builder.Append('"');
|
||||
foreach (var character in value)
|
||||
{
|
||||
switch (character)
|
||||
{
|
||||
case '"':
|
||||
builder.Append("\\\"");
|
||||
break;
|
||||
case '\\':
|
||||
builder.Append("\\\\");
|
||||
break;
|
||||
case '\n':
|
||||
builder.Append("\\n");
|
||||
break;
|
||||
case '\r':
|
||||
builder.Append("\\r");
|
||||
break;
|
||||
case '\t':
|
||||
builder.Append("\\t");
|
||||
break;
|
||||
default:
|
||||
builder.Append(character);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
builder.Append('"');
|
||||
}
|
||||
|
||||
private sealed class Parser
|
||||
{
|
||||
private readonly string _json;
|
||||
private int _index;
|
||||
|
||||
public Parser(string json)
|
||||
{
|
||||
_json = json ?? throw new ArgumentNullException(nameof(json));
|
||||
}
|
||||
|
||||
public bool IsEnd => _index >= _json.Length;
|
||||
|
||||
public object ParseValue()
|
||||
{
|
||||
SkipWhitespace();
|
||||
if (IsEnd)
|
||||
{
|
||||
throw new FormatException("Unexpected end of JSON.");
|
||||
}
|
||||
|
||||
return _json[_index] switch
|
||||
{
|
||||
'{' => ParseObject(),
|
||||
'[' => ParseArray(),
|
||||
'"' => ParseString(),
|
||||
't' => ParseLiteral("true", true),
|
||||
'f' => ParseLiteral("false", false),
|
||||
'n' => ParseLiteral("null", null),
|
||||
_ => ParseNumber()
|
||||
};
|
||||
}
|
||||
|
||||
public void SkipWhitespace()
|
||||
{
|
||||
while (!IsEnd && char.IsWhiteSpace(_json[_index]))
|
||||
{
|
||||
_index++;
|
||||
}
|
||||
}
|
||||
|
||||
private Dictionary<string, object> ParseObject()
|
||||
{
|
||||
Expect('{');
|
||||
var map = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||
SkipWhitespace();
|
||||
if (TryConsume('}'))
|
||||
{
|
||||
return map;
|
||||
}
|
||||
|
||||
while (true)
|
||||
{
|
||||
SkipWhitespace();
|
||||
var key = ParseString();
|
||||
SkipWhitespace();
|
||||
Expect(':');
|
||||
map[key] = ParseValue();
|
||||
SkipWhitespace();
|
||||
if (TryConsume('}'))
|
||||
{
|
||||
return map;
|
||||
}
|
||||
|
||||
Expect(',');
|
||||
}
|
||||
}
|
||||
|
||||
private List<object> ParseArray()
|
||||
{
|
||||
Expect('[');
|
||||
var array = new List<object>();
|
||||
SkipWhitespace();
|
||||
if (TryConsume(']'))
|
||||
{
|
||||
return array;
|
||||
}
|
||||
|
||||
while (true)
|
||||
{
|
||||
array.Add(ParseValue());
|
||||
SkipWhitespace();
|
||||
if (TryConsume(']'))
|
||||
{
|
||||
return array;
|
||||
}
|
||||
|
||||
Expect(',');
|
||||
}
|
||||
}
|
||||
|
||||
private string ParseString()
|
||||
{
|
||||
Expect('"');
|
||||
var builder = new StringBuilder();
|
||||
while (!IsEnd)
|
||||
{
|
||||
var character = _json[_index++];
|
||||
if (character == '"')
|
||||
{
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
if (character != '\\')
|
||||
{
|
||||
builder.Append(character);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsEnd)
|
||||
{
|
||||
throw new FormatException("Unexpected end of JSON escape sequence.");
|
||||
}
|
||||
|
||||
var escaped = _json[_index++];
|
||||
builder.Append(escaped switch
|
||||
{
|
||||
'"' => '"',
|
||||
'\\' => '\\',
|
||||
'/' => '/',
|
||||
'b' => '\b',
|
||||
'f' => '\f',
|
||||
'n' => '\n',
|
||||
'r' => '\r',
|
||||
't' => '\t',
|
||||
_ => throw new FormatException($"Unsupported JSON escape '\\{escaped}'.")
|
||||
});
|
||||
}
|
||||
|
||||
throw new FormatException("Unterminated JSON string.");
|
||||
}
|
||||
|
||||
private object ParseNumber()
|
||||
{
|
||||
var start = _index;
|
||||
if (!IsEnd && _json[_index] == '-')
|
||||
{
|
||||
_index++;
|
||||
}
|
||||
|
||||
while (!IsEnd && char.IsDigit(_json[_index]))
|
||||
{
|
||||
_index++;
|
||||
}
|
||||
|
||||
var isFloatingPoint = false;
|
||||
if (!IsEnd && _json[_index] == '.')
|
||||
{
|
||||
isFloatingPoint = true;
|
||||
_index++;
|
||||
while (!IsEnd && char.IsDigit(_json[_index]))
|
||||
{
|
||||
_index++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!IsEnd && (_json[_index] == 'e' || _json[_index] == 'E'))
|
||||
{
|
||||
isFloatingPoint = true;
|
||||
_index++;
|
||||
if (!IsEnd && (_json[_index] == '+' || _json[_index] == '-'))
|
||||
{
|
||||
_index++;
|
||||
}
|
||||
|
||||
while (!IsEnd && char.IsDigit(_json[_index]))
|
||||
{
|
||||
_index++;
|
||||
}
|
||||
}
|
||||
|
||||
var text = _json.Substring(start, _index - start);
|
||||
if (string.IsNullOrEmpty(text) || text == "-")
|
||||
{
|
||||
throw new FormatException("Invalid JSON number.");
|
||||
}
|
||||
|
||||
return isFloatingPoint
|
||||
? double.Parse(text, CultureInfo.InvariantCulture)
|
||||
: long.Parse(text, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private object ParseLiteral(string literal, object value)
|
||||
{
|
||||
if (_index + literal.Length > _json.Length ||
|
||||
string.CompareOrdinal(_json, _index, literal, 0, literal.Length) != 0)
|
||||
{
|
||||
throw new FormatException($"Invalid JSON literal near index {_index}.");
|
||||
}
|
||||
|
||||
_index += literal.Length;
|
||||
return value;
|
||||
}
|
||||
|
||||
private bool TryConsume(char expected)
|
||||
{
|
||||
if (!IsEnd && _json[_index] == expected)
|
||||
{
|
||||
_index++;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void Expect(char expected)
|
||||
{
|
||||
if (IsEnd || _json[_index] != expected)
|
||||
{
|
||||
throw new FormatException($"Expected '{expected}' near index {_index}.");
|
||||
}
|
||||
|
||||
_index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 988d85a7d7fa65f4786596348f72e053
|
||||
fileFormatVersion: 2
|
||||
guid: 0c9f18f342e54f8d9a4dbdc61b74a2d1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
8
My project/Assets/FlowScope/Runtime/Flow.meta
Normal file
8
My project/Assets/FlowScope/Runtime/Flow.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 87d135bef01743c29768ef28ce8acf69
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
31
My project/Assets/FlowScope/Runtime/Flow/FeatureBase.cs
Normal file
31
My project/Assets/FlowScope/Runtime/Flow/FeatureBase.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FlowScope.Flow
|
||||
{
|
||||
public abstract class FeatureBase : IFeature
|
||||
{
|
||||
protected FeatureContext Context { get; private set; }
|
||||
|
||||
public virtual Task LoadAsync(FeatureContext context)
|
||||
{
|
||||
Context = context;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public virtual Task EnterAsync(FeatureContext context)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public virtual Task ExitAsync(FeatureContext context)
|
||||
{
|
||||
context?.Disposables.Clear();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public virtual void Dispose()
|
||||
{
|
||||
Context = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user