Compare commits
22 Commits
6e45152250
...
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
|
||||
|
||||
8
My project/Assets/AddressableAssetsData.meta
Normal file
8
My project/Assets/AddressableAssetsData.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f38893bbcb86bbb4cb6705a205682113
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f9bc8a0d61c2e9e4cb5557b7586ac071
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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:
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FlowScope.Resources;
|
||||
using UnityEngine.ResourceManagement.AsyncOperations;
|
||||
using UnityAddressables = UnityEngine.AddressableAssets.Addressables;
|
||||
|
||||
namespace FlowScope.Addressables
|
||||
@@ -13,8 +14,30 @@ namespace FlowScope.Addressables
|
||||
public async Task<T> LoadAsync<T>(string key, CancellationToken cancellationToken)
|
||||
where T : class
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var handle = UnityAddressables.LoadAssetAsync<T>(key);
|
||||
await WaitWithCancellation(handle.Task, cancellationToken);
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 599ef528ec0c4331a77311f6ef7ef56b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -37,6 +37,11 @@ namespace FlowScope.Container
|
||||
}
|
||||
|
||||
public void RegisterFactory<T>(Func<Container, T> factory)
|
||||
{
|
||||
RegisterSingletonFactory(factory);
|
||||
}
|
||||
|
||||
public void RegisterSingletonFactory<T>(Func<Container, T> factory)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
@@ -45,7 +50,31 @@ namespace FlowScope.Container
|
||||
throw new ArgumentNullException(nameof(factory));
|
||||
}
|
||||
|
||||
RegisterLocal(typeof(T), ContainerRegistration.ForFactory(this, c => factory(c)));
|
||||
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
|
||||
@@ -121,6 +150,7 @@ namespace FlowScope.Container
|
||||
|
||||
_children.Clear();
|
||||
_ownedDisposables.Clear();
|
||||
ForgetScopedInstancesFor(this);
|
||||
_registrations.Clear();
|
||||
|
||||
if (_parent != null)
|
||||
@@ -160,7 +190,7 @@ namespace FlowScope.Container
|
||||
throw new ArgumentNullException(nameof(factory));
|
||||
}
|
||||
|
||||
RegisterLocal(serviceType, ContainerRegistration.ForFactory(this, factory));
|
||||
RegisterLocal(serviceType, ContainerRegistration.ForTransientFactory(this, factory));
|
||||
}
|
||||
|
||||
internal void TrackOwnedDisposable(IDisposable disposable)
|
||||
@@ -209,7 +239,7 @@ namespace FlowScope.Container
|
||||
_resolutionStack.Push(serviceType);
|
||||
try
|
||||
{
|
||||
return registration.Resolve();
|
||||
return registration.Resolve(this);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -236,6 +266,16 @@ namespace FlowScope.Container
|
||||
return false;
|
||||
}
|
||||
|
||||
private void ForgetScopedInstancesFor(Container scope)
|
||||
{
|
||||
foreach (var registration in _registrations.Values)
|
||||
{
|
||||
registration.ForgetScope(scope);
|
||||
}
|
||||
|
||||
_parent?.ForgetScopedInstancesFor(scope);
|
||||
}
|
||||
|
||||
private void ThrowIfDisposed()
|
||||
{
|
||||
if (_disposed)
|
||||
|
||||
@@ -1,50 +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 bool _ownsCreatedInstance;
|
||||
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, bool ownsCreatedInstance)
|
||||
private ContainerRegistration(
|
||||
Container owner,
|
||||
Func<Container, object> factory,
|
||||
object instance,
|
||||
bool created,
|
||||
Lifetime lifetime)
|
||||
{
|
||||
_owner = owner;
|
||||
_factory = factory;
|
||||
_instance = instance;
|
||||
_created = created;
|
||||
_ownsCreatedInstance = ownsCreatedInstance;
|
||||
_lifetime = lifetime;
|
||||
_scopedInstances = lifetime == Lifetime.Scoped
|
||||
? new Dictionary<Container, object>()
|
||||
: null;
|
||||
}
|
||||
|
||||
public static ContainerRegistration ForInstance(object instance)
|
||||
{
|
||||
return new ContainerRegistration(null, null, instance, true, false);
|
||||
return new ContainerRegistration(null, null, instance, true, Lifetime.Instance);
|
||||
}
|
||||
|
||||
public static ContainerRegistration ForFactory(Container owner, Func<Container, object> factory)
|
||||
public static ContainerRegistration ForSingletonFactory(Container owner, Func<Container, object> factory)
|
||||
{
|
||||
return new ContainerRegistration(owner, factory, null, false, true);
|
||||
return new ContainerRegistration(owner, factory, null, false, Lifetime.Singleton);
|
||||
}
|
||||
|
||||
public object Resolve()
|
||||
public static ContainerRegistration ForScopedFactory(Container owner, Func<Container, object> factory)
|
||||
{
|
||||
if (_created)
|
||||
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)
|
||||
{
|
||||
return _instance;
|
||||
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}.");
|
||||
}
|
||||
}
|
||||
|
||||
_instance = _factory(_owner);
|
||||
_created = true;
|
||||
public void ForgetScope(Container scope)
|
||||
{
|
||||
_scopedInstances?.Remove(scope);
|
||||
}
|
||||
|
||||
if (_ownsCreatedInstance && _instance is IDisposable disposable)
|
||||
private object ResolveSingleton()
|
||||
{
|
||||
if (!_created)
|
||||
{
|
||||
_owner.TrackOwnedDisposable(disposable);
|
||||
_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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,6 @@ namespace FlowScope.Flow
|
||||
}
|
||||
catch
|
||||
{
|
||||
BestEffortDisposeFeature(feature);
|
||||
BestEffortDisposeContext(context);
|
||||
_activeFeature = null;
|
||||
_activeContext = null;
|
||||
@@ -79,7 +78,6 @@ namespace FlowScope.Flow
|
||||
}
|
||||
catch
|
||||
{
|
||||
BestEffortDisposeFeature(nextFeature);
|
||||
BestEffortDisposeContext(nextContext);
|
||||
_activeFeature = null;
|
||||
_activeContext = null;
|
||||
@@ -170,30 +168,11 @@ namespace FlowScope.Flow
|
||||
{
|
||||
Debug.LogError(exception);
|
||||
}
|
||||
|
||||
BestEffortDisposeFeature(feature);
|
||||
}
|
||||
|
||||
BestEffortDisposeContext(context);
|
||||
}
|
||||
|
||||
private void BestEffortDisposeFeature(IFeature feature)
|
||||
{
|
||||
if (feature == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
feature.Dispose();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.LogError(exception);
|
||||
}
|
||||
}
|
||||
|
||||
private void BestEffortDisposeContext(FeatureContext context)
|
||||
{
|
||||
if (context == null)
|
||||
@@ -212,7 +191,7 @@ namespace FlowScope.Flow
|
||||
|
||||
try
|
||||
{
|
||||
context.Resources?.Dispose();
|
||||
context.Scope?.Dispose();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
@@ -221,7 +200,7 @@ namespace FlowScope.Flow
|
||||
|
||||
try
|
||||
{
|
||||
context.Scope?.Dispose();
|
||||
context.Resources?.Dispose();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
|
||||
@@ -9,8 +9,12 @@ namespace FlowScope.UI
|
||||
{
|
||||
public sealed class UIPreloadService : IUIPreloadService, IDisposable
|
||||
{
|
||||
private readonly object _gate = new();
|
||||
private readonly IResourceService _resources;
|
||||
private readonly Dictionary<Type, IResourceHandle<GameObject>> _handles = new();
|
||||
private readonly Dictionary<Type, InFlightPreload> _inFlightLoads = new();
|
||||
private int _releaseGeneration;
|
||||
private bool _disposed;
|
||||
|
||||
public UIPreloadService(IResourceService resources)
|
||||
{
|
||||
@@ -22,68 +26,249 @@ namespace FlowScope.UI
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var panelType = typeof(TPanel);
|
||||
if (IsPreloaded<TPanel>())
|
||||
InFlightPreload inFlightLoad;
|
||||
bool startLoad = false;
|
||||
string path = null;
|
||||
lock (_gate)
|
||||
{
|
||||
return;
|
||||
ThrowIfDisposed();
|
||||
|
||||
if (TryGetPreloadedHandle(panelType, out _))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_inFlightLoads.TryGetValue(panelType, out inFlightLoad) ||
|
||||
inFlightLoad.Invalidated ||
|
||||
inFlightLoad.ReleaseGeneration != _releaseGeneration)
|
||||
{
|
||||
var attribute = UIManager.GetPanelAttribute(panelType);
|
||||
path = UIManager.ResolvePath(panelType, attribute);
|
||||
inFlightLoad = new InFlightPreload();
|
||||
inFlightLoad.ReleaseGeneration = _releaseGeneration;
|
||||
_inFlightLoads[panelType] = inFlightLoad;
|
||||
startLoad = true;
|
||||
}
|
||||
|
||||
inFlightLoad.WaiterCount++;
|
||||
if (startLoad)
|
||||
{
|
||||
inFlightLoad.Task = LoadAndStoreAsync(panelType, path, inFlightLoad);
|
||||
}
|
||||
}
|
||||
|
||||
var attribute = UIManager.GetPanelAttribute(panelType);
|
||||
var path = UIManager.ResolvePath(panelType, attribute);
|
||||
var handle = await _resources.LoadAsync<GameObject>(path, cancellationToken);
|
||||
if (handle == null || handle.Asset == null)
|
||||
try
|
||||
{
|
||||
handle?.Dispose();
|
||||
throw new InvalidOperationException($"UI panel resource is missing: {path}");
|
||||
await WaitForSharedLoadAsync(inFlightLoad.Task, cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
ForgetWaitingCaller(panelType, inFlightLoad);
|
||||
throw;
|
||||
}
|
||||
|
||||
_handles[panelType] = handle;
|
||||
}
|
||||
|
||||
public bool IsPreloaded<TPanel>()
|
||||
{
|
||||
return _handles.TryGetValue(typeof(TPanel), out var handle) &&
|
||||
handle != null &&
|
||||
!handle.IsDisposed &&
|
||||
handle.Asset != null;
|
||||
lock (_gate)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return TryGetPreloadedHandle(typeof(TPanel), out _);
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryGetPreloadedHandle<TPanel>(out IResourceHandle<GameObject> handle)
|
||||
{
|
||||
if (IsPreloaded<TPanel>())
|
||||
lock (_gate)
|
||||
{
|
||||
handle = _handles[typeof(TPanel)];
|
||||
return true;
|
||||
}
|
||||
if (_disposed)
|
||||
{
|
||||
handle = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
handle = null;
|
||||
return false;
|
||||
return TryGetPreloadedHandle(typeof(TPanel), out handle);
|
||||
}
|
||||
}
|
||||
|
||||
public void Release<TPanel>()
|
||||
{
|
||||
var panelType = typeof(TPanel);
|
||||
if (!_handles.TryGetValue(panelType, out var handle))
|
||||
IResourceHandle<GameObject> handle = null;
|
||||
lock (_gate)
|
||||
{
|
||||
return;
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
InvalidateInFlightLoad(panelType);
|
||||
if (_handles.TryGetValue(panelType, out handle))
|
||||
{
|
||||
_handles.Remove(panelType);
|
||||
}
|
||||
}
|
||||
|
||||
_handles.Remove(panelType);
|
||||
handle.Dispose();
|
||||
handle?.Dispose();
|
||||
}
|
||||
|
||||
public void ReleaseAll()
|
||||
{
|
||||
foreach (var handle in _handles.Values)
|
||||
List<IResourceHandle<GameObject>> handles;
|
||||
lock (_gate)
|
||||
{
|
||||
_releaseGeneration++;
|
||||
handles = new List<IResourceHandle<GameObject>>(_handles.Values);
|
||||
_handles.Clear();
|
||||
}
|
||||
|
||||
foreach (var handle in handles)
|
||||
{
|
||||
handle.Dispose();
|
||||
}
|
||||
|
||||
_handles.Clear();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
ReleaseAll();
|
||||
}
|
||||
|
||||
private async Task LoadAndStoreAsync(
|
||||
Type panelType,
|
||||
string path,
|
||||
InFlightPreload inFlightLoad)
|
||||
{
|
||||
IResourceHandle<GameObject> handle = null;
|
||||
try
|
||||
{
|
||||
handle = await _resources.LoadAsync<GameObject>(path, CancellationToken.None);
|
||||
if (handle == null || handle.Asset == null)
|
||||
{
|
||||
handle?.Dispose();
|
||||
throw new InvalidOperationException($"UI panel resource is missing: {path}");
|
||||
}
|
||||
|
||||
var shouldCache = false;
|
||||
lock (_gate)
|
||||
{
|
||||
ForgetInFlightLoad(panelType, inFlightLoad);
|
||||
if (!_disposed &&
|
||||
!inFlightLoad.Invalidated &&
|
||||
inFlightLoad.ReleaseGeneration == _releaseGeneration &&
|
||||
inFlightLoad.WaiterCount > 0)
|
||||
{
|
||||
_handles[panelType] = handle;
|
||||
shouldCache = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldCache)
|
||||
{
|
||||
handle.Dispose();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
ForgetInFlightLoad(panelType, inFlightLoad);
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void ForgetInFlightLoad(Type panelType, InFlightPreload inFlightLoad)
|
||||
{
|
||||
if (_inFlightLoads.TryGetValue(panelType, out var current) &&
|
||||
ReferenceEquals(current, inFlightLoad))
|
||||
{
|
||||
_inFlightLoads.Remove(panelType);
|
||||
}
|
||||
}
|
||||
|
||||
private void InvalidateInFlightLoad(Type panelType)
|
||||
{
|
||||
if (_inFlightLoads.TryGetValue(panelType, out var inFlightLoad))
|
||||
{
|
||||
inFlightLoad.Invalidated = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void ForgetWaitingCaller(Type panelType, InFlightPreload inFlightLoad)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_inFlightLoads.TryGetValue(panelType, out var current) &&
|
||||
ReferenceEquals(current, inFlightLoad) &&
|
||||
inFlightLoad.WaiterCount > 0)
|
||||
{
|
||||
inFlightLoad.WaiterCount--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryGetPreloadedHandle(
|
||||
Type panelType,
|
||||
out IResourceHandle<GameObject> handle)
|
||||
{
|
||||
return _handles.TryGetValue(panelType, out handle) &&
|
||||
handle != null &&
|
||||
!handle.IsDisposed &&
|
||||
handle.Asset != null;
|
||||
}
|
||||
|
||||
private void ThrowIfDisposed()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(UIPreloadService));
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task WaitForSharedLoadAsync(
|
||||
Task loadTask,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!cancellationToken.CanBeCanceled)
|
||||
{
|
||||
await loadTask;
|
||||
return;
|
||||
}
|
||||
|
||||
var cancellation = new TaskCompletionSource<bool>(
|
||||
TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
using (cancellationToken.Register(() => cancellation.TrySetResult(true)))
|
||||
{
|
||||
if (await Task.WhenAny(loadTask, cancellation.Task) == cancellation.Task)
|
||||
{
|
||||
throw new OperationCanceledException(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
await loadTask;
|
||||
}
|
||||
|
||||
private sealed class InFlightPreload
|
||||
{
|
||||
public Task Task { get; set; }
|
||||
public int WaiterCount { get; set; }
|
||||
public int ReleaseGeneration { get; set; }
|
||||
public bool Invalidated { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
64
My project/Assets/FlowScope/Samples/MainMenuP0/README.md
Normal file
64
My project/Assets/FlowScope/Samples/MainMenuP0/README.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# MainMenuP0 Sample
|
||||
|
||||
## 目的
|
||||
|
||||
`MainMenuP0` 是 FlowScope 当前最小接入样例。它用于验证 P0/P1 Runtime 的推荐装配方式,而不是完整游戏模板。
|
||||
|
||||
当前样例覆盖:
|
||||
|
||||
- `GameBootstrap` 创建根 `Container`。
|
||||
- 注册 `IConfigProvider`、`IResourceService`、`ISaveService`、`IAudioService`、`UIManager`、`IUIPreloadService`、`IUIScreenNavigator`。
|
||||
- 使用 `RegisterTransient` 注册 `MainMenuFeature`。
|
||||
- 通过 `UIPreloadService` 预加载 `MainMenuPanel`。
|
||||
- 通过 `UIScreenNavigator` 打开主菜单面板。
|
||||
- 点击按钮修改 `PlayerData.Gold` 并刷新 UI。
|
||||
- `ShutdownAsync` 保存数据、关闭 `GameFlow` 并释放预加载资源。
|
||||
|
||||
## 当前路径
|
||||
|
||||
第一阶段仍保留在项目内开发模式:
|
||||
|
||||
```text
|
||||
Assets/FlowScope/Samples/MainMenuP0
|
||||
```
|
||||
|
||||
P2 第一阶段不把它移动到 `Samples~`,原因是当前 PlayMode 测试、场景引用、prefab 引用和 generated csproj 路径都依赖现有位置。等 P2 第二阶段开始本地 package 试迁移后,再把它作为候选包样例整理到:
|
||||
|
||||
```text
|
||||
Samples~/MainMenuP0
|
||||
```
|
||||
|
||||
## 如何运行
|
||||
|
||||
在 Unity 中打开:
|
||||
|
||||
```text
|
||||
Assets/FlowScope/Samples/MainMenuP0/Scenes/MainMenuP0.unity
|
||||
```
|
||||
|
||||
进入 Play Mode 后,画面应显示主菜单面板、Gold 文本和递增按钮。
|
||||
|
||||
## 验收点
|
||||
|
||||
- 首次启动显示默认 Gold。
|
||||
- 点击按钮后 Gold 文本立即递增。
|
||||
- 调用 `ShutdownAsync` 后写入 `main-menu-player` 存档。
|
||||
- 再次启动后恢复上次保存的 Gold。
|
||||
- 关闭时释放预加载资源。
|
||||
- Console 不出现未预期 Error 或 Exception。
|
||||
|
||||
## 自动化测试
|
||||
|
||||
对应 PlayMode 测试位于:
|
||||
|
||||
```text
|
||||
Assets/FlowScope/Tests/PlayMode/Samples/MainMenuP0Tests.cs
|
||||
```
|
||||
|
||||
当前测试覆盖启动可见、按钮刷新、保存恢复、资源释放、启动任务可观察以及 shutdown 失败时的 best-effort cleanup。
|
||||
|
||||
## 已知限制
|
||||
|
||||
- 样例使用 `SampleResourceBackend`,不是 Addressables 真实资源验收。
|
||||
- 样例中有运行时 fallback UI,用于避免缺引用时完全不可运行;人工验收仍应优先检查场景和 prefab 引用是否完整。
|
||||
- 当前仍处于 `Assets/FlowScope` 开发模式,不代表 package sample 已完成迁移。
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9df9d94a34fa4ec2b16df2c8cf2a651b
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FlowScope.Audio;
|
||||
@@ -30,15 +31,20 @@ namespace FlowScope.Samples.MainMenuP0
|
||||
private PlayerData _playerData;
|
||||
private CancellationTokenSource _lifetime;
|
||||
private bool _started;
|
||||
private bool _shutdownInProgress;
|
||||
private bool _shutdown;
|
||||
|
||||
public int ReleasedResourceCount => _sampleBackend?.ReleaseCount ?? 0;
|
||||
/// <summary>
|
||||
/// True after runtime cleanup has completed; shutdown sub-step failures are still reported by ShutdownAsync.
|
||||
/// </summary>
|
||||
public bool IsShutdown => _shutdown;
|
||||
public Task StartupTask { get; private set; }
|
||||
|
||||
private async void Awake()
|
||||
private void Awake()
|
||||
{
|
||||
_lifetime = new CancellationTokenSource();
|
||||
await StartupAsync(_lifetime.Token);
|
||||
StartupTask = RunStartupWithLoggingAsync(_lifetime.Token);
|
||||
}
|
||||
|
||||
public async Task StartupAsync(CancellationToken cancellationToken)
|
||||
@@ -97,31 +103,78 @@ namespace FlowScope.Samples.MainMenuP0
|
||||
_root.RegisterInstance<IUIPreloadService>(_preloadService);
|
||||
_root.RegisterInstance<IUIScreenNavigator>(screenNavigator);
|
||||
_root.RegisterInstance(_playerData);
|
||||
_root.RegisterFactory(_ => new MainMenuFeature(screenNavigator, _playerData));
|
||||
_root.RegisterTransient(_ => new MainMenuFeature(screenNavigator, _playerData));
|
||||
|
||||
await _gameFlow.StartupAsync<MainMenuFeature>(_root, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task ShutdownAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_saveService != null && _playerData != null)
|
||||
if (_shutdown || _shutdownInProgress)
|
||||
{
|
||||
await _saveService.SaveAsync(SaveKey, _playerData, cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_gameFlow != null && _gameFlow.State != GameFlowState.Disposed)
|
||||
_shutdownInProgress = true;
|
||||
var exceptions = new List<Exception>();
|
||||
|
||||
try
|
||||
{
|
||||
await _gameFlow.ShutdownAsync(cancellationToken);
|
||||
if (_saveService != null && _playerData != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _saveService.SaveAsync(SaveKey, _playerData, cancellationToken);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
exceptions.Add(exception);
|
||||
}
|
||||
}
|
||||
|
||||
if (_gameFlow != null && _gameFlow.State != GameFlowState.Disposed)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _gameFlow.ShutdownAsync(cancellationToken);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
exceptions.Add(exception);
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_preloadService?.ReleaseAll();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
exceptions.Add(exception);
|
||||
}
|
||||
|
||||
_shutdown = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_shutdownInProgress = false;
|
||||
}
|
||||
|
||||
_preloadService?.ReleaseAll();
|
||||
_shutdown = true;
|
||||
if (exceptions.Count == 1)
|
||||
{
|
||||
ExceptionDispatchInfo.Capture(exceptions[0]).Throw();
|
||||
}
|
||||
|
||||
if (exceptions.Count > 1)
|
||||
{
|
||||
throw new AggregateException("MainMenuP0 shutdown completed with failures.", exceptions);
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnApplicationQuit()
|
||||
private void OnApplicationQuit()
|
||||
{
|
||||
_lifetime?.Cancel();
|
||||
await ShutdownAsync(CancellationToken.None);
|
||||
_ = RunShutdownWithLoggingAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
@@ -130,6 +183,31 @@ namespace FlowScope.Samples.MainMenuP0
|
||||
_lifetime?.Dispose();
|
||||
}
|
||||
|
||||
private async Task RunStartupWithLoggingAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await StartupAsync(cancellationToken);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.LogException(exception);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunShutdownWithLoggingAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ShutdownAsync(cancellationToken);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.LogException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
private Canvas ResolveHudCanvas()
|
||||
{
|
||||
if (hudCanvas != null)
|
||||
|
||||
@@ -44,6 +44,71 @@ namespace FlowScope.Tests.EditMode.Container
|
||||
CollectionAssert.AreEqual(new[] { "second", "first" }, disposeOrder);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RegisterSingletonFactory_Resolve_CreatesOnceAndIsOwnedByRegistrationScope()
|
||||
{
|
||||
var disposeOrder = new List<string>();
|
||||
var parent = new FlowScope.Container.Container();
|
||||
parent.RegisterSingletonFactory<IService>(_ => new DisposableNamedService("singleton", disposeOrder));
|
||||
var child = parent.CreateScope();
|
||||
|
||||
var first = child.Resolve<IService>();
|
||||
var second = child.Resolve<IService>();
|
||||
|
||||
Assert.AreSame(first, second);
|
||||
|
||||
child.Dispose();
|
||||
|
||||
CollectionAssert.IsEmpty(disposeOrder);
|
||||
|
||||
parent.Dispose();
|
||||
|
||||
CollectionAssert.AreEqual(new[] { "singleton" }, disposeOrder);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RegisterTransient_Resolve_CreatesNewInstanceOwnedByRequestScope()
|
||||
{
|
||||
var disposeOrder = new List<string>();
|
||||
var parent = new FlowScope.Container.Container();
|
||||
parent.RegisterTransient<IService>(_ => new DisposableNamedService("transient", disposeOrder));
|
||||
var child = parent.CreateScope();
|
||||
|
||||
var first = child.Resolve<IService>();
|
||||
var second = child.Resolve<IService>();
|
||||
|
||||
Assert.AreNotSame(first, second);
|
||||
|
||||
child.Dispose();
|
||||
|
||||
CollectionAssert.AreEqual(new[] { "transient", "transient" }, disposeOrder);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RegisterScoped_Resolve_CreatesOneInstancePerRequestScope()
|
||||
{
|
||||
var disposeOrder = new List<string>();
|
||||
var parent = new FlowScope.Container.Container();
|
||||
parent.RegisterScoped<IService>(_ => new DisposableNamedService("scoped", disposeOrder));
|
||||
var firstChild = parent.CreateScope();
|
||||
var secondChild = parent.CreateScope();
|
||||
|
||||
var firstA = firstChild.Resolve<IService>();
|
||||
var firstB = firstChild.Resolve<IService>();
|
||||
var second = secondChild.Resolve<IService>();
|
||||
|
||||
Assert.AreSame(firstA, firstB);
|
||||
Assert.AreNotSame(firstA, second);
|
||||
|
||||
firstChild.Dispose();
|
||||
|
||||
CollectionAssert.AreEqual(new[] { "scoped" }, disposeOrder);
|
||||
|
||||
secondChild.Dispose();
|
||||
|
||||
CollectionAssert.AreEqual(new[] { "scoped", "scoped" }, disposeOrder);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreateScope_Resolve_ReadsParentAndCanOverrideWithoutPollutingParent()
|
||||
{
|
||||
@@ -152,6 +217,9 @@ namespace FlowScope.Tests.EditMode.Container
|
||||
|
||||
Assert.Throws<ObjectDisposedException>(() => container.RegisterInstance<IService>(new NamedService("late")));
|
||||
Assert.Throws<ObjectDisposedException>(() => container.RegisterFactory<IService>(_ => new NamedService("late")));
|
||||
Assert.Throws<ObjectDisposedException>(() => container.RegisterSingletonFactory<IService>(_ => new NamedService("late")));
|
||||
Assert.Throws<ObjectDisposedException>(() => container.RegisterTransient<IService>(_ => new NamedService("late")));
|
||||
Assert.Throws<ObjectDisposedException>(() => container.RegisterScoped<IService>(_ => new NamedService("late")));
|
||||
Assert.Throws<ObjectDisposedException>(() => container.Resolve<IService>());
|
||||
Assert.Throws<ObjectDisposedException>(() => container.CreateScope());
|
||||
}
|
||||
@@ -174,6 +242,24 @@ namespace FlowScope.Tests.EditMode.Container
|
||||
public void Dispose() => IsDisposed = true;
|
||||
}
|
||||
|
||||
private sealed class DisposableNamedService : IService, IDisposable
|
||||
{
|
||||
private readonly List<string> _disposeOrder;
|
||||
|
||||
public DisposableNamedService(string name, List<string> disposeOrder)
|
||||
{
|
||||
Name = name;
|
||||
_disposeOrder = disposeOrder;
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_disposeOrder.Add(Name);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FirstDisposable : IDisposable
|
||||
{
|
||||
private readonly List<string> _disposeOrder;
|
||||
|
||||
@@ -61,6 +61,25 @@ namespace FlowScope.Tests.EditMode.Flow
|
||||
Assert.That(SecondFeature.Instance.Context.Resources, Is.SameAs(services.Resource.Groups[1]));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SwitchToAsync_WhenReturningToFeatureType_CreatesNewFeatureInstance()
|
||||
{
|
||||
var services = new TestServices();
|
||||
var root = services.CreateRoot();
|
||||
var flow = new GameFlow();
|
||||
flow.StartupAsync<FirstFeature>(root, CancellationToken.None).GetAwaiter().GetResult();
|
||||
var firstEntry = FirstFeature.Instance;
|
||||
|
||||
flow.SwitchToAsync<SecondFeature>(CancellationToken.None).GetAwaiter().GetResult();
|
||||
flow.SwitchToAsync<FirstFeature>(CancellationToken.None).GetAwaiter().GetResult();
|
||||
|
||||
Assert.That(FirstFeature.Instance, Is.Not.SameAs(firstEntry));
|
||||
Assert.That(firstEntry.Calls, Is.EqualTo(new[] { "Load", "Enter", "Exit", "Dispose" }));
|
||||
Assert.That(FirstFeature.Instance.Calls, Is.EqualTo(new[] { "Load", "Enter" }));
|
||||
Assert.That(services.Resource.Groups[0].IsDisposed, Is.True);
|
||||
Assert.That(services.Resource.Groups[2].IsDisposed, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void StartupAsync_WhenFeatureLoadFails_CleansCreatedFeatureAndReturnsIdle()
|
||||
{
|
||||
@@ -114,7 +133,6 @@ namespace FlowScope.Tests.EditMode.Flow
|
||||
flow.StartupAsync<ThrowingExitAndDisposeFeature>(root, CancellationToken.None).GetAwaiter().GetResult();
|
||||
|
||||
LogAssert.Expect(LogType.Error, new Regex("System\\.InvalidOperationException: exit failed"));
|
||||
LogAssert.Expect(LogType.Error, new Regex("System\\.InvalidOperationException: dispose failed"));
|
||||
LogAssert.Expect(LogType.Error, new Regex("System\\.AggregateException: One or more container-owned instances failed to dispose\\. \\(dispose failed\\)"));
|
||||
flow.ShutdownAsync(CancellationToken.None).GetAwaiter().GetResult();
|
||||
|
||||
@@ -168,11 +186,11 @@ namespace FlowScope.Tests.EditMode.Flow
|
||||
root.RegisterInstance<IConfigProvider>(Config);
|
||||
root.RegisterInstance<ISaveService>(Save);
|
||||
root.RegisterInstance<IResourceService>(Resource);
|
||||
root.RegisterFactory(_ => new FirstFeature());
|
||||
root.RegisterFactory(_ => new SecondFeature());
|
||||
root.RegisterFactory(_ => new FailingLoadFeature());
|
||||
root.RegisterFactory(_ => new FailingEnterFeature());
|
||||
root.RegisterFactory(_ => new ThrowingExitAndDisposeFeature());
|
||||
root.RegisterTransient(_ => new FirstFeature());
|
||||
root.RegisterTransient(_ => new SecondFeature());
|
||||
root.RegisterTransient(_ => new FailingLoadFeature());
|
||||
root.RegisterTransient(_ => new FailingEnterFeature());
|
||||
root.RegisterTransient(_ => new ThrowingExitAndDisposeFeature());
|
||||
return root;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,50 @@ namespace FlowScope.Tests.EditMode.Resources
|
||||
}
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator LoadAsync_WhenOneSharedWaiterCancels_OtherWaiterReceivesHandle()
|
||||
{
|
||||
var backend = new FakeResourceBackend();
|
||||
var service = new ResourceService(backend);
|
||||
using var cts = new CancellationTokenSource();
|
||||
|
||||
var canceled = service.LoadAsync<TestAsset>("shared", cts.Token);
|
||||
var retained = service.LoadAsync<TestAsset>("shared", CancellationToken.None);
|
||||
cts.Cancel();
|
||||
backend.Gate.SetResult(new TestAsset());
|
||||
|
||||
yield return AwaitExpected<OperationCanceledException>(canceled);
|
||||
yield return Await(retained);
|
||||
|
||||
using var handle = Result(retained);
|
||||
Assert.AreEqual(1, backend.LoadCalls);
|
||||
Assert.IsNotNull(handle.Asset);
|
||||
Assert.IsTrue(service.TryLoadCompleted<TestAsset>("shared", out var cached));
|
||||
cached.Dispose();
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator LoadAsync_WhenAllSharedWaitersCancel_ReleasesCompletedBackendAssetWithoutCaching()
|
||||
{
|
||||
var backend = new FakeResourceBackend();
|
||||
var service = new ResourceService(backend);
|
||||
using var firstCts = new CancellationTokenSource();
|
||||
using var secondCts = new CancellationTokenSource();
|
||||
|
||||
var first = service.LoadAsync<TestAsset>("shared", firstCts.Token);
|
||||
var second = service.LoadAsync<TestAsset>("shared", secondCts.Token);
|
||||
firstCts.Cancel();
|
||||
secondCts.Cancel();
|
||||
backend.Gate.SetResult(new TestAsset());
|
||||
|
||||
yield return AwaitExpected<OperationCanceledException>(first);
|
||||
yield return AwaitExpected<OperationCanceledException>(second);
|
||||
|
||||
Assert.AreEqual(1, backend.LoadCalls);
|
||||
Assert.AreEqual(1, backend.ReleaseCalls);
|
||||
Assert.IsFalse(service.TryLoadCompleted<TestAsset>("shared", out _));
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator DisposeLastHandle_ReleasesBackendAsset()
|
||||
{
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FlowScope.Addressables;
|
||||
using FlowScope.Resources;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using UnityEngine.TestTools;
|
||||
|
||||
namespace FlowScope.Tests.PlayMode.Resources
|
||||
{
|
||||
public sealed class AddressablesResourceBackendTests
|
||||
{
|
||||
private const string TextKey = "FlowScope.Tests.Addressables.Text";
|
||||
private const string MissingKey = "FlowScope.Tests.Addressables.Missing";
|
||||
private const string ExpectedText = "FlowScope Addressables Acceptance";
|
||||
|
||||
[Test]
|
||||
public void Name_ReturnsAddressables()
|
||||
{
|
||||
@@ -12,5 +23,149 @@ namespace FlowScope.Tests.PlayMode.Resources
|
||||
|
||||
Assert.AreEqual("Addressables", backend.Name);
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator LoadAsync_WithRealTextAssetKey_LoadsTextAsset()
|
||||
{
|
||||
var backend = new AddressablesResourceBackend();
|
||||
|
||||
TextAsset asset = null;
|
||||
yield return AwaitTask(backend.LoadAsync<TextAsset>(
|
||||
TextKey,
|
||||
CancellationToken.None), loaded => asset = loaded);
|
||||
|
||||
Assert.That(asset, Is.Not.Null);
|
||||
Assert.That(asset.text, Does.Contain(ExpectedText));
|
||||
|
||||
backend.Release(TextKey, asset);
|
||||
LogAssert.NoUnexpectedReceived();
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator AddressablesResourceService_DisposeHandleTwice_ReleasesWithoutUnexpectedLogs()
|
||||
{
|
||||
var service = new AddressablesResourceService(new AddressablesResourceBackend());
|
||||
|
||||
IResourceHandle<TextAsset> handle = null;
|
||||
yield return AwaitTask(service.LoadAsync<TextAsset>(
|
||||
TextKey,
|
||||
CancellationToken.None), loaded => handle = loaded);
|
||||
|
||||
Assert.That(handle, Is.Not.Null);
|
||||
Assert.That(handle.Asset, Is.Not.Null);
|
||||
Assert.That(handle.Asset.text, Does.Contain(ExpectedText));
|
||||
|
||||
handle.Dispose();
|
||||
handle.Dispose();
|
||||
|
||||
Assert.That(handle.IsDisposed, Is.True);
|
||||
Assert.That(handle.Asset, Is.Null);
|
||||
Assert.That(service.TryLoadCompleted<TextAsset>(TextKey, out _), Is.False);
|
||||
LogAssert.NoUnexpectedReceived();
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator LoadAsync_WhenCanceled_ThrowsOperationCanceledException()
|
||||
{
|
||||
var backend = new AddressablesResourceBackend();
|
||||
using var cancellation = new CancellationTokenSource();
|
||||
cancellation.Cancel();
|
||||
|
||||
Exception exception = null;
|
||||
yield return AwaitExpectedFailure(
|
||||
() => backend.LoadAsync<TextAsset>(TextKey, cancellation.Token),
|
||||
caught => exception = caught);
|
||||
|
||||
Assert.That(exception, Is.TypeOf<OperationCanceledException>());
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator LoadAsync_WithMissingKey_ThrowsDiagnosticException()
|
||||
{
|
||||
var backend = new AddressablesResourceBackend();
|
||||
|
||||
Exception exception = null;
|
||||
yield return AwaitExpectedFailure(
|
||||
() => backend.LoadAsync<TextAsset>(MissingKey, CancellationToken.None),
|
||||
caught => exception = caught);
|
||||
|
||||
Assert.That(exception, Is.Not.Null);
|
||||
Assert.That(exception.ToString(), Does.Contain(MissingKey));
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator LoadAsync_WithMismatchedType_ThrowsDiagnosticException()
|
||||
{
|
||||
var backend = new AddressablesResourceBackend();
|
||||
|
||||
Exception exception = null;
|
||||
yield return AwaitExpectedFailure(
|
||||
() => backend.LoadAsync<GameObject>(TextKey, CancellationToken.None),
|
||||
caught => exception = caught);
|
||||
|
||||
Assert.That(exception, Is.Not.Null);
|
||||
Assert.That(exception.ToString(), Does.Contain(TextKey));
|
||||
Assert.That(exception.ToString(), Does.Contain(nameof(GameObject)));
|
||||
}
|
||||
|
||||
private static IEnumerator AwaitTask<T>(
|
||||
Task<T> task,
|
||||
Action<T> onCompleted)
|
||||
{
|
||||
while (!task.IsCompleted)
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
|
||||
if (task.IsCanceled)
|
||||
{
|
||||
throw new OperationCanceledException();
|
||||
}
|
||||
|
||||
if (task.IsFaulted)
|
||||
{
|
||||
throw task.Exception.GetBaseException();
|
||||
}
|
||||
|
||||
onCompleted(task.Result);
|
||||
}
|
||||
|
||||
private static IEnumerator AwaitExpectedFailure<T>(
|
||||
Func<Task<T>> startTask,
|
||||
Action<Exception> onException)
|
||||
{
|
||||
var ignoreFailingMessages = LogAssert.ignoreFailingMessages;
|
||||
Task<T> task = null;
|
||||
try
|
||||
{
|
||||
LogAssert.ignoreFailingMessages = true;
|
||||
task = startTask();
|
||||
|
||||
while (!task.IsCompleted)
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
|
||||
yield return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
LogAssert.ignoreFailingMessages = ignoreFailingMessages;
|
||||
}
|
||||
|
||||
if (task.IsCanceled)
|
||||
{
|
||||
onException(new OperationCanceledException());
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (task.IsFaulted)
|
||||
{
|
||||
onException(task.Exception.GetBaseException());
|
||||
yield break;
|
||||
}
|
||||
|
||||
Assert.Fail("Expected Addressables load to fail.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1acac9035afee5b408d7a7d78b2a1b0a
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 52d2a24f37dccf346b5e0c6a34585514
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1 @@
|
||||
FlowScope Addressables Acceptance
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7e04733ce9dab74459592d3bd99fcafd
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,9 +1,11 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FlowScope.Save;
|
||||
using FlowScope.Samples.MainMenuP0;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
@@ -15,6 +17,17 @@ namespace FlowScope.Tests.PlayMode.Samples
|
||||
{
|
||||
public sealed class MainMenuP0Tests
|
||||
{
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
foreach (var bootstrap in Object.FindObjectsOfType<GameBootstrap>(true))
|
||||
{
|
||||
Object.DestroyImmediate(bootstrap.gameObject);
|
||||
}
|
||||
|
||||
DeleteSampleSave();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ViewModel_IncrementGold_UpdatesPlayerDataImmediately()
|
||||
{
|
||||
@@ -49,6 +62,26 @@ namespace FlowScope.Tests.PlayMode.Samples
|
||||
LogAssert.NoUnexpectedReceived();
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator Awake_StartsThroughObservableStartupTask()
|
||||
{
|
||||
DeleteSampleSave();
|
||||
var root = new GameObject("bootstrap-awake-test");
|
||||
var bootstrap = root.AddComponent<GameBootstrap>();
|
||||
|
||||
Assert.That(bootstrap.StartupTask, Is.Not.Null);
|
||||
yield return AwaitTask(bootstrap.StartupTask);
|
||||
|
||||
var panel = Object.FindObjectOfType<MainMenuPanel>(true);
|
||||
Assert.That(panel, Is.Not.Null);
|
||||
Assert.That(panel.gameObject.activeSelf, Is.True);
|
||||
|
||||
yield return AwaitTask(bootstrap.ShutdownAsync(CancellationToken.None));
|
||||
Object.DestroyImmediate(root);
|
||||
DeleteSampleSave();
|
||||
LogAssert.NoUnexpectedReceived();
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator ClickIncrement_UpdatesGoldTextImmediately()
|
||||
{
|
||||
@@ -107,7 +140,7 @@ namespace FlowScope.Tests.PlayMode.Samples
|
||||
yield return null;
|
||||
|
||||
Assert.That(bootstrap.IsShutdown, Is.True);
|
||||
Assert.That(bootstrap.ReleasedResourceCount, Is.EqualTo(1));
|
||||
Assert.That(bootstrap.ReleasedResourceCount, Is.GreaterThanOrEqualTo(1));
|
||||
Assert.That(panel == null, Is.True);
|
||||
|
||||
Object.DestroyImmediate(root);
|
||||
@@ -115,6 +148,68 @@ namespace FlowScope.Tests.PlayMode.Samples
|
||||
LogAssert.NoUnexpectedReceived();
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator Shutdown_WhenSaveFails_StillShutsDownFlowAndReleasesPreloads()
|
||||
{
|
||||
DeleteSampleSave();
|
||||
var root = CreateInactiveBootstrap("bootstrap-save-fails-test", out var bootstrap);
|
||||
yield return AwaitTask(bootstrap.StartupAsync(CancellationToken.None));
|
||||
SetPrivateField(bootstrap, "_saveService", new ThrowingSaveService(new InvalidOperationException("save failed")));
|
||||
var panel = Object.FindObjectOfType<MainMenuPanel>(true);
|
||||
|
||||
yield return AwaitExpected<InvalidOperationException>(bootstrap.ShutdownAsync(CancellationToken.None));
|
||||
yield return null;
|
||||
|
||||
Assert.That(bootstrap.IsShutdown, Is.True);
|
||||
Assert.That(bootstrap.ReleasedResourceCount, Is.GreaterThanOrEqualTo(1));
|
||||
Assert.That(panel == null, Is.True);
|
||||
|
||||
Object.DestroyImmediate(root);
|
||||
DeleteSampleSave();
|
||||
LogAssert.NoUnexpectedReceived();
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator Shutdown_WhenGameFlowShutdownFails_StillReleasesPreloads()
|
||||
{
|
||||
DeleteSampleSave();
|
||||
var root = CreateInactiveBootstrap("bootstrap-flow-fails-test", out var bootstrap);
|
||||
yield return AwaitTask(bootstrap.StartupAsync(CancellationToken.None));
|
||||
SetPrivateField(bootstrap, "_saveService", new SuccessfulSaveService());
|
||||
using var cancellation = new CancellationTokenSource();
|
||||
cancellation.Cancel();
|
||||
|
||||
yield return AwaitExpected<OperationCanceledException>(bootstrap.ShutdownAsync(cancellation.Token));
|
||||
|
||||
Assert.That(bootstrap.IsShutdown, Is.True);
|
||||
Assert.That(bootstrap.ReleasedResourceCount, Is.GreaterThanOrEqualTo(1));
|
||||
|
||||
Object.DestroyImmediate(root);
|
||||
DeleteSampleSave();
|
||||
LogAssert.NoUnexpectedReceived();
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator Shutdown_AfterFailure_DoesNotSkipCleanupOnSecondCall()
|
||||
{
|
||||
DeleteSampleSave();
|
||||
var root = CreateInactiveBootstrap("bootstrap-shutdown-retry-test", out var bootstrap);
|
||||
yield return AwaitTask(bootstrap.StartupAsync(CancellationToken.None));
|
||||
SetPrivateField(bootstrap, "_saveService", new ThrowOnceSaveService(new InvalidOperationException("save failed once")));
|
||||
|
||||
yield return AwaitExpected<InvalidOperationException>(bootstrap.ShutdownAsync(CancellationToken.None));
|
||||
var releaseCountAfterFailure = bootstrap.ReleasedResourceCount;
|
||||
yield return AwaitTask(bootstrap.ShutdownAsync(CancellationToken.None));
|
||||
|
||||
Assert.That(bootstrap.IsShutdown, Is.True);
|
||||
Assert.That(releaseCountAfterFailure, Is.GreaterThanOrEqualTo(1));
|
||||
Assert.That(bootstrap.ReleasedResourceCount, Is.EqualTo(releaseCountAfterFailure));
|
||||
|
||||
Object.DestroyImmediate(root);
|
||||
DeleteSampleSave();
|
||||
LogAssert.NoUnexpectedReceived();
|
||||
}
|
||||
|
||||
private static IEnumerator AwaitTask(Task task)
|
||||
{
|
||||
while (!task.IsCompleted)
|
||||
@@ -133,6 +228,24 @@ namespace FlowScope.Tests.PlayMode.Samples
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerator AwaitExpected<TException>(Task task)
|
||||
where TException : Exception
|
||||
{
|
||||
while (!task.IsCompleted)
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
|
||||
Assert.That(task.IsFaulted || task.IsCanceled, Is.True);
|
||||
if (task.IsCanceled)
|
||||
{
|
||||
Assert.That(typeof(TException), Is.EqualTo(typeof(OperationCanceledException)));
|
||||
yield break;
|
||||
}
|
||||
|
||||
Assert.That(task.Exception.GetBaseException(), Is.TypeOf<TException>());
|
||||
}
|
||||
|
||||
private static GameObject CreateInactiveBootstrap(string name, out GameBootstrap bootstrap)
|
||||
{
|
||||
var root = new GameObject(name);
|
||||
@@ -141,6 +254,18 @@ namespace FlowScope.Tests.PlayMode.Samples
|
||||
return root;
|
||||
}
|
||||
|
||||
private static void SetPrivateField<TValue>(
|
||||
GameBootstrap bootstrap,
|
||||
string fieldName,
|
||||
TValue value)
|
||||
{
|
||||
var field = typeof(GameBootstrap).GetField(
|
||||
fieldName,
|
||||
BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
Assert.That(field, Is.Not.Null);
|
||||
field.SetValue(bootstrap, value);
|
||||
}
|
||||
|
||||
private static void DeleteSampleSave()
|
||||
{
|
||||
var path = Path.Combine(
|
||||
@@ -152,5 +277,83 @@ namespace FlowScope.Tests.PlayMode.Samples
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class SuccessfulSaveService : ISaveService
|
||||
{
|
||||
public Task SaveAsync<T>(string key, T data, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<T> LoadAsync<T>(string key, T defaultValue, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(defaultValue);
|
||||
}
|
||||
|
||||
public void Delete(string key)
|
||||
{
|
||||
}
|
||||
|
||||
public bool Exists(string key) => false;
|
||||
}
|
||||
|
||||
private sealed class ThrowingSaveService : ISaveService
|
||||
{
|
||||
private readonly Exception _exception;
|
||||
|
||||
public ThrowingSaveService(Exception exception)
|
||||
{
|
||||
_exception = exception;
|
||||
}
|
||||
|
||||
public Task SaveAsync<T>(string key, T data, CancellationToken cancellationToken)
|
||||
{
|
||||
throw _exception;
|
||||
}
|
||||
|
||||
public Task<T> LoadAsync<T>(string key, T defaultValue, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(defaultValue);
|
||||
}
|
||||
|
||||
public void Delete(string key)
|
||||
{
|
||||
}
|
||||
|
||||
public bool Exists(string key) => false;
|
||||
}
|
||||
|
||||
private sealed class ThrowOnceSaveService : ISaveService
|
||||
{
|
||||
private readonly Exception _exception;
|
||||
private bool _thrown;
|
||||
|
||||
public ThrowOnceSaveService(Exception exception)
|
||||
{
|
||||
_exception = exception;
|
||||
}
|
||||
|
||||
public Task SaveAsync<T>(string key, T data, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_thrown)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
_thrown = true;
|
||||
throw _exception;
|
||||
}
|
||||
|
||||
public Task<T> LoadAsync<T>(string key, T defaultValue, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(defaultValue);
|
||||
}
|
||||
|
||||
public void Delete(string key)
|
||||
{
|
||||
}
|
||||
|
||||
public bool Exists(string key) => false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FlowScope.UI;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
@@ -58,6 +60,214 @@ namespace FlowScope.Tests.PlayMode.UI
|
||||
Assert.That(resources.Handles, Has.Count.EqualTo(1));
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator PreloadAsync_WhenSamePanelLoadsConcurrently_UsesOneResourceLoad()
|
||||
{
|
||||
var prefab = CreatePrefab<ExplicitPathPanel>("ExplicitPathPrefab");
|
||||
var resources = new GatedUITestResources("Custom/Explicit", prefab);
|
||||
var preload = new UIPreloadService(resources);
|
||||
|
||||
var first = preload.PreloadAsync<ExplicitPathPanel>(CancellationToken.None);
|
||||
var second = preload.PreloadAsync<ExplicitPathPanel>(CancellationToken.None);
|
||||
|
||||
Assert.That(resources.LoadCalls, Is.EqualTo(1));
|
||||
|
||||
resources.Complete();
|
||||
yield return UITestAsync.Await(first);
|
||||
yield return UITestAsync.Await(second);
|
||||
|
||||
Assert.That(resources.LoadCalls, Is.EqualTo(1));
|
||||
Assert.That(resources.Handle.IsDisposed, Is.False);
|
||||
Assert.That(preload.IsPreloaded<ExplicitPathPanel>(), Is.True);
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator PreloadAsync_WhenFirstWaiterCancels_SecondWaiterStillSucceeds()
|
||||
{
|
||||
var prefab = CreatePrefab<ExplicitPathPanel>("ExplicitPathPrefab");
|
||||
var resources = new GatedUITestResources("Custom/Explicit", prefab);
|
||||
var preload = new UIPreloadService(resources);
|
||||
using var firstCancellation = new CancellationTokenSource();
|
||||
|
||||
var first = preload.PreloadAsync<ExplicitPathPanel>(firstCancellation.Token);
|
||||
var second = preload.PreloadAsync<ExplicitPathPanel>(CancellationToken.None);
|
||||
|
||||
Assert.That(resources.LoadCalls, Is.EqualTo(1));
|
||||
Assert.That(resources.LoadCancellationTokenCanBeCanceled, Is.False);
|
||||
|
||||
firstCancellation.Cancel();
|
||||
yield return UITestAsync.AwaitExpected<OperationCanceledException>(first);
|
||||
|
||||
resources.Complete();
|
||||
yield return UITestAsync.Await(second);
|
||||
|
||||
Assert.That(resources.LoadCalls, Is.EqualTo(1));
|
||||
Assert.That(resources.Handle.IsDisposed, Is.False);
|
||||
Assert.That(preload.IsPreloaded<ExplicitPathPanel>(), Is.True);
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator PreloadAsync_WhenSecondWaiterCancels_FirstWaiterStillSucceeds()
|
||||
{
|
||||
var prefab = CreatePrefab<ExplicitPathPanel>("ExplicitPathPrefab");
|
||||
var resources = new GatedUITestResources("Custom/Explicit", prefab);
|
||||
var preload = new UIPreloadService(resources);
|
||||
using var secondCancellation = new CancellationTokenSource();
|
||||
|
||||
var first = preload.PreloadAsync<ExplicitPathPanel>(CancellationToken.None);
|
||||
var second = preload.PreloadAsync<ExplicitPathPanel>(secondCancellation.Token);
|
||||
|
||||
Assert.That(resources.LoadCalls, Is.EqualTo(1));
|
||||
|
||||
secondCancellation.Cancel();
|
||||
yield return UITestAsync.AwaitExpected<OperationCanceledException>(second);
|
||||
|
||||
resources.Complete();
|
||||
yield return UITestAsync.Await(first);
|
||||
|
||||
Assert.That(resources.LoadCalls, Is.EqualTo(1));
|
||||
Assert.That(resources.Handle.IsDisposed, Is.False);
|
||||
Assert.That(preload.IsPreloaded<ExplicitPathPanel>(), Is.True);
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator PreloadAsync_WhenAllWaitersCancel_ReleasesLoadedHandleWithoutCaching()
|
||||
{
|
||||
var prefab = CreatePrefab<ExplicitPathPanel>("ExplicitPathPrefab");
|
||||
var resources = new GatedUITestResources("Custom/Explicit", prefab);
|
||||
var preload = new UIPreloadService(resources);
|
||||
using var firstCancellation = new CancellationTokenSource();
|
||||
using var secondCancellation = new CancellationTokenSource();
|
||||
|
||||
var first = preload.PreloadAsync<ExplicitPathPanel>(firstCancellation.Token);
|
||||
var second = preload.PreloadAsync<ExplicitPathPanel>(secondCancellation.Token);
|
||||
|
||||
firstCancellation.Cancel();
|
||||
secondCancellation.Cancel();
|
||||
yield return UITestAsync.AwaitExpected<OperationCanceledException>(first);
|
||||
yield return UITestAsync.AwaitExpected<OperationCanceledException>(second);
|
||||
|
||||
resources.Complete();
|
||||
yield return null;
|
||||
|
||||
Assert.That(resources.LoadCalls, Is.EqualTo(1));
|
||||
Assert.That(resources.Handle.IsDisposed, Is.True);
|
||||
Assert.That(preload.IsPreloaded<ExplicitPathPanel>(), Is.False);
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator ReleaseAll_WhenLoadInFlight_DoesNotCacheAndDisposesLoadedHandle()
|
||||
{
|
||||
var prefab = CreatePrefab<ExplicitPathPanel>("ExplicitPathPrefab");
|
||||
var resources = new GatedUITestResources("Custom/Explicit", prefab);
|
||||
var preload = new UIPreloadService(resources);
|
||||
|
||||
var task = preload.PreloadAsync<ExplicitPathPanel>(CancellationToken.None);
|
||||
Assert.That(resources.LoadCalls, Is.EqualTo(1));
|
||||
|
||||
preload.ReleaseAll();
|
||||
resources.Complete();
|
||||
yield return UITestAsync.Await(task);
|
||||
|
||||
Assert.That(resources.Handle.IsDisposed, Is.True);
|
||||
Assert.That(preload.IsPreloaded<ExplicitPathPanel>(), Is.False);
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator PreloadAsync_AfterReleaseAllWhileOldLoadInFlight_StartsNewLoadAndCachesNewHandle()
|
||||
{
|
||||
var oldPrefab = CreatePrefab<ExplicitPathPanel>("OldExplicitPathPrefab");
|
||||
var newPrefab = CreatePrefab<ExplicitPathPanel>("NewExplicitPathPrefab");
|
||||
var resources = new SequencedGatedUITestResources("Custom/Explicit", oldPrefab, newPrefab);
|
||||
var preload = new UIPreloadService(resources);
|
||||
|
||||
var oldTask = preload.PreloadAsync<ExplicitPathPanel>(CancellationToken.None);
|
||||
Assert.That(resources.LoadCalls, Is.EqualTo(1));
|
||||
|
||||
preload.ReleaseAll();
|
||||
|
||||
var newTask = preload.PreloadAsync<ExplicitPathPanel>(CancellationToken.None);
|
||||
Assert.That(resources.LoadCalls, Is.EqualTo(2));
|
||||
|
||||
resources.Complete(0);
|
||||
yield return UITestAsync.Await(oldTask);
|
||||
|
||||
Assert.That(resources.HandleAt(0).IsDisposed, Is.True);
|
||||
Assert.That(preload.IsPreloaded<ExplicitPathPanel>(), Is.False);
|
||||
|
||||
resources.Complete(1);
|
||||
yield return UITestAsync.Await(newTask);
|
||||
|
||||
Assert.That(resources.HandleAt(1).IsDisposed, Is.False);
|
||||
Assert.That(preload.TryGetPreloadedHandle<ExplicitPathPanel>(out var handle), Is.True);
|
||||
Assert.That(handle.Asset, Is.SameAs(newPrefab));
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator OldInFlightCompletion_AfterReleaseAll_DoesNotRemoveNewInFlight()
|
||||
{
|
||||
var oldPrefab = CreatePrefab<ExplicitPathPanel>("OldExplicitPathPrefab");
|
||||
var newPrefab = CreatePrefab<ExplicitPathPanel>("NewExplicitPathPrefab");
|
||||
var resources = new SequencedGatedUITestResources("Custom/Explicit", oldPrefab, newPrefab);
|
||||
var preload = new UIPreloadService(resources);
|
||||
|
||||
var oldTask = preload.PreloadAsync<ExplicitPathPanel>(CancellationToken.None);
|
||||
Assert.That(resources.LoadCalls, Is.EqualTo(1));
|
||||
|
||||
preload.ReleaseAll();
|
||||
|
||||
var newTask = preload.PreloadAsync<ExplicitPathPanel>(CancellationToken.None);
|
||||
Assert.That(resources.LoadCalls, Is.EqualTo(2));
|
||||
|
||||
resources.Complete(0);
|
||||
yield return UITestAsync.Await(oldTask);
|
||||
|
||||
Assert.That(preload.IsPreloaded<ExplicitPathPanel>(), Is.False);
|
||||
|
||||
var thirdTask = preload.PreloadAsync<ExplicitPathPanel>(CancellationToken.None);
|
||||
Assert.That(resources.LoadCalls, Is.EqualTo(2));
|
||||
|
||||
resources.Complete(1);
|
||||
yield return UITestAsync.Await(newTask);
|
||||
yield return UITestAsync.Await(thirdTask);
|
||||
|
||||
Assert.That(resources.HandleAt(1).IsDisposed, Is.False);
|
||||
Assert.That(preload.IsPreloaded<ExplicitPathPanel>(), Is.True);
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator Dispose_WhenLoadInFlight_DoesNotCacheAndDisposesLoadedHandle()
|
||||
{
|
||||
var prefab = CreatePrefab<ExplicitPathPanel>("ExplicitPathPrefab");
|
||||
var resources = new GatedUITestResources("Custom/Explicit", prefab);
|
||||
var preload = new UIPreloadService(resources);
|
||||
|
||||
var task = preload.PreloadAsync<ExplicitPathPanel>(CancellationToken.None);
|
||||
Assert.That(resources.LoadCalls, Is.EqualTo(1));
|
||||
|
||||
preload.Dispose();
|
||||
resources.Complete();
|
||||
yield return UITestAsync.Await(task);
|
||||
|
||||
Assert.That(resources.Handle.IsDisposed, Is.True);
|
||||
Assert.That(preload.IsPreloaded<ExplicitPathPanel>(), Is.False);
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator PreloadAsync_AfterDispose_ThrowsObjectDisposedException()
|
||||
{
|
||||
var prefab = CreatePrefab<ExplicitPathPanel>("ExplicitPathPrefab");
|
||||
var resources = new UITestResources();
|
||||
resources.Register("Custom/Explicit", prefab);
|
||||
var preload = new UIPreloadService(resources);
|
||||
|
||||
preload.Dispose();
|
||||
|
||||
yield return UITestAsync.AwaitExpected<ObjectDisposedException>(
|
||||
preload.PreloadAsync<ExplicitPathPanel>(CancellationToken.None));
|
||||
Assert.That(resources.LoadedKeys, Is.Empty);
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator OpenAsync_WhenPanelIsPreloaded_ReusesPreloadedHandle()
|
||||
{
|
||||
@@ -96,6 +306,24 @@ namespace FlowScope.Tests.PlayMode.UI
|
||||
Assert.That(preload.IsPreloaded<ExplicitPathPanel>(), Is.False);
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator Release_WhenLoadInFlight_DoesNotCacheAndDisposesLoadedHandle()
|
||||
{
|
||||
var prefab = CreatePrefab<ExplicitPathPanel>("ExplicitPathPrefab");
|
||||
var resources = new GatedUITestResources("Custom/Explicit", prefab);
|
||||
var preload = new UIPreloadService(resources);
|
||||
|
||||
var task = preload.PreloadAsync<ExplicitPathPanel>(CancellationToken.None);
|
||||
Assert.That(resources.LoadCalls, Is.EqualTo(1));
|
||||
|
||||
preload.Release<ExplicitPathPanel>();
|
||||
resources.Complete();
|
||||
yield return UITestAsync.Await(task);
|
||||
|
||||
Assert.That(resources.Handle.IsDisposed, Is.True);
|
||||
Assert.That(preload.IsPreloaded<ExplicitPathPanel>(), Is.False);
|
||||
}
|
||||
|
||||
private Canvas CreateCanvas(string name)
|
||||
{
|
||||
var gameObject = new GameObject(name);
|
||||
@@ -111,5 +339,118 @@ namespace FlowScope.Tests.PlayMode.UI
|
||||
gameObject.AddComponent<TPanel>();
|
||||
return gameObject;
|
||||
}
|
||||
|
||||
private sealed class GatedUITestResources : FlowScope.Resources.IResourceService
|
||||
{
|
||||
private readonly string _key;
|
||||
private readonly GameObject _asset;
|
||||
private readonly TaskCompletionSource<FlowScope.Resources.IResourceHandle<GameObject>> _gate =
|
||||
new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
public GatedUITestResources(string key, GameObject asset)
|
||||
{
|
||||
_key = key;
|
||||
_asset = asset;
|
||||
Handle = new UITestResourceHandle<GameObject>(key, asset);
|
||||
}
|
||||
|
||||
public int LoadCalls { get; private set; }
|
||||
public UITestResourceHandle<GameObject> Handle { get; }
|
||||
public bool LoadCancellationTokenCanBeCanceled { get; private set; }
|
||||
|
||||
public Task<FlowScope.Resources.IResourceHandle<T>> LoadAsync<T>(
|
||||
string key,
|
||||
CancellationToken cancellationToken)
|
||||
where T : class
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
LoadCancellationTokenCanBeCanceled = cancellationToken.CanBeCanceled;
|
||||
if (cancellationToken.CanBeCanceled)
|
||||
{
|
||||
cancellationToken.Register(() => _gate.TrySetCanceled(cancellationToken));
|
||||
}
|
||||
|
||||
LoadCalls++;
|
||||
Assert.That(key, Is.EqualTo(_key));
|
||||
return AwaitHandle<T>();
|
||||
}
|
||||
|
||||
public FlowScope.Resources.IResourceGroup CreateGroup()
|
||||
{
|
||||
throw new System.NotSupportedException();
|
||||
}
|
||||
|
||||
public void Complete()
|
||||
{
|
||||
_gate.TrySetResult(Handle);
|
||||
}
|
||||
|
||||
private async Task<FlowScope.Resources.IResourceHandle<T>> AwaitHandle<T>()
|
||||
where T : class
|
||||
{
|
||||
var handle = await _gate.Task;
|
||||
Assert.That(handle.Asset, Is.SameAs(_asset));
|
||||
return (FlowScope.Resources.IResourceHandle<T>)(object)handle;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class SequencedGatedUITestResources : FlowScope.Resources.IResourceService
|
||||
{
|
||||
private readonly string _key;
|
||||
private readonly List<GameObject> _assets = new();
|
||||
private readonly List<UITestResourceHandle<GameObject>> _handles = new();
|
||||
private readonly List<TaskCompletionSource<FlowScope.Resources.IResourceHandle<GameObject>>> _gates = new();
|
||||
|
||||
public SequencedGatedUITestResources(string key, params GameObject[] assets)
|
||||
{
|
||||
_key = key;
|
||||
foreach (var asset in assets)
|
||||
{
|
||||
_assets.Add(asset);
|
||||
_handles.Add(new UITestResourceHandle<GameObject>(key, asset));
|
||||
_gates.Add(new TaskCompletionSource<FlowScope.Resources.IResourceHandle<GameObject>>(
|
||||
TaskCreationOptions.RunContinuationsAsynchronously));
|
||||
}
|
||||
}
|
||||
|
||||
public int LoadCalls { get; private set; }
|
||||
|
||||
public UITestResourceHandle<GameObject> HandleAt(int index)
|
||||
{
|
||||
return _handles[index];
|
||||
}
|
||||
|
||||
public Task<FlowScope.Resources.IResourceHandle<T>> LoadAsync<T>(
|
||||
string key,
|
||||
CancellationToken cancellationToken)
|
||||
where T : class
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
Assert.That(key, Is.EqualTo(_key));
|
||||
|
||||
var loadIndex = LoadCalls;
|
||||
LoadCalls++;
|
||||
Assert.That(loadIndex, Is.LessThan(_gates.Count), "Unexpected extra resource load.");
|
||||
return AwaitHandle<T>(loadIndex);
|
||||
}
|
||||
|
||||
public FlowScope.Resources.IResourceGroup CreateGroup()
|
||||
{
|
||||
throw new System.NotSupportedException();
|
||||
}
|
||||
|
||||
public void Complete(int index)
|
||||
{
|
||||
_gates[index].TrySetResult(_handles[index]);
|
||||
}
|
||||
|
||||
private async Task<FlowScope.Resources.IResourceHandle<T>> AwaitHandle<T>(int index)
|
||||
where T : class
|
||||
{
|
||||
var handle = await _gates[index].Task;
|
||||
Assert.That(handle.Asset, Is.SameAs(_assets[index]));
|
||||
return (FlowScope.Resources.IResourceHandle<T>)(object)handle;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FlowScope.Resources;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
|
||||
namespace FlowScope.Tests.PlayMode.UI
|
||||
@@ -100,5 +101,23 @@ namespace FlowScope.Tests.PlayMode.UI
|
||||
|
||||
onCompleted(task.Result);
|
||||
}
|
||||
|
||||
public static IEnumerator AwaitExpected<TException>(Task task)
|
||||
where TException : Exception
|
||||
{
|
||||
while (!task.IsCompleted)
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
|
||||
Assert.That(task.IsFaulted || task.IsCanceled, Is.True);
|
||||
if (task.IsCanceled)
|
||||
{
|
||||
Assert.That(typeof(TException), Is.EqualTo(typeof(OperationCanceledException)));
|
||||
yield break;
|
||||
}
|
||||
|
||||
Assert.That(task.Exception.GetBaseException(), Is.TypeOf<TException>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,4 +5,5 @@ EditorBuildSettings:
|
||||
m_ObjectHideFlags: 0
|
||||
serializedVersion: 2
|
||||
m_Scenes: []
|
||||
m_configObjects: {}
|
||||
m_configObjects:
|
||||
com.unity.addressableassets: {fileID: 11400000, guid: f9fd621d22433a64aa0741deda630f70, type: 2}
|
||||
|
||||
8
Packages/com.flowscope.gamecore/Addressables.meta
Normal file
8
Packages/com.flowscope.gamecore/Addressables.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4c7d6d9b0f184e67bc1bdf6d1e6f9c02
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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:
|
||||
17
Packages/com.flowscope.gamecore/CHANGELOG.md
Normal file
17
Packages/com.flowscope.gamecore/CHANGELOG.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Changelog
|
||||
|
||||
## Unreleased
|
||||
|
||||
## 0.2.0-preview.2
|
||||
|
||||
- Added `Samples~/MainMenuP0` package sample metadata and content.
|
||||
- Added the first `Tests~/EditMode` package smoke test slice.
|
||||
- Updated package installation and layout documentation to reflect the current P2 package state.
|
||||
- Fixed the package `Documentation~` content so consuming projects no longer see an empty documentation folder warning.
|
||||
- Validated Git package installation, package smoke references, and manual `MainMenuP0` sample import in a clean consumer project.
|
||||
|
||||
## 0.2.0-preview.1
|
||||
|
||||
- Added the first Git-consumable FlowScope Game Core package layout.
|
||||
- Mirrored Runtime, Addressables, and Editor sources from `Assets/FlowScope`.
|
||||
- Added package metadata and Git installation notes for consuming Unity projects.
|
||||
7
Packages/com.flowscope.gamecore/CHANGELOG.md.meta
Normal file
7
Packages/com.flowscope.gamecore/CHANGELOG.md.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3773cb09e514417d99ba8cc66185589b
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Packages/com.flowscope.gamecore/Documentation~.meta
Normal file
8
Packages/com.flowscope.gamecore/Documentation~.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7ffb7756a0504c53b42e8f8f069e4fbf
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,38 @@
|
||||
# FlowScope Git Package Installation
|
||||
|
||||
## Consumer Manifest
|
||||
|
||||
Add FlowScope, R3.Unity, and the NuGet R3 core dependency to the consuming project's `Packages/manifest.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"scopedRegistries": [
|
||||
{
|
||||
"name": "Unity NuGet",
|
||||
"url": "https://unitynuget-registry.openupm.com",
|
||||
"scopes": [
|
||||
"org.nuget"
|
||||
]
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"com.cysharp.r3": "https://github.com/Cysharp/R3.git?path=src/R3.Unity/Assets/R3.Unity#1.3.0",
|
||||
"com.flowscope.gamecore": "ssh://git@git.zz.com:2222/Developer/FlowScope.git?path=/Packages/com.flowscope.gamecore#v0.2.0-preview.2",
|
||||
"org.nuget.r3": "1.3.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Keep both R3 entries in the project manifest. Unity Package Manager can install a Git package from the project manifest, but Git dependencies declared inside another package's `package.json` are not resolved reliably. `org.nuget.r3` supplies the core R3 and BCL DLLs required by `R3.Unity`.
|
||||
|
||||
## Validation
|
||||
|
||||
After Unity resolves the package:
|
||||
|
||||
1. Confirm `FlowScope.Runtime`, `FlowScope.Addressables`, and `FlowScope.Editor` compile.
|
||||
2. Open `Tools/FlowScope/Diagnostics`.
|
||||
3. Confirm Addressables and UGUI dependencies are present.
|
||||
4. Add a small probe script in the consuming project that references `FlowScope.Container.Container` and `FlowScope.Flow.GameFlow`.
|
||||
5. Confirm the consuming project has zero compile errors and zero Console errors after package resolution.
|
||||
|
||||
Do not copy `Assets/FlowScope` into the consuming project when testing the Git package path. That would validate project-internal source import rather than package consumption.
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d9b2393cf81b4b96a9a32c0d141606b8
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Packages/com.flowscope.gamecore/Editor.meta
Normal file
8
Packages/com.flowscope.gamecore/Editor.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 90b29a69f53947678afc1973c1c1f5a4
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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:
|
||||
@@ -0,0 +1,283 @@
|
||||
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 PackageRuntimeAsmdefPath = "Packages/com.flowscope.gamecore/Runtime/FlowScope.Runtime.asmdef";
|
||||
private const string PackageAddressablesAsmdefPath = "Packages/com.flowscope.gamecore/Addressables/FlowScope.Addressables.asmdef";
|
||||
private const string AssetsRuntimeAsmdefPath = "Assets/FlowScope/Runtime/FlowScope.Runtime.asmdef";
|
||||
private const string AssetsAddressablesAsmdefPath = "Assets/FlowScope/Addressables/FlowScope.Addressables.asmdef";
|
||||
private const string AssetsMainMenuScenePath = "Assets/FlowScope/Samples/MainMenuP0/Scenes/MainMenuP0.unity";
|
||||
private const string AssetsMainMenuPanelPrefabPath = "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", PackageRuntimeAsmdefPath, AssetsRuntimeAsmdefPath),
|
||||
new AssetCheck("Addressables asmdef", PackageAddressablesAsmdefPath, AssetsAddressablesAsmdefPath),
|
||||
new AssetCheck("MainMenuP0 scene", AssetsMainMenuScenePath),
|
||||
new AssetCheck("MainMenuPanel prefab", AssetsMainMenuPanelPrefabPath),
|
||||
};
|
||||
|
||||
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)
|
||||
{
|
||||
string foundPath = FindExistingAssetPath(check.Paths);
|
||||
_assetResults.Add(foundPath != null
|
||||
? DiagnosticResult.Pass(check.Label, foundPath)
|
||||
: DiagnosticResult.Fail(check.Label, string.Join(" or ", check.Paths)));
|
||||
}
|
||||
}
|
||||
|
||||
private static string FindExistingAssetPath(IReadOnlyList<string> paths)
|
||||
{
|
||||
foreach (string path in paths)
|
||||
{
|
||||
if (AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(path) != null)
|
||||
{
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
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, params string[] paths)
|
||||
{
|
||||
Label = label;
|
||||
Paths = paths;
|
||||
}
|
||||
|
||||
public string Label { get; }
|
||||
|
||||
public IReadOnlyList<string> Paths { 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 599ef528ec0c4331a77311f6ef7ef56b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
40
Packages/com.flowscope.gamecore/README.md
Normal file
40
Packages/com.flowscope.gamecore/README.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# FlowScope Game Core
|
||||
|
||||
FlowScope Game Core is a lightweight Unity runtime foundation for feature flow, dependency registration, resources, config, save, audio, and UGUI panel management.
|
||||
|
||||
This package is currently a P2 preview package. Runtime, Addressables, and Editor sources are mirrored from the project-internal `Assets/FlowScope` implementation so a consuming Unity project can validate Git-based package installation.
|
||||
|
||||
## Install From Git
|
||||
|
||||
Add the package to the consuming project's `Packages/manifest.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"scopedRegistries": [
|
||||
{
|
||||
"name": "Unity NuGet",
|
||||
"url": "https://unitynuget-registry.openupm.com",
|
||||
"scopes": [
|
||||
"org.nuget"
|
||||
]
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"com.cysharp.r3": "https://github.com/Cysharp/R3.git?path=src/R3.Unity/Assets/R3.Unity#1.3.0",
|
||||
"com.flowscope.gamecore": "ssh://git@git.zz.com:2222/Developer/FlowScope.git?path=/Packages/com.flowscope.gamecore#v0.2.0-preview.2",
|
||||
"org.nuget.r3": "1.3.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`com.cysharp.r3` and `org.nuget.r3` must be declared by the consuming project. Unity Package Manager can install Git packages from the project manifest, but Git dependencies declared inside another package's `package.json` are not resolved reliably. `org.nuget.r3` supplies the core R3 and BCL DLLs required by `R3.Unity`.
|
||||
|
||||
## Assembly Boundaries
|
||||
|
||||
- `FlowScope.Runtime` references `R3.Unity`.
|
||||
- `FlowScope.Addressables` references `FlowScope.Runtime`, `Unity.Addressables`, and `Unity.ResourceManager`.
|
||||
- `FlowScope.Editor` is Editor-only and only provides read-only diagnostics.
|
||||
- `Samples~/MainMenuP0` is a package sample candidate mirrored from the project-internal MainMenuP0 sample.
|
||||
- `Tests~/EditMode` contains the first package-level smoke tests.
|
||||
|
||||
Runtime must not depend on Addressables, Editor, Samples, or Tests.
|
||||
7
Packages/com.flowscope.gamecore/README.md.meta
Normal file
7
Packages/com.flowscope.gamecore/README.md.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5a34f60c59414ed5836c5d06427f58e3
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Packages/com.flowscope.gamecore/Runtime.meta
Normal file
8
Packages/com.flowscope.gamecore/Runtime.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 397a4bbdf96047cd869e89f0f9d268e5
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Packages/com.flowscope.gamecore/Runtime/Audio.meta
Normal file
8
Packages/com.flowscope.gamecore/Runtime/Audio.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: faa0e59581ea46ed8994f743336decdc
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
25
Packages/com.flowscope.gamecore/Runtime/Audio/AudioHandle.cs
Normal file
25
Packages/com.flowscope.gamecore/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
Packages/com.flowscope.gamecore/Runtime/Audio/AudioService.cs
Normal file
392
Packages/com.flowscope.gamecore/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:
|
||||
@@ -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
Packages/com.flowscope.gamecore/Runtime/Common.meta
Normal file
8
Packages/com.flowscope.gamecore/Runtime/Common.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 76e811c4a8bd4f6497926d5befb64b83
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
22
Packages/com.flowscope.gamecore/Runtime/Common/ResultLog.cs
Normal file
22
Packages/com.flowscope.gamecore/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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a4063b7b41014f9399532dd3cc83e68f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Packages/com.flowscope.gamecore/Runtime/Config.meta
Normal file
8
Packages/com.flowscope.gamecore/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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user